mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 21:54:56 +03:00
233 lines
8.6 KiB
Python
233 lines
8.6 KiB
Python
#!/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()
|