Safeguard
Vulnerability Analysis•

What EPSS Said the Day Before CISA Added 61 CVEs to KEV

For KEV additions from 1 Sep to 16 Oct 2026, 26 of 61 CVEs had no EPSS score the day before, and 22 more scored below 0.01.

Safeguard Security Team
6 min read

EPSS estimates the probability that a CVE will see exploitation activity in the next 30 days. CISA's KEV catalog records exploitation that has already been observed. We wanted to know how well the first predicted the second for the 61 CVEs CISA added to KEV between 1 September and 16 October 2026.

So we pulled the EPSS score for each CVE on the day before it entered KEV, and again on 17 October 2026. The short answer: most of these CVEs were either unscored or scored very low the day before, and EPSS caught up afterward.

How we counted

The KEV JSON feed (catalog version 2026.10.16, downloaded 18 October 2026) gives the CVE list and each dateAdded. The daily EPSS files give the score per CVE per day. All files used model version v2026.06.15, as stated in each file's header line.

curl -sS -o kev.json \
  https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json

# One EPSS file for each "day before" date, plus 2026-10-17
for day in $(python3 -I -c '
import json, datetime as dt
days = {"2026-10-17"}
for v in json.load(open("kev.json"))["vulnerabilities"]:
    if "2026-09-01" <= v["dateAdded"] <= "2026-10-16":
        days.add(str(dt.date.fromisoformat(v["dateAdded"]) - dt.timedelta(days=1)))
print(" ".join(sorted(days)))'); do
  curl -sSL -o "epss_$day.csv.gz" \
    "https://epss.empiricalsecurity.com/epss_scores-$day.csv.gz"
done
import json, gzip, csv, collections, datetime as dt

def epss(day):
    with gzip.open(f"epss_{day}.csv.gz", "rt") as f:
        f.readline()  # model_version and score_date comment line
        return {r["cve"]: float(r["epss"]) for r in csv.DictReader(f)}

def bucket(x):
    if x is None: return "not scored"
    if x < 0.01:  return "below 0.01"
    if x < 0.1:   return "0.01 to 0.1"
    if x < 0.5:   return "0.1 to 0.5"
    return "0.5 or higher"

d = json.load(open("kev.json"))
w = [v for v in d["vulnerabilities"]
     if "2026-09-01" <= v["dateAdded"] <= "2026-10-16"]

latest = epss("2026-10-17")
before, after, cache = [], [], {}
for v in w:
    day = str(dt.date.fromisoformat(v["dateAdded"]) - dt.timedelta(days=1))
    if day not in cache:
        cache[day] = epss(day)
    before.append(cache[day].get(v["cveID"]))
    after.append(latest.get(v["cveID"]))

print("day before:", collections.Counter(map(bucket, before)))
print("2026-10-17:", collections.Counter(map(bucket, after)))
scored = sorted(x for x in before if x is not None)
print("median of scored, day before:", scored[len(scored) // 2], len(scored))

What's affected

EPSS scoreDay before KEV additionOn 17 Oct 2026
Not scored261
Below 0.012210
0.01 to 0.1832
0.1 to 0.5210
0.5 or higher38
Total6161

For the 35 CVEs that had a score the day before, the median was 0.00535.

The 26 unscored CVEs were simply too new. Every one of them was published within one day of its KEV addition, so the previous day's EPSS file did not contain it yet. This group includes NetScaler, PAN-OS, Ivanti Connect Secure, FortiMail, Cisco Secure Email Gateway and the September Windows and Chromium entries.

The three CVEs at 0.5 or higher the day before were all old: ProFTPD CVE-2015-3306, ISC BIND CVE-2015-5477 and Apache Struts CVE-2016-3081. The two in the 0.1 to 0.5 range were Cisco FMC CVE-2026-20079 (0.35946) and Adobe Commerce CVE-2026-71362 (0.2452).

The one CVE unscored on 17 October is CVE-2026-21589 (Atlassian). Its CVE record was rejected by the CNA on 16 October.

Some scores moved sharply after KEV listing. Check Point CVE-2026-85102 went from 0.00329 to 0.504. WordPress CVE-2026-87902 went from 0.02877 to 0.39979. JFrog Artifactory CVE-2026-82329 went from 0.00377 to 0.14121.

How to check if you're exposed

If your vulnerability policy uses an EPSS threshold, run your own open findings through the same test. Export CVE IDs from your scanner and look them up in today's EPSS file and the KEV feed:

curl -sSL -o epss_today.csv.gz \
  https://epss.empiricalsecurity.com/epss_scores-current.csv.gz

python3 -I -c '
import gzip, csv, json
mine = set(open("my_cves.txt").read().split())
kev = {v["cveID"] for v in json.load(open("kev.json"))["vulnerabilities"]}
with gzip.open("epss_today.csv.gz", "rt") as f:
    f.readline()
    s = {r["cve"]: float(r["epss"]) for r in csv.DictReader(f)}
for c in sorted(mine):
    print(c, s.get(c, "unscored"), "KEV" if c in kev else "")
'

Pay attention to rows marked KEV with a low or missing score. Those are the cases where a threshold alone would have put the fix at the back of the queue.

What to do now

  1. Make KEV an override, not an input. If a CVE is in KEV, it goes to the top regardless of its EPSS score.
  2. Treat "unscored" as unknown, not low. 26 of these 61 had no score the day before. A rule like "fix if EPSS is 0.1 or higher" silently skips them.
  3. Use EPSS to order the long tail. It is useful for the thousands of CVEs without exploitation evidence. That's the job it is built for.
  4. Re-pull scores daily. Scores change quickly after public exploitation. A weekly snapshot can be days behind.
  5. Combine with reachability. A high score on code you never call matters less than a modest score on a function your service calls on every request.

Why it matters

EPSS is a forecast built from signals that build up over time. KEV is a record of exploitation evidence, which often arrives with the advisory itself, especially for edge devices. In this window, the CVEs that went straight from disclosure to KEV are exactly the ones EPSS hadn't seen yet.

That isn't a flaw in EPSS. It means the two signals answer different questions, and a policy that uses only one will be wrong in predictable ways.

Try it yourself

Pick five CVEs from your current backlog that are in KEV. Look up their EPSS scores for the day before they were added, using the archived daily files. If any of them were below your threshold, your policy would have missed them. The demo below shows an exploitability check that combines several of these signals.

Click through it at your own pace, or open it full screen.

Quick check

How many of the 61 KEV additions had no EPSS score the day before they were added?
  1. They were published too recently to appear in the previous day's EPSS file.
What was the median EPSS score the day before addition, for CVEs that had one?

0.00535, across 35 scored CVEs.

Which entries were already above 0.5 the day before?

Three old CVEs: ProFTPD CVE-2015-3306, ISC BIND CVE-2015-5477 and Apache Struts CVE-2016-3081.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.