#!/usr/bin/env python3 """ Vulnerability Aging and SLA Tracking Engine Calculates vulnerability aging, SLA compliance, and generates escalation reports and KPI dashboards. Requirements: pip install pandas Usage: python process.py analyze --csv vulns.csv --output aging_report.csv python process.py kpis --csv vulns.csv python process.py escalations --csv vulns.csv --output escalations.csv """ import argparse import sys from datetime import datetime, timedelta import pandas as pd SLA_DAYS = { "Critical": 14, "High": 30, "Medium": 60, "Low": 90, } def calculate_aging(df, sla_config=None): """Add aging and SLA columns to vulnerability dataframe.""" sla = sla_config or SLA_DAYS today = pd.Timestamp.now() df["discovery_date"] = pd.to_datetime(df["discovery_date"]) df["remediation_date"] = pd.to_datetime(df["remediation_date"], errors="coerce") df["age_days"] = df.apply( lambda r: (r["remediation_date"] - r["discovery_date"]).days if pd.notna(r["remediation_date"]) else (today - r["discovery_date"]).days, axis=1 ) df["sla_days"] = df["severity"].map(sla).fillna(90).astype(int) df["sla_deadline"] = df["discovery_date"] + pd.to_timedelta(df["sla_days"], unit="D") df["is_open"] = df["remediation_date"].isna() df["is_overdue"] = df["is_open"] & (df["age_days"] > df["sla_days"]) df["days_overdue"] = df.apply( lambda r: max(0, r["age_days"] - r["sla_days"]) if r["is_overdue"] else 0, axis=1 ) df["sla_pct_elapsed"] = (df["age_days"] / df["sla_days"] * 100).round(1) df["within_sla"] = df.apply( lambda r: r["age_days"] <= r["sla_days"] if pd.notna(r["remediation_date"]) else None, axis=1 ) return df def generate_kpis(df): """Generate KPI summary.""" open_df = df[df["is_open"]] closed_df = df[~df["is_open"]] print(f"\n{'=' * 60}") print("VULNERABILITY AGING KPI REPORT") print(f"{'=' * 60}") print(f"Report Date: {datetime.now().strftime('%Y-%m-%d')}") print(f"Total Vulnerabilities: {len(df)}") print(f"Open: {len(open_df)}") print(f"Closed: {len(closed_df)}") print(f"Overdue: {open_df['is_overdue'].sum()}") if len(closed_df) > 0: mttr = closed_df["age_days"].mean() sla_rate = closed_df["within_sla"].mean() * 100 print(f"\nMTTR (all): {mttr:.1f} days") print(f"SLA Compliance Rate: {sla_rate:.1f}%") for sev in ["Critical", "High", "Medium", "Low"]: sev_df = closed_df[closed_df["severity"] == sev] if len(sev_df) > 0: print(f" {sev} MTTR: {sev_df['age_days'].mean():.1f}d " f"| SLA: {sev_df['within_sla'].mean() * 100:.1f}%") print(f"\nOpen Vulnerabilities by Age:") bins = [0, 7, 14, 30, 60, 90, float("inf")] labels = ["0-7d", "8-14d", "15-30d", "31-60d", "61-90d", "90+d"] if len(open_df) > 0: open_df = open_df.copy() open_df["age_bucket"] = pd.cut(open_df["age_days"], bins=bins, labels=labels) print(open_df["age_bucket"].value_counts().sort_index().to_string()) print(f"\nOverdue by Severity:") overdue = open_df[open_df["is_overdue"]] if len(overdue) > 0: print(overdue.groupby("severity")["days_overdue"].agg(["count", "mean", "max"]).to_string()) def generate_escalations(df): """Generate escalation list.""" open_df = df[df["is_open"]].copy() escalations = [] for _, row in open_df.iterrows(): pct = row["sla_pct_elapsed"] if pct >= 120: level = "VP/CTO Escalation" elif pct >= 100: level = "CISO Notification" elif pct >= 75: level = "Manager Escalation" elif pct >= 50: level = "Owner Reminder" else: continue escalations.append({ "cve_id": row.get("cve_id", ""), "severity": row["severity"], "asset": row.get("asset", ""), "owner": row.get("owner", ""), "age_days": row["age_days"], "sla_days": row["sla_days"], "days_overdue": row["days_overdue"], "sla_pct": pct, "escalation_level": level, }) return pd.DataFrame(escalations).sort_values("sla_pct", ascending=False) def main(): parser = argparse.ArgumentParser(description="Vulnerability Aging and SLA Tracker") subparsers = parser.add_subparsers(dest="command") analyze_p = subparsers.add_parser("analyze", help="Calculate aging metrics") analyze_p.add_argument("--csv", required=True) analyze_p.add_argument("--output", default="aging_report.csv") subparsers.add_parser("kpis", help="Generate KPI summary").add_argument("--csv", required=True) esc_p = subparsers.add_parser("escalations", help="Generate escalation list") esc_p.add_argument("--csv", required=True) esc_p.add_argument("--output", default="escalations.csv") args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) df = pd.read_csv(args.csv) df = calculate_aging(df) if args.command == "analyze": df.to_csv(args.output, index=False) print(f"[+] Aging report saved to {args.output}") generate_kpis(df) elif args.command == "kpis": generate_kpis(df) elif args.command == "escalations": esc_df = generate_escalations(df) esc_df.to_csv(args.output, index=False) print(f"[+] {len(esc_df)} escalations saved to {args.output}") if len(esc_df) > 0: print(esc_df["escalation_level"].value_counts().to_string()) if __name__ == "__main__": main()