<?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>Epss | FirewallSync</title><link>https://firewallsync.com/tags/epss/</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/epss/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><item><title>Vulnerability Prioritization Workflow: CVE Record to Ticket</title><link>https://firewallsync.com/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/</link><guid>https://firewallsync.com/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/</guid><pubDate>Mon, 02 Nov 2026 04:30:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Security Operations</category><description>A six-step patch-decision flow that takes a scanner finding from CVE record to ticket: check the record state, confirm you are affected, apply supplier VEX, read KEV and CISA's SSVC values, add EPSS, then let your exposure decide. Includes a tested Python script that writes the ticket fields.</description><enclosure url="https://firewallsync.com/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/cover_hu7197188011340061090.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/cover_hu7197188011340061090.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/cover_hu7197188011340061090.jpg" alt="View from the side of a passenger train of a railway switch where the tracks split, with a red point indicator, dry golden grassland and brown mountains behind" width="1200" height="630"></p><p><strong>Scope.</strong> This is a workflow guide built on public specifications and feeds, plus one script we ran. On November 2, 2026 we read the CVE JSON schema (5.2.0), CISA&rsquo;s Vulnrichment README, CERT/CC&rsquo;s SSVC deployer documentation and decision table, the OpenVEX specification and the EPSS repository README, and ran the script below against the KEV catalog (version 2026.10.30), the EPSS file scored 2026-11-01 and CVE records from cvelistV5. The assets, exposure ratings and the VEX document in the test are invented for the demonstration. The policy choices in the script are ours, not a standard.</p>
<h2 id="the-workflow-in-one-table">The workflow in one table</h2>
<p>Our comparison of <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS, EPSS and KEV</a> explains what each signal measures. This guide is the plumbing: the order in which a finding from a scanner or SBOM passes through the data, and what lands in the ticket.</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>Question</th>
          <th>Data you read</th>
          <th>Possible outcome</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Is the CVE record valid?</td>
          <td><code>cveMetadata.state</code> in the CVE record</td>
          <td>Close if <code>REJECTED</code></td>
      </tr>
      <tr>
          <td>2</td>
          <td>Do we run an affected version?</td>
          <td><code>containers.cna.affected</code>: <code>versions</code>, <code>defaultStatus</code></td>
          <td>Close if unaffected; escalate if unknown</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Has the supplier said we are not affected?</td>
          <td>VEX statement for this product</td>
          <td>Close with the VEX justification</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Is it being exploited?</td>
          <td>KEV entry; CISA ADP SSVC <code>Exploitation</code></td>
          <td>Sets the Exploitation input</td>
      </tr>
      <tr>
          <td>5</td>
          <td>How likely is exploitation?</td>
          <td>EPSS <code>epss</code>, <code>percentile</code></td>
          <td>Re-orders the non-exploited queue</td>
      </tr>
      <tr>
          <td>6</td>
          <td>How exposed and important is this asset?</td>
          <td>Your inventory: System Exposure, Human Impact</td>
          <td>Priority: Defer, Scheduled, Out-of-cycle, Immediate</td>
      </tr>
  </tbody>
</table>
<p>Steps 1 to 3 remove work. Steps 4 to 6 order what remains.</p>
<h2 id="step-1-check-the-record-state">Step 1: check the record state</h2>
<p>A CVE record is either <code>PUBLISHED</code> or <code>REJECTED</code> (the schema&rsquo;s two states), and published records can carry tags such as <code>disputed</code>. A rejected ID has no affected products to evaluate. In our test run, CVE-2026-23915 is a Fortinet-assigned ID that was rejected on January 20, 2026 with the reason &ldquo;Not used&rdquo;, so the finding closes. Our guide to <a href="/posts/cve-record-states-published-rejected-and-disputed-what-each-means-for-vulnerability-triage/">CVE record states</a> covers the tags and edge cases.</p>
<h2 id="step-2-confirm-you-are-affected">Step 2: confirm you are affected</h2>
<p>The CNA&rsquo;s <code>affected</code> block is an algorithm, not a list: walk <code>versions</code>, take the first match, otherwise use <code>defaultStatus</code>, and if that is missing the schema says the status is <code>unknown</code>. Scanner results that only match a product name skip this. Our explainer on <a href="/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/">CVE affected versions</a> shows how <code>lessThan</code>, <code>defaultStatus</code> and <code>versionType</code> combine, with a tested evaluator. Keep three outcomes apart in the ticket: affected, unaffected, and unknown or undetermined. The last needs a person or the vendor advisory.</p>
<h2 id="step-3-apply-supplier-vex">Step 3: apply supplier VEX</h2>
<p>If the vulnerable component ships inside someone else&rsquo;s product, the supplier may publish a VEX (Vulnerability Exploitability eXchange) statement. OpenVEX defines four statuses: <code>not_affected</code> (&ldquo;No remediation is required regarding this vulnerability&rdquo;), <code>affected</code>, <code>fixed</code> and <code>under_investigation</code>. A <code>not_affected</code> statement must carry a justification or an impact statement.</p>
<p>Two cautions. A VEX statement is about a specific product and version, so match on the product identifier, not on the CVE alone; a <code>not_affected</code> statement from an appliance vendor says nothing about your own services that use the same library. And read the justification before closing. Our guide to <a href="/posts/vex-explained-what-a-not-affected-statement-means-and-how-to-read-one/">reading a VEX not-affected statement</a> covers the five justifications and what to question.</p>
<h2 id="step-4-read-the-exploitation-evidence">Step 4: read the exploitation evidence</h2>
<p>Two public sources answer &ldquo;is this being exploited?&rdquo;:</p>
<ul>
<li><strong>KEV.</strong> The <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">CISA KEV JSON feed</a> lists CVEs with evidence of exploitation, with <code>dateAdded</code>, <code>dueDate</code>, <code>knownRansomwareCampaignUse</code> and, on newer entries, <code>forensicTriage</code>. The catalog we used (version 2026.10.30, released October 30, 2026) had 1,766 entries.</li>
<li><strong>CISA&rsquo;s SSVC values in the CVE record.</strong> CISA&rsquo;s Vulnrichment project writes three SSVC decision points (Exploitation, Automatable, Technical Impact) into a CISA-ADP container. Its README says CISA is &ldquo;assessing new and recent CVEs&rdquo;, and that it will not overwrite the CNA&rsquo;s data. Each value set carries a timestamp. Our <a href="/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/">Vulnrichment guide</a> shows where they sit and how to pull them with jq.</li>
</ul>
<p>The SSVC documentation describes Exploitation as &ldquo;the present state of exploitation&rdquo;, not a prediction. A <code>none</code> from months ago can be stale, so a KEV listing should outrank an older ADP value. That is the rule our script applies.</p>
<h2 id="step-5-add-epss-for-likelihood">Step 5: add EPSS for likelihood</h2>
<p>For findings with no exploitation evidence, EPSS estimates &ldquo;the probability [0-1] of exploitation in the wild in the next 30 days&rdquo;, and <code>percentile</code> gives the share of scored CVEs with the same or a lower score. The file we used (model v2026.06.15, scored 2026-11-01) held 398,784 CVEs. EPSS is a ranking aid within the non-exploited queue. It does not replace step 4: in our run the KEV-listed FortiMail flaw scored 0.04573. See the <a href="/posts/epss-data-feed-columns-percentile-vs-probability-and-how-to-join-it-to-a-scanner-export/">EPSS data feed guide</a> for the columns, model-version breaks and a scanner join.</p>
<h2 id="step-6-let-exposure-and-impact-decide">Step 6: let exposure and impact decide</h2>
<p>CERT/CC&rsquo;s SSVC deployer model maps four inputs to a patch priority:</p>
<table>
  <thead>
      <tr>
          <th>Input</th>
          <th>Values</th>
          <th>Who supplies it</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Exploitation</td>
          <td>none, public poc, active</td>
          <td>CISA ADP or KEV, or your own research</td>
      </tr>
      <tr>
          <td>System Exposure</td>
          <td>small, controlled, open</td>
          <td>You, per asset</td>
      </tr>
      <tr>
          <td>Automatable</td>
          <td>no, yes</td>
          <td>CISA ADP, or your own analysis</td>
      </tr>
      <tr>
          <td>Human Impact</td>
          <td>low, medium, high, very high</td>
          <td>You, per asset</td>
      </tr>
  </tbody>
</table>
<p>The decision table (<code>deployer_patch_application_priority_1_0_0.csv</code>, 72 rows) returns one of four outcomes, which the deployer documentation defines as: Defer (&ldquo;Do not act at present&rdquo;), Scheduled (&ldquo;Act during regularly scheduled maintenance time&rdquo;), Out-of-cycle (&ldquo;Act more quickly than usual&hellip; during the next available opportunity&rdquo;) and Immediate (&ldquo;Act immediately; focus all resources on applying the fix&rdquo;). In the table, Immediate appears in 3 rows, all with active exploitation and open exposure. Our <a href="/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/">SSVC deployer explainer</a> walks through the inputs, including where Human Impact is easy to get wrong.</p>
<p>One mapping detail: CISA&rsquo;s ADP data writes <code>poc</code>, while the deployer table uses <code>public poc</code>. They are not identical definitions. CISA&rsquo;s tree uses the older PoC value, which also covers privately attested exploitation and widespread hearsay; the newer Public PoC value covers only public exploit code or a well-known method. Our script maps one to the other, which can understate cases CISA marked <code>poc</code> on private evidence.</p>
<h2 id="a-tested-script-that-writes-the-ticket-fields">A tested script that writes the ticket fields</h2>
<p>The script reads a findings CSV, the KEV JSON, the EPSS daily file, the SSVC deployer CSV, a folder of CVE records and an optional OpenVEX file, and prints one ticket row per finding. Python 3.10 or later, standard library only.</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;Turn scanner findings into patch-decision ticket rows.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Inputs : findings.csv (asset,product,cve,exposure,human_impact)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">         KEV JSON, EPSS daily CSV (.csv.gz), SSVC deployer table CSV,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">         a directory of cvelistV5 records, optional OpenVEX file.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Output : tickets.csv on stdout. Python 3.10+, standard library only.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#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 style="color:#f92672">,</span> argparse<span style="color:#f92672">,</span> pathlib
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">load_kev</span>(path):
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(path))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {v[<span style="color:#e6db74">&#34;cveID&#34;</span>]<span style="color:#f92672">.</span>strip()<span style="color:#f92672">.</span>upper(): v <span style="color:#66d9ef">for</span> v <span style="color:#f92672">in</span> data[<span style="color:#e6db74">&#34;vulnerabilities&#34;</span>]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">load_epss</span>(path):
</span></span><span style="display:flex;"><span>    scores <span style="color:#f92672">=</span> {}
</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> fh:
</span></span><span style="display:flex;"><span>        rows <span style="color:#f92672">=</span> (line <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> fh <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> line<span style="color:#f92672">.</span>startswith(<span style="color:#e6db74">&#34;#&#34;</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> csv<span style="color:#f92672">.</span>DictReader(rows):
</span></span><span style="display:flex;"><span>            scores[r[<span style="color:#e6db74">&#34;cve&#34;</span>]] <span style="color:#f92672">=</span> (float(r[<span style="color:#e6db74">&#34;epss&#34;</span>]), float(r[<span style="color:#e6db74">&#34;percentile&#34;</span>]))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> scores
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">load_deployer</span>(path):
</span></span><span style="display:flex;"><span>    rows <span style="color:#f92672">=</span> list(csv<span style="color:#f92672">.</span>reader(open(path)))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {tuple(r[<span style="color:#ae81ff">1</span>:<span style="color:#ae81ff">5</span>]): r[<span style="color:#ae81ff">5</span>] <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> rows[<span style="color:#ae81ff">1</span>:]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">load_vex</span>(path):
</span></span><span style="display:flex;"><span>    out <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> path:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> s <span style="color:#f92672">in</span> json<span style="color:#f92672">.</span>load(open(path))<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;statements&#34;</span>, []):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> s<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;products&#34;</span>, []):
</span></span><span style="display:flex;"><span>                out[(s[<span style="color:#e6db74">&#34;vulnerability&#34;</span>][<span style="color:#e6db74">&#34;name&#34;</span>]<span style="color:#f92672">.</span>upper(), p[<span style="color:#e6db74">&#34;@id&#34;</span>])] <span style="color:#f92672">=</span> s
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> out
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">cisa_ssvc</span>(record):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Return CISA ADP SSVC values (Exploitation, Automatable, Technical Impact, timestamp).&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> adp <span style="color:#f92672">in</span> record[<span style="color:#e6db74">&#34;containers&#34;</span>]<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;adp&#34;</span>, []):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> adp<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;metrics&#34;</span>, []):
</span></span><span style="display:flex;"><span>            other <span style="color:#f92672">=</span> m<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;other&#34;</span>, {})
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> other<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;type&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;ssvc&#34;</span>:
</span></span><span style="display:flex;"><span>                c <span style="color:#f92672">=</span> other[<span style="color:#e6db74">&#34;content&#34;</span>]
</span></span><span style="display:flex;"><span>                opts <span style="color:#f92672">=</span> {k: v <span style="color:#66d9ef">for</span> o <span style="color:#f92672">in</span> c[<span style="color:#e6db74">&#34;options&#34;</span>] <span style="color:#66d9ef">for</span> k, v <span style="color:#f92672">in</span> o<span style="color:#f92672">.</span>items()}
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> (opts<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;Exploitation&#34;</span>), opts<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;Automatable&#34;</span>),
</span></span><span style="display:flex;"><span>                        opts<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;Technical Impact&#34;</span>), c<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;timestamp&#34;</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (<span style="color:#66d9ef">None</span>, <span style="color:#66d9ef">None</span>, <span style="color:#66d9ef">None</span>, <span style="color:#66d9ef">None</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    ap <span style="color:#f92672">=</span> argparse<span style="color:#f92672">.</span>ArgumentParser()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> a <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;findings&#34;</span>, <span style="color:#e6db74">&#34;kev&#34;</span>, <span style="color:#e6db74">&#34;epss&#34;</span>, <span style="color:#e6db74">&#34;deployer&#34;</span>, <span style="color:#e6db74">&#34;records&#34;</span>):
</span></span><span style="display:flex;"><span>        ap<span style="color:#f92672">.</span>add_argument(a)
</span></span><span style="display:flex;"><span>    ap<span style="color:#f92672">.</span>add_argument(<span style="color:#e6db74">&#34;--vex&#34;</span>)
</span></span><span style="display:flex;"><span>    args <span style="color:#f92672">=</span> ap<span style="color:#f92672">.</span>parse_args()
</span></span><span style="display:flex;"><span>    kev, epss <span style="color:#f92672">=</span> load_kev(args<span style="color:#f92672">.</span>kev), load_epss(args<span style="color:#f92672">.</span>epss)
</span></span><span style="display:flex;"><span>    table, vex <span style="color:#f92672">=</span> load_deployer(args<span style="color:#f92672">.</span>deployer), load_vex(args<span style="color:#f92672">.</span>vex)
</span></span><span style="display:flex;"><span>    out <span style="color:#f92672">=</span> csv<span style="color:#f92672">.</span>writer(sys<span style="color:#f92672">.</span>stdout)
</span></span><span style="display:flex;"><span>    out<span style="color:#f92672">.</span>writerow([<span style="color:#e6db74">&#34;asset&#34;</span>, <span style="color:#e6db74">&#34;cve&#34;</span>, <span style="color:#e6db74">&#34;record_state&#34;</span>, <span style="color:#e6db74">&#34;vex_status&#34;</span>, <span style="color:#e6db74">&#34;kev_dateAdded&#34;</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#34;kev_dueDate&#34;</span>, <span style="color:#e6db74">&#34;kev_ransomware&#34;</span>, <span style="color:#e6db74">&#34;cisa_exploitation&#34;</span>, <span style="color:#e6db74">&#34;cisa_automatable&#34;</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#34;cisa_technical_impact&#34;</span>, <span style="color:#e6db74">&#34;cisa_ssvc_timestamp&#34;</span>, <span style="color:#e6db74">&#34;epss&#34;</span>, <span style="color:#e6db74">&#34;epss_percentile&#34;</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#34;deployer_exploitation&#34;</span>, <span style="color:#e6db74">&#34;deployer_priority&#34;</span>, <span style="color:#e6db74">&#34;next_step&#34;</span>])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> f <span style="color:#f92672">in</span> csv<span style="color:#f92672">.</span>DictReader(open(args<span style="color:#f92672">.</span>findings)):
</span></span><span style="display:flex;"><span>        cve <span style="color:#f92672">=</span> f[<span style="color:#e6db74">&#34;cve&#34;</span>]<span style="color:#f92672">.</span>strip()<span style="color:#f92672">.</span>upper()
</span></span><span style="display:flex;"><span>        rec_path <span style="color:#f92672">=</span> pathlib<span style="color:#f92672">.</span>Path(args<span style="color:#f92672">.</span>records) <span style="color:#f92672">/</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>cve<span style="color:#e6db74">}</span><span style="color:#e6db74">.json&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> rec_path<span style="color:#f92672">.</span>exists():
</span></span><span style="display:flex;"><span>            out<span style="color:#f92672">.</span>writerow([f[<span style="color:#e6db74">&#34;asset&#34;</span>], cve, <span style="color:#e6db74">&#34;NO RECORD&#34;</span>] <span style="color:#f92672">+</span> [<span style="color:#e6db74">&#34;&#34;</span>] <span style="color:#f92672">*</span> <span style="color:#ae81ff">12</span> <span style="color:#f92672">+</span> [<span style="color:#e6db74">&#34;check the ID&#34;</span>])
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        rec <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(rec_path))
</span></span><span style="display:flex;"><span>        state <span style="color:#f92672">=</span> rec[<span style="color:#e6db74">&#34;cveMetadata&#34;</span>][<span style="color:#e6db74">&#34;state&#34;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> state <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;REJECTED&#34;</span>:
</span></span><span style="display:flex;"><span>            out<span style="color:#f92672">.</span>writerow([f[<span style="color:#e6db74">&#34;asset&#34;</span>], cve, state] <span style="color:#f92672">+</span> [<span style="color:#e6db74">&#34;&#34;</span>] <span style="color:#f92672">*</span> <span style="color:#ae81ff">12</span> <span style="color:#f92672">+</span> [<span style="color:#e6db74">&#34;close: CVE ID rejected&#34;</span>])
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        v <span style="color:#f92672">=</span> vex<span style="color:#f92672">.</span>get((cve, f<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;product&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>)), {})  <span style="color:#75715e"># VEX applies to one product, not every asset</span>
</span></span><span style="display:flex;"><span>        k <span style="color:#f92672">=</span> kev<span style="color:#f92672">.</span>get(cve, {})
</span></span><span style="display:flex;"><span>        exp, auto, ti, ts <span style="color:#f92672">=</span> cisa_ssvc(rec)
</span></span><span style="display:flex;"><span>        e, pct <span style="color:#f92672">=</span> epss<span style="color:#f92672">.</span>get(cve, (<span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Our policy choice: a KEV listing counts as active exploitation, whatever</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># the (possibly older) CISA ADP snapshot says. CISA writes &#34;poc&#34;; the</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># deployer table writes &#34;public poc&#34;.</span>
</span></span><span style="display:flex;"><span>        dexp <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;active&#34;</span> <span style="color:#66d9ef">if</span> k <span style="color:#66d9ef">else</span> {<span style="color:#e6db74">&#34;poc&#34;</span>: <span style="color:#e6db74">&#34;public poc&#34;</span>}<span style="color:#f92672">.</span>get(exp, exp)
</span></span><span style="display:flex;"><span>        prio <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> dexp <span style="color:#f92672">and</span> auto:
</span></span><span style="display:flex;"><span>            prio <span style="color:#f92672">=</span> table<span style="color:#f92672">.</span>get((dexp, f[<span style="color:#e6db74">&#34;exposure&#34;</span>], auto, f[<span style="color:#e6db74">&#34;human_impact&#34;</span>]), <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;status&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;not_affected&#34;</span>:
</span></span><span style="display:flex;"><span>            step <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;close: supplier VEX not_affected (&#34;</span> <span style="color:#f92672">+</span> v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;justification&#34;</span>, <span style="color:#e6db74">&#34;no justification&#34;</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;)&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> <span style="color:#f92672">not</span> prio:
</span></span><span style="display:flex;"><span>            step <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;manual triage: no CISA SSVC data&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            step <span style="color:#f92672">=</span> prio
</span></span><span style="display:flex;"><span>        out<span style="color:#f92672">.</span>writerow([f[<span style="color:#e6db74">&#34;asset&#34;</span>], cve, state, v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;status&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>), k<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;dateAdded&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>),
</span></span><span style="display:flex;"><span>                      k<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;dueDate&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>), k<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;knownRansomwareCampaignUse&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>),
</span></span><span style="display:flex;"><span>                      exp <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;&#34;</span>, auto <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;&#34;</span>, ti <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;&#34;</span>, ts <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;&#34;</span>, e, pct, dexp <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;&#34;</span>, prio, step])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    main()
</span></span></code></pre></div><p>The test input (<code>findings.csv</code>) used invented assets with real CVE IDs:</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>asset,product,cve,exposure,human_impact
</span></span><span style="display:flex;"><span>edge-mail-01,fortimail,CVE-2026-104286,open,high
</span></span><span style="display:flex;"><span>k8s-node-17,linux-kernel,CVE-2026-46300,controlled,medium
</span></span><span style="display:flex;"><span>build-runner-03,pkg:cargo/gix-fs@0.22.1,CVE-2026-100419,small,medium
</span></span><span style="display:flex;"><span>billing-api,pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1,CVE-2021-44228,open,very high
</span></span><span style="display:flex;"><span>vendor-appliance-2,pkg:generic/example-appliance@4.2,CVE-2021-44228,open,very high
</span></span><span style="display:flex;"><span>legacy-scan,unknown,CVE-2026-23915,open,low
</span></span></code></pre></div><p>The VEX file is an invented OpenVEX document from an &ldquo;Example Appliance Vendor&rdquo; stating <code>not_affected</code> with justification <code>vulnerable_code_not_present</code> for CVE-2021-44228 in <code>pkg:generic/example-appliance@4.2</code>. We ran:</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-bash" data-lang="bash"><span style="display:flex;"><span>python3 patch_decision.py findings.csv known_exploited_vulnerabilities.json <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  epss_scores-2026-11-01.csv.gz deployer_patch_application_priority_1_0_0.csv <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  records/ --vex vendor.openvex.json &gt; tickets.csv
</span></span></code></pre></div><p>The data came from <code>raw.githubusercontent.com/cisagov/kev-data</code>, the <code>empiricalsec/epss_scores</code> repository (2026/epss_scores-2026-11-01.csv.gz), <code>CERTCC/SSVC</code> (data/csv/ssvc/) and <code>CVEProject/cvelistV5</code> at commit 451e97a. Output, with some columns dropped for width:</p>
<table>
  <thead>
      <tr>
          <th>Asset</th>
          <th>CVE</th>
          <th>KEV added</th>
          <th>CISA SSVC (E / A / TI)</th>
          <th>EPSS (percentile)</th>
          <th>Deployer priority</th>
          <th>Next step</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>edge-mail-01</td>
          <td>CVE-2026-104286</td>
          <td>2026-10-01</td>
          <td>active / yes / total</td>
          <td>0.04573 (0.916)</td>
          <td>immediate</td>
          <td>immediate</td>
      </tr>
      <tr>
          <td>k8s-node-17</td>
          <td>CVE-2026-46300</td>
          <td>2026-10-28</td>
          <td>active / no / total</td>
          <td>0.1613 (0.969)</td>
          <td>scheduled</td>
          <td>scheduled</td>
      </tr>
      <tr>
          <td>build-runner-03</td>
          <td>CVE-2026-100419</td>
          <td>-</td>
          <td>poc / no / total</td>
          <td>0.00222 (0.129)</td>
          <td>scheduled</td>
          <td>scheduled</td>
      </tr>
      <tr>
          <td>billing-api</td>
          <td>CVE-2021-44228</td>
          <td>2021-12-10</td>
          <td>active / yes / total</td>
          <td>0.99999 (1.0)</td>
          <td>immediate</td>
          <td>immediate</td>
      </tr>
      <tr>
          <td>vendor-appliance-2</td>
          <td>CVE-2021-44228</td>
          <td>2021-12-10</td>
          <td>active / yes / total</td>
          <td>0.99999 (1.0)</td>
          <td>immediate</td>
          <td>close: supplier VEX not_affected (vulnerable_code_not_present)</td>
      </tr>
      <tr>
          <td>legacy-scan</td>
          <td>CVE-2026-23915</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>-</td>
          <td>close: CVE ID rejected</td>
      </tr>
  </tbody>
</table>
<p>Three things in that output are worth noticing:</p>
<ul>
<li><strong>The same CVE got two different answers</strong> (Log4Shell rows) because the VEX statement applies to one product only.</li>
<li><strong>A KEV-listed kernel flaw came out Scheduled.</strong> CVE-2026-46300 is in KEV with <code>forensicTriage: Yes</code> and a three-day federal due date, but with controlled exposure, no automatability and medium Human Impact the deployer table says Scheduled. If you follow CISA&rsquo;s directives, the due date wins. If you do not, the table is telling you the exposure is what drives urgency. Either way, keep both fields in the ticket, and consider a compromise check when <code>forensicTriage</code> is Yes; our <a href="/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/">BOD 26-04 explainer</a> covers what that flag asks agencies to do.</li>
<li><strong>EPSS and KEV disagreed</strong> on FortiMail, as expected: one estimates future observed activity, the other records evidence already seen.</li>
</ul>
<h2 id="what-the-ticket-should-hold">What the ticket should hold</h2>
<p>Store the inputs, not just the priority, so a reviewer can see why. A suggested field list (ours, not a standard):</p>
<table>
  <thead>
      <tr>
          <th>Field</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Asset, product identifier (purl or CPE), version</td>
          <td>Ties VEX and affected checks to the right thing</td>
      </tr>
      <tr>
          <td>CVE ID, record state, record <code>dateUpdated</code></td>
          <td>Detects rejected IDs and record changes</td>
      </tr>
      <tr>
          <td>Affected status and how it was decided</td>
          <td>&ldquo;unknown&rdquo; needs a different owner from &ldquo;affected&rdquo;</td>
      </tr>
      <tr>
          <td>VEX status, justification, document author and timestamp</td>
          <td>Audit trail for closures</td>
      </tr>
      <tr>
          <td>KEV <code>dateAdded</code>, <code>dueDate</code>, <code>knownRansomwareCampaignUse</code>, <code>forensicTriage</code></td>
          <td>Exploitation evidence and any federal deadline</td>
      </tr>
      <tr>
          <td>CISA SSVC Exploitation, Automatable, Technical Impact, timestamp</td>
          <td>Shows how fresh the assessment is</td>
      </tr>
      <tr>
          <td>EPSS score, percentile, model version, score date</td>
          <td>Scores shift when the model changes</td>
      </tr>
      <tr>
          <td>Your System Exposure and Human Impact</td>
          <td>The inputs only you can supply</td>
      </tr>
      <tr>
          <td>Priority and data pull date</td>
          <td>Lets you re-run and compare</td>
      </tr>
  </tbody>
</table>
<h2 id="keeping-it-current">Keeping it current</h2>
<p>The feeds move at different speeds: CISA updates KEV on weekdays, EPSS publishes daily, and CVE records and VEX documents change when their authors revise them. Re-run the flow on a schedule and on any change to a ticket&rsquo;s inputs, and alert when a finding&rsquo;s priority rises. Set your service-level targets per priority (for example, how many days &ldquo;Out-of-cycle&rdquo; means for you) in policy, not in the script; the deployer model does not define days.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The script is a working example, not a product. It reads CVE records from a local folder (it does not fetch them), does not evaluate affected versions itself (pair it with a version check such as the evaluator in our affected-versions guide), handles OpenVEX JSON only, and uses one policy choice (KEV overrides an older CISA Exploitation value) that you may want to change. The four-value deployer inputs simplify real decisions: SSVC expects organizations to adapt trees to their context. We did not test it against commercial scanner exports.</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>CVE record states, <code>affected</code> algorithm, <code>defaultStatus</code> defaults to unknown</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE JSON schema (CVE_Record_Format.json, 5.2.0)</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE-2026-23915 rejected, reason &ldquo;Not used&rdquo;; SSVC values and timestamps for the test CVEs</td>
          <td><a href="https://github.com/CVEProject/cvelistV5">cvelistV5</a>, commit 451e97a</td>
          <td>Verified (read November 2, 2026)</td>
      </tr>
      <tr>
          <td>Vulnrichment assesses &ldquo;new and recent CVEs&rdquo;, adds SSVC decision points, never overwrites CNA data</td>
          <td><a href="https://github.com/cisagov/vulnrichment">cisagov/vulnrichment README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>KEV catalog 2026.10.30, 1,766 entries; dates and flags for the test CVEs</td>
          <td><a href="https://github.com/cisagov/kev-data">cisagov/kev-data</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>EPSS field definitions; file model v2026.06.15, 398,784 CVEs; scores for the test CVEs</td>
          <td><a href="https://github.com/empiricalsec/epss_scores">empiricalsec/epss_scores</a></td>
          <td>Verified (computed from the file)</td>
      </tr>
      <tr>
          <td>Deployer inputs, values, 72-row table, outcome definitions, active never Defer</td>
          <td><a href="https://github.com/CERTCC/SSVC/blob/main/docs/howto/deployer_tree.md">CERT/CC SSVC deployer tree</a> and <a href="https://github.com/CERTCC/SSVC/blob/main/data/csv/ssvc/deployer_patch_application_priority_1_0_0.csv">decision table CSV</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Exploitation is &ldquo;the present state of exploitation&rdquo;; PoC vs Public PoC definitions</td>
          <td><a href="https://github.com/CERTCC/SSVC/blob/main/docs/reference/decision_points/exploitation.md">SSVC Exploitation decision point</a> and its source definitions</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>OpenVEX statuses and the justification requirement</td>
          <td><a href="https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md">OpenVEX specification</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Script output</td>
          <td>Script above, Python 3.13, run November 2, 2026</td>
          <td>Verified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>