Files

265 lines
9.5 KiB
Python

#!/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()