mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 21:54:56 +03:00
347 lines
12 KiB
Python
347 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""SSVC Vulnerability Triage Processor.
|
|
|
|
Evaluates vulnerabilities against CISA's Stakeholder-Specific Vulnerability
|
|
Categorization (SSVC) decision tree and produces prioritized triage reports.
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import sys
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
|
|
EPSS_API = "https://api.first.org/data/v1/epss"
|
|
NVD_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
|
|
|
SSVC_SLA = {
|
|
"Act": 2,
|
|
"Attend": 14,
|
|
"Track*": 60,
|
|
"Track": 90,
|
|
}
|
|
|
|
|
|
def fetch_kev_catalog():
|
|
"""Download the CISA Known Exploited Vulnerabilities catalog."""
|
|
resp = requests.get(KEV_URL, timeout=30)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return {v["cveID"] for v in data.get("vulnerabilities", [])}
|
|
|
|
|
|
def fetch_epss_scores(cve_ids):
|
|
"""Fetch EPSS scores for a list of CVE IDs from FIRST API."""
|
|
scores = {}
|
|
batch_size = 100
|
|
for i in range(0, len(cve_ids), batch_size):
|
|
batch = cve_ids[i : i + batch_size]
|
|
params = {"cve": ",".join(batch)}
|
|
resp = requests.get(EPSS_API, params=params, timeout=30)
|
|
if resp.status_code == 200:
|
|
for entry in resp.json().get("data", []):
|
|
scores[entry["cve"]] = {
|
|
"epss": float(entry.get("epss", 0)),
|
|
"percentile": float(entry.get("percentile", 0)),
|
|
}
|
|
time.sleep(1)
|
|
return scores
|
|
|
|
|
|
def fetch_nvd_cve(cve_id, api_key=None):
|
|
"""Fetch CVE details from NVD API v2."""
|
|
params = {"cveId": cve_id}
|
|
headers = {}
|
|
if api_key:
|
|
headers["apiKey"] = api_key
|
|
resp = requests.get(NVD_API, params=params, headers=headers, timeout=30)
|
|
if resp.status_code == 200:
|
|
vulns = resp.json().get("vulnerabilities", [])
|
|
if vulns:
|
|
return vulns[0].get("cve", {})
|
|
return None
|
|
|
|
|
|
def evaluate_exploitation(cve_id, kev_set, epss_scores):
|
|
"""Determine exploitation status: active, poc, or none."""
|
|
if cve_id in kev_set:
|
|
return "active"
|
|
epss_data = epss_scores.get(cve_id, {})
|
|
if epss_data.get("epss", 0) > 0.5:
|
|
return "poc"
|
|
if epss_data.get("epss", 0) > 0.1:
|
|
return "poc"
|
|
return "none"
|
|
|
|
|
|
def evaluate_technical_impact(cvss_vector):
|
|
"""Assess technical impact from CVSS vector string."""
|
|
if not cvss_vector:
|
|
return "partial"
|
|
vector_upper = cvss_vector.upper()
|
|
if "S:C" in vector_upper:
|
|
return "total"
|
|
if "C:H" in vector_upper and "I:H" in vector_upper and "A:H" in vector_upper:
|
|
return "total"
|
|
if "C:H" in vector_upper and "I:H" in vector_upper:
|
|
return "total"
|
|
return "partial"
|
|
|
|
|
|
def evaluate_automatability(cvss_vector):
|
|
"""Determine if exploitation can be automated."""
|
|
if not cvss_vector:
|
|
return "no"
|
|
vector_upper = cvss_vector.upper()
|
|
network = "AV:N" in vector_upper
|
|
low_complexity = "AC:L" in vector_upper
|
|
no_user_interaction = "UI:N" in vector_upper
|
|
if network and low_complexity and no_user_interaction:
|
|
return "yes"
|
|
return "no"
|
|
|
|
|
|
def ssvc_decision(exploitation, tech_impact, automatability, mission_prevalence, public_wellbeing):
|
|
"""Apply CISA SSVC decision tree to produce triage outcome.
|
|
|
|
Returns one of: Act, Attend, Track*, Track
|
|
"""
|
|
if exploitation == "active":
|
|
if automatability == "yes":
|
|
return "Act"
|
|
if tech_impact == "total":
|
|
if mission_prevalence in ("essential", "support"):
|
|
return "Act"
|
|
return "Attend"
|
|
if mission_prevalence == "essential":
|
|
return "Attend"
|
|
if public_wellbeing in ("irreversible", "material"):
|
|
return "Attend"
|
|
return "Attend"
|
|
|
|
if exploitation == "poc":
|
|
if automatability == "yes" and tech_impact == "total":
|
|
if mission_prevalence in ("essential", "support"):
|
|
return "Attend"
|
|
return "Track*"
|
|
if tech_impact == "total" and mission_prevalence == "essential":
|
|
return "Attend"
|
|
if public_wellbeing == "irreversible":
|
|
return "Attend"
|
|
return "Track*"
|
|
|
|
# exploitation == "none"
|
|
if tech_impact == "total" and mission_prevalence == "essential":
|
|
return "Track*"
|
|
if automatability == "yes" and mission_prevalence == "essential":
|
|
return "Track*"
|
|
return "Track"
|
|
|
|
|
|
def parse_nessus_csv(filepath):
|
|
"""Parse Nessus CSV export into vulnerability records."""
|
|
vulns = []
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
cve = row.get("CVE", "").strip()
|
|
if not cve or not cve.startswith("CVE-"):
|
|
continue
|
|
vulns.append(
|
|
{
|
|
"cve_id": cve,
|
|
"host": row.get("Host", "unknown"),
|
|
"port": row.get("Port", ""),
|
|
"plugin_name": row.get("Name", ""),
|
|
"severity": row.get("Severity", ""),
|
|
"cvss_vector": row.get("CVSS V3 Vector", ""),
|
|
"description": row.get("Synopsis", ""),
|
|
}
|
|
)
|
|
return vulns
|
|
|
|
|
|
def parse_openvas_xml(filepath):
|
|
"""Parse OpenVAS XML report into vulnerability records."""
|
|
vulns = []
|
|
tree = ET.parse(filepath)
|
|
root = tree.getroot()
|
|
for result in root.iter("result"):
|
|
nvt = result.find("nvt")
|
|
if nvt is None:
|
|
continue
|
|
cve_elem = nvt.find("cve")
|
|
if cve_elem is None or not cve_elem.text or cve_elem.text == "NOCVE":
|
|
continue
|
|
host_elem = result.find("host")
|
|
port_elem = result.find("port")
|
|
vulns.append(
|
|
{
|
|
"cve_id": cve_elem.text.strip(),
|
|
"host": host_elem.text.strip() if host_elem is not None else "unknown",
|
|
"port": port_elem.text.strip() if port_elem is not None else "",
|
|
"plugin_name": nvt.findtext("name", ""),
|
|
"severity": result.findtext("severity", ""),
|
|
"cvss_vector": nvt.findtext("tags", ""),
|
|
"description": result.findtext("description", ""),
|
|
}
|
|
)
|
|
return vulns
|
|
|
|
|
|
def parse_generic_csv(filepath):
|
|
"""Parse generic CSV with cve_id, host, cvss_vector columns."""
|
|
vulns = []
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
cve = row.get("cve_id", "").strip()
|
|
if not cve:
|
|
continue
|
|
vulns.append(
|
|
{
|
|
"cve_id": cve,
|
|
"host": row.get("host", "unknown"),
|
|
"port": row.get("port", ""),
|
|
"plugin_name": row.get("plugin_name", ""),
|
|
"severity": row.get("severity", ""),
|
|
"cvss_vector": row.get("cvss_vector", ""),
|
|
"description": row.get("description", ""),
|
|
"mission_prevalence": row.get("mission_prevalence", "support"),
|
|
"public_wellbeing": row.get("public_wellbeing", "minimal"),
|
|
}
|
|
)
|
|
return vulns
|
|
|
|
|
|
def run_triage(vulns, kev_set, epss_scores, default_mission="support", default_wellbeing="minimal"):
|
|
"""Run SSVC triage on a list of vulnerability records."""
|
|
results = []
|
|
for vuln in vulns:
|
|
cve_id = vuln["cve_id"]
|
|
exploitation = evaluate_exploitation(cve_id, kev_set, epss_scores)
|
|
tech_impact = evaluate_technical_impact(vuln.get("cvss_vector", ""))
|
|
automatability = evaluate_automatability(vuln.get("cvss_vector", ""))
|
|
mission = vuln.get("mission_prevalence", default_mission)
|
|
wellbeing = vuln.get("public_wellbeing", default_wellbeing)
|
|
|
|
outcome = ssvc_decision(exploitation, tech_impact, automatability, mission, wellbeing)
|
|
epss_data = epss_scores.get(cve_id, {})
|
|
|
|
results.append(
|
|
{
|
|
"cve_id": cve_id,
|
|
"host": vuln.get("host", "unknown"),
|
|
"port": vuln.get("port", ""),
|
|
"plugin_name": vuln.get("plugin_name", ""),
|
|
"ssvc_outcome": outcome,
|
|
"sla_days": SSVC_SLA[outcome],
|
|
"exploitation_status": exploitation,
|
|
"technical_impact": tech_impact,
|
|
"automatability": automatability,
|
|
"mission_prevalence": mission,
|
|
"public_wellbeing": wellbeing,
|
|
"epss_score": epss_data.get("epss", 0),
|
|
"epss_percentile": epss_data.get("percentile", 0),
|
|
"in_kev": cve_id in kev_set,
|
|
}
|
|
)
|
|
|
|
outcome_order = {"Act": 0, "Attend": 1, "Track*": 2, "Track": 3}
|
|
results.sort(key=lambda r: (outcome_order.get(r["ssvc_outcome"], 4), -r["epss_score"]))
|
|
return results
|
|
|
|
|
|
def generate_report(results, output_path, report_format="json"):
|
|
"""Generate triage report in JSON or CSV format."""
|
|
summary = {
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"total_vulnerabilities": len(results),
|
|
"outcome_counts": {},
|
|
"results": results,
|
|
}
|
|
for r in results:
|
|
outcome = r["ssvc_outcome"]
|
|
summary["outcome_counts"][outcome] = summary["outcome_counts"].get(outcome, 0) + 1
|
|
|
|
if report_format == "csv":
|
|
if results:
|
|
with open(output_path, "w", newline="", encoding="utf-8") as f:
|
|
writer = csv.DictWriter(f, fieldnames=results[0].keys())
|
|
writer.writeheader()
|
|
writer.writerows(results)
|
|
else:
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
json.dump(summary, f, indent=2)
|
|
|
|
return summary
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="SSVC Vulnerability Triage Processor")
|
|
parser.add_argument("--input", required=True, help="Path to vulnerability scan results")
|
|
parser.add_argument("--output", default="ssvc_triage_report.json", help="Output report path")
|
|
parser.add_argument(
|
|
"--format",
|
|
choices=["nessus", "openvas", "generic"],
|
|
default="generic",
|
|
help="Input format",
|
|
)
|
|
parser.add_argument(
|
|
"--output-format", choices=["json", "csv"], default="json", help="Output format"
|
|
)
|
|
parser.add_argument("--nvd-api-key", help="NVD API key for higher rate limits")
|
|
parser.add_argument(
|
|
"--mission-prevalence",
|
|
choices=["minimal", "support", "essential"],
|
|
default="support",
|
|
help="Default mission prevalence",
|
|
)
|
|
parser.add_argument(
|
|
"--public-wellbeing",
|
|
choices=["minimal", "material", "irreversible"],
|
|
default="minimal",
|
|
help="Default public well-being impact",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
print("[*] Fetching CISA KEV catalog...")
|
|
kev_set = fetch_kev_catalog()
|
|
print(f" Loaded {len(kev_set)} known exploited vulnerabilities")
|
|
|
|
print(f"[*] Parsing input file: {args.input}")
|
|
if args.format == "nessus":
|
|
vulns = parse_nessus_csv(args.input)
|
|
elif args.format == "openvas":
|
|
vulns = parse_openvas_xml(args.input)
|
|
else:
|
|
vulns = parse_generic_csv(args.input)
|
|
print(f" Found {len(vulns)} vulnerability records")
|
|
|
|
cve_ids = list({v["cve_id"] for v in vulns})
|
|
print(f"[*] Fetching EPSS scores for {len(cve_ids)} unique CVEs...")
|
|
epss_scores = fetch_epss_scores(cve_ids)
|
|
|
|
print("[*] Running SSVC triage...")
|
|
results = run_triage(
|
|
vulns, kev_set, epss_scores, args.mission_prevalence, args.public_wellbeing
|
|
)
|
|
|
|
print(f"[*] Generating report: {args.output}")
|
|
summary = generate_report(results, args.output, args.output_format)
|
|
|
|
print("\n[+] SSVC Triage Summary:")
|
|
for outcome, count in sorted(summary["outcome_counts"].items()):
|
|
print(f" {outcome}: {count}")
|
|
print(f" Total: {summary['total_vulnerabilities']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|