TL;DR
CVSS measures potential impact, not likelihood of exploitation. Fix-firsting on raw CVSS burns time on low-likelihood high-severity vulns while CRITICALs with public PoCs sit. A multi-factor model — CVSS + EPSS + KEV + asset criticality + exploit maturity — produces a priority score that correlates with real-world risk. Ship one, and your top-10 list shrinks to the eight that actually matter.
The Problem with CVSS-Only Prioritization
CVSS v3.1 base score 10.0 means “unauthenticated remote code execution with no user interaction.” It is a severity score, not a priority score. Two CRITICALs can differ in exploitability by orders of magnitude: one has a public PoC and a Metasploit module, the other requires a rare configuration and has no known attack path.
Organizations that fix-first on CVSS:
- Waste remediation cycles on high-severity, low-likelihood vulns
- Miss CRITICALs that are actively exploited in the wild (KEV)
- Produce long backlogs with no clear top-10
- Audit-ready but operationally blind
This is not CVSS’s fault. CVSS measures impact, not likelihood. You need additional signals.
The Multi-Factor Model
A practical prioritization model weights five signals:
| Signal | Weight | Source |
|---|---|---|
| CVSS base score | 25% | NVD / vendor advisory |
| EPSS probability | 20% | EPSS API (first.org) |
| CISA KEV membership | 20% | CISA catalog |
| Asset criticality × exposure × data sensitivity | 20% | Asset inventory |
| Exploit maturity (public PoC, Metasploit, etc.) | 15% | Exploit-DB, vendor notes, OSS-SEC |
Composite score 0-100:
| Score | Category | SLA |
|---|---|---|
| ≥ 80 | P1 Critical | 24 hours |
| ≥ 60 | P2 High | 7 days |
| ≥ 40 | P3 Medium | 30 days |
| < 40 | P4 Low | 90 days |
CVSS Component (25%)
Normalize CVSS 0-10 to 0-100 by multiplying by 10. Simple, bounded, deterministic. If an Environmental CVSS is available (local asset context), use it; otherwise fall back to Base.
cvss_score = (env_cvss or temp_cvss or base_cvss) * 10
EPSS Component (20%)
EPSS gives the probability (0-1) that a CVE will be exploited in the wild within 30 days. The EPSS API at api.first.org returns this per-CVE. Convert to 0-100 by multiplying by 100.
import requests
epss = requests.get(f"https://api.first.org/data/v1/epss?cve={cve}").json()["data"][0]["epss"]
epss_score = epss * 100
EPSS correlates well with actual exploitation. A CVSS 7.2 with EPSS 0.95 is a higher-priority fix than CVSS 9.8 with EPSS 0.001.
KEV Component (20%)
CISA’s Known Exploited Vulnerabilities catalog is binary: in or out. In = 100, out = 0. KEV entries have due dates; an overdue KEV is operationally critical.
kev_score = 100 if in_kev else 0
KEV alone often bumps a vuln to P1. CISA mandates accelerated remediation.
Asset Context (20%)
The same CVE on a critical internet-facing asset with confidential data scores higher than on an internal test box. Multiply asset criticality (25-100), exposure (0.7-1.0), and data sensitivity (0.5-1.0).
asset_score = criticality * exposure * data_sensitivity
Exploit Maturity (15%)
Public PoC = 50, functional exploit = 80, no evidence = 20. Public PoCs cap at 60 minimum regardless of other maturity. A Metasploit module or ransomware TTP maps at least at PoC level.
exploit_score = maturity_map.get(maturity, 20)
exploit_score = max(exploit_score, 60) if public_poc else exploit_score
Putting It Together
priority = (
0.25 * cvss_score +
0.20 * epss_score +
0.20 * kev_score +
0.20 * asset_score +
0.15 * exploit_score
)
Example: CVE-2021-44228 (Log4Shell)
- CVSS 10.0 → 100 × 0.25 = 25
- EPSS 0.975 → 97.5 × 0.20 = 19.5
- KEV yes → 100 × 0.20 = 20
- Asset critical external confidential → 100 × 1.0 × 1.0 = 100 × 0.20 = 20
- Public exploit high maturity → 100 × 0.15 = 15
- Total: 99.0 → P1 Critical, 24h SLA
Example: a CVSS 8.1 with EPSS 0.02, no KEV, internal asset
- CVSS 81 × 0.25 = 20.25
- EPSS 2 × 0.20 = 0.4
- KEV 0 × 0.20 = 0
- Asset 50 × 0.7 × 0.7 = 24.5 × 0.20 = 4.9
- Exploit 20 × 0.15 = 3
- Total: 28.55 → P4 Low, 90 days
Same CRITICAL by CVSS, bottom of the priority list by composite.
Implementation Notes
- Cache EPSS bulk queries (one HTTP call per batch, not per-CVE).
- Cache the KEV catalog locally; refresh daily.
- CVSS calculation: the formula is deterministic — cache results.
- Re-prioritize automatically when new signals arrive (PoC published, KEV added, EPSS updated).
Trade-offs and Limitations
No model is perfect. A multi-factor score:
- Requires an asset inventory and classification — garbage in, garbage out
- Weights are opinionated; tune to your org’s risk appetite
- EPSS is probabilistic, not deterministic — a 0.05 EPSS still gets exploited
- KEV lags: a vuln actively exploited today may not be in KEV yet
- Cannot replace human judgment on business-critical systems
But it is demonstrably better than CVSS-only. The top-10 list produced by this model correlates with real exploitation and reduces mean time to remediate the things that actually matter.
Further Reading
- EPSS data: https://www.first.org/cve/epss
- CISA KEV catalog: https://www.cisa.gov/known-exploited-vulnerabilities-catalog
- NVD API: https://nvd.nist.gov/vulnerability-metrics/epss