Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions apps/backend/analysis/security_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import json
import logging
import os
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
Expand Down Expand Up @@ -370,6 +371,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:
Expand Down Expand Up @@ -656,6 +662,105 @@ 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",
)

# 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 (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."""
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)
self._parse_osv_output(osv_output, result)
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)}")

@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", []):
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=self._osv_severity(vuln),
source="osv_scanner",
title=(
f"Vulnerable dependency: {pkg_name} "
f"({vuln.get('id', '?')})"
),
description=vuln.get("summary", ""),
file=file,
)
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def _run_predictive_scan(
self, project_dir: Path, result: SecurityScanResult
) -> None:
Expand Down
176 changes: 176 additions & 0 deletions apps/backend/security/language_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand Down Expand Up @@ -59,6 +63,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
},
}


Expand Down Expand Up @@ -288,6 +325,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_SECURE_RANDOM_JVM,
"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_SECURE_RANDOM_JVM,
"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_SECURE_RANDOM_JVM,
"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
"Uri.http", # Cleartext transport constructor
],
"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",
],
},
}


Expand Down
28 changes: 28 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -1657,3 +1657,31 @@ 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", "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, 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"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading