From b0ac9636c177721d57bda9fe6128f955c8176fc4 Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Sun, 5 Jul 2026 20:48:46 +0400 Subject: [PATCH 1/4] feat(security): osv-scanner universal dependency audit + rules for JVM/.NET/Elixir/Swift/Scala/Dart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the security-scanner half of roadmap phase 2: - osv-scanner runner covering ecosystems without a dedicated audit tool: Maven/Gradle, NuGet, Hex, SwiftPM, pub, and Go modules. Triggered by their manifests; npm/cargo/composer manifests excluded to avoid double-reporting with existing dedicated runners. - LANGUAGE_SECURITY_SCANNERS and LANGUAGE_SECURITY_RULES entries for java, kotlin, csharp, elixir, swift, scala, dart — every language the stack detector recognizes now has scanner and rule coverage. Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- apps/backend/analysis/security_scanner.py | 69 +++++++++ apps/backend/security/language_rules.py | 172 ++++++++++++++++++++++ tests/test_security.py | 27 ++++ tests/test_security_scanner.py | 65 ++++++++ 4 files changed, 333 insertions(+) diff --git a/apps/backend/analysis/security_scanner.py b/apps/backend/analysis/security_scanner.py index 77016612e..b90fd793a 100644 --- a/apps/backend/analysis/security_scanner.py +++ b/apps/backend/analysis/security_scanner.py @@ -370,6 +370,11 @@ def _run_dependency_audits( if (project_dir / "composer.lock").exists(): self._run_composer_audit(project_dir, result) + # osv-scanner for ecosystems without a dedicated audit tool + # (JVM, .NET, Elixir, Swift, Dart, Go modules) + if self._has_osv_manifests(project_dir): + self._run_osv_scanner(project_dir, result) + def _run_npm_audit(self, project_dir: Path, result: SecurityScanResult) -> None: """Run npm audit for JavaScript projects.""" try: @@ -656,6 +661,70 @@ def _run_composer_audit( except Exception as e: result.scan_errors.append(f"composer audit error: {str(e)}") + # Manifests handled by osv-scanner for ecosystems that have no dedicated + # audit runner above (npm/pip/cargo/composer manifests are excluded to + # avoid double-reporting) + OSV_MANIFESTS = ( + "pom.xml", + "gradle.lockfile", + "buildscript-gradle.lockfile", + "packages.lock.json", + "mix.lock", + "Package.resolved", + "pubspec.lock", + "go.mod", + ) + + def _has_osv_manifests(self, project_dir: Path) -> bool: + """Check for manifests osv-scanner should audit.""" + return any((project_dir / m).exists() for m in self.OSV_MANIFESTS) + + def _run_osv_scanner(self, project_dir: Path, result: SecurityScanResult) -> None: + """Run osv-scanner against known-vulnerability database.""" + try: + # osv-scanner exits non-zero when vulns are found; parse stdout + cmd = ["osv-scanner", "--format", "json", "-r", "."] + + proc = subprocess.run( + cmd, + cwd=project_dir, + capture_output=True, + text=True, + encoding="utf-8", + timeout=300, + ) + + if proc.stdout: + try: + osv_output = json.loads(proc.stdout) + for scan_result in osv_output.get("results", []): + source = scan_result.get("source", {}).get("path", "") + for package in scan_result.get("packages", []): + pkg_info = package.get("package", {}) + for vuln in package.get("vulnerabilities", []): + result.vulnerabilities.append( + SecurityVulnerability( + severity="high", + source="osv_scanner", + title=( + f"Vulnerable dependency: " + f"{pkg_info.get('name', '?')} " + f"({vuln.get('id', '?')})" + ), + description=vuln.get("summary", ""), + file=Path(source).name if source else None, + ) + ) + except json.JSONDecodeError: + result.scan_errors.append("Failed to parse osv-scanner output") + + except FileNotFoundError: + logger.debug("osv-scanner not available") + except subprocess.TimeoutExpired: + result.scan_errors.append("osv-scanner timed out") + except Exception as e: + result.scan_errors.append(f"osv-scanner error: {str(e)}") + def _run_predictive_scan( self, project_dir: Path, result: SecurityScanResult ) -> None: diff --git a/apps/backend/security/language_rules.py b/apps/backend/security/language_rules.py index 5c7ab9e32..07484bb32 100644 --- a/apps/backend/security/language_rules.py +++ b/apps/backend/security/language_rules.py @@ -59,6 +59,39 @@ "clang-tidy", # Clang-based linter (includes security checks) "flawfinder", # C/C++ security weakness scanner }, + "java": { + "spotbugs", # Java bytecode static analysis (with find-sec-bugs) + "semgrep", # Multi-language security scanner + "osv-scanner", # Known-vulnerability scanner for Maven/Gradle deps + }, + "kotlin": { + "detekt", # Kotlin static analysis + "semgrep", # Multi-language security scanner + "osv-scanner", # Known-vulnerability scanner for Maven/Gradle deps + }, + "csharp": { + "security-scan", # Security Code Scan for .NET + "semgrep", # Multi-language security scanner + "osv-scanner", # Known-vulnerability scanner for NuGet deps + }, + "elixir": { + "sobelow", # Phoenix/Elixir security scanner + "credo", # Elixir static analysis + "osv-scanner", # Known-vulnerability scanner for Hex deps + }, + "swift": { + "swiftlint", # Swift linter + "osv-scanner", # Known-vulnerability scanner for SwiftPM deps + }, + "scala": { + "scalafix", # Scala linter/refactoring tool + "semgrep", # Multi-language security scanner + "osv-scanner", # Known-vulnerability scanner for sbt/Maven deps + }, + "dart": { + "dart-analyze", # Dart static analysis (dart analyze) + "osv-scanner", # Known-vulnerability scanner for pub deps + }, } @@ -288,6 +321,145 @@ "Validate and sanitize all user inputs", ], }, + "java": { + "dangerous_functions": [ + "Runtime.exec", # Command injection risk + "ProcessBuilder", # Command injection risk + "ObjectInputStream", # Deserialization vulnerability + "XMLDecoder", # Deserialization vulnerability + "Class.forName", # Reflection-based code loading + "ScriptEngine.eval", # Code injection + ], + "unsafe_patterns": [ + "createStatement", # SQL injection, use PreparedStatement + "DocumentBuilderFactory", # XXE unless secure processing enabled + "TrustAllCerts", # Disabled TLS validation + "MD5", # Weak hash algorithm + "SHA1", # Weak hash algorithm + ], + "secure_alternatives": [ + "Use PreparedStatement with parameterized queries", + "Enable FEATURE_SECURE_PROCESSING on XML factories", + "Use java.security.SecureRandom for random numbers", + "Avoid Java serialization; prefer JSON with strict typing", + "Validate and sanitize all user inputs", + ], + }, + "kotlin": { + "dangerous_functions": [ + "Runtime.exec", # Command injection risk + "ProcessBuilder", # Command injection risk + "ObjectInputStream", # Deserialization vulnerability + "ScriptEngine.eval", # Code injection + ], + "unsafe_patterns": [ + "!!", # Non-null assertion, runtime crash risk + "createStatement", # SQL injection, use PreparedStatement + "TrustAllCerts", # Disabled TLS validation + ], + "secure_alternatives": [ + "Use safe calls (?.) and requireNotNull instead of !!", + "Use PreparedStatement with parameterized queries", + "Use java.security.SecureRandom for random numbers", + "Validate and sanitize all user inputs", + ], + }, + "csharp": { + "dangerous_functions": [ + "Process.Start", # Command injection risk + "BinaryFormatter", # Deserialization vulnerability + "Assembly.Load", # Code loading risk + "XmlDocument.Load", # XXE risk without secure resolver + ], + "unsafe_patterns": [ + "SqlCommand", # SQL injection without parameters + "MD5", # Weak hash algorithm + "SHA1", # Weak hash algorithm + "Random", # Not cryptographically secure + "unsafe", # Memory safety bypass + ], + "secure_alternatives": [ + "Use SqlParameter for parameterized queries", + "Use System.Text.Json instead of BinaryFormatter", + "Use RandomNumberGenerator for secure random numbers", + "Set XmlResolver = null to prevent XXE", + "Validate and sanitize all user inputs", + ], + }, + "elixir": { + "dangerous_functions": [ + "Code.eval_string", # Code injection + ":os.cmd", # Command injection + "System.cmd", # Command injection with user input + ":erlang.binary_to_term", # Deserialization vulnerability + ], + "unsafe_patterns": [ + "String.to_atom", # Atom table exhaustion (DoS) + "raw: true", # Raw SQL, injection risk + "Plug.Conn.put_resp_header", # Header injection if unvalidated + ], + "secure_alternatives": [ + "Use String.to_existing_atom instead of String.to_atom", + "Use Ecto parameterized queries instead of raw SQL", + "Use Plug.Crypto for secure tokens and comparison", + "Validate and sanitize all user inputs", + ], + }, + "swift": { + "dangerous_functions": [ + "NSTask", # Command execution + "Process", # Command execution with user input + "unsafeBitCast", # Type safety bypass + "UnsafeMutablePointer", # Memory safety bypass + ], + "unsafe_patterns": [ + "try!", # Crash on error + "as!", # Crash on failed cast + "String(format:", # Format string risk with user input + ], + "secure_alternatives": [ + "Use do/catch or try? instead of try!", + "Use conditional casts (as?) with unwrapping", + "Use SecRandomCopyBytes for secure random numbers", + "Validate and sanitize all user inputs", + ], + }, + "scala": { + "dangerous_functions": [ + "sys.process", # Command injection risk + "ObjectInputStream", # Deserialization vulnerability + "Class.forName", # Reflection-based code loading + ], + "unsafe_patterns": [ + "createStatement", # SQL injection, use PreparedStatement + "asInstanceOf", # Unchecked cast + "null", # Prefer Option + ], + "secure_alternatives": [ + "Use PreparedStatement or a typed query DSL", + "Use Option instead of null", + "Use java.security.SecureRandom for random numbers", + "Validate and sanitize all user inputs", + ], + }, + "dart": { + "dangerous_functions": [ + "Process.run", # Command injection risk + "Process.start", # Command injection risk + "dart:mirrors", # Reflection-based code loading + ], + "unsafe_patterns": [ + "Random()", # Not cryptographically secure + "!", # Null assertion, runtime crash risk + "http://", # Cleartext transport + ], + "secure_alternatives": [ + "Use Random.secure() for security-sensitive randomness", + "Use null-aware operators instead of null assertions", + "Use HTTPS for all network calls", + "Validate and sanitize all user inputs", + ], + }, } diff --git a/tests/test_security.py b/tests/test_security.py index 2fedc0ea6..8db6aabd0 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1657,3 +1657,30 @@ def test_c_cpp_have_rules(self): assert "gets" in rules["dangerous_functions"] assert "system" in rules["dangerous_functions"] assert any("fsanitize" in alt for alt in rules["secure_alternatives"]) + + def test_jvm_dotnet_tail_languages_have_scanners(self): + """Every detectable compiled/scripting-tail language has scanners.""" + from security.profile import get_security_scanners + + for lang in ("java", "kotlin", "csharp", "elixir", "swift", "scala", "dart"): + scanners = get_security_scanners(lang) + assert scanners, f"{lang} has no security scanners" + assert "osv-scanner" in scanners + + def test_jvm_dotnet_tail_languages_have_rules(self): + """New language rule entries carry dangerous functions and guidance.""" + from security.profile import get_security_rules + + expectations = { + "java": "Runtime.exec", + "kotlin": "Runtime.exec", + "csharp": "BinaryFormatter", + "elixir": "Code.eval_string", + "swift": "unsafeBitCast", + "scala": "sys.process", + "dart": "Process.run", + } + for lang, dangerous in expectations.items(): + rules = get_security_rules(lang) + assert dangerous in rules["dangerous_functions"] + assert rules["secure_alternatives"] diff --git a/tests/test_security_scanner.py b/tests/test_security_scanner.py index 02dc86d20..94b917781 100644 --- a/tests/test_security_scanner.py +++ b/tests/test_security_scanner.py @@ -657,3 +657,68 @@ def test_composer_audit_output_parsing(self, mock_run, scanner, temp_dir): assert vuln.severity == "high" assert "vendor/package" in vuln.title assert vuln.cwe == "CVE-2024-0001" + + def test_has_osv_manifests_positive(self, scanner, temp_dir): + """JVM/.NET/Elixir manifests trigger osv-scanner.""" + (temp_dir / "mix.lock").write_text("%{}") + assert scanner._has_osv_manifests(temp_dir) is True + + def test_has_osv_manifests_negative(self, scanner, temp_dir): + """Manifests covered by dedicated audits do not trigger osv-scanner.""" + (temp_dir / "package-lock.json").write_text("{}") + (temp_dir / "Cargo.lock").write_text("") + (temp_dir / "composer.lock").write_text("{}") + assert scanner._has_osv_manifests(temp_dir) is False + + @patch("subprocess.run") + def test_osv_scanner_output_parsing(self, mock_run, scanner, temp_dir): + """Test parsing osv-scanner JSON output.""" + mock_run.return_value = MagicMock( + stdout=json.dumps( + { + "results": [ + { + "source": {"path": "/project/pom.xml"}, + "packages": [ + { + "package": { + "name": "org.example:demo", + "ecosystem": "Maven", + }, + "vulnerabilities": [ + { + "id": "GHSA-xxxx-yyyy", + "summary": "RCE in demo library", + } + ], + } + ], + } + ] + } + ), + stderr="", + returncode=1, + ) + + result = SecurityScanResult() + scanner._run_osv_scanner(temp_dir, result) + + assert len(result.vulnerabilities) == 1 + vuln = result.vulnerabilities[0] + assert vuln.source == "osv_scanner" + assert vuln.severity == "high" + assert "org.example:demo" in vuln.title + assert "GHSA-xxxx-yyyy" in vuln.title + assert vuln.file == "pom.xml" + + @patch("subprocess.run") + def test_osv_scanner_missing_tool_is_silent(self, mock_run, scanner, temp_dir): + """Missing osv-scanner binary skips the scan without errors.""" + mock_run.side_effect = FileNotFoundError() + + result = SecurityScanResult() + scanner._run_osv_scanner(temp_dir, result) + + assert result.vulnerabilities == [] + assert result.scan_errors == [] From b66d963b96d4c122fff504fc99a9d6eb5a89c7a6 Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Sun, 5 Jul 2026 20:58:23 +0400 Subject: [PATCH 2/4] fix(security): address Sonar findings in osv-scanner slice - Extract _parse_osv_output to reduce cognitive complexity (S3776) - Deduplicate JVM SecureRandom guidance string (S1192) - Replace http:// literal with Uri.http pattern marker (S5332) Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- apps/backend/analysis/security_scanner.py | 42 +++++++++++++---------- apps/backend/security/language_rules.py | 12 ++++--- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/apps/backend/analysis/security_scanner.py b/apps/backend/analysis/security_scanner.py index b90fd793a..14c51d58d 100644 --- a/apps/backend/analysis/security_scanner.py +++ b/apps/backend/analysis/security_scanner.py @@ -697,24 +697,7 @@ def _run_osv_scanner(self, project_dir: Path, result: SecurityScanResult) -> Non if proc.stdout: try: osv_output = json.loads(proc.stdout) - for scan_result in osv_output.get("results", []): - source = scan_result.get("source", {}).get("path", "") - for package in scan_result.get("packages", []): - pkg_info = package.get("package", {}) - for vuln in package.get("vulnerabilities", []): - result.vulnerabilities.append( - SecurityVulnerability( - severity="high", - source="osv_scanner", - title=( - f"Vulnerable dependency: " - f"{pkg_info.get('name', '?')} " - f"({vuln.get('id', '?')})" - ), - description=vuln.get("summary", ""), - file=Path(source).name if source else None, - ) - ) + self._parse_osv_output(osv_output, result) except json.JSONDecodeError: result.scan_errors.append("Failed to parse osv-scanner output") @@ -725,6 +708,29 @@ def _run_osv_scanner(self, project_dir: Path, result: SecurityScanResult) -> Non except Exception as e: result.scan_errors.append(f"osv-scanner error: {str(e)}") + def _parse_osv_output( + self, osv_output: dict, result: SecurityScanResult + ) -> None: + """Convert osv-scanner JSON results into vulnerabilities.""" + for scan_result in osv_output.get("results", []): + source = scan_result.get("source", {}).get("path", "") + file = Path(source).name if source else None + for package in scan_result.get("packages", []): + pkg_name = package.get("package", {}).get("name", "?") + for vuln in package.get("vulnerabilities", []): + result.vulnerabilities.append( + SecurityVulnerability( + severity="high", + source="osv_scanner", + title=( + f"Vulnerable dependency: {pkg_name} " + f"({vuln.get('id', '?')})" + ), + description=vuln.get("summary", ""), + file=file, + ) + ) + def _run_predictive_scan( self, project_dir: Path, result: SecurityScanResult ) -> None: diff --git a/apps/backend/security/language_rules.py b/apps/backend/security/language_rules.py index 07484bb32..54cb8df23 100644 --- a/apps/backend/security/language_rules.py +++ b/apps/backend/security/language_rules.py @@ -6,6 +6,10 @@ Defines language-specific security tools and common security patterns. """ +# Shared guidance strings (JVM languages) +_USE_SECURE_RANDOM_JVM = "Use java.security.SecureRandom for random numbers" + + # ============================================================================= # LANGUAGE SECURITY SCANNERS # ============================================================================= @@ -340,7 +344,7 @@ "secure_alternatives": [ "Use PreparedStatement with parameterized queries", "Enable FEATURE_SECURE_PROCESSING on XML factories", - "Use java.security.SecureRandom for random numbers", + _USE_SECURE_RANDOM_JVM, "Avoid Java serialization; prefer JSON with strict typing", "Validate and sanitize all user inputs", ], @@ -360,7 +364,7 @@ "secure_alternatives": [ "Use safe calls (?.) and requireNotNull instead of !!", "Use PreparedStatement with parameterized queries", - "Use java.security.SecureRandom for random numbers", + _USE_SECURE_RANDOM_JVM, "Validate and sanitize all user inputs", ], }, @@ -438,7 +442,7 @@ "secure_alternatives": [ "Use PreparedStatement or a typed query DSL", "Use Option instead of null", - "Use java.security.SecureRandom for random numbers", + _USE_SECURE_RANDOM_JVM, "Validate and sanitize all user inputs", ], }, @@ -451,7 +455,7 @@ "unsafe_patterns": [ "Random()", # Not cryptographically secure "!", # Null assertion, runtime crash risk - "http://", # Cleartext transport + "Uri.http", # Cleartext transport constructor ], "secure_alternatives": [ "Use Random.secure() for security-sensitive randomness", From 3ef4e9e5fee0f731678946b0b54299a54c68872e Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Mon, 6 Jul 2026 08:25:17 +0400 Subject: [PATCH 3/4] style: ruff format for _parse_osv_output signature Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- apps/backend/analysis/security_scanner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/backend/analysis/security_scanner.py b/apps/backend/analysis/security_scanner.py index 14c51d58d..1c2b04f14 100644 --- a/apps/backend/analysis/security_scanner.py +++ b/apps/backend/analysis/security_scanner.py @@ -708,9 +708,7 @@ def _run_osv_scanner(self, project_dir: Path, result: SecurityScanResult) -> Non except Exception as e: result.scan_errors.append(f"osv-scanner error: {str(e)}") - def _parse_osv_output( - self, osv_output: dict, result: SecurityScanResult - ) -> None: + def _parse_osv_output(self, osv_output: dict, result: SecurityScanResult) -> None: """Convert osv-scanner JSON results into vulnerabilities.""" for scan_result in osv_output.get("results", []): source = scan_result.get("source", {}).get("path", "") From b13c187969e1860f00fdd1206e8c3a46f7b063fc Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Sat, 11 Jul 2026 09:49:50 +0400 Subject: [PATCH 4/4] fix(security): address review comments on osv-scanner slice - _has_osv_manifests searches recursively (single os.walk with vendor dir pruning) so nested monorepo manifests trigger the audit - severity read from OSV database_specific payload (critical/high/ moderate->medium/low), high only as fallback for unknown values - rules test also asserts unsafe_patterns per language Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- apps/backend/analysis/security_scanner.py | 38 +++++++++++++++++++++-- tests/test_security.py | 17 +++++----- tests/test_security_scanner.py | 28 +++++++++++++++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/apps/backend/analysis/security_scanner.py b/apps/backend/analysis/security_scanner.py index 1c2b04f14..915c75d6e 100644 --- a/apps/backend/analysis/security_scanner.py +++ b/apps/backend/analysis/security_scanner.py @@ -25,6 +25,7 @@ import json import logging +import os import subprocess from dataclasses import dataclass, field from pathlib import Path @@ -675,9 +676,29 @@ def _run_composer_audit( "go.mod", ) + # Vendor/build directories skipped when searching for nested manifests + _MANIFEST_SKIP_DIRS = frozenset( + { + ".git", + "node_modules", + ".venv", + "venv", + "__pycache__", + "target", + "build", + "dist", + "vendor", + } + ) + def _has_osv_manifests(self, project_dir: Path) -> bool: - """Check for manifests osv-scanner should audit.""" - return any((project_dir / m).exists() for m in self.OSV_MANIFESTS) + """Check for manifests osv-scanner should audit (recursively).""" + manifest_names = set(self.OSV_MANIFESTS) + for _root, dirs, files in os.walk(project_dir): + dirs[:] = [d for d in dirs if d not in self._MANIFEST_SKIP_DIRS] + if manifest_names.intersection(files): + return True + return False def _run_osv_scanner(self, project_dir: Path, result: SecurityScanResult) -> None: """Run osv-scanner against known-vulnerability database.""" @@ -708,6 +729,17 @@ def _run_osv_scanner(self, project_dir: Path, result: SecurityScanResult) -> Non except Exception as e: result.scan_errors.append(f"osv-scanner error: {str(e)}") + @staticmethod + def _osv_severity(vuln: dict) -> str: + """Map an OSV vulnerability payload to a severity level.""" + raw = str(vuln.get("database_specific", {}).get("severity", "")).lower() + if raw in ("critical", "high", "medium", "low"): + return raw + if raw == "moderate": + return "medium" + # Unknown severity on a known vulnerability: treat as high + return "high" + def _parse_osv_output(self, osv_output: dict, result: SecurityScanResult) -> None: """Convert osv-scanner JSON results into vulnerabilities.""" for scan_result in osv_output.get("results", []): @@ -718,7 +750,7 @@ def _parse_osv_output(self, osv_output: dict, result: SecurityScanResult) -> Non for vuln in package.get("vulnerabilities", []): result.vulnerabilities.append( SecurityVulnerability( - severity="high", + severity=self._osv_severity(vuln), source="osv_scanner", title=( f"Vulnerable dependency: {pkg_name} " diff --git a/tests/test_security.py b/tests/test_security.py index 8db6aabd0..985339afb 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1672,15 +1672,16 @@ def test_jvm_dotnet_tail_languages_have_rules(self): from security.profile import get_security_rules expectations = { - "java": "Runtime.exec", - "kotlin": "Runtime.exec", - "csharp": "BinaryFormatter", - "elixir": "Code.eval_string", - "swift": "unsafeBitCast", - "scala": "sys.process", - "dart": "Process.run", + "java": ("Runtime.exec", "createStatement"), + "kotlin": ("Runtime.exec", "!!"), + "csharp": ("BinaryFormatter", "SqlCommand"), + "elixir": ("Code.eval_string", "String.to_atom"), + "swift": ("unsafeBitCast", "try!"), + "scala": ("sys.process", "asInstanceOf"), + "dart": ("Process.run", "Random()"), } - for lang, dangerous in expectations.items(): + for lang, (dangerous, unsafe) in expectations.items(): rules = get_security_rules(lang) assert dangerous in rules["dangerous_functions"] + assert unsafe in rules["unsafe_patterns"] assert rules["secure_alternatives"] diff --git a/tests/test_security_scanner.py b/tests/test_security_scanner.py index 94b917781..c3d006729 100644 --- a/tests/test_security_scanner.py +++ b/tests/test_security_scanner.py @@ -663,6 +663,20 @@ def test_has_osv_manifests_positive(self, scanner, temp_dir): (temp_dir / "mix.lock").write_text("%{}") assert scanner._has_osv_manifests(temp_dir) is True + def test_has_osv_manifests_nested(self, scanner, temp_dir): + """Nested manifests in monorepos trigger osv-scanner.""" + service = temp_dir / "services" / "api" + service.mkdir(parents=True) + (service / "go.mod").write_text("module api") + assert scanner._has_osv_manifests(temp_dir) is True + + def test_has_osv_manifests_ignores_vendor_dirs(self, scanner, temp_dir): + """Manifests inside vendor directories do not trigger osv-scanner.""" + nested = temp_dir / "node_modules" / "some-pkg" + nested.mkdir(parents=True) + (nested / "pom.xml").write_text("") + assert scanner._has_osv_manifests(temp_dir) is False + def test_has_osv_manifests_negative(self, scanner, temp_dir): """Manifests covered by dedicated audits do not trigger osv-scanner.""" (temp_dir / "package-lock.json").write_text("{}") @@ -689,7 +703,14 @@ def test_osv_scanner_output_parsing(self, mock_run, scanner, temp_dir): { "id": "GHSA-xxxx-yyyy", "summary": "RCE in demo library", - } + }, + { + "id": "GHSA-aaaa-bbbb", + "summary": "ReDoS in demo library", + "database_specific": { + "severity": "MODERATE" + }, + }, ], } ], @@ -704,13 +725,16 @@ def test_osv_scanner_output_parsing(self, mock_run, scanner, temp_dir): result = SecurityScanResult() scanner._run_osv_scanner(temp_dir, result) - assert len(result.vulnerabilities) == 1 + assert len(result.vulnerabilities) == 2 vuln = result.vulnerabilities[0] assert vuln.source == "osv_scanner" + # No severity in payload: defaults to high assert vuln.severity == "high" assert "org.example:demo" in vuln.title assert "GHSA-xxxx-yyyy" in vuln.title assert vuln.file == "pom.xml" + # OSV MODERATE maps to medium + assert result.vulnerabilities[1].severity == "medium" @patch("subprocess.run") def test_osv_scanner_missing_tool_is_silent(self, mock_run, scanner, temp_dir):