#!/usr/bin/env python3 """Vulnerability SLA breach alerting agent. Monitors vulnerability remediation timelines and generates alerts when SLA breaches occur or are imminent. Supports webhook notifications (Slack, Teams, PagerDuty), email alerts, and escalation workflows. """ import argparse import json import os import sys from datetime import datetime, timezone, timedelta try: import requests except ImportError: requests = None DEFAULT_SLA_DAYS = {"CRITICAL": 7, "HIGH": 30, "MEDIUM": 90, "LOW": 180} def load_vulnerabilities(source_path): """Load vulnerability data from JSON file.""" with open(source_path, "r") as f: data = json.load(f) if isinstance(data, list): return data return data.get("vulnerabilities", data.get("findings", [])) def check_sla_breaches(vulns, sla_days=None, warn_days_before=7): """Check for SLA breaches and upcoming deadlines.""" if sla_days is None: sla_days = DEFAULT_SLA_DAYS now = datetime.now(timezone.utc) breaches = [] warnings = [] for vuln in vulns: status = (vuln.get("status") or vuln.get("state") or "open").lower() if status not in ("open", "new", "active", "unresolved"): continue severity = (vuln.get("severity") or "MEDIUM").upper() target_days = sla_days.get(severity, 90) disc_str = (vuln.get("discovered_date") or vuln.get("first_found") or vuln.get("discovered") or "") try: if "T" in disc_str: discovered = datetime.fromisoformat(disc_str.replace("Z", "+00:00")) elif disc_str: discovered = datetime.strptime(disc_str[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc) else: continue except (ValueError, TypeError): continue deadline = discovered + timedelta(days=target_days) days_remaining = (deadline - now).days vuln_id = vuln.get("id") or vuln.get("cve_id") or vuln.get("vulnerability_id") or "unknown" asset = vuln.get("asset") or vuln.get("host") or vuln.get("ip") or "unknown" title = vuln.get("title") or vuln.get("name") or "Unknown" alert_entry = { "id": vuln_id, "severity": severity, "asset": asset, "title": title[:80], "discovered": disc_str[:10], "deadline": deadline.isoformat()[:10], "days_remaining": days_remaining, "sla_target_days": target_days, } if days_remaining < 0: alert_entry["alert_type"] = "BREACH" alert_entry["overdue_days"] = abs(days_remaining) breaches.append(alert_entry) elif days_remaining <= warn_days_before: alert_entry["alert_type"] = "WARNING" warnings.append(alert_entry) breaches.sort(key=lambda x: -x.get("overdue_days", 0)) warnings.sort(key=lambda x: x.get("days_remaining", 999)) return breaches, warnings def send_slack_alert(webhook_url, breaches, warnings): """Send SLA breach alert to Slack via webhook.""" if not requests: print("[!] requests library required for Slack alerts", file=sys.stderr) return False blocks = [ {"type": "header", "text": {"type": "plain_text", "text": f"Vulnerability SLA Alert - {len(breaches)} Breaches, {len(warnings)} Warnings"}}, ] if breaches: breach_text = "*SLA BREACHES (Immediate Action Required):*\n" for b in breaches[:10]: breach_text += (f"- [{b['severity']}] `{b['id']}` on {b['asset']} - " f"*{b['overdue_days']}d overdue*\n") blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": breach_text}}) if warnings: warn_text = "*Approaching SLA Deadline:*\n" for w in warnings[:10]: warn_text += (f"- [{w['severity']}] `{w['id']}` on {w['asset']} - " f"{w['days_remaining']}d remaining\n") blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": warn_text}}) payload = {"blocks": blocks} resp = requests.post(webhook_url, json=payload, timeout=15) if resp.status_code == 200: print(f"[+] Slack alert sent successfully") return True else: print(f"[!] Slack alert failed: {resp.status_code}", file=sys.stderr) return False def send_teams_alert(webhook_url, breaches, warnings): """Send SLA breach alert to Microsoft Teams via webhook.""" if not requests: return False facts = [] for b in breaches[:5]: facts.append({"name": f"[BREACH] {b['id']}", "value": f"{b['severity']} - {b['overdue_days']}d overdue on {b['asset']}"}) for w in warnings[:5]: facts.append({"name": f"[WARNING] {w['id']}", "value": f"{w['severity']} - {w['days_remaining']}d left on {w['asset']}"}) payload = { "@type": "MessageCard", "themeColor": "FF0000" if breaches else "FFA500", "summary": f"SLA Alert: {len(breaches)} breaches, {len(warnings)} warnings", "sections": [{ "activityTitle": "Vulnerability SLA Alert", "facts": facts, }], } resp = requests.post(webhook_url, json=payload, timeout=15) return resp.status_code == 200 def send_pagerduty_alert(routing_key, breaches): """Send PagerDuty incident for critical SLA breaches.""" if not requests or not breaches: return False critical_breaches = [b for b in breaches if b["severity"] == "CRITICAL"] if not critical_breaches: return False payload = { "routing_key": routing_key, "event_action": "trigger", "payload": { "summary": f"{len(critical_breaches)} CRITICAL vulnerability SLA breaches", "severity": "critical", "source": "vulnerability-sla-agent", "custom_details": { "breaches": critical_breaches[:5], "total_critical_breaches": len(critical_breaches), }, }, } resp = requests.post( "https://events.pagerduty.com/v2/enqueue", json=payload, timeout=15, ) if resp.status_code == 202: print(f"[+] PagerDuty incident created") return True return False def format_summary(breaches, warnings): """Print alert summary.""" print(f"\n{'='*60}") print(f" Vulnerability SLA Breach Alert Report") print(f"{'='*60}") print(f" SLA Breaches : {len(breaches)}") print(f" SLA Warnings : {len(warnings)}") if breaches: critical = sum(1 for b in breaches if b["severity"] == "CRITICAL") high = sum(1 for b in breaches if b["severity"] == "HIGH") print(f" Critical breaches: {critical}") print(f" High breaches : {high}") print(f"\n Breached Vulnerabilities:") for b in breaches[:15]: print(f" [{b['severity']:8s}] {b['id']:20s} | {b['asset']:20s} | " f"{b['overdue_days']}d overdue (deadline: {b['deadline']})") if warnings: print(f"\n Approaching Deadline:") for w in warnings[:10]: print(f" [{w['severity']:8s}] {w['id']:20s} | {w['asset']:20s} | " f"{w['days_remaining']}d remaining") def main(): parser = argparse.ArgumentParser(description="Vulnerability SLA breach alerting agent") parser.add_argument("--source", required=True, help="Vulnerability data JSON file") parser.add_argument("--sla-critical", type=int, default=7) parser.add_argument("--sla-high", type=int, default=30) parser.add_argument("--sla-medium", type=int, default=90) parser.add_argument("--sla-low", type=int, default=180) parser.add_argument("--warn-days", type=int, default=7, help="Warn N days before deadline") parser.add_argument("--slack-webhook", help="Slack webhook URL for alerts") parser.add_argument("--teams-webhook", help="Teams webhook URL for alerts") parser.add_argument("--pagerduty-key", help="PagerDuty routing key for critical breaches") 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") breaches, warnings = check_sla_breaches(vulns, sla_days, args.warn_days) format_summary(breaches, warnings) alerts_sent = [] if args.slack_webhook and (breaches or warnings): if send_slack_alert(args.slack_webhook, breaches, warnings): alerts_sent.append("slack") if args.teams_webhook and (breaches or warnings): if send_teams_alert(args.teams_webhook, breaches, warnings): alerts_sent.append("teams") if args.pagerduty_key and breaches: if send_pagerduty_alert(args.pagerduty_key, breaches): alerts_sent.append("pagerduty") report = { "timestamp": datetime.now(timezone.utc).isoformat(), "tool": "SLA Breach Alerting", "sla_targets": sla_days, "breaches": breaches, "warnings": warnings, "alerts_sent": alerts_sent, } 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()