#!/usr/bin/env python3 """Vulnerability remediation SLA tracking agent. Tracks vulnerability remediation against defined SLA targets based on severity. Ingests vulnerability data from scanners (JSON/CSV format), calculates SLA compliance, identifies overdue items, and generates remediation priority reports. """ import argparse import csv import json import os import sys from datetime import datetime, timezone, timedelta DEFAULT_SLA_DAYS = { "CRITICAL": 7, "HIGH": 30, "MEDIUM": 90, "LOW": 180, } def load_vulnerabilities(source_path): """Load vulnerabilities from a JSON or CSV file.""" ext = os.path.splitext(source_path)[1].lower() if ext == ".json": with open(source_path, "r") as f: data = json.load(f) if isinstance(data, list): return data return data.get("vulnerabilities", data.get("findings", data.get("results", []))) elif ext == ".csv": vulns = [] with open(source_path, "r", newline="") as f: reader = csv.DictReader(f) for row in reader: vulns.append(row) return vulns else: print(f"[!] Unsupported file format: {ext}", file=sys.stderr) return [] def normalize_vulnerability(vuln): """Normalize vulnerability fields from various scanner formats.""" return { "id": (vuln.get("id") or vuln.get("vulnerability_id") or vuln.get("cve_id") or vuln.get("CVE") or vuln.get("plugin_id") or "unknown"), "severity": (vuln.get("severity") or vuln.get("risk") or vuln.get("Severity") or "MEDIUM").upper(), "title": (vuln.get("title") or vuln.get("name") or vuln.get("vulnerability_name") or vuln.get("Title") or "Unknown"), "asset": (vuln.get("asset") or vuln.get("host") or vuln.get("ip") or vuln.get("hostname") or "unknown"), "discovered_date": (vuln.get("discovered_date") or vuln.get("first_found") or vuln.get("discovered") or vuln.get("date_found") or datetime.now(timezone.utc).isoformat()), "status": (vuln.get("status") or vuln.get("state") or "open").lower(), "remediation": (vuln.get("remediation") or vuln.get("fix") or vuln.get("solution") or ""), } def calculate_sla_status(vulns, sla_days=None): """Calculate SLA compliance for each vulnerability.""" if sla_days is None: sla_days = DEFAULT_SLA_DAYS now = datetime.now(timezone.utc) results = [] for vuln in vulns: norm = normalize_vulnerability(vuln) if norm["status"] not in ("open", "new", "active", "unresolved"): norm["sla_status"] = "RESOLVED" norm["sla_days_remaining"] = None results.append(norm) continue severity = norm["severity"] target_days = sla_days.get(severity, sla_days.get("MEDIUM", 90)) try: disc_str = norm["discovered_date"] if "T" in disc_str: discovered = datetime.fromisoformat(disc_str.replace("Z", "+00:00")) else: discovered = datetime.strptime(disc_str[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc) except (ValueError, TypeError): discovered = now norm["parse_warning"] = "Could not parse discovered_date" age_days = (now - discovered).days sla_deadline = discovered + timedelta(days=target_days) days_remaining = (sla_deadline - now).days norm["age_days"] = age_days norm["sla_target_days"] = target_days norm["sla_deadline"] = sla_deadline.isoformat() norm["sla_days_remaining"] = days_remaining if days_remaining < 0: norm["sla_status"] = "BREACHED" norm["sla_overdue_days"] = abs(days_remaining) elif days_remaining <= 7: norm["sla_status"] = "AT_RISK" else: norm["sla_status"] = "ON_TRACK" results.append(norm) return results def generate_metrics(results): """Generate SLA compliance metrics.""" open_vulns = [r for r in results if r.get("sla_status") != "RESOLVED"] breached = [r for r in open_vulns if r.get("sla_status") == "BREACHED"] at_risk = [r for r in open_vulns if r.get("sla_status") == "AT_RISK"] on_track = [r for r in open_vulns if r.get("sla_status") == "ON_TRACK"] compliance_rate = ((len(on_track) + len(at_risk)) / len(open_vulns) * 100) if open_vulns else 100.0 by_severity = {} for r in open_vulns: sev = r.get("severity", "MEDIUM") by_severity.setdefault(sev, {"total": 0, "breached": 0, "at_risk": 0}) by_severity[sev]["total"] += 1 if r.get("sla_status") == "BREACHED": by_severity[sev]["breached"] += 1 elif r.get("sla_status") == "AT_RISK": by_severity[sev]["at_risk"] += 1 oldest_breach = None if breached: oldest = max(breached, key=lambda r: r.get("sla_overdue_days", 0)) oldest_breach = { "id": oldest["id"], "severity": oldest["severity"], "overdue_days": oldest.get("sla_overdue_days", 0), "asset": oldest["asset"], } return { "total_open": len(open_vulns), "breached": len(breached), "at_risk": len(at_risk), "on_track": len(on_track), "resolved": len(results) - len(open_vulns), "compliance_rate": round(compliance_rate, 1), "by_severity": by_severity, "oldest_breach": oldest_breach, } def format_summary(metrics, results): """Print SLA tracking summary.""" print(f"\n{'='*60}") print(f" Vulnerability Remediation SLA Report") print(f"{'='*60}") print(f" Open Vulnerabilities : {metrics['total_open']}") print(f" SLA Breached : {metrics['breached']}") print(f" At Risk (<7 days) : {metrics['at_risk']}") print(f" On Track : {metrics['on_track']}") print(f" Resolved : {metrics['resolved']}") print(f" Compliance Rate : {metrics['compliance_rate']}%") print(f"\n By Severity:") for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: data = metrics["by_severity"].get(sev, {}) if data.get("total", 0) > 0: print(f" {sev:10s}: {data['total']} open, {data['breached']} breached, {data['at_risk']} at-risk") breached = [r for r in results if r.get("sla_status") == "BREACHED"] if breached: print(f"\n SLA Breached ({len(breached)}):") for r in sorted(breached, key=lambda x: -x.get("sla_overdue_days", 0))[:15]: print(f" [{r['severity']:8s}] {r['id']:20s} | {r['asset']:20s} | " f"{r.get('sla_overdue_days', 0)}d overdue | {r['title'][:30]}") if metrics.get("oldest_breach"): ob = metrics["oldest_breach"] print(f"\n Worst Breach: {ob['id']} ({ob['severity']}) on {ob['asset']} - " f"{ob['overdue_days']} days overdue") def main(): parser = argparse.ArgumentParser(description="Vulnerability remediation SLA tracking agent") parser.add_argument("--source", required=True, help="Vulnerability data file (JSON or CSV)") parser.add_argument("--sla-critical", type=int, default=7, help="SLA days for CRITICAL (default: 7)") parser.add_argument("--sla-high", type=int, default=30, help="SLA days for HIGH (default: 30)") parser.add_argument("--sla-medium", type=int, default=90, help="SLA days for MEDIUM (default: 90)") parser.add_argument("--sla-low", type=int, default=180, help="SLA days for LOW (default: 180)") parser.add_argument("--output", "-o", help="Output JSON report") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() sla_days = { "CRITICAL": args.sla_critical, "HIGH": args.sla_high, "MEDIUM": args.sla_medium, "LOW": args.sla_low, } vulns = load_vulnerabilities(args.source) print(f"[*] Loaded {len(vulns)} vulnerabilities from {args.source}") results = calculate_sla_status(vulns, sla_days) metrics = generate_metrics(results) format_summary(metrics, results) report = { "timestamp": datetime.now(timezone.utc).isoformat(), "tool": "Vulnerability SLA Tracker", "source": args.source, "sla_targets": sla_days, "metrics": metrics, "vulnerabilities": results, } if args.output: with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"\n[+] Report saved to {args.output}") elif args.verbose: print(json.dumps(report, indent=2)) if __name__ == "__main__": main()