<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Vulnerability Prioritization | FirewallSync</title><link>https://firewallsync.com/tags/vulnerability-prioritization/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Sat, 07 Nov 2026 04:50:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/vulnerability-prioritization/index.xml" rel="self" type="application/rss+xml"/><item><title>EPSS Scores for KEV CVEs: Why New Entries Score Low</title><link>https://firewallsync.com/posts/epss-scores-for-kev-cves-low-after-listing-threshold-audit/</link><guid>https://firewallsync.com/posts/epss-scores-for-kev-cves-low-after-listing-threshold-audit/</guid><pubDate>Sat, 07 Nov 2026 04:50:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Security Operations</category><description>An EPSS cutoff of 0.1 misses a quarter of CISA's KEV catalog today, and half of the entries added in the last 90 days. We tracked 113 KEV additions through 145 daily EPSS v5 files: the median score on the day of listing was 0.0076, and entries that crossed 0.1 after listing took a median of 14 days. Data, a tested audit script and the rule it implies.</description><enclosure url="https://firewallsync.com/posts/epss-scores-for-kev-cves-low-after-listing-threshold-audit/cover_hu7102096413798716976.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/epss-scores-for-kev-cves-low-after-listing-threshold-audit/cover_hu7102096413798716976.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/epss-scores-for-kev-cves-low-after-listing-threshold-audit/cover_hu7102096413798716976.jpg" alt="A wooden-framed hourglass with grey sand running from the upper bulb into the lower one, photographed against a black background" width="1200" height="630"></p><p>If your patch pipeline uses an EPSS cutoff to decide what is urgent, it will miss about a quarter of CISA&rsquo;s Known Exploited Vulnerabilities (KEV) catalog, and roughly <strong>half of the entries CISA added in the last 90 days</strong>. Joining KEV catalog 2026.11.06 to the EPSS file for November 6, 2026, 436 of 1,768 scored KEV CVEs sit below 0.1. Tracking recent additions day by day shows why: EPSS scores for newly listed CVEs typically start low and take about two weeks to cross 0.1, which is the window in which a KEV listing matters most.</p>
<p>We argued in our <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS vs EPSS vs KEV comparison</a> that KEV should be checked before EPSS. This article measures what happens when it is not, and gives you a script to test your own cutoff.</p>
<h2 id="the-snapshot-kev-cves-below-common-epss-cutoffs">The snapshot: KEV CVEs below common EPSS cutoffs</h2>
<p>Data: the <a href="https://github.com/cisagov/kev-data">KEV JSON</a> (catalog version 2026.11.06, released 2026-11-06T19:44Z, 1,769 entries) and the <a href="https://github.com/empiricalsec/epss_scores">EPSS daily file</a> for November 6, 2026 (<code>model_version:v2026.06.15</code>, 400,985 scored CVEs). One KEV entry, CVE-2026-19868, added that day, had no EPSS score yet.</p>
<table>
  <thead>
      <tr>
          <th>EPSS cutoff</th>
          <th>KEV CVEs below it (of 1,768)</th>
          <th>Share</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0.5</td>
          <td>861</td>
          <td>48.7%</td>
      </tr>
      <tr>
          <td>0.1</td>
          <td>436</td>
          <td>24.7%</td>
      </tr>
      <tr>
          <td>0.05</td>
          <td>307</td>
          <td>17.4%</td>
      </tr>
      <tr>
          <td>0.01</td>
          <td>54</td>
          <td>3.1%</td>
      </tr>
      <tr>
          <td>90th percentile</td>
          <td>248</td>
          <td>14.0%</td>
      </tr>
      <tr>
          <td>95th percentile</td>
          <td>405</td>
          <td>22.9%</td>
      </tr>
  </tbody>
</table>
<p>The median KEV CVE scores 0.536 (99.0th percentile), so the typical entry is well above any cutoff. The problem is the tail, and the tail is not random. If you are unsure what the <code>epss</code> and <code>percentile</code> columns mean, our <a href="/posts/epss-data-feed-columns-percentile-vs-probability-and-how-to-join-it-to-a-scanner-export/">EPSS data feed guide</a> covers them.</p>
<h2 id="newest-kev-entries-score-lowest">Newest KEV entries score lowest</h2>
<p>Split by <code>dateAdded</code>, the share below 0.1 falls as entries age:</p>
<table>
  <thead>
      <tr>
          <th>Listed in KEV</th>
          <th>KEV CVEs</th>
          <th>Below 0.1</th>
          <th>Share</th>
          <th>Median EPSS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Within 90 days</td>
          <td>106</td>
          <td>52</td>
          <td>49.1%</td>
          <td>0.104</td>
      </tr>
      <tr>
          <td>91 to 365 days ago</td>
          <td>207</td>
          <td>57</td>
          <td>27.5%</td>
          <td>0.444</td>
      </tr>
      <tr>
          <td>More than 1 year ago</td>
          <td>1,455</td>
          <td>327</td>
          <td>22.5%</td>
          <td>0.583</td>
      </tr>
  </tbody>
</table>
<p>Several recent listings we covered are in that first row. On November 6, WordPress CVE-2026-119977 (added November 3) scored 0.0116, Ivanti CVE-2026-111357 and CVE-2026-118527 (added October 30) scored 0.0053 and 0.0082, and VMware ESX CVE-2026-41709 (added October 28) scored 0.0069. Each would sit below even a 0.05 cutoff.</p>
<h2 id="tracking-the-lag-113-kev-additions-day-by-day">Tracking the lag: 113 KEV additions, day by day</h2>
<p>A snapshot cannot show whether low scores are permanent or just early. So we took the 113 KEV entries added between June 16 and October 7, 2026, and looked up each one in every daily EPSS file from June 15 to November 6 (145 files). That window starts the day after EPSS v5 replaced v4, so every score comes from the same model (<code>v2026.06.15</code>); our <a href="/posts/epss-v5-score-shift-model-version-v2026-06-15/">EPSS v5 analysis</a> explains why mixing models would distort this. The October 7 end date gives every entry at least 30 days of follow-up.</p>
<table>
  <thead>
      <tr>
          <th>Day relative to KEV listing</th>
          <th>Entries with a score</th>
          <th>At or above 0.1</th>
          <th>Share</th>
          <th>Median EPSS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Day before</td>
          <td>77</td>
          <td>15</td>
          <td>19.5%</td>
          <td>0.0072</td>
      </tr>
      <tr>
          <td>Day of listing</td>
          <td>91</td>
          <td>15</td>
          <td>16.5%</td>
          <td>0.0076</td>
      </tr>
      <tr>
          <td>7 days after</td>
          <td>113</td>
          <td>38</td>
          <td>33.6%</td>
          <td>0.0220</td>
      </tr>
      <tr>
          <td>30 days after</td>
          <td>113</td>
          <td>68</td>
          <td>60.2%</td>
          <td>0.1689</td>
      </tr>
  </tbody>
</table>
<p>Notes on reading it:</p>
<ul>
<li><strong>Missing scores are real.</strong> 36 of the 113 had no EPSS score the day before listing and 22 had none on the day itself, usually because the CVE had only just been published. By day 7 all 113 were scored.</li>
<li><strong>Crossing time.</strong> 15 entries were already at or above 0.1 the day before listing. Of the rest, 64 crossed 0.1 later, after a median of <strong>14 days</strong> (23 within a week). 34 had not reached 0.1 by November 6, 36 to 115 days after listing.</li>
<li><strong>Timing, not cause.</strong> EPSS files publish only the score. We cannot see which inputs moved a score, so we do not claim the KEV listing itself caused any rise.</li>
</ul>
<p>Why the gap exists is consistent with how each list is built. CISA adds a CVE when there is &ldquo;reliable evidence that the vulnerability has been actively exploited in the wild&rdquo; (<a href="https://cisa.gov/known-exploited-vulnerabilities">CISA</a>). EPSS is &ldquo;the probability [0-1] of exploitation in the wild in the next 30 days (following score publication)&rdquo;, estimated from data (<a href="https://github.com/empiricalsec/epss_scores">EPSS scores README</a>). The community <a href="https://riskbasedprioritization.github.io/epss/EPSS_and_CISA_KEV">Risk Based Prioritization guide</a> puts it this way: EPSS reflects &ldquo;the automated data we receive and collect&rdquo;, while KEV is &ldquo;a list of human curated CVEs with historical exploitation activity&rdquo;, so a CVE can be in one and score low in the other. It also quotes FIRST&rsquo;s EPSS FAQ: where exploitation is evidenced, &ldquo;that information should supersede anything EPSS has to say&rdquo;.</p>
<h2 id="what-to-change-in-a-pipeline">What to change in a pipeline</h2>
<p>These are our recommendations from the data above; we have not measured them on anyone&rsquo;s backlog.</p>
<ol>
<li><strong>Route on KEV membership before any EPSS rule.</strong> A listed CVE goes to the urgent queue whatever its score. This is the first step in our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">prioritization workflow</a>.</li>
<li><strong>Do not &ldquo;promote&rdquo; KEV entries by adding a bonus to the EPSS score.</strong> A bonus sized for the median KEV entry (0.536) does nothing for an entry at 0.005.</li>
<li><strong>Treat a missing EPSS score as &ldquo;unknown&rdquo;, not zero.</strong> 22 of the 113 entries had no score on the day they were listed. A join that fills nulls with 0 drops exactly those.</li>
<li><strong>Refresh KEV more often than you refresh thresholds.</strong> The <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">KEV JSON feed</a> updates on listing days; the lag above means the KEV check is doing nearly all of the work for the first two weeks.</li>
<li><strong>Re-run the audit after every EPSS model change</strong>, since cutoffs shift with the model.</li>
</ol>
<h2 id="audit-your-own-cutoff">Audit your own cutoff</h2>
<p>This script joins the two public files and reports how many KEV entries your cutoff misses, by listing age. We ran it with Python 3.12 on the files named above; the output below is for 0.1.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;How many CISA KEV entries would an EPSS cutoff miss?
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">usage: kev_epss_audit.py known_exploited_vulnerabilities.json epss_scores-YYYY-MM-DD.csv.gz CUTOFF&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> csv<span style="color:#f92672">,</span> gzip<span style="color:#f92672">,</span> json<span style="color:#f92672">,</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> date
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>kev_path, epss_path, cutoff <span style="color:#f92672">=</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>], sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">2</span>], float(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">3</span>])
</span></span><span style="display:flex;"><span>catalog <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(kev_path))
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> gzip<span style="color:#f92672">.</span>open(epss_path, <span style="color:#e6db74">&#34;rt&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>    header <span style="color:#f92672">=</span> f<span style="color:#f92672">.</span>readline()<span style="color:#f92672">.</span>strip()                 <span style="color:#75715e"># &#34;#model_version:...,score_date:...&#34;</span>
</span></span><span style="display:flex;"><span>    scores <span style="color:#f92672">=</span> {r[<span style="color:#e6db74">&#34;cve&#34;</span>]: float(r[<span style="color:#e6db74">&#34;epss&#34;</span>]) <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> csv<span style="color:#f92672">.</span>DictReader(f)}
</span></span><span style="display:flex;"><span>score_day <span style="color:#f92672">=</span> date<span style="color:#f92672">.</span>fromisoformat(header<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;score_date:&#34;</span>)[<span style="color:#ae81ff">1</span>][:<span style="color:#ae81ff">10</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;KEV catalog </span><span style="color:#e6db74">{</span>catalog[<span style="color:#e6db74">&#39;catalogVersion&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">, EPSS </span><span style="color:#e6db74">{</span>header[<span style="color:#ae81ff">1</span>:]<span style="color:#e6db74">}</span><span style="color:#e6db74">, cutoff </span><span style="color:#e6db74">{</span>cutoff<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>buckets <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;added &lt;= 90 days ago&#34;</span>: [], <span style="color:#e6db74">&#34;91-365 days&#34;</span>: [], <span style="color:#e6db74">&#34;over 1 year&#34;</span>: []}
</span></span><span style="display:flex;"><span>unscored <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> v <span style="color:#f92672">in</span> catalog[<span style="color:#e6db74">&#34;vulnerabilities&#34;</span>]:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> v[<span style="color:#e6db74">&#34;cveID&#34;</span>] <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> scores:
</span></span><span style="display:flex;"><span>        unscored<span style="color:#f92672">.</span>append(v[<span style="color:#e6db74">&#34;cveID&#34;</span>])
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>    age <span style="color:#f92672">=</span> (score_day <span style="color:#f92672">-</span> date<span style="color:#f92672">.</span>fromisoformat(v[<span style="color:#e6db74">&#34;dateAdded&#34;</span>]))<span style="color:#f92672">.</span>days
</span></span><span style="display:flex;"><span>    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;added &lt;= 90 days ago&#34;</span> <span style="color:#66d9ef">if</span> age <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">90</span> <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;91-365 days&#34;</span> <span style="color:#66d9ef">if</span> age <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">365</span> <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;over 1 year&#34;</span>
</span></span><span style="display:flex;"><span>    buckets[name]<span style="color:#f92672">.</span>append((v[<span style="color:#e6db74">&#34;cveID&#34;</span>], scores[v[<span style="color:#e6db74">&#34;cveID&#34;</span>]]))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>total <span style="color:#f92672">=</span> below <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> name, rows <span style="color:#f92672">in</span> buckets<span style="color:#f92672">.</span>items():
</span></span><span style="display:flex;"><span>    n_below <span style="color:#f92672">=</span> sum(<span style="color:#ae81ff">1</span> <span style="color:#66d9ef">for</span> _, s <span style="color:#f92672">in</span> rows <span style="color:#66d9ef">if</span> s <span style="color:#f92672">&lt;</span> cutoff)
</span></span><span style="display:flex;"><span>    total <span style="color:#f92672">+=</span> len(rows); below <span style="color:#f92672">+=</span> n_below
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">:</span><span style="color:#e6db74">22</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>len(rows)<span style="color:#e6db74">:</span><span style="color:#e6db74">5</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> KEV CVEs, </span><span style="color:#e6db74">{</span>n_below<span style="color:#e6db74">:</span><span style="color:#e6db74">4</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> below cutoff (</span><span style="color:#e6db74">{</span>n_below<span style="color:#f92672">/</span>len(rows)<span style="color:#e6db74">:</span><span style="color:#e6db74">.1%</span><span style="color:#e6db74">}</span><span style="color:#e6db74">)&#34;</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span><span style="color:#e6db74">&#39;all scored&#39;</span><span style="color:#e6db74">:</span><span style="color:#e6db74">22</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>total<span style="color:#e6db74">:</span><span style="color:#e6db74">5</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> KEV CVEs, </span><span style="color:#e6db74">{</span>below<span style="color:#e6db74">:</span><span style="color:#e6db74">4</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> below cutoff (</span><span style="color:#e6db74">{</span>below<span style="color:#f92672">/</span>total<span style="color:#e6db74">:</span><span style="color:#e6db74">.1%</span><span style="color:#e6db74">}</span><span style="color:#e6db74">)&#34;</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;not in EPSS file: </span><span style="color:#e6db74">{</span>len(unscored)<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>unscored[:<span style="color:#ae81ff">5</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>$ python3 kev_epss_audit.py known_exploited_vulnerabilities.json epss_scores-2026-11-06.csv.gz 0.1
</span></span><span style="display:flex;"><span>KEV catalog 2026.11.06, EPSS model_version:v2026.06.15,score_date:2026-11-06T12:00:23Z, cutoff 0.1
</span></span><span style="display:flex;"><span>added &lt;= 90 days ago     106 KEV CVEs,   52 below cutoff (49.1%)
</span></span><span style="display:flex;"><span>91-365 days              207 KEV CVEs,   57 below cutoff (27.5%)
</span></span><span style="display:flex;"><span>over 1 year             1455 KEV CVEs,  327 below cutoff (22.5%)
</span></span><span style="display:flex;"><span>all scored              1768 KEV CVEs,  436 below cutoff (24.7%)
</span></span><span style="display:flex;"><span>not in EPSS file: 1 [&#39;CVE-2026-19868&#39;]
</span></span></code></pre></div><p>Get the KEV file from <code>raw.githubusercontent.com/cisagov/kev-data/main/known_exploited_vulnerabilities.json</code> and the EPSS file from the <code>empiricalsec/epss_scores</code> repository (one gzip file per day under the year folder). The script reads the whole EPSS file into memory, which is fine for about 400,000 rows.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The snapshot is one day (November 6, 2026). The day-by-day sample is 113 entries over one model version, which is enough to show a pattern but not to estimate a precise lag for any vendor or CVE type. &ldquo;Day of listing&rdquo; compares KEV <code>dateAdded</code> to the EPSS file of the same date; their publication times within the day differ. We did not examine why the 34 entries that stayed below 0.1 scored as they did.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>KEV counts, below-cutoff shares, medians, age buckets, per-CVE scores</td>
          <td><a href="https://github.com/cisagov/kev-data">cisagov/kev-data</a> catalog 2026.11.06 joined to <a href="https://github.com/empiricalsec/epss_scores">empiricalsec/epss_scores</a> 2026-11-06, computed with the script above</td>
          <td>Verified (computed)</td>
      </tr>
      <tr>
          <td>113-entry day-by-day results (medians, shares, 14-day median crossing, 34 never)</td>
          <td>Same KEV file and 145 daily EPSS files 2026-06-15 to 2026-11-06, all <code>v2026.06.15</code>, computed in Python</td>
          <td>Verified (computed)</td>
      </tr>
      <tr>
          <td>EPSS column definitions and 30-day window</td>
          <td><a href="https://github.com/empiricalsec/epss_scores">EPSS scores README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>KEV inclusion criterion</td>
          <td><a href="https://cisa.gov/known-exploited-vulnerabilities">CISA KEV catalog page</a></td>
          <td>Verified, Nov 7, 2026</td>
      </tr>
      <tr>
          <td>Data-generating difference between EPSS and KEV; FIRST FAQ &ldquo;supersede&rdquo; quote</td>
          <td><a href="https://riskbasedprioritization.github.io/epss/EPSS_and_CISA_KEV">Risk Based Prioritization guide</a> (community guide quoting FIRST)</td>
          <td>Qualified: secondary for the FIRST quote</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>EPSS v5: How Scores Shifted and What to Re-baseline</title><link>https://firewallsync.com/posts/epss-v5-score-shift-model-version-v2026-06-15/</link><guid>https://firewallsync.com/posts/epss-v5-score-shift-model-version-v2026-06-15/</guid><pubDate>Fri, 06 Nov 2026 04:45:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Security Operations</category><description>EPSS v5 (model v2026.06.15) replaced v4 in the daily feed on June 15, 2026. We diffed the last v4 file against the first v5 file: 26% fewer CVEs at or above 0.1, 41% fewer at or above 0.5, a median score almost three times higher, and only 588 of the top 1,000 CVEs unchanged. Here is what that means for threshold rules, and a tested guard that stops your pipeline on the next model change.</description><enclosure url="https://firewallsync.com/posts/epss-v5-score-shift-model-version-v2026-06-15/cover_hu8687825451073364504.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/epss-v5-score-shift-model-version-v2026-06-15/cover_hu8687825451073364504.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/epss-v5-score-shift-model-version-v2026-06-15/cover_hu8687825451073364504.jpg" alt="Close-up of an oval dial thermometer whose glass is covered in ice crystals, with a red needle visible beneath the frost" width="1200" height="630"></p><p>The EPSS daily file changed models on <strong>June 15, 2026</strong>. The first line of the June 14 file reads <code>#model_version:v2025.03.14</code> (v4). The June 15 file reads <code>#model_version:v2026.06.15</code> (v5). We compared those two files, which together hold 340,204 CVEs scored on both days. If your pipeline gates on a fixed EPSS probability, it lost about a quarter of its flagged CVEs overnight: <strong>23,069 CVEs scored 0.1 or higher under v4 and 17,041 under v5</strong>. Nothing about those vulnerabilities changed that day; the model did.</p>
<p>This article covers what changed in v5, what the shift looks like in the data, and what to re-baseline. For the file format itself (columns, header line, how to join it to a scanner export), see our <a href="/posts/epss-data-feed-columns-percentile-vs-probability-and-how-to-join-it-to-a-scanner-export/">EPSS data feed guide</a>. That guide already lists the five model versions and their start dates; this one measures the most recent change.</p>
<h2 id="what-changed-in-v5">What changed in v5</h2>
<p>Empirical Security, which runs the EPSS model and hosts the data, <a href="https://lifestyle.kbew98country.com/story/220743/empirical-security-releases-epss-v5/">announced EPSS V5 on May 13, 2026</a>. The release says v5 &ldquo;incorporates improved modeling techniques and optimization algorithms&rdquo; and that &ldquo;the calibration step has been refined.&rdquo; It also cites a better classifier for published exploit code and &ldquo;the addition of new data feeds.&rdquo;</p>
<p>The headline claim is a 23% improvement. Empirical&rsquo;s <a href="https://research.empiricalsecurity.com/research/epss-v5-is-here">research post</a> defines the metric only loosely, as one that rewards &ldquo;ranking genuinely exploited vulnerabilities above ones that pose no real threat.&rdquo; On data from May 4, 2026, it reports that v4 scored 0.514 and v5 scored 0.633, where &ldquo;a random guess would score around 0.025, and a perfect model would score 1.0.&rdquo; We did not reproduce that evaluation.</p>
<p>The rollout is visible in the public <a href="https://github.com/empiricalsec/epss_scores">empiricalsec/epss_scores</a> repository:</p>
<table>
  <thead>
      <tr>
          <th>Date</th>
          <th>What the repository shows</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2026-05-22 to 2026-06-08</td>
          <td><code>beta_scores/epssv5_beta-*.csv.gz</code>, header <code>#model_version:v2026.05.12</code></td>
      </tr>
      <tr>
          <td>2026-06-14</td>
          <td>Last daily file on <code>v2025.03.14</code> (v4), <code>score_date</code> 12:55:00Z</td>
      </tr>
      <tr>
          <td>2026-06-15</td>
          <td>First daily file on <code>v2026.06.15</code> (v5), <code>score_date</code> 12:03:41Z</td>
      </tr>
      <tr>
          <td>2026-11-05</td>
          <td>Still <code>v2026.06.15</code></td>
      </tr>
  </tbody>
</table>
<p>The beta files carry a different model label (<code>v2026.05.12</code>) from the production model, so treat beta scores as a preview of v5, not as v5 itself. The repository README lists the model changes for v2, v3 and v4 but, as of November 6, 2026, has no v5 entry. The file header was the reliable signal.</p>
<p>The CSV format did not change. Both files have the same comment line followed by <code>cve,epss,percentile</code>.</p>
<h2 id="how-the-scores-moved">How the scores moved</h2>
<p>We loaded <code>2026/epss_scores-2026-06-14.csv.gz</code> and <code>2026/epss_scores-2026-06-15.csv.gz</code> from the repository and compared CVEs present in both. For a baseline, we ran the same comparison on June 13 and June 14, two consecutive v4 days.</p>
<table>
  <thead>
      <tr>
          <th>Measure</th>
          <th>v4 (June 14)</th>
          <th>v5 (June 15)</th>
          <th>Normal day (June 13 to 14)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVEs scored</td>
          <td>341,350</td>
          <td>340,247</td>
          <td>341,309 to 341,350</td>
      </tr>
      <tr>
          <td>Median EPSS</td>
          <td>0.00262</td>
          <td>0.00751</td>
          <td>unchanged</td>
      </tr>
      <tr>
          <td>Mean EPSS</td>
          <td>0.03328</td>
          <td>0.02934</td>
          <td>unchanged</td>
      </tr>
      <tr>
          <td>99th percentile of EPSS</td>
          <td>0.77230</td>
          <td>0.59470</td>
          <td>n/a</td>
      </tr>
      <tr>
          <td>CVEs at or above 0.1</td>
          <td>23,069</td>
          <td>17,041</td>
          <td>23,071 to 23,069</td>
      </tr>
      <tr>
          <td>CVEs at or above 0.5</td>
          <td>7,193</td>
          <td>4,249</td>
          <td>7,194 to 7,193</td>
      </tr>
      <tr>
          <td>Crossed 0.1 upward / downward</td>
          <td>3,820 / 9,847</td>
          <td></td>
          <td>9 / 11</td>
      </tr>
      <tr>
          <td>Crossed the 90th percentile in / out</td>
          <td>9,837 / 9,944</td>
          <td></td>
          <td>25 / 29</td>
      </tr>
      <tr>
          <td>Moved by 2x or more</td>
          <td>243,259 of 340,204 (71.5%)</td>
          <td></td>
          <td>313 of 341,309</td>
      </tr>
      <tr>
          <td>Top 1,000 by EPSS that stayed top 1,000</td>
          <td>588</td>
          <td></td>
          <td>999</td>
      </tr>
  </tbody>
</table>
<p>Three patterns stand out.</p>
<p><strong>The distribution got narrower.</strong> The lowest v4 score was 0.00001 and 93,484 CVEs sat below 0.001. Under v5 the minimum is 0.00046 and only 3,552 CVEs are below 0.001. At the top end, the 99th percentile fell from 0.772 to 0.595. The median ratio of v5 to v4 score was 2.89: most low-scored CVEs went up, while many of the high scorers came down. That fits the release&rsquo;s description of recalibration, but the release does not say this is the intended effect, so treat it as our reading of the data.</p>
<p><strong>Fixed cutoffs shrank.</strong> A rule like <code>epss &gt;= 0.1</code> matched 26.1% fewer CVEs the day v5 went live, and <code>epss &gt;= 0.5</code> matched 40.9% fewer. Of the CVEs that crossed 0.1, 9,847 dropped below it and only 3,820 rose above it.</p>
<p><strong>Percentile rules kept their size, not their members.</strong> A percentile is a rank, so the 90th-percentile bucket stays about 10% of scored CVEs. But 9,837 CVEs entered it and 9,944 left it in one day, compared with 25 and 29 on a normal v4 day. If you track &ldquo;new CVEs in the top decile&rdquo; as a work queue, June 15 would have looked like a flood.</p>
<p>1,146 CVEs scored on June 14 were missing from the June 15 file and 43 appeared for the first time. By November 5, all but 119 of the missing ones were back in the feed. We did not establish why they were absent that day.</p>
<h2 id="what-happened-to-kev-listed-cves">What happened to KEV-listed CVEs</h2>
<p>To see whether v5 demoted known-exploited vulnerabilities, we took the <a href="https://github.com/cisagov/kev-data">CISA KEV catalog</a> (catalog version 2026.11.04) and kept only the 1,619 entries whose <code>dateAdded</code> is on or before June 14, 2026, so the set matches what was known on the switch date. All 1,619 were scored on both days.</p>
<table>
  <thead>
      <tr>
          <th>KEV CVEs (added by 2026-06-14)</th>
          <th>v4</th>
          <th>v5</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Median EPSS</td>
          <td>0.704</td>
          <td>0.535</td>
      </tr>
      <tr>
          <td>At or above 0.1</td>
          <td>1,224</td>
          <td>1,219</td>
      </tr>
      <tr>
          <td>At or above the 90th percentile</td>
          <td>1,323</td>
          <td>1,366</td>
      </tr>
  </tbody>
</table>
<p>The probability for KEV entries came down along with the rest of the high end. But almost the same number cleared 0.1, and more of them landed in the top decile. KEV-listed CVEs that a 0.1 rule already caught were still caught. A rule set to 0.5 or 0.7 on the assumption that KEV CVEs sit there would have lost many of them, since the median is now about 0.54.</p>
<p>This is one reason KEV should stay a separate input instead of being folded into an EPSS cutoff. Our <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS vs EPSS vs KEV comparison</a> covers why; the <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">prioritization workflow</a> shows where each signal sits in the patch decision.</p>
<h2 id="what-to-re-baseline">What to re-baseline</h2>
<p>These are our recommendations, based on the data above. We have not measured their effect on any team&rsquo;s backlog.</p>
<ol>
<li><strong>Find every fixed probability cutoff.</strong> SLA rules, ticket triggers, dashboard colours and scanner policies. If one was tuned against v4 data, check how many CVEs it matches today versus before June 15.</li>
<li><strong>Decide what the cutoff is for.</strong> If it sizes a queue (&ldquo;the top few thousand CVEs&rdquo;), switch it to a percentile. If it expresses risk (&ldquo;at least a 10% chance of exploitation in 30 days&rdquo;), keep the probability, because v5&rsquo;s calibration is meant to make that reading more accurate, but accept that the queue size will move.</li>
<li><strong>Do not compare scores across the switch.</strong> A CVE that went from 0.4 to 0.2 on June 15 did not become less likely to be exploited that day. If you chart EPSS history, mark June 15, 2026 (and the earlier switches on 2022-02-04, 2023-03-07 and 2025-03-17).</li>
<li><strong>Keep KEV as an override.</strong> Do not rely on EPSS to bring KEV entries above a high cutoff.</li>
<li><strong>Store <code>model_version</code> next to every score.</strong> Then you can always tell which model produced a score your system acted on.</li>
</ol>
<h2 id="a-guard-for-the-next-model-change">A guard for the next model change</h2>
<p>There is no API callback when the model changes, but the file tells you. This script compares the header of yesterday&rsquo;s and today&rsquo;s file and exits non-zero on a model change, so a CI job or cron wrapper can stop before ingesting.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Stop an EPSS ingest when the model version changes.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Usage: python3 epss_model_guard.py yesterday.csv.gz today.csv.gz&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> gzip<span style="color:#f92672">,</span> sys
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">header</span>(path):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> gzip<span style="color:#f92672">.</span>open(path, <span style="color:#e6db74">&#34;rt&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>        first <span style="color:#f92672">=</span> f<span style="color:#f92672">.</span>readline()<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> first<span style="color:#f92672">.</span>startswith(<span style="color:#e6db74">&#34;#&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;model_version&#34;</span>: <span style="color:#e6db74">&#34;pre-v2 (no header)&#34;</span>, <span style="color:#e6db74">&#34;score_date&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> dict(kv<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;:&#34;</span>, <span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">for</span> kv <span style="color:#f92672">in</span> first<span style="color:#f92672">.</span>lstrip(<span style="color:#e6db74">&#34;#&#34;</span>)<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;,&#34;</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>old, new <span style="color:#f92672">=</span> header(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>]), header(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">2</span>])
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;previous: </span><span style="color:#e6db74">{</span>old[<span style="color:#e6db74">&#39;model_version&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> scored </span><span style="color:#e6db74">{</span>old[<span style="color:#e6db74">&#39;score_date&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;current:  </span><span style="color:#e6db74">{</span>new[<span style="color:#e6db74">&#39;model_version&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> scored </span><span style="color:#e6db74">{</span>new[<span style="color:#e6db74">&#39;score_date&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> old[<span style="color:#e6db74">&#34;model_version&#34;</span>] <span style="color:#f92672">!=</span> new[<span style="color:#e6db74">&#34;model_version&#34;</span>]:
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;MODEL CHANGE: hold threshold-based alerts and re-baseline before ingesting&#34;</span>)
</span></span><span style="display:flex;"><span>    sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">2</span>)
</span></span></code></pre></div><p>We ran it with Python 3 against files from the repository:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>$ python3 epss_model_guard.py epss_scores-2026-06-14.csv.gz epss_scores-2026-06-15.csv.gz
</span></span><span style="display:flex;"><span>previous: v2025.03.14 scored 2026-06-14T12:55:00Z
</span></span><span style="display:flex;"><span>current:  v2026.06.15 scored 2026-06-15T12:03:41Z
</span></span><span style="display:flex;"><span>MODEL CHANGE: hold threshold-based alerts and re-baseline before ingesting
</span></span><span style="display:flex;"><span>exit=2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$ python3 epss_model_guard.py epss_scores-2026-11-04.csv.gz epss_scores-2026-11-05.csv.gz
</span></span><span style="display:flex;"><span>previous: v2026.06.15 scored 2026-11-04T12:03:12Z
</span></span><span style="display:flex;"><span>current:  v2026.06.15 scored 2026-11-05T12:03:04Z
</span></span><span style="display:flex;"><span>exit=0
</span></span></code></pre></div><p>When the guard trips, run the same comparison we did above (counts at your cutoffs, percentile churn, KEV coverage) before you turn alerts back on. Note the <code>score_date</code> format also changed with v4 (from <code>+0000</code> offsets to <code>Z</code>), so parse it as ISO 8601 instead of matching a fixed string.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<ul>
<li>We compared one pair of days. Scores on later v5 days have kept moving as new signals arrive, so today&rsquo;s counts differ from June 15.</li>
<li>The 23% improvement figure and the description of model changes come from Empirical Security. We did not test model accuracy.</li>
<li>KEV is a list of vulnerabilities known to be exploited, not a ground truth for EPSS&rsquo;s 30-day prediction window, so the KEV table shows coverage, not accuracy.</li>
<li>We did not find a FIRST page describing v5 in the sources we could reach. If FIRST publishes a different start date or model description, the file headers above are still what your pipeline will see.</li>
</ul>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Model version and score date of each daily and beta file; all score counts, medians, percentiles and crossings</td>
          <td><a href="https://github.com/empiricalsec/epss_scores">empiricalsec/epss_scores</a> files for 2026-06-13, 06-14, 06-15, 11-04, 11-05 and <code>beta_scores/</code>, computed with Python</td>
          <td>Verified (computed)</td>
      </tr>
      <tr>
          <td>README lists v2, v3 and v4 start dates and no v5 entry</td>
          <td><a href="https://github.com/empiricalsec/epss_scores">epss_scores README</a></td>
          <td>Verified, Nov 6, 2026</td>
      </tr>
      <tr>
          <td>v5 announced May 13, 2026; modeling, calibration, exploit-code classifier and data-feed changes; 23% improvement</td>
          <td><a href="https://lifestyle.kbew98country.com/story/220743/empirical-security-releases-epss-v5/">Empirical Security press release</a></td>
          <td>Verified (vendor claim)</td>
      </tr>
      <tr>
          <td>v4 scored 0.514 and v5 0.633 on May 4, 2026 data; random about 0.025</td>
          <td><a href="https://research.empiricalsecurity.com/research/epss-v5-is-here">EPSS V5 Is Here</a></td>
          <td>Qualified: metric not fully specified</td>
      </tr>
      <tr>
          <td>KEV entries added by 2026-06-14 (1,619) and their scores</td>
          <td><a href="https://github.com/cisagov/kev-data">cisagov/kev-data</a>, catalog 2026.11.04, joined to the EPSS files</td>
          <td>Verified (computed)</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>