Mohak Rathod
feat: SBOM generation complete, GET /sbom supports CycloneDX and SPDX-JSON formats
e233eaf | 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 |