#!/usr/bin/env python3 """ Snyk SCA Dependency Scanning Pipeline Script Orchestrates Snyk dependency scans, evaluates quality gates, and generates consolidated vulnerability reports. Usage: python process.py --project-path /path/to/project --severity-threshold high python process.py --project-path . --manifest package.json --output report.json """ import argparse import json import os import subprocess import sys from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Optional SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3} @dataclass class VulnFinding: snyk_id: str title: str severity: str cvss_score: float package_name: str installed_version: str fixed_version: str exploit_maturity: str is_upgradable: bool is_patchable: bool dependency_path: list = field(default_factory=list) cwe: list = field(default_factory=list) @dataclass class LicenseIssue: package_name: str version: str license_id: str severity: str dependency_type: str def run_snyk_test(project_path: str, manifest: Optional[str] = None, severity_threshold: str = "low", all_projects: bool = False) -> dict: """Execute Snyk test and return JSON results.""" cmd = ["snyk", "test", "--json"] if manifest: cmd.extend(["--file", manifest]) if all_projects: cmd.append("--all-projects") cmd.extend(["--severity-threshold", severity_threshold]) try: proc = subprocess.run( cmd, cwd=project_path, capture_output=True, text=True, timeout=300 ) if proc.stdout: try: return json.loads(proc.stdout) except json.JSONDecodeError: return {"error": "Failed to parse Snyk JSON output"} return {"error": proc.stderr[:500]} except subprocess.TimeoutExpired: return {"error": "Snyk test timed out after 300 seconds"} except FileNotFoundError: return {"error": "snyk CLI not found. Install with: npm install -g snyk"} def parse_vulnerabilities(snyk_json: dict) -> list: """Parse Snyk JSON output into VulnFinding objects.""" vulns = [] vuln_list = snyk_json.get("vulnerabilities", []) for v in vuln_list: vulns.append(VulnFinding( snyk_id=v.get("id", ""), title=v.get("title", ""), severity=v.get("severity", "low"), cvss_score=v.get("cvssScore", 0.0), package_name=v.get("packageName", ""), installed_version=v.get("version", ""), fixed_version=v.get("fixedIn", ["none"])[0] if v.get("fixedIn") else "none", exploit_maturity=v.get("exploit", "No Known Exploit"), is_upgradable=v.get("isUpgradable", False), is_patchable=v.get("isPatchable", False), dependency_path=v.get("from", []), cwe=v.get("identifiers", {}).get("CWE", []) )) return vulns def deduplicate_vulns(vulns: list) -> list: """Remove duplicate vulnerability entries (same ID + package).""" seen = set() unique = [] for v in vulns: key = f"{v.snyk_id}:{v.package_name}:{v.installed_version}" if key not in seen: seen.add(key) unique.append(v) return unique def evaluate_quality_gate(vulns: list, threshold: str, fail_on: str = "all") -> dict: """Evaluate quality gate based on vulnerability severity.""" threshold_level = SEVERITY_ORDER.get(threshold.lower(), 1) blocking = [] for v in vulns: if SEVERITY_ORDER.get(v.severity.lower(), 3) <= threshold_level: if fail_on == "upgradable" and not v.is_upgradable: continue blocking.append(v) severity_counts = {} for v in vulns: sev = v.severity.lower() severity_counts[sev] = severity_counts.get(sev, 0) + 1 fixable_count = sum(1 for v in vulns if v.is_upgradable or v.is_patchable) return { "passed": len(blocking) == 0, "threshold": threshold, "fail_on": fail_on, "total_vulnerabilities": len(vulns), "blocking_count": len(blocking), "fixable_count": fixable_count, "severity_counts": severity_counts, "blocking_details": [ { "id": v.snyk_id, "title": v.title, "severity": v.severity, "package": f"{v.package_name}@{v.installed_version}", "fix": v.fixed_version, "upgradable": v.is_upgradable, "exploit": v.exploit_maturity } for v in blocking[:20] ] } def generate_report(vulns: list, quality_gate: dict, snyk_json: dict, project_path: str) -> dict: """Generate consolidated SCA report.""" dep_count = snyk_json.get("dependencyCount", 0) exploit_summary = {} for v in vulns: exploit_summary[v.exploit_maturity] = exploit_summary.get(v.exploit_maturity, 0) + 1 return { "report_metadata": { "project": project_path, "scan_date": datetime.now(timezone.utc).isoformat(), "total_dependencies": dep_count }, "quality_gate": quality_gate, "exploit_maturity_breakdown": exploit_summary, "vulnerabilities": [ { "id": v.snyk_id, "title": v.title, "severity": v.severity, "cvss": v.cvss_score, "package": v.package_name, "version": v.installed_version, "fixed_in": v.fixed_version, "exploit": v.exploit_maturity, "upgradable": v.is_upgradable, "path": " > ".join(v.dependency_path[:4]), "cwe": v.cwe } for v in sorted(vulns, key=lambda x: SEVERITY_ORDER.get(x.severity.lower(), 3)) ] } def main(): parser = argparse.ArgumentParser(description="Snyk SCA Dependency Scanning Pipeline") parser.add_argument("--project-path", required=True, help="Path to project") parser.add_argument("--manifest", default=None, help="Manifest file (e.g., package.json)") parser.add_argument("--output", default="snyk-report.json", help="Output report path") parser.add_argument("--severity-threshold", default="high", choices=["critical", "high", "medium", "low"]) parser.add_argument("--fail-on", default="all", choices=["all", "upgradable"], help="Fail on all vulns or only upgradable ones") parser.add_argument("--fail-on-findings", action="store_true") parser.add_argument("--all-projects", action="store_true", help="Scan all projects in monorepo") parser.add_argument("--monitor", action="store_true", help="Also run snyk monitor for continuous tracking") args = parser.parse_args() project_path = os.path.abspath(args.project_path) print(f"[*] Scanning dependencies in {project_path}") snyk_json = run_snyk_test( project_path, manifest=args.manifest, severity_threshold="low", all_projects=args.all_projects ) if "error" in snyk_json: print(f"[ERROR] {snyk_json['error']}") sys.exit(2) vulns = parse_vulnerabilities(snyk_json) vulns = deduplicate_vulns(vulns) quality_gate = evaluate_quality_gate(vulns, args.severity_threshold, args.fail_on) report = generate_report(vulns, quality_gate, snyk_json, project_path) output_path = os.path.abspath(args.output) with open(output_path, "w") as f: json.dump(report, f, indent=2) print(f"[*] Report: {output_path}") print(f"\n[*] Dependencies: {snyk_json.get('dependencyCount', 'N/A')}") print(f"[*] Vulnerabilities: {len(vulns)} (fixable: {quality_gate['fixable_count']})") for sev, count in sorted(quality_gate["severity_counts"].items(), key=lambda x: SEVERITY_ORDER.get(x[0], 3)): print(f" {sev.upper()}: {count}") if quality_gate["passed"]: print(f"\n[PASS] Quality gate passed.") else: print(f"\n[FAIL] {quality_gate['blocking_count']} blocking vulnerabilities.") for d in quality_gate["blocking_details"][:10]: fix_info = f"fix: {d['fix']}" if d['upgradable'] else "no fix available" print(f" [{d['severity'].upper()}] {d['id']}: {d['package']} ({fix_info})") if args.monitor: print("\n[*] Running Snyk monitor for continuous tracking...") subprocess.run( ["snyk", "monitor", "--project-name", os.path.basename(project_path)], cwd=project_path ) if args.fail_on_findings and not quality_gate["passed"]: sys.exit(1) if __name__ == "__main__": main()