#!/usr/bin/env python3 """Check security scan results and fail on CRITICAL or HIGH findings. Parses bandit JSON output and semgrep JSON output, reports findings, and exits non-zero if any CRITICAL or HIGH severity issues are found. Usage: python tools/security/check_results.py \ --bandit bandit-results.json \ --semgrep semgrep-results.json \ [--pip-audit pip-audit-results.json] """ from __future__ import annotations import argparse import json import sys from pathlib import Path def check_bandit(path: Path) -> list[dict]: """Parse bandit JSON results, return HIGH/CRITICAL findings.""" if not path.exists(): print(f"WARNING: bandit results not found at {path}") return [] data = json.loads(path.read_text()) results = data.get("results", []) serious = [] for r in results: sev = r.get("issue_severity", "").upper() conf = r.get("issue_confidence", "").upper() if sev in ("HIGH", "CRITICAL") and conf in ("HIGH", "MEDIUM"): serious.append(r) return serious def check_semgrep(path: Path) -> list[dict]: """Parse semgrep JSON results, return ERROR-level findings.""" if not path.exists(): print(f"WARNING: semgrep results not found at {path}") return [] data = json.loads(path.read_text()) results = data.get("results", []) serious = [] for r in results: sev = r.get("extra", {}).get("severity", "").upper() if sev == "ERROR": serious.append(r) return serious def check_pip_audit(path: Path) -> list[dict]: """Parse pip-audit JSON results, return all vulnerabilities.""" if not path.exists(): print(f"WARNING: pip-audit results not found at {path}") return [] data = json.loads(path.read_text()) # pip-audit JSON format: {"dependencies": [...], "fixes": [...]} deps = data.get("dependencies", []) if isinstance(data, dict) else data # Skip build-tool packages — these aren't our project dependencies build_tools = {"pip", "setuptools", "wheel", "hatchling"} serious = [] for dep in deps: if dep.get("name", "").lower() in build_tools: continue vulns = dep.get("vulns", []) if vulns: serious.append(dep) return serious def main() -> int: parser = argparse.ArgumentParser(description="Check security scan results") parser.add_argument("--bandit", type=Path, help="Path to bandit JSON results") parser.add_argument("--semgrep", type=Path, help="Path to semgrep JSON results") parser.add_argument("--pip-audit", type=Path, help="Path to pip-audit JSON results") args = parser.parse_args() total_issues = 0 if args.bandit: findings = check_bandit(args.bandit) if findings: print(f"\n=== BANDIT: {len(findings)} HIGH/CRITICAL findings ===") for f in findings: print(f" {f['issue_severity']}/{f['issue_confidence']}: " f"{f['issue_text']}") print(f" {f['filename']}:{f['line_number']}") total_issues += len(findings) else: print("BANDIT: No HIGH/CRITICAL findings") if args.semgrep: findings = check_semgrep(args.semgrep) if findings: print(f"\n=== SEMGREP: {len(findings)} ERROR findings ===") for f in findings: loc = f.get("path", "?") line = f.get("start", {}).get("line", "?") msg = f.get("extra", {}).get("message", f.get("check_id", "?")) print(f" ERROR: {msg}") print(f" {loc}:{line}") total_issues += len(findings) else: print("SEMGREP: No ERROR findings") if args.pip_audit: findings = check_pip_audit(args.pip_audit) if findings: print(f"\n=== PIP-AUDIT: {len(findings)} vulnerable packages ===") for dep in findings: name = dep.get("name", "?") ver = dep.get("version", "?") for v in dep.get("vulns", []): vid = v.get("id", "?") desc = v.get("description", "")[:100] print(f" {name}=={ver}: {vid} — {desc}") total_issues += len(findings) else: print("PIP-AUDIT: No vulnerabilities found") if total_issues > 0: print(f"\nFAILED: {total_issues} security issue(s) found") return 1 print("\nPASSED: No critical security issues") return 0 if __name__ == "__main__": sys.exit(main())