#!/usr/bin/env python3 """Agent for running OWASP ZAP DAST scans and parsing results for CI/CD integration.""" import subprocess import json import argparse import sys import os def run_baseline_scan(target_url, output_dir): """Run ZAP baseline scan (passive only, fast).""" print(f"[*] Running ZAP baseline scan on {target_url}...") report_path = os.path.join(output_dir, "zap_baseline.json") cmd = ["docker", "run", "--rm", "-v", f"{os.path.abspath(output_dir)}:/zap/wrk", "zaproxy/zap-stable", "zap-baseline.py", "-t", target_url, "-J", "zap_baseline.json", "-I"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) print(f" Return code: {result.returncode} (0=pass, 1=warn, 2=fail)") return report_path, result.returncode def run_full_scan(target_url, output_dir, mins=5): """Run ZAP full scan (active + passive, slower).""" print(f"[*] Running ZAP full scan on {target_url} ({mins} min)...") report_path = os.path.join(output_dir, "zap_full.json") cmd = ["docker", "run", "--rm", "-v", f"{os.path.abspath(output_dir)}:/zap/wrk", "zaproxy/zap-stable", "zap-full-scan.py", "-t", target_url, "-J", "zap_full.json", "-m", str(mins), "-I"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=mins * 120) print(f" Return code: {result.returncode}") return report_path, result.returncode def run_api_scan(target_spec, output_dir, spec_format="openapi"): """Run ZAP API scan against an OpenAPI/Swagger spec.""" print(f"[*] Running ZAP API scan with {spec_format} spec...") report_path = os.path.join(output_dir, "zap_api.json") cmd = ["docker", "run", "--rm", "-v", f"{os.path.abspath(output_dir)}:/zap/wrk", "zaproxy/zap-stable", "zap-api-scan.py", "-t", target_spec, "-f", spec_format, "-J", "zap_api.json", "-I"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) print(f" Return code: {result.returncode}") return report_path, result.returncode def parse_zap_report(report_path): """Parse ZAP JSON report and extract findings by risk level.""" if not os.path.exists(report_path): print(f" [-] Report not found: {report_path}") return [] with open(report_path) as f: data = json.load(f) alerts = [] for site in data.get("site", []): for alert in site.get("alerts", []): alerts.append({ "name": alert.get("name"), "risk": alert.get("riskdesc", "").split()[0], "confidence": alert.get("confidence"), "count": alert.get("count", 0), "cweid": alert.get("cweid"), "wascid": alert.get("wascid"), "solution": alert.get("solution", "")[:200], }) risk_counts = {} for a in alerts: risk_counts[a["risk"]] = risk_counts.get(a["risk"], 0) + 1 print(f"\n[*] Findings: {len(alerts)} unique alerts") for risk, count in sorted(risk_counts.items()): print(f" {risk}: {count}") return alerts def apply_quality_gate(alerts, fail_on="High"): """Apply quality gate: fail if findings at or above threshold.""" severity_order = {"Informational": 0, "Low": 1, "Medium": 2, "High": 3} threshold = severity_order.get(fail_on, 3) blocking = [a for a in alerts if severity_order.get(a.get("risk", ""), 0) >= threshold] if blocking: print(f"\n[!] QUALITY GATE FAILED: {len(blocking)} findings at {fail_on}+ severity") for b in blocking[:5]: print(f" - {b['risk']}: {b['name']}") return False print(f"\n[+] Quality gate passed (threshold: {fail_on})") return True def generate_sarif(alerts, output_path): """Convert ZAP findings to SARIF format for GitHub Advanced Security.""" rules, results = [], [] for i, a in enumerate(alerts): rule_id = f"ZAP-{a.get('cweid', i)}" rules.append({"id": rule_id, "shortDescription": {"text": a["name"]}, "defaultConfiguration": {"level": "warning"}}) results.append({"ruleId": rule_id, "message": {"text": a["name"]}, "level": "warning" if a["risk"] != "High" else "error"}) sarif = {"$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", "runs": [{"tool": {"driver": {"name": "OWASP ZAP", "rules": rules}}, "results": results}]} with open(output_path, "w") as f: json.dump(sarif, f, indent=2) print(f"[*] SARIF report: {output_path}") def main(): parser = argparse.ArgumentParser(description="OWASP ZAP DAST Pipeline Agent") parser.add_argument("action", choices=["baseline", "full", "api", "parse", "gate"]) parser.add_argument("--target", help="Target URL or API spec URL") parser.add_argument("--report", help="Path to existing ZAP JSON report") parser.add_argument("--fail-on", default="High", choices=["Low", "Medium", "High"]) parser.add_argument("--scan-mins", type=int, default=5) parser.add_argument("-o", "--output", default=".") args = parser.parse_args() os.makedirs(args.output, exist_ok=True) if args.action == "baseline": rpt, _ = run_baseline_scan(args.target, args.output) alerts = parse_zap_report(rpt) apply_quality_gate(alerts, args.fail_on) elif args.action == "full": rpt, _ = run_full_scan(args.target, args.output, args.scan_mins) alerts = parse_zap_report(rpt) apply_quality_gate(alerts, args.fail_on) elif args.action == "api": rpt, _ = run_api_scan(args.target, args.output) alerts = parse_zap_report(rpt) apply_quality_gate(alerts, args.fail_on) elif args.action == "parse": alerts = parse_zap_report(args.report) generate_sarif(alerts, os.path.join(args.output, "zap.sarif")) elif args.action == "gate": alerts = parse_zap_report(args.report) passed = apply_quality_gate(alerts, args.fail_on) sys.exit(0 if passed else 1) if __name__ == "__main__": main()