Files

402 lines
15 KiB
Python

#!/usr/bin/env python3
"""
CVSS Vulnerability Prioritization Engine
Calculates CVSS v4.0 base scores, enriches with EPSS threat intelligence,
and generates risk-weighted prioritization for vulnerability remediation.
Requirements:
pip install requests pandas
Usage:
python process.py score --cve CVE-2024-3094
python process.py prioritize --csv vulns.csv --output prioritized.csv
python process.py enrich --csv vulns.csv
"""
import argparse
import json
import math
import sys
from datetime import datetime
import pandas as pd
import requests
class CVSSv4Calculator:
"""CVSS v4.0 Base Score Calculator."""
# CVSS v4.0 metric value mappings
METRIC_VALUES = {
"AV": {"N": 0.0, "A": 0.1, "L": 0.2, "P": 0.3},
"AC": {"L": 0.0, "H": 0.1},
"AT": {"N": 0.0, "P": 0.1},
"PR": {"N": 0.0, "L": 0.1, "H": 0.2},
"UI": {"N": 0.0, "P": 0.1, "A": 0.2},
"VC": {"H": 0.0, "L": 0.1, "N": 0.2},
"VI": {"H": 0.0, "L": 0.1, "N": 0.2},
"VA": {"H": 0.0, "L": 0.1, "N": 0.2},
"SC": {"H": 0.0, "L": 0.1, "N": 0.2},
"SI": {"H": 0.0, "L": 0.1, "N": 0.2},
"SA": {"H": 0.0, "L": 0.1, "N": 0.2},
}
# Severity thresholds
SEVERITY_RATINGS = [
(0.0, 0.0, "None"),
(0.1, 3.9, "Low"),
(4.0, 6.9, "Medium"),
(7.0, 8.9, "High"),
(9.0, 10.0, "Critical"),
]
@staticmethod
def parse_vector(vector_string: str) -> dict:
"""Parse a CVSS v4.0 vector string into metric components."""
metrics = {}
parts = vector_string.replace("CVSS:4.0/", "").replace("CVSS:3.1/", "").split("/")
for part in parts:
if ":" in part:
key, value = part.split(":", 1)
metrics[key] = value
return metrics
@staticmethod
def get_severity(score: float) -> str:
"""Map a numeric CVSS score to its severity rating."""
for low, high, rating in CVSSv4Calculator.SEVERITY_RATINGS:
if low <= score <= high:
return rating
return "Unknown"
@classmethod
def estimate_base_score(cls, vector_string: str) -> float:
"""
Estimate CVSS v4.0 base score from a vector string.
Note: Full CVSS v4.0 scoring uses complex lookup tables from FIRST.
This implements a simplified scoring approximation.
"""
metrics = cls.parse_vector(vector_string)
# Exploitability sub-score
av = cls.METRIC_VALUES["AV"].get(metrics.get("AV", "N"), 0)
ac = cls.METRIC_VALUES["AC"].get(metrics.get("AC", "L"), 0)
at = cls.METRIC_VALUES["AT"].get(metrics.get("AT", "N"), 0)
pr = cls.METRIC_VALUES["PR"].get(metrics.get("PR", "N"), 0)
ui = cls.METRIC_VALUES["UI"].get(metrics.get("UI", "N"), 0)
exploitability = 1.0 - (av + ac + at + pr + ui) / 1.0
# Vulnerable system impact
vc = cls.METRIC_VALUES["VC"].get(metrics.get("VC", "N"), 0.2)
vi = cls.METRIC_VALUES["VI"].get(metrics.get("VI", "N"), 0.2)
va = cls.METRIC_VALUES["VA"].get(metrics.get("VA", "N"), 0.2)
vuln_impact = 1.0 - (vc + vi + va) / 0.6
# Subsequent system impact
sc = cls.METRIC_VALUES["SC"].get(metrics.get("SC", "N"), 0.2)
si = cls.METRIC_VALUES["SI"].get(metrics.get("SI", "N"), 0.2)
sa = cls.METRIC_VALUES["SA"].get(metrics.get("SA", "N"), 0.2)
subseq_impact = 1.0 - (sc + si + sa) / 0.6
# Combined impact (weighted)
total_impact = 0.6 * vuln_impact + 0.4 * max(subseq_impact, 0)
if total_impact <= 0:
return 0.0
# Approximate base score
score = min(10.0, (exploitability * 4.0 + total_impact * 6.0))
return round(score, 1)
class VulnerabilityEnricher:
"""Enrich vulnerability data with EPSS scores and CISA KEV status."""
NVD_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"
EPSS_API = "https://api.first.org/data/v1/epss"
CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
def __init__(self):
self.kev_cache = None
self.session = requests.Session()
self.session.headers.update({"User-Agent": "CVSS-Prioritization-Tool/1.0"})
def get_nvd_data(self, cve_id: str) -> dict:
"""Fetch CVE details from NVD API v2.0."""
try:
response = self.session.get(
self.NVD_API, params={"cveId": cve_id}, timeout=30
)
if response.status_code == 200:
data = response.json()
vulns = data.get("vulnerabilities", [])
if vulns:
cve_data = vulns[0].get("cve", {})
metrics = cve_data.get("metrics", {})
# Try CVSS v4.0 first, then v3.1
cvss_data = {}
if "cvssMetricV40" in metrics:
m = metrics["cvssMetricV40"][0]["cvssData"]
cvss_data = {
"version": "4.0",
"vector": m.get("vectorString", ""),
"base_score": m.get("baseScore", 0),
"severity": m.get("baseSeverity", ""),
}
elif "cvssMetricV31" in metrics:
m = metrics["cvssMetricV31"][0]["cvssData"]
cvss_data = {
"version": "3.1",
"vector": m.get("vectorString", ""),
"base_score": m.get("baseScore", 0),
"severity": m.get("baseSeverity", ""),
}
descriptions = cve_data.get("descriptions", [])
desc = next(
(d["value"] for d in descriptions if d["lang"] == "en"),
""
)
return {
"cve_id": cve_id,
"description": desc[:300],
"published": cve_data.get("published", ""),
"cvss": cvss_data,
}
except Exception as e:
print(f" [!] NVD API error for {cve_id}: {e}")
return {}
def get_epss_score(self, cve_id: str) -> dict:
"""Fetch EPSS score for a CVE from FIRST API."""
try:
response = self.session.get(
self.EPSS_API, params={"cve": cve_id}, timeout=15
)
if response.status_code == 200:
data = response.json()
if data.get("data"):
entry = data["data"][0]
return {
"epss_score": float(entry.get("epss", 0)),
"epss_percentile": float(entry.get("percentile", 0)),
}
except Exception as e:
print(f" [!] EPSS API error for {cve_id}: {e}")
return {"epss_score": 0.0, "epss_percentile": 0.0}
def load_kev_catalog(self) -> set:
"""Load CISA Known Exploited Vulnerabilities catalog."""
if self.kev_cache is not None:
return self.kev_cache
try:
response = self.session.get(self.CISA_KEV_URL, timeout=30)
if response.status_code == 200:
data = response.json()
self.kev_cache = {
v["cveID"] for v in data.get("vulnerabilities", [])
}
print(f"[+] Loaded {len(self.kev_cache)} CVEs from CISA KEV catalog")
return self.kev_cache
except Exception as e:
print(f"[!] Failed to load CISA KEV: {e}")
self.kev_cache = set()
return self.kev_cache
def is_in_kev(self, cve_id: str) -> bool:
"""Check if a CVE is in the CISA KEV catalog."""
kev = self.load_kev_catalog()
return cve_id in kev
def enrich_cve(self, cve_id: str) -> dict:
"""Fully enrich a CVE with NVD, EPSS, and KEV data."""
result = {"cve_id": cve_id}
nvd = self.get_nvd_data(cve_id)
if nvd:
result.update({
"description": nvd.get("description", ""),
"published": nvd.get("published", ""),
"cvss_version": nvd.get("cvss", {}).get("version", ""),
"cvss_vector": nvd.get("cvss", {}).get("vector", ""),
"cvss_base_score": nvd.get("cvss", {}).get("base_score", 0),
"cvss_severity": nvd.get("cvss", {}).get("severity", ""),
})
epss = self.get_epss_score(cve_id)
result.update(epss)
result["in_cisa_kev"] = self.is_in_kev(cve_id)
return result
class VulnerabilityPrioritizer:
"""Risk-weighted vulnerability prioritization engine."""
def __init__(self, weights: dict = None):
self.weights = weights or {
"cvss": 0.25,
"epss": 0.25,
"asset_criticality": 0.20,
"kev": 0.15,
"exposure": 0.15,
}
def calculate_priority_score(self, vuln: dict) -> float:
"""Calculate composite priority score for a vulnerability."""
cvss_score = float(vuln.get("cvss_base_score", 0)) / 10.0
epss_score = float(vuln.get("epss_score", 0))
asset_crit = float(vuln.get("asset_criticality", 3)) / 5.0
kev_score = 1.0 if vuln.get("in_cisa_kev", False) else 0.0
exposure = float(vuln.get("exposure_score", 3)) / 5.0
priority = (
cvss_score * self.weights["cvss"] +
epss_score * self.weights["epss"] +
asset_crit * self.weights["asset_criticality"] +
kev_score * self.weights["kev"] +
exposure * self.weights["exposure"]
)
return round(priority * 10, 2)
def assign_sla(self, priority_score: float, cvss_score: float,
in_kev: bool = False) -> dict:
"""Assign remediation SLA based on priority score."""
if in_kev or priority_score >= 8.0:
return {"level": "P1-Emergency", "sla_days": 2, "sla": "48 hours"}
elif priority_score >= 6.5 or cvss_score >= 9.0:
return {"level": "P2-Critical", "sla_days": 7, "sla": "7 days"}
elif priority_score >= 5.0 or cvss_score >= 7.0:
return {"level": "P3-High", "sla_days": 14, "sla": "14 days"}
elif priority_score >= 3.0 or cvss_score >= 4.0:
return {"level": "P4-Medium", "sla_days": 30, "sla": "30 days"}
else:
return {"level": "P5-Low", "sla_days": 90, "sla": "90 days"}
def prioritize(self, vulnerabilities: list) -> pd.DataFrame:
"""Prioritize a list of vulnerabilities and return sorted DataFrame."""
results = []
for vuln in vulnerabilities:
score = self.calculate_priority_score(vuln)
sla = self.assign_sla(
score,
float(vuln.get("cvss_base_score", 0)),
vuln.get("in_cisa_kev", False)
)
results.append({
**vuln,
"priority_score": score,
"priority_level": sla["level"],
"sla_days": sla["sla_days"],
"remediation_sla": sla["sla"],
})
df = pd.DataFrame(results)
df = df.sort_values("priority_score", ascending=False)
return df
def main():
parser = argparse.ArgumentParser(description="CVSS Vulnerability Prioritization Engine")
subparsers = parser.add_subparsers(dest="command")
# Score a single CVE
score_parser = subparsers.add_parser("score", help="Score and enrich a single CVE")
score_parser.add_argument("--cve", required=True, help="CVE identifier (e.g., CVE-2024-3094)")
# Prioritize a CSV of vulnerabilities
pri_parser = subparsers.add_parser("prioritize", help="Prioritize vulnerabilities from CSV")
pri_parser.add_argument("--csv", required=True, help="Input CSV with cve_id column")
pri_parser.add_argument("--output", required=True, help="Output CSV with priorities")
# Enrich a CSV with EPSS/KEV data
enrich_parser = subparsers.add_parser("enrich", help="Enrich CVE list with EPSS and KEV")
enrich_parser.add_argument("--csv", required=True, help="Input CSV with cve_id column")
enrich_parser.add_argument("--output", default=None, help="Output enriched CSV")
args = parser.parse_args()
if args.command == "score":
enricher = VulnerabilityEnricher()
print(f"[*] Scoring {args.cve}...")
result = enricher.enrich_cve(args.cve)
print(f"\n{'='*60}")
print(f"CVE: {result.get('cve_id')}")
print(f"Description: {result.get('description', 'N/A')[:200]}")
print(f"Published: {result.get('published', 'N/A')}")
print(f"CVSS Version: {result.get('cvss_version', 'N/A')}")
print(f"CVSS Vector: {result.get('cvss_vector', 'N/A')}")
print(f"CVSS Base Score: {result.get('cvss_base_score', 'N/A')}")
print(f"CVSS Severity: {result.get('cvss_severity', 'N/A')}")
print(f"EPSS Score: {result.get('epss_score', 0):.4f} ({result.get('epss_percentile', 0)*100:.1f}th percentile)")
print(f"In CISA KEV: {'Yes' if result.get('in_cisa_kev') else 'No'}")
prioritizer = VulnerabilityPrioritizer()
priority = prioritizer.calculate_priority_score(result)
sla = prioritizer.assign_sla(
priority, float(result.get("cvss_base_score", 0)),
result.get("in_cisa_kev", False)
)
print(f"\nPriority Score: {priority}")
print(f"Priority Level: {sla['level']}")
print(f"Remediation SLA: {sla['sla']}")
elif args.command == "prioritize":
df = pd.read_csv(args.csv)
if "cve_id" not in df.columns:
print("[-] CSV must contain 'cve_id' column")
sys.exit(1)
enricher = VulnerabilityEnricher()
enriched = []
for _, row in df.iterrows():
cve = row["cve_id"]
print(f"[*] Processing {cve}...")
data = enricher.enrich_cve(cve)
data.update(row.to_dict())
enriched.append(data)
prioritizer = VulnerabilityPrioritizer()
result_df = prioritizer.prioritize(enriched)
result_df.to_csv(args.output, index=False)
print(f"\n[+] Prioritized results saved to: {args.output}")
print("\n=== Priority Summary ===")
print(result_df["priority_level"].value_counts().to_string())
elif args.command == "enrich":
df = pd.read_csv(args.csv)
enricher = VulnerabilityEnricher()
enriched = []
for _, row in df.iterrows():
cve = row.get("cve_id", "")
if cve:
print(f"[*] Enriching {cve}...")
data = enricher.enrich_cve(cve)
data.update(row.to_dict())
enriched.append(data)
result_df = pd.DataFrame(enriched)
output = args.output or args.csv.replace(".csv", "_enriched.csv")
result_df.to_csv(output, index=False)
print(f"[+] Enriched data saved to: {output}")
else:
parser.print_help()
if __name__ == "__main__":
main()