VulnGraph / app /scanner.py
Mohak Rathod
feat: SBOM generation complete, GET /sbom supports CycloneDX and SPDX-JSON formats
e233eaf
Raw
History Blame Contribute Delete
6.77 kB
import subprocess
import json
import os
from typing import List, Dict
from pathlib import Path
BASE_DIR = Path(__file__).parent.parent.resolve()
TOOLS_DIR = BASE_DIR / "tools"
GITLEAKS_EXE = TOOLS_DIR / "gitleaks.exe"
TRIVY_EXE = TOOLS_DIR / "trivy.exe"
def normalize_path(path: str) -> str:
"Normalize file paths to use forward slashes for consistency across platforms."
path_str = str(path).replace("\\", "/").strip()
base_str = str(BASE_DIR).replace("\\", "/") + "/"
if path_str.startswith(base_str):
path_str = path_str[len(base_str):]
path_str = path_str.lstrip("./")
return path_str if path_str else ""
def run_gitleaks(repo_path: str= ".") -> list:
if not GITLEAKS_EXE.exists():
print("gitleaks.exe not found at", GITLEAKS_EXE)
return []
report_path= BASE_DIR/ "tmp" / "gitleaks-report.json"
os.makedirs(BASE_DIR / "tmp",exist_ok=True)
try:
cmd = [
str(GITLEAKS_EXE), "detect",
"--source", str(repo_path),
"--config", str(BASE_DIR / "gitleaks.toml"),
"--report-format", "json",
"--report-path", str(report_path),
"--no-git",
"--redact"
]
print("Running gitleaks with command:", " ".join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True, cwd= str(repo_path),timeout = 300)
print(f"Gitleaks exited with code : {result.returncode}")
if result.returncode not in [0,1]:
print("Gitleaks error output:", result.stderr.strip())
return []
if report_path.exists():
with open(report_path,"r",encoding="utf-8") as f:
findings = json.loads(f.read())
print(f"Gitleaks found {len(findings)} secrets.")
return findings if isinstance(findings, list ) else []
else:
print("Gitleaks found 0 secrets")
return []
except Exception as e:
print("Gitleaks execution error:", str(e))
return []
def run_trivy_fs(path: str = ".") -> list:
if not TRIVY_EXE.exists():
print("trivy.exe not found at", TRIVY_EXE)
return []
try:
cmd = [
str(TRIVY_EXE), "fs",
"--format", "json",
"--scanners", "vuln,secret,misconfig",
"--quiet",
"--skip-dirs",".venv,data,tmp,tools,node_modules",
str(path)
]
print("Running Trivy with command:", " ".join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True, cwd= str(path),timeout = 300)
print(f"Trivy exited with code : {result.returncode}")
if result.returncode != 0:
print("Trivy error output:", result.stderr.strip())
return []
data = json.loads(result.stdout) if result.stdout.strip() else {}
results = data.get("Results", [])
total_items = sum(
len(r.get("Vulnerabilities", [])) +
len(r.get("Secrets", [])) +
len(r.get("Misconfigurations", []))
for r in results
)
print(f"Trivy processed {len(results)} results with a total of {total_items} findings.")
return results
except Exception as e:
print("Trivy execution error:", str(e))
return []
def run_bandit(path: str = ".") -> list:
try:
scan_path=Path(path).resolve()
exclude_dirs = ",".join([
str(scan_path / ".venv"),
str(scan_path / "__pycache__"),
str(scan_path / "node_modules"),
str(scan_path / "tools"),
str(scan_path / "tmp"),
str(scan_path / "data"),
])
cmd = [
"bandit", "-r", str(scan_path),
"-f", "json",
"--quiet",
"--exclude", exclude_dirs
]
print("Running Bandit with command:", " ".join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True, cwd= str(scan_path),timeout = 300)
print(f"Bandit exited with code : {result.returncode}")
data= json.loads(result.stdout) if result.stdout.strip() else {}
findings = data.get("results", [])
print(f"Bandit found {len(findings)} issues.")
return findings
except Exception as e:
print("Bandit execution error:", str(e))
return []
def scan_all(target_dir: str=None) -> Dict[str, List[Dict]]:
"""Run all scanners. Returns dict with keys: gitleaks, trivy, bandit.
Empty list per key means either clean scan OR scanner failure — check logs to distinguish."""
scan_target=Path(target_dir).resolve() if target_dir else BASE_DIR
print(f"Starting comprehensive scan from : {scan_target}")
findings={
"gitleaks": run_gitleaks(str(scan_target)),
"trivy": run_trivy_fs(str(scan_target)),
"bandit": run_bandit(str(scan_target))
}
total_findings ={k : len(v) for k,v in findings.items()}
print(f"Scan completed with findings: {total_findings}")
return findings
def generate_sbom(path: str = None, format: str = "cyclonedx") -> dict:
"""
Generate Software Bill of Materials using Trivy.
Args:
path: directory to scan (defaults to BASE_DIR)
format: 'cyclonedx' or 'spdx-json'
Returns:
dict with sbom data and metadata
"""
if not TRIVY_EXE.exists():
return {"error": "trivy.exe not found"}
scan_path = Path(path).resolve() if path else BASE_DIR
output_file = BASE_DIR / "tmp" / f"sbom-{format}.json"
os.makedirs(BASE_DIR / "tmp", exist_ok=True)
cmd = [
str(TRIVY_EXE), "fs",
"--format", format,
"--output", str(output_file),
"--quiet",
str(scan_path)
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True,
cwd=str(scan_path), timeout=120
)
if result.returncode != 0:
return {"error": result.stderr.strip()}
if output_file.exists():
with open(output_file, "r", encoding="utf-8") as f:
sbom_data = json.load(f)
return {
"format": format,
"path": str(scan_path),
"component_count": _count_components(sbom_data, format),
"sbom": sbom_data
}
return {"error": "SBOM file not generated"}
except Exception as e:
return {"error": str(e)}
def _count_components(sbom_data: dict, format: str) -> int:
"""Count components in SBOM output."""
if format == "cyclonedx":
return len(sbom_data.get("components", []))
elif format == "spdx-json":
return len(sbom_data.get("packages", []))
return 0