Part 1 of 3 in the Vulnerability management that works series.
Every vulnerability programme I’ve seen fail failed the same way. The scanner produced thousands of findings, someone decided “fix all criticals and highs within 30 days”, the backlog grew faster than anyone could patch, and within a quarter the SLA dashboard was a wall of red that everyone had learned to ignore. The problem usually isn’t effort. It’s that the team is sorting by the wrong number.
This series is about running vulnerability management so it actually reduces risk. Part one is about the sorting, because if you get that wrong nothing downstream matters.
CVSS measures severity, not risk

The Common Vulnerability Scoring System, maintained by FIRST, is the score everyone knows. A CVSS base score describes the intrinsic characteristics of a vulnerability: attack vector, complexity, privileges required, user interaction, and the impact on confidentiality, integrity and availability. It answers “how bad is this if someone exploits it?”
It doesn’t answer “will anyone exploit it?”, and the specification says so. The CVSS v3.1 spec states plainly that CVSS measures severity, not risk. Teams use the base score as a risk score anyway, because it’s the column the scanner shows.
The practical problem is distribution. A large share of published CVEs land at 7.0 or above, so “fix all highs and criticals” means fixing a majority of everything. When half your backlog is priority one, you don’t have priorities.
CVSS v4.0, released by FIRST in November 2023, tries to address this by making the extra metric groups more visible. There are now Base, Threat, Environmental and Supplemental groups, and the spec asks you to label which ones a score includes (CVSS-B, CVSS-BT, CVSS-BE, CVSS-BTE). The Threat group’s Exploit Maturity metric lets you lower a score when there’s no known exploitation. That’s an improvement in principle. In practice almost every feed you consume publishes base scores only, and the threat data has to come from somewhere else anyway. Which brings us to the other two.
EPSS estimates likelihood
The Exploit Prediction Scoring System, also run by a FIRST special interest group, is a model that outputs a probability between 0 and 1 that a given CVE will see exploitation activity in the wild in the next 30 days. It’s retrained periodically and the scores are recomputed daily, using features like whether public exploit code exists, mentions in security lists, the vendor and the vulnerability type, and it’s trained on observed exploitation telemetry.
Each CVE gets two numbers: the probability and a percentile. The percentile tells you how it ranks against every other scored CVE. Most CVEs have a very low EPSS probability, which is the whole point. The model is telling you that the vast majority of vulnerabilities never get exploited at all, and it’s trying to tell you which few will.
You can query it for free:
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2021-44228,CVE-2023-4863" \
| jq -r '.data[] | "\(.cve) epss=\(.epss) pct=\(.percentile) as_of=\(.date)"'
FIRST also publishes the full daily score set as a compressed CSV, which is what you want if you’re enriching a whole scanner export rather than looking up a handful.
EPSS has limits I’d keep in mind. It’s a prediction about the internet at large, not about your environment. A CVE with a low EPSS score in a product exposed directly to the internet on your perimeter deserves more attention than the model gives it. It also lags. A brand new CVE often scores low on day one simply because the model hasn’t seen any signal yet. And it’s a probability, so treat a score of 0.02 as “unlikely”, never as “safe”.
KEV is evidence, not prediction
CISA’s Known Exploited Vulnerabilities catalog launched in November 2021 alongside Binding Operational Directive 22-01. A CVE goes into KEV when three things are true: it has a CVE ID, there’s reliable evidence of active exploitation, and there’s clear remediation guidance such as a vendor patch. BOD 22-01 requires US federal civilian agencies to remediate each entry by a due date CISA sets when adding it, often two or three weeks out.
KEV is small compared to the CVE corpus, well over a thousand entries against hundreds of thousands of CVEs, and that’s its strength. Every entry is something an attacker has actually used. It’s also available as a JSON feed:
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
| jq -r '.vulnerabilities[] | [.cveID, .dateAdded, .dueDate, .knownRansomwareCampaignUse] | @tsv' \
> kev.tsv
The knownRansomwareCampaignUse field is worth carrying into your tooling. “Known” there means CISA has evidence ransomware operators used it, which in my experience is the single fastest way to get an executive to approve an emergency change window.
What KEV doesn’t give you is coverage. Exploitation that nobody reported to CISA, or that hit products CISA doesn’t track, isn’t in there. Absence from KEV tells you very little.
Combining them
None of these three is a prioritisation scheme on its own. Together they’re pretty good. The way I’d layer them:
- In KEV: fix now, on the shortest SLA you have. Someone is using this.
- High EPSS and exposed: treat almost like KEV. Exposure means reachable by an attacker who doesn’t already have a foothold, so internet-facing services, VPN concentrators, email gateways.
- High EPSS or critical CVSS on exposed systems: the next tier, on a normal fast SLA.
- Everything else with a high CVSS: the standard backlog, fixed by routine patching and image rebuilds.
- The long tail: fixed when the component gets upgraded anyway.
As code, roughly:
def priority(cvss: float, epss: float, in_kev: bool, exposed: bool) -> str:
if in_kev:
return "P1"
if epss >= 0.10 and exposed:
return "P1"
if epss >= 0.10 or (cvss >= 9.0 and exposed):
return "P2"
if cvss >= 7.0:
return "P3"
return "P4"
The 0.10 cutoff is my choice, not a standard. Pick yours by looking at how many of your current findings land in each bucket. If P1 has more than a few dozen items in a mid-sized environment, your thresholds are too loose, or you have a real emergency and should treat it as one.
If you want something more formal, CISA’s Stakeholder-Specific Vulnerability Categorization (SSVC) is a decision tree that takes exploitation status, exposure, automatability and mission impact and outputs one of four actions: Track, Track*, Attend or Act. It’s the same idea with more rigour, and it’s easier to defend in an audit than a Python function someone wrote on a Friday.
The part nobody wants to hear
The inputs that matter most for prioritisation aren’t in any public feed. Whether a vulnerable package is actually loaded at runtime, whether the vulnerable function is reachable, whether the host is exposed to the internet: that’s your asset inventory and your architecture. A Log4Shell finding in a jar that sits in an unused test directory and a Log4Shell finding in your public login service have identical CVSS, EPSS and KEV values. One of them is an incident.
So the real first step is tagging assets with exposure and ownership. Once you have that, the public scores do most of the remaining work. Part two looks at SBOMs, which are the other half of knowing what you’re actually running.
Other parts: Part 2: SBOMs: what they’re actually good for, Part 3: Patch SLAs that teams actually meet.

