vulnerability management

Implementing Vulnerability SLA Breach Alerting

Build automated alerting for vulnerability remediation SLA breaches with severity-based timelines, escalation workflows, and compliance reporting dashboards.

alertingcomplianceescalationremediation-trackingsla-breachvulnerability-managementvulnerability-sla
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Vulnerability remediation SLAs define maximum timeframes for addressing security findings based on severity. This skill covers building an automated alerting system that tracks remediation timelines, detects SLA breaches, sends escalation notifications, and generates compliance reports. Industry-standard SLA targets are: Critical (24-48 hours), High (15-30 days), Medium (60 days), Low (90 days).

When to Use

  • When deploying or configuring implementing vulnerability sla breach alerting capabilities in your environment
  • When establishing security controls aligned to compliance requirements
  • When building or improving security architecture for this domain
  • When conducting security assessments that require this implementation

Prerequisites

  • Python 3.9+ with requests, pandas, jinja2, smtplib libraries
  • Vulnerability management platform with API access (DefectDojo, Qualys, Tenable)
  • SMTP server or webhook endpoint (Slack, Microsoft Teams, PagerDuty)
  • Database for SLA tracking (PostgreSQL or SQLite)

SLA Policy Definition

Standard SLA Tiers

Severity Remediation SLA Grace Period Escalation Level
Critical (CVSS 9.0-10.0) 48 hours 12 hours VP Engineering + CISO
High (CVSS 7.0-8.9) 15 days 5 days Director of Engineering
Medium (CVSS 4.0-6.9) 60 days 14 days Team Lead
Low (CVSS 0.1-3.9) 90 days 30 days Asset Owner

SLA Configuration File

# sla_policy.yaml
sla_tiers:
  critical:
    cvss_min: 9.0
    cvss_max: 10.0
    remediation_days: 2
    grace_period_days: 0.5
    escalation_contacts:
      - ciso@company.com
      - vp-engineering@company.com
    pagerduty_severity: critical
  high:
    cvss_min: 7.0
    cvss_max: 8.9
    remediation_days: 15
    grace_period_days: 5
    escalation_contacts:
      - security-director@company.com
    pagerduty_severity: high
  medium:
    cvss_min: 4.0
    cvss_max: 6.9
    remediation_days: 60
    grace_period_days: 14
    escalation_contacts:
      - team-lead@company.com
    pagerduty_severity: warning
  low:
    cvss_min: 0.1
    cvss_max: 3.9
    remediation_days: 90
    grace_period_days: 30
    escalation_contacts:
      - asset-owner@company.com
    pagerduty_severity: info
 
notification_channels:
  slack:
    webhook_url: "${SLACK_WEBHOOK_URL}"
    channel: "#vulnerability-alerts"
  email:
    smtp_host: smtp.company.com
    smtp_port: 587
    from_address: vuln-alerts@company.com
  pagerduty:
    api_key: "${PAGERDUTY_API_KEY}"
    service_id: "${PAGERDUTY_SERVICE_ID}"
 
alert_schedules:
  approaching_breach:
    percentage_elapsed: 80
    frequency_hours: 24
  at_breach:
    notification: immediate
    escalation: true
  post_breach:
    frequency_hours: 12
    escalation_increase: true

Workflow

Step 1: Database Schema for SLA Tracking

CREATE TABLE vulnerability_sla (
    id SERIAL PRIMARY KEY,
    cve_id VARCHAR(20) NOT NULL,
    finding_id VARCHAR(100) NOT NULL,
    asset_hostname VARCHAR(255),
    severity VARCHAR(20) NOT NULL,
    cvss_score DECIMAL(3,1),
    discovered_at TIMESTAMP NOT NULL,
    sla_deadline TIMESTAMP NOT NULL,
    remediated_at TIMESTAMP,
    status VARCHAR(20) DEFAULT 'open',
    owner_email VARCHAR(255),
    escalation_level INTEGER DEFAULT 0,
    last_alert_sent TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
CREATE INDEX idx_sla_status ON vulnerability_sla(status);
CREATE INDEX idx_sla_deadline ON vulnerability_sla(sla_deadline);
CREATE INDEX idx_sla_severity ON vulnerability_sla(severity);

Step 2: SLA Breach Detection Logic

from datetime import datetime, timedelta, timezone
import yaml
 
def load_sla_policy(policy_path="sla_policy.yaml"):
    with open(policy_path, "r") as f:
        return yaml.safe_load(f)
 
def get_sla_tier(cvss_score, policy):
    for tier_name, tier in policy["sla_tiers"].items():
        if tier["cvss_min"] <= cvss_score <= tier["cvss_max"]:
            return tier_name, tier
    return "low", policy["sla_tiers"]["low"]
 
def calculate_sla_deadline(discovered_at, cvss_score, policy):
    tier_name, tier = get_sla_tier(cvss_score, policy)
    deadline = discovered_at + timedelta(days=tier["remediation_days"])
    return deadline, tier_name
 
def check_sla_status(discovered_at, sla_deadline, remediated_at=None):
    now = datetime.now(timezone.utc)
    if remediated_at:
        if remediated_at <= sla_deadline:
            return "remediated_within_sla"
        return "remediated_breach"
    if now > sla_deadline:
        overdue_days = (now - sla_deadline).days
        return f"breached_{overdue_days}d_overdue"
    remaining = sla_deadline - now
    total_sla = sla_deadline - discovered_at
    pct_elapsed = ((total_sla - remaining) / total_sla) * 100
    if pct_elapsed >= 80:
        return "approaching_breach"
    return "within_sla"

Step 3: Notification Dispatch

import requests
import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
 
def send_slack_alert(webhook_url, vuln_data, sla_status):
    color = {"breached": "#FF0000", "approaching_breach": "#FFA500", "within_sla": "#36A64F"}
    status_color = color.get("breached" if "breached" in sla_status else sla_status, "#808080")
    payload = {
        "attachments": [{
            "color": status_color,
            "title": f"Vulnerability SLA Alert: {vuln_data['cve_id']}",
            "fields": [
                {"title": "Severity", "value": vuln_data["severity"], "short": True},
                {"title": "CVSS", "value": str(vuln_data["cvss_score"]), "short": True},
                {"title": "Asset", "value": vuln_data["asset_hostname"], "short": True},
                {"title": "SLA Status", "value": sla_status, "short": True},
                {"title": "Deadline", "value": vuln_data["sla_deadline"].strftime("%Y-%m-%d %H:%M UTC"), "short": True},
                {"title": "Owner", "value": vuln_data.get("owner_email", "Unassigned"), "short": True},
            ],
        }]
    }
    requests.post(webhook_url, json=payload, timeout=10)
 
def send_pagerduty_alert(api_key, service_id, vuln_data, severity):
    payload = {
        "routing_key": api_key,
        "event_action": "trigger",
        "payload": {
            "summary": f"SLA Breach: {vuln_data['cve_id']} on {vuln_data['asset_hostname']}",
            "severity": severity,
            "source": vuln_data["asset_hostname"],
            "custom_details": {
                "cve_id": vuln_data["cve_id"],
                "cvss_score": vuln_data["cvss_score"],
                "sla_deadline": vuln_data["sla_deadline"].isoformat(),
            }
        }
    }
    requests.post(
        "https://events.pagerduty.com/v2/enqueue",
        json=payload, timeout=10
    )
 
def send_email_alert(smtp_config, to_addresses, vuln_data, sla_status):
    msg = MIMEMultipart("alternative")
    msg["Subject"] = f"[SLA {sla_status.upper()}] {vuln_data['cve_id']} - {vuln_data['severity']}"
    msg["From"] = smtp_config["from_address"]
    msg["To"] = ", ".join(to_addresses)
    body = f"""
    Vulnerability SLA Alert
 
    CVE: {vuln_data['cve_id']}
    Severity: {vuln_data['severity']} (CVSS {vuln_data['cvss_score']})
    Asset: {vuln_data['asset_hostname']}
    SLA Deadline: {vuln_data['sla_deadline'].strftime('%Y-%m-%d %H:%M UTC')}
    Status: {sla_status}
    Owner: {vuln_data.get('owner_email', 'Unassigned')}
 
    Please take immediate action to remediate this vulnerability.
    """
    msg.attach(MIMEText(body, "plain"))
    with smtplib.SMTP(smtp_config["smtp_host"], smtp_config["smtp_port"]) as server:
        server.starttls()
        server.send_message(msg)

Step 4: Scheduled SLA Check Runner

# Run SLA breach check every hour via cron
echo "0 * * * * cd /opt/vuln-sla && python3 scripts/process.py --check-sla" | crontab -
 
# Manual check
python3 scripts/process.py --check-sla --policy sla_policy.yaml
 
# Generate SLA compliance report
python3 scripts/process.py --report --period monthly --output sla_report.html

SLA Metrics Dashboard

Key Performance Indicators

def calculate_sla_metrics(db_connection, period_start, period_end):
    metrics = {
        "total_findings": 0,
        "remediated_within_sla": 0,
        "sla_breach_count": 0,
        "mean_time_to_remediate": {},
        "sla_compliance_rate": 0.0,
        "current_overdue": 0,
    }
    # Query findings in period grouped by severity
    query = """
        SELECT severity, COUNT(*) as total,
               SUM(CASE WHEN remediated_at <= sla_deadline THEN 1 ELSE 0 END) as within_sla,
               AVG(EXTRACT(EPOCH FROM (COALESCE(remediated_at, NOW()) - discovered_at))/86400) as avg_days
        FROM vulnerability_sla
        WHERE discovered_at BETWEEN %s AND %s
        GROUP BY severity
    """
    return metrics

References

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md5.6 KB

API Reference: Vulnerability SLA Breach Alerting

Libraries Used

Library Purpose
requests Slack webhook and Jira API integration
smtplib Send email alerts for SLA breaches
json Parse vulnerability and SLA data
datetime Calculate SLA deadlines and breach timing
email.mime.text Compose HTML email notifications

Installation

pip install requests

Alert Channels

Slack Webhook Alert

import requests
import os
 
SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK_URL"]
 
def send_slack_alert(breaches):
    blocks = [
        {
            "type": "header",
            "text": {"type": "plain_text", "text": "SLA Breach Alert"}
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*{len(breaches)} vulnerabilities have breached SLA*",
            }
        },
    ]
    for breach in breaches[:10]:
        blocks.append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": (
                    f"*{breach['cve']}* — {breach['severity'].upper()}\n"
                    f"Host: `{breach['host']}` | Overdue: {breach['hours_overdue']}h\n"
                    f"Owner: {breach.get('owner', 'Unassigned')}"
                ),
            }
        })
 
    resp = requests.post(
        SLACK_WEBHOOK,
        json={"blocks": blocks},
        timeout=10,
    )
    return resp.status_code == 200

Email Alert (SMTP)

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
 
def send_email_alert(breaches, recipients):
    smtp_host = os.environ["SMTP_HOST"]
    smtp_port = int(os.environ.get("SMTP_PORT", "587"))
    smtp_user = os.environ["SMTP_USER"]
    smtp_pass = os.environ["SMTP_PASS"]
 
    msg = MIMEMultipart("alternative")
    msg["Subject"] = f"SLA Breach: {len(breaches)} vulnerabilities overdue"
    msg["From"] = smtp_user
    msg["To"] = ", ".join(recipients)
 
    html = "<h2>SLA Breach Report</h2><table border='1'>"
    html += "<tr><th>CVE</th><th>Severity</th><th>Host</th><th>Hours Overdue</th></tr>"
    for b in breaches:
        html += f"<tr><td>{b['cve']}</td><td>{b['severity']}</td>"
        html += f"<td>{b['host']}</td><td>{b['hours_overdue']}</td></tr>"
    html += "</table>"
 
    msg.attach(MIMEText(html, "html"))
 
    with smtplib.SMTP(smtp_host, smtp_port) as server:
        server.starttls()
        server.login(smtp_user, smtp_pass)
        server.sendmail(smtp_user, recipients, msg.as_string())

Jira Ticket Creation

JIRA_URL = os.environ["JIRA_URL"]
JIRA_AUTH = (os.environ["JIRA_USER"], os.environ["JIRA_TOKEN"])
 
def create_jira_ticket(breach):
    ticket = {
        "fields": {
            "project": {"key": os.environ.get("JIRA_PROJECT", "VULN")},
            "summary": f"SLA Breach: {breach['cve']} on {breach['host']}",
            "description": (
                f"Vulnerability {breach['cve']} ({breach['severity']}) "
                f"has breached its remediation SLA.\n\n"
                f"Host: {breach['host']}\n"
                f"Hours overdue: {breach['hours_overdue']}\n"
                f"Discovery date: {breach['discovery_date']}\n"
                f"SLA deadline: {breach['deadline']}\n\n"
                f"Required action: Remediate immediately."
            ),
            "issuetype": {"name": "Bug"},
            "priority": {"name": "Highest" if breach["severity"] == "critical" else "High"},
            "labels": ["sla-breach", "security", breach["severity"]],
        }
    }
    resp = requests.post(
        f"{JIRA_URL}/rest/api/2/issue",
        auth=JIRA_AUTH,
        json=ticket,
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["key"]

SLA Breach Detection

from datetime import datetime, timedelta
 
SLA_TIERS = {
    "critical": timedelta(hours=24),
    "high": timedelta(hours=72),
    "medium": timedelta(days=30),
    "low": timedelta(days=90),
}
 
def detect_breaches(vulnerabilities):
    breaches = []
    now = datetime.now()
    for vuln in vulnerabilities:
        if vuln.get("remediated"):
            continue
        discovery = datetime.fromisoformat(vuln["discovery_date"])
        sla = SLA_TIERS.get(vuln["severity"].lower(), timedelta(days=90))
        deadline = discovery + sla
        if now > deadline:
            breaches.append({
                **vuln,
                "deadline": deadline.isoformat(),
                "hours_overdue": round((now - deadline).total_seconds() / 3600, 1),
            })
    return sorted(breaches, key=lambda b: b["hours_overdue"], reverse=True)

Orchestration

def run_sla_breach_alerting(vulnerabilities):
    breaches = detect_breaches(vulnerabilities)
    if not breaches:
        return {"breaches": 0, "alerts_sent": False}
 
    # Send alerts through all channels
    send_slack_alert(breaches)
    send_email_alert(breaches, os.environ.get("ALERT_RECIPIENTS", "").split(","))
 
    # Create Jira tickets for critical/high breaches only
    for breach in breaches:
        if breach["severity"] in ("critical", "high"):
            create_jira_ticket(breach)
 
    return {"breaches": len(breaches), "alerts_sent": True}

Output Format

{
  "run_time": "2025-01-15T10:00:00Z",
  "breaches_detected": 5,
  "alerts": {
    "slack": true,
    "email": true,
    "jira_tickets_created": 3
  },
  "breaches": [
    {
      "cve": "CVE-2024-21887",
      "severity": "critical",
      "host": "web-prod-01",
      "hours_overdue": 48.5,
      "deadline": "2025-01-13T10:00:00",
      "owner": "platform-team"
    }
  ]
}
standards.md2.6 KB

Standards and References - Vulnerability SLA Breach Alerting

Primary Standards

NIST SP 800-40 Rev 4

CISA Binding Operational Directive 22-01

PCI DSS v4.0 Requirement 6.3

SOC 2 Type II - CC7.1

  • Title: Detection and Monitoring of Security Events
  • Relevance: Requires evidence of vulnerability management program with defined remediation timelines and tracking

ISO 27001:2022 - Control A.8.8

  • Title: Management of Technical Vulnerabilities
  • Relevance: Requires timely identification and remediation of technical vulnerabilities with defined response timelines

Industry SLA Benchmarks

SANS Vulnerability Management Maturity

  • Critical: 24-48 hours
  • High: 7-30 days
  • Medium: 30-90 days
  • Low: 90-180 days

CIS Controls v8 - Control 7

Integration APIs

PagerDuty Events API v2

Slack Incoming Webhooks

Microsoft Teams Incoming Webhook

Jira REST API

workflows.md2.5 KB

Workflows - Vulnerability SLA Breach Alerting

Workflow 1: SLA Assignment on New Findings

Trigger

New vulnerability findings imported from scanner.

Steps

  1. Parse incoming vulnerability data (CVE ID, CVSS score, affected asset)
  2. Look up asset criticality from CMDB to determine if SLA should be tightened
  3. Calculate SLA tier based on CVSS score and asset criticality
  4. Compute SLA deadline: discovered_at + remediation_days
  5. Insert SLA record into tracking database
  6. Assign finding owner based on asset ownership mapping
  7. Send initial notification to asset owner with SLA deadline

Workflow 2: Hourly SLA Breach Check

Trigger

Cron job running every hour.

Steps

  1. Query all open vulnerability SLA records
  2. For each record, calculate current SLA status:
    • within_sla: Less than 80% of SLA window elapsed
    • approaching_breach: 80-100% of SLA window elapsed
    • breached: Past SLA deadline
  3. For approaching_breach findings (first notification):
    • Send Slack/Teams warning to asset owner
    • Send email notification to asset owner and team lead
  4. For breached findings:
    • Send immediate Slack alert to security team channel
    • Trigger PagerDuty incident for critical/high severity
    • Send escalation email to management chain
    • Update escalation_level in database
  5. For post-breach findings (already breached, escalation increase):
    • Every 12 hours, increase escalation level
    • Level 1: Team lead notification
    • Level 2: Director notification
    • Level 3: VP/CISO notification

Workflow 3: Remediation Confirmation

Trigger

Vulnerability scanner re-scan confirms finding resolved.

Steps

  1. Match resolved finding to SLA record
  2. Record remediation timestamp
  3. Calculate if remediation was within SLA
  4. Update SLA record status to remediated_within_sla or remediated_breach
  5. Close any associated PagerDuty incidents
  6. Send confirmation notification to asset owner
  7. Update metrics dashboard

Workflow 4: Monthly SLA Compliance Report

Trigger

First business day of each month.

Steps

  1. Query all SLA records for the previous month
  2. Calculate metrics by severity tier:
    • Total findings per tier
    • SLA compliance rate per tier
    • Mean time to remediate per tier
    • Count of currently overdue findings
  3. Identify top 10 assets with most SLA breaches
  4. Identify teams with lowest compliance rates
  5. Generate HTML report with charts
  6. Email report to security leadership
  7. Update executive dashboard

Scripts 2

agent.py9.5 KB
Display-only source. This catalog never executes bundled scripts.
#!/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()
process.py11.4 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Vulnerability SLA Breach Alerting System.

Tracks vulnerability remediation timelines, detects SLA breaches,
and dispatches notifications through multiple channels.
"""

import argparse
import csv
import json
import os
import smtplib
import sqlite3
from datetime import datetime, timedelta, timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pathlib import Path

import requests
import yaml

DB_PATH = os.environ.get("SLA_DB_PATH", "vulnerability_sla.db")

SLA_TIERS = {
    "critical": {"cvss_min": 9.0, "cvss_max": 10.0, "days": 2},
    "high": {"cvss_min": 7.0, "cvss_max": 8.9, "days": 15},
    "medium": {"cvss_min": 4.0, "cvss_max": 6.9, "days": 60},
    "low": {"cvss_min": 0.1, "cvss_max": 3.9, "days": 90},
}


def init_db(db_path=DB_PATH):
    """Initialize SQLite database for SLA tracking."""
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS vulnerability_sla (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            cve_id TEXT NOT NULL,
            finding_id TEXT NOT NULL,
            asset_hostname TEXT,
            severity TEXT NOT NULL,
            cvss_score REAL,
            discovered_at TEXT NOT NULL,
            sla_deadline TEXT NOT NULL,
            remediated_at TEXT,
            status TEXT DEFAULT 'open',
            owner_email TEXT,
            escalation_level INTEGER DEFAULT 0,
            last_alert_sent TEXT,
            created_at TEXT DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    return conn


def get_severity_tier(cvss_score):
    """Map CVSS score to severity tier."""
    for tier_name, tier in SLA_TIERS.items():
        if tier["cvss_min"] <= cvss_score <= tier["cvss_max"]:
            return tier_name, tier["days"]
    return "low", 90


def import_findings(conn, csv_path):
    """Import vulnerability findings from CSV and assign SLA deadlines."""
    imported = 0
    with open(csv_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            cve_id = row.get("cve_id", "").strip()
            cvss = float(row.get("cvss_score", 0))
            if not cve_id or cvss == 0:
                continue
            discovered = row.get("discovered_at", datetime.now(timezone.utc).isoformat())
            discovered_dt = datetime.fromisoformat(discovered.replace("Z", "+00:00"))
            severity, sla_days = get_severity_tier(cvss)
            deadline = discovered_dt + timedelta(days=sla_days)

            conn.execute(
                """INSERT INTO vulnerability_sla
                   (cve_id, finding_id, asset_hostname, severity, cvss_score,
                    discovered_at, sla_deadline, owner_email, status)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open')""",
                (
                    cve_id,
                    row.get("finding_id", f"{cve_id}_{row.get('host', 'unknown')}"),
                    row.get("host", "unknown"),
                    severity,
                    cvss,
                    discovered_dt.isoformat(),
                    deadline.isoformat(),
                    row.get("owner_email", ""),
                ),
            )
            imported += 1
    conn.commit()
    print(f"[+] Imported {imported} findings with SLA deadlines")
    return imported


def check_sla_breaches(conn):
    """Check all open findings for SLA status and return categorized results."""
    now = datetime.now(timezone.utc)
    cursor = conn.execute(
        "SELECT * FROM vulnerability_sla WHERE status = 'open'"
    )
    columns = [d[0] for d in cursor.description]
    breached = []
    approaching = []
    within_sla = []

    for row in cursor.fetchall():
        record = dict(zip(columns, row))
        deadline = datetime.fromisoformat(record["sla_deadline"])
        discovered = datetime.fromisoformat(record["discovered_at"])
        if deadline.tzinfo is None:
            deadline = deadline.replace(tzinfo=timezone.utc)
        if discovered.tzinfo is None:
            discovered = discovered.replace(tzinfo=timezone.utc)

        if now > deadline:
            overdue_days = (now - deadline).days
            record["sla_status"] = f"breached_{overdue_days}d_overdue"
            record["overdue_days"] = overdue_days
            breached.append(record)
        else:
            total_window = (deadline - discovered).total_seconds()
            elapsed = (now - discovered).total_seconds()
            pct = (elapsed / total_window * 100) if total_window > 0 else 0
            if pct >= 80:
                record["sla_status"] = "approaching_breach"
                record["pct_elapsed"] = round(pct, 1)
                approaching.append(record)
            else:
                record["sla_status"] = "within_sla"
                record["pct_elapsed"] = round(pct, 1)
                within_sla.append(record)

    return {"breached": breached, "approaching": approaching, "within_sla": within_sla}


def send_slack_notification(webhook_url, findings, alert_type):
    """Send SLA alert to Slack channel."""
    if not webhook_url or not findings:
        return
    color_map = {"breached": "#FF0000", "approaching": "#FFA500", "within_sla": "#36A64F"}
    for finding in findings[:10]:
        payload = {
            "attachments": [
                {
                    "color": color_map.get(alert_type, "#808080"),
                    "title": f"SLA {alert_type.upper()}: {finding['cve_id']}",
                    "fields": [
                        {"title": "Severity", "value": finding["severity"], "short": True},
                        {"title": "CVSS", "value": str(finding["cvss_score"]), "short": True},
                        {"title": "Asset", "value": finding["asset_hostname"], "short": True},
                        {"title": "Deadline", "value": finding["sla_deadline"][:16], "short": True},
                        {"title": "Owner", "value": finding.get("owner_email", "Unassigned"), "short": True},
                        {"title": "Status", "value": finding["sla_status"], "short": True},
                    ],
                }
            ]
        }
        try:
            requests.post(webhook_url, json=payload, timeout=10)
        except requests.RequestException as e:
            print(f"[-] Slack notification failed: {e}")


def send_email_notification(smtp_config, findings, alert_type):
    """Send SLA alert via email."""
    if not smtp_config or not findings:
        return
    recipients = set()
    for f in findings:
        if f.get("owner_email"):
            recipients.add(f["owner_email"])
    if not recipients:
        return

    body_lines = [f"Vulnerability SLA {alert_type.upper()} Report", "=" * 50, ""]
    for f in findings:
        body_lines.extend([
            f"CVE: {f['cve_id']}",
            f"Severity: {f['severity']} (CVSS {f['cvss_score']})",
            f"Asset: {f['asset_hostname']}",
            f"Deadline: {f['sla_deadline'][:16]}",
            f"Status: {f['sla_status']}",
            "-" * 40,
        ])

    msg = MIMEMultipart()
    msg["Subject"] = f"[VULN SLA {alert_type.upper()}] {len(findings)} findings require attention"
    msg["From"] = smtp_config.get("from_address", "vuln-alerts@company.com")
    msg["To"] = ", ".join(recipients)
    msg.attach(MIMEText("\n".join(body_lines), "plain"))

    try:
        with smtplib.SMTP(smtp_config["host"], smtp_config.get("port", 587)) as server:
            server.starttls()
            if smtp_config.get("username"):
                server.login(smtp_config["username"], smtp_config["password"])
            server.send_message(msg)
        print(f"[+] Email sent to {len(recipients)} recipients")
    except Exception as e:
        print(f"[-] Email notification failed: {e}")


def generate_compliance_report(conn, output_path, period_days=30):
    """Generate SLA compliance report."""
    cutoff = (datetime.now(timezone.utc) - timedelta(days=period_days)).isoformat()
    cursor = conn.execute(
        """SELECT severity,
                  COUNT(*) as total,
                  SUM(CASE WHEN remediated_at IS NOT NULL
                       AND remediated_at <= sla_deadline THEN 1 ELSE 0 END) as within_sla,
                  SUM(CASE WHEN remediated_at IS NOT NULL
                       AND remediated_at > sla_deadline THEN 1 ELSE 0 END) as breached_remediated,
                  SUM(CASE WHEN status = 'open'
                       AND datetime('now') > sla_deadline THEN 1 ELSE 0 END) as currently_overdue
           FROM vulnerability_sla
           WHERE discovered_at >= ?
           GROUP BY severity
           ORDER BY CASE severity
                WHEN 'critical' THEN 1
                WHEN 'high' THEN 2
                WHEN 'medium' THEN 3
                WHEN 'low' THEN 4 END""",
        (cutoff,),
    )

    report = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "period_days": period_days,
        "tiers": [],
    }
    total_findings = 0
    total_compliant = 0
    for row in cursor.fetchall():
        severity, total, within_sla, breached_remediated, currently_overdue = row
        compliance_rate = (within_sla / total * 100) if total > 0 else 0
        report["tiers"].append({
            "severity": severity,
            "total": total,
            "within_sla": within_sla,
            "breached_remediated": breached_remediated,
            "currently_overdue": currently_overdue,
            "compliance_rate": round(compliance_rate, 1),
        })
        total_findings += total
        total_compliant += within_sla

    report["overall_compliance"] = round(
        (total_compliant / total_findings * 100) if total_findings > 0 else 0, 1
    )
    report["total_findings"] = total_findings

    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2)
    print(f"[+] Compliance report written to {output_path}")
    print(f"    Overall SLA Compliance: {report['overall_compliance']}%")
    return report


def main():
    parser = argparse.ArgumentParser(description="Vulnerability SLA Breach Alerting System")
    parser.add_argument("--import-findings", help="Import findings from CSV")
    parser.add_argument("--check-sla", action="store_true", help="Check for SLA breaches")
    parser.add_argument("--report", action="store_true", help="Generate compliance report")
    parser.add_argument("--output", default="sla_compliance_report.json", help="Report output path")
    parser.add_argument("--period", type=int, default=30, help="Report period in days")
    parser.add_argument("--slack-webhook", help="Slack webhook URL for notifications")
    parser.add_argument("--db", default=DB_PATH, help="Database path")
    args = parser.parse_args()

    conn = init_db(args.db)

    if args.import_findings:
        import_findings(conn, args.import_findings)

    if args.check_sla:
        results = check_sla_breaches(conn)
        print(f"\n[*] SLA Check Results:")
        print(f"    Breached: {len(results['breached'])}")
        print(f"    Approaching: {len(results['approaching'])}")
        print(f"    Within SLA: {len(results['within_sla'])}")

        if args.slack_webhook:
            if results["breached"]:
                send_slack_notification(args.slack_webhook, results["breached"], "breached")
            if results["approaching"]:
                send_slack_notification(args.slack_webhook, results["approaching"], "approaching")

    if args.report:
        generate_compliance_report(conn, args.output, args.period)

    conn.close()


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 3.2 KB
Keep exploring