EPSS published scores for 390,724 CVEs on 17 October 2026. We split them into the 1,747 that are in CISA's KEV catalog and the 388,977 that are not, then compared the distributions.
The gap is large, as you'd expect: the median KEV CVE scores 0.49083 and the median non-KEV CVE scores 0.00643. The more useful finding is the overlap. 460 KEV CVEs score below 0.1, and 3,463 non-KEV CVEs score 0.5 or higher.
How we counted
Two files: the EPSS daily scores for 2026-10-17 (header: model_version:v2026.06.15, score_date:2026-10-17T12:00:28Z) and KEV catalog version 2026.10.16, both downloaded on 18 October 2026.
curl -sS -o kev.json \
https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json
curl -sSL -o epss_2026-10-17.csv.gz \
https://epss.empiricalsecurity.com/epss_scores-2026-10-17.csv.gz
import json, gzip, csv, collections
kev = {v["cveID"] for v in json.load(open("kev.json"))["vulnerabilities"]}
with gzip.open("epss_2026-10-17.csv.gz", "rt") as f:
print(f.readline().strip())
s = {r["cve"]: float(r["epss"]) for r in csv.DictReader(f)}
def bucket(x):
if x < 0.001: return "a. below 0.001"
if x < 0.01: return "b. 0.001 to 0.01"
if x < 0.1: return "c. 0.01 to 0.1"
if x < 0.5: return "d. 0.1 to 0.5"
return "e. 0.5 or higher"
k = [x for c, x in s.items() if c in kev]
n = [x for c, x in s.items() if c not in kev]
print(len(s), "scored;", len(k), "KEV;", len(n), "not KEV")
ck, cn = collections.Counter(map(bucket, k)), collections.Counter(map(bucket, n))
for b in sorted(set(ck) | set(cn)):
print(b, ck[b], round(100 * ck[b] / len(k), 1), cn[b], round(100 * cn[b] / len(n), 2))
med = lambda xs: sorted(xs)[len(xs) // 2]
print("median KEV", med(k), "median not KEV", med(n))
for t in (0.1, 0.5, 0.9):
hi = [c for c, x in s.items() if x >= t]
print(t, len(hi), sum(c in kev for c in hi))
top = sorted(s, key=s.get, reverse=True)
print("top 100 in KEV:", sum(c in kev for c in top[:100]),
"top 1000 in KEV:", sum(c in kev for c in top[:1000]))
KEV had 1,748 entries. One, CVE-2026-21589, had no EPSS score on 17 October; its CVE record was rejected on 16 October. For the top 100 and top 1,000 counts, ties are broken by order in the file.
What's affected
Distribution of EPSS scores on 17 October 2026:
| EPSS score | KEV CVEs | Share of KEV | Non-KEV CVEs | Share of non-KEV |
|---|---|---|---|---|
| Below 0.001 | 0 | 0.0% | 3,076 | 0.79% |
| 0.001 to 0.01 | 61 | 3.5% | 239,736 | 61.63% |
| 0.01 to 0.1 | 399 | 22.8% | 130,133 | 33.46% |
| 0.1 to 0.5 | 417 | 23.9% | 12,569 | 3.23% |
| 0.5 or higher | 870 | 49.8% | 3,463 | 0.89% |
| Total | 1,747 | 388,977 |
Looking from the other direction, at how much of each high score band is KEV:
| Threshold | CVEs at or above | Of which in KEV |
|---|---|---|
| 0.1 | 17,319 | 1,287 |
| 0.5 | 4,333 | 870 |
| 0.9 | 804 | 470 |
Of the 100 highest scored CVEs, 94 are in KEV. Of the top 1,000, 527 are.
How to check if you're exposed
The question for your program is which of these two failure modes it is more exposed to. Export the open CVE IDs from your scanner, one per line, into my_cves.txt, then:
python3 -I -c '
import json, gzip, csv
kev = {v["cveID"] for v in json.load(open("kev.json"))["vulnerabilities"]}
with gzip.open("epss_2026-10-17.csv.gz", "rt") as f:
f.readline()
s = {r["cve"]: float(r["epss"]) for r in csv.DictReader(f)}
mine = set(open("my_cves.txt").read().split())
low_kev = [c for c in mine if c in kev and s.get(c, 0) < 0.1]
high_not = [c for c in mine if c not in kev and s.get(c, 0) >= 0.5]
print("KEV but EPSS below 0.1:", len(low_kev), sorted(low_kev)[:20])
print("Not KEV, EPSS 0.5 or higher:", len(high_not), sorted(high_not)[:20])
'
The first list is the one a pure EPSS threshold would deprioritize. The second is the one a pure KEV policy would leave alone.
What to do now
- Use both signals with clear roles. KEV says exploitation has been observed. EPSS estimates the chance of exploitation activity in the next 30 days. Neither replaces the other.
- Don't drop KEV CVEs on low EPSS. 460 KEV CVEs score below 0.1 today. A threshold policy would push every one of them down the list.
- Look hard at high EPSS non-KEV CVEs. 3,463 CVEs score 0.5 or higher without a KEV listing. Those are candidates for urgent work even without a catalog entry.
- Pick thresholds from your own capacity. If your team can close 200 findings a week, find the EPSS cut that yields about that many from your backlog, then review it monthly.
- Add reachability before effort. A KEV or high EPSS CVE in a function your code never calls can wait behind one you call on every request.
Why it matters
The two lists agree at the extreme top: 94 of the top 100 EPSS scores are KEV CVEs. Below that, they diverge fast. Just over half of KEV CVEs (877 of 1,747) score below 0.5, and 3,463 of the 4,333 CVEs scoring 0.5 or higher are not in KEV.
That divergence is expected. KEV is a curated list of confirmed exploitation, and CISA adds an entry only when it has evidence and clear remediation guidance. EPSS models the likelihood of activity across every published CVE. Treating them as the same thing on different scales is where prioritization goes wrong.
Try it yourself
Run the snippet above against your own backlog. Count each list, then pick one CVE from each and look up whether the vulnerable function is reachable from your code. The demo below shows how reachability changes a long list of findings into a short one.
Click through it at your own pace, or open it full screen.
Quick check
What were the median EPSS scores for KEV and non-KEV CVEs on 17 October 2026?
0.49083 for the 1,747 scored KEV CVEs and 0.00643 for the 388,977 non-KEV CVEs.
How many KEV CVEs had an EPSS score below 0.1?
460: 61 between 0.001 and 0.01, and 399 between 0.01 and 0.1.
Of the CVEs scoring 0.9 or higher, how many are in KEV?
470 of 804.