<?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>Cve Json 5 | FirewallSync</title><link>https://firewallsync.com/tags/cve-json-5/</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/cve-json-5/index.xml" rel="self" type="application/rss+xml"/><item><title>Linux Kernel CVE Records: Read Stable-Branch Affected Ranges</title><link>https://firewallsync.com/posts/linux-kernel-cve-records-affected-versions-stable-branches/</link><guid>https://firewallsync.com/posts/linux-kernel-cve-records-affected-versions-stable-branches/</guid><pubDate>Sat, 07 Nov 2026 04:50:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>Every Linux kernel CVE record carries two product entries: git commit ranges and kernel version ranges. We read all 7,987 kernel CNA records published in 2026 to show how the two fit together, where reading only one goes wrong (1,056 records), why the CPE block over-matches fixed kernels, and a tested Python checker for a kernel.org version.</description><enclosure url="https://firewallsync.com/posts/linux-kernel-cve-records-affected-versions-stable-branches/cover_hu12897958757966638349.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/linux-kernel-cve-records-affected-versions-stable-branches/cover_hu12897958757966638349.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/linux-kernel-cve-records-affected-versions-stable-branches/cover_hu12897958757966638349.jpg" alt="A grove of leafless trees with dark, twisting trunks that split into many crossing branches above a pale yellow lawn" width="1200" height="630"></p><p>A Linux kernel CVE record answers &ldquo;is my kernel affected?&rdquo; in two places at once: a list of <strong>git commit ranges</strong> and a list of <strong>kernel version ranges</strong>, each in its own product entry with a different <code>defaultStatus</code>. Read the version entry from top to bottom, stop at the first match, and fall back to <code>affected</code> for anything unlisted. Then check the git entry too, because in about one record in eight it carries version ranges the other entry does not.</p>
<p>We read every record the Linux kernel CNA published in 2026 (7,987 records, January 21 to November 6) from the CVE Program&rsquo;s <a href="https://github.com/CVEProject/cvelistV5">cvelistV5</a> bulk export of November 7, 2026, and counted how they are built. This article covers the kernel-specific layout. For the general rules of <code>version</code>, <code>lessThan</code>, <code>lessThanOrEqual</code> and <code>defaultStatus</code> in any CVE record, see our <a href="/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/">guide to CVE affected versions</a>.</p>
<h2 id="what-a-kernel-cve-record-contains">What a kernel CVE record contains</h2>
<p>The kernel&rsquo;s <a href="https://docs.kernel.org/process/cve.html">CVE process document</a> sets the ground rules. CVEs are assigned &ldquo;after a fix is available and applied to a stable kernel tree&rdquo;, and are tracked &ldquo;by the git commit id of the original fix&rdquo;. No CVEs are assigned for kernel versions that are not &ldquo;currently being actively supported by the Stable/LTS kernel team&rdquo;. And applicability is yours to decide: &ldquo;the applicability of any specific CVE is up to the user of Linux to determine, it is not up to the CVE assignment team.&rdquo;</p>
<p>That produces a very regular structure. Across the 7,987 records:</p>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Records</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Exactly two entries in <code>affected</code>, both product &ldquo;Linux&rdquo;, vendor &ldquo;Linux&rdquo;</td>
          <td>7,987</td>
      </tr>
      <tr>
          <td><code>repo</code> set to the stable tree (<code>git.kernel.org/.../stable/linux.git</code>)</td>
          <td>7,987</td>
      </tr>
      <tr>
          <td><code>programFiles</code> listing the changed source files</td>
          <td>7,987</td>
      </tr>
      <tr>
          <td>A <code>cpeApplicability</code> block</td>
          <td>7,987</td>
      </tr>
      <tr>
          <td>Version entry starts with the introducing release, then <code>0</code> to it unaffected</td>
          <td>7,911</td>
      </tr>
      <tr>
          <td>Version entry ends with an <code>original_commit_for_fix</code> range to <code>*</code></td>
          <td>7,917</td>
      </tr>
      <tr>
          <td>CNA-supplied CVSS v3.1 metrics</td>
          <td>3,154</td>
      </tr>
  </tbody>
</table>
<p>Every record has the two entries. In 7,923 of them the order is the same: the git entry (<code>defaultStatus</code> unaffected) first, the version entry (<code>defaultStatus</code> affected) second. In the remaining 64, both entries have <code>defaultStatus</code> unaffected, so only explicitly listed versions are affected. The checker below handles both shapes because it evaluates every entry.</p>
<h2 id="entry-1-git-commit-ranges">Entry 1: git commit ranges</h2>
<p>The first product entry has <code>&quot;defaultStatus&quot;: &quot;unaffected&quot;</code> and a list of commit ranges. Here is CVE-2026-99190 (gpiolib, published October 28, 2026), trimmed to the version list:</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-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;d73d2a569e6212f58d89efc6e66ffbb2386a47b1&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;68a2cd5381b8d89dc80bd16dd9cc9ca2d5c25e7f&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;git&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;9b407312755fd5db012413ca005f0f3a661db8dd&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;78189c0e35abbcdab3b8ab0b764d578c02900734&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;git&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;9b407312755fd5db012413ca005f0f3a661db8dd&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;890c3bf08e0fc09eff788a6cfeded7d21b4d3e9d&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;git&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;9b407312755fd5db012413ca005f0f3a661db8dd&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;c28846ca1ac6134881114d272314623c8433e372&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;git&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;6.12.114&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;6.12.115&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}
</span></span></code></pre></div><p>Each git range reads &ldquo;from the commit that introduced the bug, up to but not including a commit that fixed it&rdquo;. The fix commits are the same ones listed in the record&rsquo;s <code>references</code> as <code>git.kernel.org/stable/c/&lt;hash&gt;</code> links, one per branch that received the fix. The CVE schema says <code>repo</code> exists &ldquo;to resolve git hash version ranges&rdquo;, which is the point: comparing commits is not a string comparison. You need a kernel git checkout to ask whether your build contains the introducing commit and not the fix.</p>
<p>If you build kernels from source, this is the most precise data in the record. Most teams run a released version instead, which is what the second entry is for. But note the last line above: a kernel version range inside the git entry. We come back to it below.</p>
<h2 id="entry-2-kernel-version-ranges-first-match-wins">Entry 2: kernel version ranges, first match wins</h2>
<p>The second entry has <code>&quot;defaultStatus&quot;: &quot;affected&quot;</code>. For CVE-2026-99190 it is:</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-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;6.15&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;6.15&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;6.12.115&#34;</span>, <span style="color:#f92672">&#34;lessThanOrEqual&#34;</span>: <span style="color:#e6db74">&#34;6.12.*&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;6.18.59&#34;</span>, <span style="color:#f92672">&#34;lessThanOrEqual&#34;</span>: <span style="color:#e6db74">&#34;6.18.*&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;7.2.13&#34;</span>, <span style="color:#f92672">&#34;lessThanOrEqual&#34;</span>: <span style="color:#e6db74">&#34;7.2.*&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;7.3&#34;</span>, <span style="color:#f92672">&#34;lessThanOrEqual&#34;</span>: <span style="color:#e6db74">&#34;*&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;original_commit_for_fix&#34;</span>}
</span></span></code></pre></div><p>The <a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE schema</a> gives the rule: walk the list, return the status of the first entry that matches, and return <code>defaultStatus</code> if none do. Applied here:</p>
<ol>
<li><strong>6.15 is affected.</strong> A bare <code>version</code> with no range is an exact match. This is the release that introduced the bug.</li>
<li><strong>Anything before 6.15 is unaffected.</strong> <code>0</code> is the schema&rsquo;s convention for &ldquo;the earliest possible version&rdquo;.</li>
<li><strong>One unaffected range per fixed stable branch.</strong> <code>6.12.115</code> to <code>6.12.*</code> means 6.12.115 and every later 6.12 release. The schema says a trailing <code>*</code> means &ldquo;an arbitrarily large number in the version ordering&rdquo;, so <code>6.12.*</code> closes the range at the end of the 6.12 branch without touching 6.13.</li>
<li><strong>The mainline fix onward is unaffected.</strong> The <code>7.3 ... *</code> entry covers 7.3 and every later release.</li>
<li><strong>Everything else is affected.</strong> 6.18.58, any 6.19 kernel, or 7.2.12 matches no entry and takes <code>defaultStatus</code>.</li>
</ol>
<p>Point 5 is the reason the order and the default both matter. A branch that does not appear at all, such as 6.19 here, has no fix listed and falls through to affected. The kernel&rsquo;s <a href="https://kernel.org/releases.html">releases page</a> currently lists 6.18, 6.12, 6.6, 6.1, 5.15 and 5.10 as longterm branches. If your kernel sits on a branch that a record does not list, the record gives you no fixed version on that branch; the way out is moving to a branch that has one.</p>
<h3 id="the-original_commit_for_fix-type-is-not-defined">The <code>original_commit_for_fix</code> type is not defined</h3>
<p>The last entry&rsquo;s <code>versionType</code> is <code>original_commit_for_fix</code>. It is not among the schema&rsquo;s <code>versionType</code> examples (&ldquo;custom&rdquo;, &ldquo;git&rdquo;, &ldquo;maven&rdquo;, &ldquo;python&rdquo;, &ldquo;rpm&rdquo;, &ldquo;semver&rdquo;), and the kernel&rsquo;s CVE process document does not mention it. From the records, the value looks like the first mainline release containing the original fix, and every one we saw was a release number such as <code>7.3</code> or <code>7.4-rc1</code>. That is our reading of the data, not a published definition.</p>
<p>The practical effect: a parser that refuses unknown version types stops at this entry for every version not matched earlier, which in practice means &ldquo;unknown&rdquo; for any kernel above the newest fixed stable branch. Our <a href="/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/">generic affected-versions guide</a> shows a strict evaluator doing exactly that on a kernel record. If you choose to compare it as a normal kernel version, which is what the checker below does, make that a documented decision.</p>
<h2 id="why-you-must-read-both-entries">Why you must read both entries</h2>
<p>The <code>6.12.114 lessThan 6.12.115</code> line in the git entry is the trap. In CVE-2026-99190, the bug arrived in mainline at 6.15, so the version entry says every 6.12 kernel is &ldquo;before 6.15&rdquo; and unaffected. But the git entry marks 6.12.114 as affected and the version entry lists a 6.12.115 fix. The commit ranges hint at why: the first range starts from a different commit (<code>d73d2a5...</code>) than the other three (<code>9b40731...</code>), consistent with the bug reaching 6.12 as a stable backport. Read only the version entry and 6.12.114 comes out clean.</p>
<p>This is not rare. In 1,084 of the 7,987 records, the first (git) entry also contains <code>semver</code> ranges. In <strong>1,056 records</strong> (3,136 ranges), at least one of those ranges marks a stable release affected that the version entry, evaluated alone, calls unaffected.</p>
<p>Running the checker below against the record:</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 kernel_affected.py CVE-2026-99190.json 6.12.113 6.12.114 6.18.58 6.18.59 6.19.10 7.3 6.12.114-1-amd64
</span></span><span style="display:flex;"><span>CVE-2026-99190 6.12.113: unaffected
</span></span><span style="display:flex;"><span>CVE-2026-99190 6.12.114: affected
</span></span><span style="display:flex;"><span>CVE-2026-99190 6.18.58: affected
</span></span><span style="display:flex;"><span>CVE-2026-99190 6.18.59: unaffected
</span></span><span style="display:flex;"><span>CVE-2026-99190 6.19.10: affected
</span></span><span style="display:flex;"><span>CVE-2026-99190 7.3: unaffected
</span></span><span style="display:flex;"><span>CVE-2026-99190 6.12.114-1-amd64: distribution kernel suffix; use your distro&#39;s tracker
</span></span></code></pre></div><p>(Per-entry lines trimmed; for 6.12.114 the checker reports entry[0] affected via the <code>6.12.114</code>-<code>6.12.115</code> range and entry[1] unaffected via <code>0</code>-<code>6.15</code>.)</p>
<h2 id="why-cpe-matching-over-flags-fixed-kernels">Why CPE matching over-flags fixed kernels</h2>
<p>Every 2026 kernel record also has a <code>cpeApplicability</code> block. For CVE-2026-100075, it is one OR node of eight ranges on <code>cpe:2.3:o:linux:linux_kernel</code>, each starting at 4.7 and ending before one fixed version: 5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.110, 6.18.52, 7.2.6, and 7.3. OR&rsquo;d together, those ranges collapse to &ldquo;4.7 up to 7.3&rdquo;, so 5.10.300, a fixed 5.10 kernel, still matches the 7.3 range.</p>
<p>We checked this across the corpus. The version entries name 36,296 fixed stable releases (the start of each <code>X.Y.N</code> to <code>X.Y.*</code> unaffected range). The OR&rsquo;d CPE ranges in the same records match <strong>35,340 of them (97.4%) as vulnerable</strong>, in 7,917 records. CPE ranges have no way to say &ldquo;fixed on the 6.12 branch at 6.12.110&rdquo;; the affected block does. We covered this limit with one example in our <a href="/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/">cpeApplicability guide</a>; the corpus count shows it is the norm for kernel records, not an edge case.</p>
<p>If your scanner or SBOM tool matches kernel CVEs by CPE and reports patched LTS kernels as vulnerable, this is the first thing to check.</p>
<h2 id="distribution-kernels-stop-here">Distribution kernels: stop here</h2>
<p>kernel.org is direct about this: &ldquo;If you see anything at all after the dash, you are running a distribution kernel&rdquo;, and &ldquo;kernel developers can provide no support for them&rdquo;. A RHEL, Ubuntu or Debian kernel version reflects the distribution&rsquo;s base plus its own backports, so <code>5.14.0-...</code> or <code>6.8.0-...</code> cannot be compared to the kernel.org ranges in a CVE record. The kernel&rsquo;s CVE document also says issues only present in a distribution&rsquo;s kernel get CVEs from that distribution, not from the kernel team.</p>
<p>For those kernels, use the distribution&rsquo;s security tracker or its VEX/CSAF data. Our <a href="/posts/vex-explained-what-a-not-affected-statement-means-and-how-to-read-one/">VEX explainer</a> covers how to read a &ldquo;not affected&rdquo; statement from a vendor.</p>
<h2 id="a-tested-checker-for-kernelorg-versions">A tested checker for kernel.org versions</h2>
<p>The script below implements the schema&rsquo;s first-match algorithm over <strong>both</strong> product entries, skips git commit ranges (they need a kernel checkout), and reports affected if either entry says so. It refuses versions with a distribution suffix. We ran it with Python 3.12 against CVE-2026-99190 (output above) and across all 7,987 2026 records to produce the counts in this article.</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;Evaluate a kernel.org version against a Linux kernel CNA record (CVE JSON 5).
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">usage: kernel_affected.py CVE-record.json VERSION [VERSION ...]&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json<span style="color:#f92672">,</span> re<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">key</span>(v):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Kernel version -&gt; comparable tuple. &#39;*&#39; sorts above any number; -rcN sorts before final.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> v <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;*&#34;</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> ((float(<span style="color:#e6db74">&#34;inf&#34;</span>),),)
</span></span><span style="display:flex;"><span>    m <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>fullmatch(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#34;(\d+(?:\.\d+)*)(?:\.(\*))?(?:-rc(\d+))?&#34;</span>, v)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> m:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;not a kernel.org version: </span><span style="color:#e6db74">{</span>v<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    nums <span style="color:#f92672">=</span> tuple(int(p) <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> m<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>)<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;.&#34;</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> m<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">2</span>):                                   <span style="color:#75715e"># &#34;6.12.*&#34; -&gt; 6.12.&lt;infinity&gt;</span>
</span></span><span style="display:flex;"><span>        nums <span style="color:#f92672">+=</span> (float(<span style="color:#e6db74">&#34;inf&#34;</span>),)
</span></span><span style="display:flex;"><span>    rc <span style="color:#f92672">=</span> int(m<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">3</span>)) <span style="color:#66d9ef">if</span> m<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">3</span>) <span style="color:#66d9ef">else</span> float(<span style="color:#e6db74">&#34;inf&#34;</span>)  <span style="color:#75715e"># 7.4-rc1 &lt; 7.4</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (nums <span style="color:#f92672">+</span> (<span style="color:#ae81ff">0</span>,) <span style="color:#f92672">*</span> (<span style="color:#ae81ff">4</span> <span style="color:#f92672">-</span> len(nums)), rc)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">status</span>(product, v):
</span></span><span style="display:flex;"><span>    k <span style="color:#f92672">=</span> key(v)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> e <span style="color:#f92672">in</span> product<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;versions&#34;</span>, []):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> e<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;versionType&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;git&#34;</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>                                 <span style="color:#75715e"># commit ranges need a kernel git checkout</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;lessThan&#34;</span> <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> e <span style="color:#f92672">and</span> <span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span> <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> e:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> key(e[<span style="color:#e6db74">&#34;version&#34;</span>]) <span style="color:#f92672">==</span> k:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> e[<span style="color:#e6db74">&#34;status&#34;</span>], e
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        lo <span style="color:#f92672">=</span> key(e[<span style="color:#e6db74">&#34;version&#34;</span>])
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;lessThan&#34;</span> <span style="color:#f92672">in</span> e <span style="color:#f92672">and</span> lo <span style="color:#f92672">&lt;=</span> k <span style="color:#f92672">&lt;</span> key(e[<span style="color:#e6db74">&#34;lessThan&#34;</span>]):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> e[<span style="color:#e6db74">&#34;status&#34;</span>], e
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span> <span style="color:#f92672">in</span> e <span style="color:#f92672">and</span> lo <span style="color:#f92672">&lt;=</span> k <span style="color:#f92672">&lt;=</span> key(e[<span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span>]):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> e[<span style="color:#e6db74">&#34;status&#34;</span>], e
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> product<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;defaultStatus&#34;</span>, <span style="color:#e6db74">&#34;unknown&#34;</span>), <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>rec <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>]))
</span></span><span style="display:flex;"><span>cve <span style="color:#f92672">=</span> rec[<span style="color:#e6db74">&#34;cveMetadata&#34;</span>][<span style="color:#e6db74">&#34;cveId&#34;</span>]
</span></span><span style="display:flex;"><span>prods <span style="color:#f92672">=</span> rec[<span style="color:#e6db74">&#34;containers&#34;</span>][<span style="color:#e6db74">&#34;cna&#34;</span>][<span style="color:#e6db74">&#34;affected&#34;</span>]       <span style="color:#75715e"># kernel records carry two product entries</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> v <span style="color:#f92672">in</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">2</span>:]:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;-&#34;</span> <span style="color:#f92672">in</span> v <span style="color:#f92672">and</span> <span style="color:#f92672">not</span> re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#34;-rc\d+$&#34;</span>, v):
</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>cve<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>v<span style="color:#e6db74">}</span><span style="color:#e6db74">: distribution kernel suffix; use your distro&#39;s tracker&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>    verdicts <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i, p <span style="color:#f92672">in</span> enumerate(prods):
</span></span><span style="display:flex;"><span>        s, e <span style="color:#f92672">=</span> status(p, v)
</span></span><span style="display:flex;"><span>        why <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;defaultStatus&#34;</span> <span style="color:#66d9ef">if</span> e <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#66d9ef">else</span> json<span style="color:#f92672">.</span>dumps({k: e[k] <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> e <span style="color:#66d9ef">if</span> k <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;status&#34;</span>})
</span></span><span style="display:flex;"><span>        verdicts<span style="color:#f92672">.</span>append(s)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  entry[</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">] defaultStatus=</span><span style="color:#e6db74">{</span>p<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;defaultStatus&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>s<span style="color:#e6db74">}</span><span style="color:#e6db74"> (matched </span><span style="color:#e6db74">{</span>why<span style="color:#e6db74">}</span><span style="color:#e6db74">)&#34;</span>)
</span></span><span style="display:flex;"><span>    final <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;affected&#34;</span> <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;affected&#34;</span> <span style="color:#f92672">in</span> verdicts <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;unaffected&#34;</span> <span style="color:#66d9ef">if</span> all(x <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;unaffected&#34;</span> <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> verdicts) <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;unknown&#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>cve<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>v<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>final<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span></code></pre></div><p>Limits: it treats <code>original_commit_for_fix</code> as a kernel version comparison (see above), ignores git ranges, and only understands kernel.org version strings. It is a reference for reading the data, not a vulnerability scanner.</p>
<h2 id="turning-the-answer-into-a-decision">Turning the answer into a decision</h2>
<p>&ldquo;Affected&rdquo; in a kernel record means the code is in your build, not that it is reachable. The kernel team says plainly that &ldquo;large numbers of assigned CVEs are not relevant for their systems&rdquo;, and the <code>programFiles</code> list (here <code>drivers/gpio/gpiolib.c</code>) is a quick first filter against your kernel config. After that, the usual signals apply: is it in KEV, what does EPSS say, what is exposed. Our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">vulnerability prioritization workflow</a> walks through that decision from the CVE record to a ticket.</p>
<p>The kernel team&rsquo;s own advice is to take whole stable releases rather than cherry-pick: &ldquo;it is best to take all released kernel changes, as they are tested together in a unified whole&rdquo;. For most teams, staying on a current LTS point release answers more of these records than evaluating them one by one.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The counts cover Linux kernel CNA records with IDs CVE-2026-* in the PUBLISHED state, from the cvelistV5 export of November 7, 2026. The CNA updates records, so counts will drift. We did not check git ranges against a kernel tree, so the backport explanation for CVE-2026-99190 is an inference from the record. The meaning of <code>original_commit_for_fix</code> is our reading of the data, not a published definition.</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>Record counts, entry structure, 1,056 records / 3,136 ranges, 36,296 fixed releases and 35,340 CPE matches</td>
          <td><a href="https://github.com/CVEProject/cvelistV5">CVEProject/cvelistV5</a> bulk export 2026-11-07, computed with the script above</td>
          <td>Verified (computed)</td>
      </tr>
      <tr>
          <td>CVE-2026-99190 and CVE-2026-100075 affected and cpeApplicability data</td>
          <td><a href="https://github.com/CVEProject/cvelistV5">cvelistV5 records</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>First-match algorithm, <code>defaultStatus</code> fallback, <code>*</code> wildcard, <code>repo</code> for git ranges, <code>versionType</code> examples</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE JSON schema</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVEs assigned after a fix reaches stable, tracked by original fix commit; only supported branches; applicability is the user&rsquo;s; take all stable changes</td>
          <td><a href="https://docs.kernel.org/process/cve.html">Linux kernel CVE process</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Longterm branches; text after the dash means a distribution kernel</td>
          <td><a href="https://kernel.org/releases.html">kernel.org releases</a></td>
          <td>Verified, Nov 7, 2026</td>
      </tr>
      <tr>
          <td>Meaning of <code>original_commit_for_fix</code></td>
          <td>Record data only; no definition found</td>
          <td>Qualified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>CVE packageURL: Join CVE JSON 5.2 Records to SBOM purls</title><link>https://firewallsync.com/posts/cve-packageurl-purl-cve-json-5-2-affected-sbom-matching/</link><guid>https://firewallsync.com/posts/cve-packageurl-purl-cve-json-5-2-affected-sbom-matching/</guid><pubDate>Thu, 05 Nov 2026 04:40:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>CVE Record Format 5.2.0 added an optional packageURL field to affected entries. We counted it across all 400,445 published records: 1.6% carry one, 12 CNAs produce them, and a third of GitHub purls are not in canonical lowercase. Here is what the field means, how to normalize it, and a tested Python join against a CycloneDX SBOM.</description><enclosure url="https://firewallsync.com/posts/cve-packageurl-purl-cve-json-5-2-affected-sbom-matching/cover_hu10950611722794385026.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cve-packageurl-purl-cve-json-5-2-affected-sbom-matching/cover_hu10950611722794385026.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cve-packageurl-purl-cve-json-5-2-affected-sbom-matching/cover_hu10950611722794385026.jpg" alt="Black-and-white close-up of a bank of grey metal mailboxes, each door marked with a box number and a four-digit unit number" width="1200" height="630"></p><p><strong>Scope.</strong> This is a specification explainer with measurements from public data. We read the CVE Record Format schema on the CVEProject/cve-schema main branch (default <code>dataVersion</code> 5.2.0), RFD 0002 in the same repository, and the Package URL specification in the package-url/purl-spec repository. We counted fields across every record in the cvelistV5 release <code>cve_2026-11-05_0400Z</code> (the &ldquo;all CVEs at midnight&rdquo; bundle for 5 November 2026) with Python 3.13, and ran the join script below against that data. The script is illustrative.</p>
<h2 id="the-short-answer">The short answer</h2>
<p>Since CVE Record Format 5.2.0, each entry in a record&rsquo;s <code>affected</code> array can carry a <code>packageURL</code>:</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;vendor&#34;</span>: <span style="color:#e6db74">&#34;fast-uri&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;product&#34;</span>: <span style="color:#e6db74">&#34;fast-uri&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;packageURL&#34;</span>: <span style="color:#e6db74">&#34;pkg:npm/fast-uri&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;defaultStatus&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;versions&#34;</span>: [
</span></span><span style="display:flex;"><span>    {<span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.4.2&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;2.4.8&#34;</span>}
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That fragment is from CVE-2026-110180 (trimmed to its first version range). Three rules decide how you use it:</p>
<ol>
<li><strong>No version in the purl.</strong> The schema says the Package URL &ldquo;MUST NOT include a version&rdquo;. Strip the version from your SBOM purl before you look it up.</li>
<li><strong>The versions array still decides.</strong> A purl match finds the records about your package. Whether your version is affected comes from <code>versions</code> and <code>defaultStatus</code>, read with the record&rsquo;s <code>versionType</code>.</li>
<li><strong>It is optional and still rare.</strong> 1.6% of published records carry it today, from 12 CNAs. Treat it as a fast, precise path where it exists, and keep your CPE and name-based matching for the rest.</li>
</ol>
<h2 id="what-the-schema-says">What the schema says</h2>
<p>The field definition in the 5.2.0 schema:</p>
<blockquote>
<p>&ldquo;A Package URL, a unified URL specification for identifying packages hosted by known package hosts. The Package URL MUST NOT include a version.&rdquo;</p>
</blockquote>
<p>The schema types it as a URI and lists examples such as <code>pkg:deb/debian/curl?arch=i386&amp;distro=jessie</code>, <code>pkg:npm/%40angular/animation</code> and <code>pkg:golang/google.golang.org/genproto#googleapis/api/annotations</code>. So qualifiers and subpaths are allowed; only the version is out.</p>
<p><code>packageURL</code> does not satisfy the identifier requirement on its own. The <code>product</code> definition still requires either <code>vendor</code> and <code>product</code> or <code>collectionURL</code> and <code>packageName</code>, plus either <code>versions</code> or <code>defaultStatus</code>. In our scan, every one of the 7,324 entries with a purl also had one of those identifier pairs.</p>
<h3 id="why-it-was-added">Why it was added</h3>
<p>RFD 0002, &ldquo;Supporting Package URLs in CVE&rdquo;, is the design record. It argues that the existing identifier fields &ldquo;are of limited value for automated applicability matching&rdquo; and that Package URLs &ldquo;are better suited than CPEs for capturing distinctions between different distributions of particular open source software&rdquo;.</p>
<p>Two design choices in the RFD matter to consumers:</p>
<ul>
<li><strong>Validation is mostly outside the schema.</strong> The JSON schema only checks that the value is a URI; the RFD says it &ldquo;would be further constrained by CVE Services&rdquo; to ensure the values are valid purls, because encoding purl rules in a regular expression &ldquo;would be excessive and difficult to maintain&rdquo;. Do not assume the schema has validated purl syntax for you.</li>
<li><strong>Versions live in one place.</strong> &ldquo;All version information should only be included within the existing <code>versions</code> field.&rdquo; That keeps one version-matching code path for consumers.</li>
</ul>
<p>The cve-schema repository tags <code>v5.2.0</code>; the tagged commit is dated 3 September 2025. Records that declare <code>dataVersion</code> 5.1 cannot use the field. In our snapshot, 144,619 published records (36.1%) declared 5.2 and 255,826 declared 5.1.</p>
<h2 id="what-the-data-shows">What the data shows</h2>
<p>We read every record in the 5 November 2026 bundle: 400,445 published and 18,890 rejected. Only CNA containers carried the field; no ADP container had a <code>packageURL</code>.</p>
<table>
  <thead>
      <tr>
          <th>Measure</th>
          <th>Count</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Published records with a CNA <code>packageURL</code></td>
          <td>6,284 (1.57%)</td>
      </tr>
      <tr>
          <td><code>affected</code> entries with a <code>packageURL</code></td>
          <td>7,324</td>
      </tr>
      <tr>
          <td>CNAs that use it</td>
          <td>12</td>
      </tr>
      <tr>
          <td>Purls that embed a version</td>
          <td>0</td>
      </tr>
      <tr>
          <td>Purls with qualifiers</td>
          <td>76</td>
      </tr>
      <tr>
          <td>Entries with both <code>packageURL</code> and <code>cpes</code></td>
          <td>863</td>
      </tr>
  </tbody>
</table>
<h3 id="who-writes-it">Who writes it</h3>
<table>
  <thead>
      <tr>
          <th>CNA (short name)</th>
          <th>Records</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VulnCheck</td>
          <td>4,949</td>
      </tr>
      <tr>
          <td>apache</td>
          <td>455</td>
      </tr>
      <tr>
          <td>EEF</td>
          <td>387</td>
      </tr>
      <tr>
          <td>CPANSec</td>
          <td>199</td>
      </tr>
      <tr>
          <td>openjs</td>
          <td>147</td>
      </tr>
      <tr>
          <td>mitre</td>
          <td>61</td>
      </tr>
      <tr>
          <td>bcorg</td>
          <td>51</td>
      </tr>
      <tr>
          <td>RTI</td>
          <td>31</td>
      </tr>
      <tr>
          <td>Four others</td>
          <td>1 each</td>
      </tr>
  </tbody>
</table>
<p>VulnCheck accounts for 78.8% of records with a purl. If you build matching around this field, most of your hits will come from one CNA&rsquo;s conventions, so test against its records.</p>
<h3 id="which-purl-types">Which purl types</h3>
<p>The most common types across the 7,324 entries: <code>github</code> 2,371, <code>npm</code> 1,729, <code>pypi</code> 805, <code>maven</code> 533, <code>golang</code> 498, <code>composer</code> 314, <code>hex</code> 309, <code>cargo</code> 242, <code>cpan</code> 210, <code>otp</code> 82, <code>software-id</code> 72, <code>nuget</code> 40, <code>gem</code> 31, <code>generic</code> 31.</p>
<p>Two of those need care:</p>
<ul>
<li><strong><code>pkg:github</code></strong> identifies a source repository, not a registry package. VulnCheck wrote 1,917 of the GitHub entries and EEF 413. Your SBOM will usually describe the same software as <code>pkg:npm/...</code> or <code>pkg:pypi/...</code>, so a <code>pkg:github</code> record will not match it directly. You need a mapping from repository to registry package, or a fallback on <code>collectionURL</code> and <code>packageName</code>.</li>
<li><strong><code>pkg:software-id</code></strong> appears 72 times (for example <code>pkg:software-id/erlang.org/otp</code>), but the purl-spec decision record that registers it, dated 21 October 2026, still has the status &ldquo;proposed&rdquo;. Expect parsers to reject it until the type is registered.</li>
</ul>
<h3 id="how-fast-it-is-growing">How fast it is growing</h3>
<p>By the month each record was first published (as the records stand today, so later updates are included):</p>
<table>
  <thead>
      <tr>
          <th>Month</th>
          <th>Records with a purl</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>March 2026</td>
          <td>256</td>
      </tr>
      <tr>
          <td>June 2026</td>
          <td>447</td>
      </tr>
      <tr>
          <td>August 2026</td>
          <td>976</td>
      </tr>
      <tr>
          <td>September 2026</td>
          <td>1,155</td>
      </tr>
      <tr>
          <td>October 2026</td>
          <td>2,000</td>
      </tr>
  </tbody>
</table>
<p>October&rsquo;s 2,000 are 11.7% of the 17,058 records first published that month.</p>
<h2 id="normalize-before-you-join">Normalize before you join</h2>
<p>The purl specification makes the <code>type</code> case-insensitive with lowercase as the canonical form, and some types go further. The type definitions say a GitHub namespace and name are &ldquo;not case sensitive and shall be lowercased&rdquo;, and a PyPI name &ldquo;shall be lowercased and underscore _ replaced with a dash -&rdquo;. npm names are case-sensitive.</p>
<p>CVE records do not always follow those rules:</p>
<ul>
<li>756 of the 2,371 <code>pkg:github</code> entries (31.9%) contain uppercase letters, for example <code>pkg:github/Finsys/dockhand</code>.</li>
<li>9 <code>pkg:pypi</code> entries have uppercase letters and 24 contain an underscore.</li>
<li>GitPython is a concrete case. In the 34 records we found for it, 28 use <code>pkg:pypi/gitpython</code> and 6 use <code>pkg:pypi/GitPython</code>. An exact string join misses one set or the other, depending on how your SBOM tool spells it.</li>
</ul>
<p>So normalize both sides the same way before comparing: lowercase the type; lowercase <code>github</code> and <code>pypi</code> paths; replace <code>_</code> with <code>-</code> for <code>pypi</code>; drop the version and qualifiers from the SBOM purl. Leave npm, Maven and other case-sensitive types alone.</p>
<h2 id="a-tested-join">A tested join</h2>
<p>Two scripts. The first builds an index from the cvelistV5 bundle; the second joins a CycloneDX SBOM to it and applies the <code>versions</code> ranges.</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;build_index.py: index CNA affected[] entries that carry packageURL,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">keyed by a normalized purl (no version, no qualifiers).&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json<span style="color:#f92672">,</span> sys<span style="color:#f92672">,</span> zipfile
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">norm</span>(purl):
</span></span><span style="display:flex;"><span>    core <span style="color:#f92672">=</span> purl<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;#&#34;</span>)[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;?&#34;</span>)[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;@&#34;</span> <span style="color:#f92672">in</span> core<span style="color:#f92672">.</span>rsplit(<span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#ae81ff">1</span>)[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]:                 <span style="color:#75715e"># drop a version (SBOM side)</span>
</span></span><span style="display:flex;"><span>        core <span style="color:#f92672">=</span> core<span style="color:#f92672">.</span>rsplit(<span style="color:#e6db74">&#34;@&#34;</span>, <span style="color:#ae81ff">1</span>)[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>    scheme, rest <span style="color:#f92672">=</span> core<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;:&#34;</span>, <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    ptype, _, path <span style="color:#f92672">=</span> rest<span style="color:#f92672">.</span>partition(<span style="color:#e6db74">&#34;/&#34;</span>)
</span></span><span style="display:flex;"><span>    ptype <span style="color:#f92672">=</span> ptype<span style="color:#f92672">.</span>lower()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ptype <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;github&#34;</span>, <span style="color:#e6db74">&#34;pypi&#34;</span>):                     <span style="color:#75715e"># case-insensitive types per purl-spec</span>
</span></span><span style="display:flex;"><span>        path <span style="color:#f92672">=</span> path<span style="color:#f92672">.</span>lower()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ptype <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;pypi&#34;</span>:
</span></span><span style="display:flex;"><span>        path <span style="color:#f92672">=</span> path<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;_&#34;</span>, <span style="color:#e6db74">&#34;-&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;pkg:</span><span style="color:#e6db74">{</span>ptype<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>path<span style="color:#e6db74">}</span><span style="color:#e6db74">&#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">main</span>(zip_path, out_path):
</span></span><span style="display:flex;"><span>    z <span style="color:#f92672">=</span> zipfile<span style="color:#f92672">.</span>ZipFile(zip_path)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> open(out_path, <span style="color:#e6db74">&#34;w&#34;</span>) <span style="color:#66d9ef">as</span> out:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> n <span style="color:#f92672">in</span> z<span style="color:#f92672">.</span>namelist():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> n<span style="color:#f92672">.</span>endswith(<span style="color:#e6db74">&#34;.json&#34;</span>) <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;/CVE-&#34;</span> <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> n:
</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>loads(z<span style="color:#f92672">.</span>read(n))
</span></span><span style="display:flex;"><span>            meta <span style="color:#f92672">=</span> rec[<span style="color:#e6db74">&#34;cveMetadata&#34;</span>]
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> meta<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;state&#34;</span>) <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;PUBLISHED&#34;</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> a <span style="color:#f92672">in</span> rec[<span style="color:#e6db74">&#34;containers&#34;</span>]<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;cna&#34;</span>, {})<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;affected&#34;</span>, []):
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;packageURL&#34;</span>):
</span></span><span style="display:flex;"><span>                    out<span style="color:#f92672">.</span>write(json<span style="color:#f92672">.</span>dumps({<span style="color:#e6db74">&#34;key&#34;</span>: norm(a[<span style="color:#e6db74">&#34;packageURL&#34;</span>]), <span style="color:#e6db74">&#34;cve&#34;</span>: meta[<span style="color:#e6db74">&#34;cveId&#34;</span>],
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#34;defaultStatus&#34;</span>: a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;defaultStatus&#34;</span>), <span style="color:#e6db74">&#34;versions&#34;</span>: a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;versions&#34;</span>, [])}) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</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(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>])
</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-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;join_sbom.py: join CycloneDX component purls to the CVE packageURL index, then apply versions[] ranges.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Illustrative: only semver-like versionType values are compared; everything else is flagged for review.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json<span style="color:#f92672">,</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> packaging.version <span style="color:#f92672">import</span> Version, InvalidVersion
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> build_index <span style="color:#f92672">import</span> norm
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">status_for</span>(ver, entry):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        v <span style="color:#f92672">=</span> Version(ver)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> InvalidVersion:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;review: unparseable version&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> entry[<span style="color:#e6db74">&#34;versions&#34;</span>]:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> r<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;versionType&#34;</span>) <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;semver&#34;</span>, <span style="color:#e6db74">&#34;maven&#34;</span>, <span style="color:#e6db74">&#34;python&#34;</span>, <span style="color:#66d9ef">None</span>):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;review: versionType=</span><span style="color:#e6db74">{</span>r<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;versionType&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            lo <span style="color:#f92672">=</span> Version(r[<span style="color:#e6db74">&#34;version&#34;</span>]) <span style="color:#66d9ef">if</span> r[<span style="color:#e6db74">&#34;version&#34;</span>] <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;0&#34;</span>, <span style="color:#e6db74">&#34;*&#34;</span>) <span style="color:#66d9ef">else</span> Version(<span style="color:#e6db74">&#34;0&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;lessThan&#34;</span> <span style="color:#f92672">in</span> r:
</span></span><span style="display:flex;"><span>                inside <span style="color:#f92672">=</span> lo <span style="color:#f92672">&lt;=</span> v <span style="color:#f92672">&lt;</span> Version(r[<span style="color:#e6db74">&#34;lessThan&#34;</span>])
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">elif</span> <span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span> <span style="color:#f92672">in</span> r:
</span></span><span style="display:flex;"><span>                inside <span style="color:#f92672">=</span> lo <span style="color:#f92672">&lt;=</span> v <span style="color:#f92672">&lt;=</span> Version(r[<span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span>])
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                inside <span style="color:#f92672">=</span> v <span style="color:#f92672">==</span> lo
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> InvalidVersion:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;review: unparseable range&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> inside:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> r[<span style="color:#e6db74">&#34;status&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> entry<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;defaultStatus&#34;</span>) <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;unknown&#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">main</span>(index_path, sbom_path):
</span></span><span style="display:flex;"><span>    index <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> open(index_path):
</span></span><span style="display:flex;"><span>        e <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>loads(line)
</span></span><span style="display:flex;"><span>        index<span style="color:#f92672">.</span>setdefault(e[<span style="color:#e6db74">&#34;key&#34;</span>], [])<span style="color:#f92672">.</span>append(e)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> c <span style="color:#f92672">in</span> json<span style="color:#f92672">.</span>load(open(sbom_path))[<span style="color:#e6db74">&#34;components&#34;</span>]:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> e <span style="color:#f92672">in</span> index<span style="color:#f92672">.</span>get(norm(c[<span style="color:#e6db74">&#34;purl&#34;</span>]), []):
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>c[<span style="color:#e6db74">&#34;purl&#34;</span>]<span style="color:#e6db74">}</span><span style="color:#ae81ff">\t</span><span style="color:#e6db74">{</span>e[<span style="color:#e6db74">&#34;cve&#34;</span>]<span style="color:#e6db74">}</span><span style="color:#ae81ff">\t</span><span style="color:#e6db74">{</span>status_for(c[<span style="color:#e6db74">&#34;version&#34;</span>], e)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>)
</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(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>])
</span></span></code></pre></div><h3 id="test-run">Test run</h3>
<p><code>python3 build_index.py cves.zip purl-index.jsonl</code> took about 30 seconds and wrote 7,324 lines, one per entry. We then ran <code>python3 join_sbom.py purl-index.jsonl sbom.cdx.json</code> (packaging 26.3) with a three-component CycloneDX 1.6 SBOM:</p>
<table>
  <thead>
      <tr>
          <th>SBOM purl</th>
          <th>Records matched</th>
          <th>Affected</th>
          <th>Unaffected</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>pkg:npm/fast-uri@2.4.5</code></td>
          <td>17</td>
          <td>10</td>
          <td>7</td>
      </tr>
      <tr>
          <td><code>pkg:pypi/GitPython@3.1.62</code></td>
          <td>34</td>
          <td>6</td>
          <td>28</td>
      </tr>
      <tr>
          <td><code>pkg:maven/org.bouncycastle/bcprov-jdk18on@1.86?type=jar</code></td>
          <td>23</td>
          <td>0</td>
          <td>23</td>
      </tr>
  </tbody>
</table>
<p>GitPython 3.1.62 comes out affected by the six records whose ranges end at <code>lessThan: 3.2.0</code>, and unaffected by the 28 whose fixes landed at or before 3.1.62. Without normalization, the SBOM spelling <code>GitPython</code> would have matched 6 records and missed 28. The <code>?type=jar</code> qualifier on the Maven purl is dropped before lookup.</p>
<p>What this does not handle: <code>versionType</code> values such as <code>custom</code>, <code>git</code> or distribution schemes (it flags them for review), <code>changes</code> arrays inside version ranges, purl types that need a repository-to-package mapping, and the order sensitivity of overlapping ranges. The <a href="/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/">CVE affected versions guide</a> covers those rules in detail.</p>
<h2 id="when-the-cve-has-no-purl">When the CVE has no purl</h2>
<p>For the other 98% of records, you still need the older paths. The <a href="/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/">cpeApplicability explainer</a> covers CPE-based matching, and <code>collectionURL</code> plus <code>packageName</code> is often enough to build a purl yourself: <code>https://www.npmjs.com/package/x</code> with <code>packageName</code> <code>x</code> is <code>pkg:npm/x</code>. Record which path produced each match, because a purl match and a name match deserve different confidence.</p>
<p>A match is only the start of the patch decision. In our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">vulnerability prioritization workflow</a>, the purl join is the &ldquo;does this apply to us&rdquo; step; KEV, EPSS and SSVC decide how fast. If your supplier also ships VEX, apply it after the join; our <a href="/posts/csaf-vex-product-tree-product-status-purl-sbom-matching/">CSAF VEX guide</a> shows how to resolve its product IDs to purls.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The counts describe one snapshot (the 5 November 2026 midnight bundle); CNAs update records, so the numbers will move. Month counts use each record&rsquo;s original <code>datePublished</code>, so a purl added later to an old record is counted in the month the record was first published. We checked purl spelling only for the <code>github</code> and <code>pypi</code> rules; other types have their own normalization rules we did not test. We did not check how CVE Services validates purls at submission time. The join script uses Python&rsquo;s <code>packaging</code> version ordering, which is close to, but not the same as, every ecosystem&rsquo;s own ordering.</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>packageURL definition, &ldquo;MUST NOT include a version&rdquo;, examples; identifier and versions/defaultStatus requirements</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE Record Format schema (main)</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Rationale, CVE Services validation, versions only in versions field</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/rfds/0002-supporting-package-urls.md">RFD 0002: Supporting Package URLs in CVE</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>v5.2.0 tag commit dated 3 September 2025</td>
          <td><a href="https://github.com/CVEProject/cve-schema/releases/tag/v5.2.0">cve-schema tag v5.2.0</a></td>
          <td>Qualified (commit date, not an announcement)</td>
      </tr>
      <tr>
          <td>All counts (records, CNAs, types, normalization issues, monthly figures)</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/releases/tag/cve_2026-11-05_0400Z">cvelistV5 release cve_2026-11-05_0400Z</a>, counted with Python</td>
          <td>Verified (our count)</td>
      </tr>
      <tr>
          <td>CVE-2026-110180 affected fragment</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/110xxx/CVE-2026-110180.json">CVE-2026-110180 record</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>GitHub and PyPI normalization rules; npm case sensitivity</td>
          <td><a href="https://github.com/package-url/purl-spec/tree/main/types">purl-spec type definitions</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>purl type is case-insensitive, lowercase canonical form</td>
          <td><a href="https://github.com/package-url/purl-spec/blob/main/docs/specification/standard/Clause-5-Package-URL-Specification.md">purl-spec, Clause 5</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>software-id type decision status &ldquo;proposed&rdquo;, dated 21 October 2026</td>
          <td><a href="https://github.com/package-url/purl-spec/blob/main/docs/decisions/001-Register-software-id-PURL-type.md">purl-spec decision 001</a></td>
          <td>Verified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>CVE cpeApplicability: CPE Match Nodes, Operators, Ranges</title><link>https://firewallsync.com/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/</link><guid>https://firewallsync.com/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/</guid><pubDate>Wed, 04 Nov 2026 04:30:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>Since CVE Record Format 5.1.1, a CNA can put CPE Applicability Language statements in a cpeApplicability block. Here is how nodes, operator, negate, vulnerable and the four version bounds combine, what a 2,000-record sample of 2026 CVEs contains, and a tested Python evaluator that shows where the block and the affected block disagree.</description><enclosure url="https://firewallsync.com/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/cover_hu3781550322919701848.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/cover_hu3781550322919701848.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/cover_hu3781550322919701848.jpg" alt="Top-down view of a dense heap of loose jigsaw puzzle pieces, picture side and grey backs mixed together, in orange, yellow, green and red" width="1200" height="630"></p><p><strong>Scope.</strong> This is a specification explainer with measurements from public data. We read the CVE Record Format schema on the CVEProject/cve-schema main branch (default <code>dataVersion</code> 5.2.0) and its advanced example record, sampled 2,000 random 2026 records from the cvelistV5 repository at commit f0b25a9 (November 4, 2026), and ran the Python script below with Python 3.13 against real records. We did not test commercial scanners or NVD&rsquo;s own matching engine.</p>
<h2 id="the-short-answer">The short answer</h2>
<p><code>cpeApplicability</code> is where a CNA can say, in CPE terms, which configurations a CVE applies to. Each array element is one statement:</p>
<ol>
<li>A statement has <code>nodes</code>, and optionally an <code>operator</code> and <code>negate</code> for combining them.</li>
<li>Each node has a required <code>operator</code> (<code>AND</code> or <code>OR</code>), an optional <code>negate</code>, and a <code>cpeMatch</code> list.</li>
<li>Each <code>cpeMatch</code> has a CPE 2.3 <code>criteria</code> string, a required <code>vulnerable</code> boolean, and optional bounds: <code>versionStartIncluding</code>, <code>versionStartExcluding</code>, <code>versionEndIncluding</code>, <code>versionEndExcluding</code>.</li>
<li>A host is in scope when the logic evaluates true against its CPE inventory <strong>and</strong> at least one matched entry has <code>vulnerable: true</code>.</li>
</ol>
<p>If you already parse NVD configurations, the shape is familiar. The schema says so directly: the block is &ldquo;mostly copied/forked from the NIST NVD CVE API v2.0 schema&rdquo;.</p>
<h2 id="where-the-block-lives">Where the block lives</h2>
<p>The schema defines <code>cpeApplicability</code> as an optional array on both the published CNA container and the ADP container. It was added in CVE Record Format 5.1.1 (the <code>v5.1.1</code> tag in CVEProject/cve-schema dates from October 2024), which the release notes describe as adding &ldquo;support for Common Platform Enumeration (CPE) Identifiers using the CPE Applicability Language&rdquo;.</p>
<p>It is separate from the older <code>cpes</code> array inside each <code>affected[]</code> product. That array is a flat list of CPE names; its own schema description now ends: &ldquo;Consider using the newer cpeApplicability block for defining CPE data using the CPE Applicability Language which includes more options for defining CPE Names.&rdquo;</p>
<p>The schema adds one instruction that matters for consumers: &ldquo;it is recommended that it align with (as much as possible) the product data provided within the affected block.&rdquo; Recommended, not required. If you already read the <code>affected</code> block with the rules in <a href="/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/">our guide to lessThan, defaultStatus and versionType</a>, treat <code>cpeApplicability</code> as a second encoding of the same claim, and expect the two to disagree sometimes.</p>
<h2 id="the-fields-one-by-one">The fields, one by one</h2>
<table>
  <thead>
      <tr>
          <th>Level</th>
          <th>Field</th>
          <th>Required</th>
          <th>Meaning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Element</td>
          <td><code>nodes</code></td>
          <td>yes</td>
          <td>Array of nodes</td>
      </tr>
      <tr>
          <td>Element</td>
          <td><code>operator</code></td>
          <td>no</td>
          <td><code>AND</code> or <code>OR</code> between nodes</td>
      </tr>
      <tr>
          <td>Element</td>
          <td><code>negate</code></td>
          <td>no</td>
          <td>Invert the element&rsquo;s result</td>
      </tr>
      <tr>
          <td>Node</td>
          <td><code>operator</code></td>
          <td>yes</td>
          <td><code>AND</code> or <code>OR</code> between the node&rsquo;s cpeMatch entries</td>
      </tr>
      <tr>
          <td>Node</td>
          <td><code>negate</code></td>
          <td>no</td>
          <td>Invert the node&rsquo;s result</td>
      </tr>
      <tr>
          <td>Node</td>
          <td><code>cpeMatch</code></td>
          <td>yes</td>
          <td>Array of match entries</td>
      </tr>
      <tr>
          <td>cpeMatch</td>
          <td><code>criteria</code></td>
          <td>yes</td>
          <td>CPE 2.3 formatted string, e.g. <code>cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*</code></td>
      </tr>
      <tr>
          <td>cpeMatch</td>
          <td><code>vulnerable</code></td>
          <td>yes</td>
          <td><code>true</code> for the vulnerable product, <code>false</code> for a platform or context CPE</td>
      </tr>
      <tr>
          <td>cpeMatch</td>
          <td><code>versionStartIncluding</code> / <code>versionStartExcluding</code></td>
          <td>no</td>
          <td>Lower bound, inclusive or exclusive</td>
      </tr>
      <tr>
          <td>cpeMatch</td>
          <td><code>versionEndIncluding</code> / <code>versionEndExcluding</code></td>
          <td>no</td>
          <td>Upper bound, inclusive or exclusive</td>
      </tr>
      <tr>
          <td>cpeMatch</td>
          <td><code>matchCriteriaId</code></td>
          <td>no</td>
          <td>UUID, carried over from NVD&rsquo;s schema</td>
      </tr>
  </tbody>
</table>
<p>Two details trip people up.</p>
<p><strong>Version bounds apply only when the CPE&rsquo;s version field is a wildcard.</strong> A criteria string such as <code>cpe:2.3:a:ibm:mq_operator:3.4.1:*:...</code> names one exact version; a string with <code>*</code> in the version position plus bounds names a range. Our evaluator treats a specific version in the criteria as an exact match and ignores bounds in that case.</p>
<p><strong>There is no <code>versionType</code>.</strong> The <code>affected</code> block tells you whether versions are semver, git commits, or a custom scheme; <code>cpeMatch</code> does not. Whatever compares <code>6.18.9</code> with <code>6.19</code> has to guess the ordering, and for strings like <code>alpha_0.3</code> or <code>1.3-beta</code> there may be no safe guess.</p>
<h2 id="and-or-and-vulnerable-false">AND, OR and vulnerable: false</h2>
<p>The schema&rsquo;s advanced example record shows the pattern that makes this block more expressive than a flat list. Its single element has <code>&quot;operator&quot;: &quot;AND&quot;</code> and two nodes:</p>
<ul>
<li>Node 1 (<code>OR</code>): <code>example_org:example_enterprise</code> from 1.0.0 up to (excluding) 1.0.6, or from 2.1.6 up to 2.1.9, each with <code>vulnerable: true</code>.</li>
<li>Node 2 (<code>OR</code>): Windows, macOS or a hardware model <code>xt-4500</code>, each with <code>vulnerable: false</code>.</li>
</ul>
<p>Read it as: the application is vulnerable in those ranges <strong>when running on</strong> one of those platforms. The platform CPEs are conditions, not affected products. A tool that flags every host whose inventory contains <code>cpe:2.3:o:microsoft:windows:...</code> has misread the record.</p>
<h2 id="what-a-sample-of-2026-records-contains">What a sample of 2026 records contains</h2>
<p>We took a seeded random sample of 2,000 files from the 84,849 2026 records in cvelistV5 at commit f0b25a9; 1,969 were published. The counting script parsed each record&rsquo;s CNA and ADP containers.</p>
<table>
  <thead>
      <tr>
          <th>Measure</th>
          <th>Count</th>
          <th>Share</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Published records with a CNA <code>cpeApplicability</code> block</td>
          <td>538 of 1,969</td>
          <td>27.3%</td>
      </tr>
      <tr>
          <td>&hellip;of which from Linux, VulnCheck, Microsoft or Oracle (186, 110, 97, 84)</td>
          <td>477 of 538</td>
          <td>88.7%</td>
      </tr>
      <tr>
          <td>ADP containers carrying <code>cpeApplicability</code></td>
          <td>0</td>
          <td>0%</td>
      </tr>
      <tr>
          <td>Records still using the flat <code>affected[].cpes</code> list</td>
          <td>195 of 1,969</td>
          <td>9.9%</td>
      </tr>
      <tr>
          <td>Elements with no element-level <code>operator</code></td>
          <td>489 of 582</td>
          <td>84.0%</td>
      </tr>
      <tr>
          <td>Nodes, all with <code>operator: OR</code></td>
          <td>598</td>
          <td>100%</td>
      </tr>
      <tr>
          <td>cpeMatch entries</td>
          <td>2,667</td>
          <td></td>
      </tr>
      <tr>
          <td>&hellip;with <code>vulnerable: false</code></td>
          <td>10</td>
          <td>0.4%</td>
      </tr>
      <tr>
          <td>&hellip;with a specific version in <code>criteria</code> rather than <code>*</code></td>
          <td>170</td>
          <td>6.4%</td>
      </tr>
      <tr>
          <td>&hellip;with <code>versionStartIncluding</code> / <code>versionEndExcluding</code> / <code>versionEndIncluding</code></td>
          <td>2,397 / 2,333 / 113</td>
          <td>89.9% / 87.5% / 4.2%</td>
      </tr>
      <tr>
          <td>&hellip;with <code>matchCriteriaId</code></td>
          <td>0</td>
          <td>0%</td>
      </tr>
  </tbody>
</table>
<p>Three practical readings. First, most records have no CPE statement from the CNA at all, so a CPE-only matcher misses most of the corpus. Second, the real-world block is simpler than the spec allows: almost everything is one OR node of ranges, and <code>AND</code> with platform CPEs is rare. Third, coverage depends on a handful of CNAs, so your hit rate will swing with your vendor mix. An earlier 1,000-record sample we took for the affected-versions guide found 29.5%; treat the figure as a range, not a constant.</p>
<p>The absence in ADP containers fits CISA&rsquo;s stated policy. The Vulnrichment README says: &ldquo;as of December 10, 2024, CISA will no longer be adding CPE strings to the enriched dataset.&rdquo; See <a href="/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/">our explainer on CVE ADP containers</a> for what CISA-ADP does still add.</p>
<h2 id="a-tested-evaluator">A tested evaluator</h2>
<p>The script takes one CVE record and every CPE installed on one host, and answers <code>True</code>, <code>False</code> or <code>unknown</code>. It is illustrative: it compares only dotted numeric versions, handles the <code>\:</code>-style escaping in CPE 2.3 names only for splitting, and matches part, vendor and product but ignores the remaining CPE fields.</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;Evaluate a CVE record&#39;s cna.cpeApplicability against one host&#39;s CPE inventory.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Illustrative: numeric-dotted versions only; anything else is reported as &#39;unknown&#39;.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json<span style="color:#f92672">,</span> re<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">split_cpe</span>(cpe):
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># split on &#39;:&#39; not preceded by a backslash (CPE 2.3 formatted-string escaping)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> re<span style="color:#f92672">.</span>split(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#39;(?&lt;!</span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">):&#39;</span>, cpe)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">vkey</span>(v):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> re<span style="color:#f92672">.</span>fullmatch(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#39;\d+(\.\d+)*&#39;</span>, v):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(v)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> tuple(int(x) <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> v<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;.&#39;</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">cmp</span>(a, b):
</span></span><span style="display:flex;"><span>    a, b <span style="color:#f92672">=</span> vkey(a), vkey(b)
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> max(len(a), len(b))
</span></span><span style="display:flex;"><span>    a, b <span style="color:#f92672">=</span> a <span style="color:#f92672">+</span> (<span style="color:#ae81ff">0</span>,) <span style="color:#f92672">*</span> (n <span style="color:#f92672">-</span> len(a)), b <span style="color:#f92672">+</span> (<span style="color:#ae81ff">0</span>,) <span style="color:#f92672">*</span> (n <span style="color:#f92672">-</span> len(b))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (a <span style="color:#f92672">&gt;</span> b) <span style="color:#f92672">-</span> (a <span style="color:#f92672">&lt;</span> b)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">match</span>(m, installed_cpe):
</span></span><span style="display:flex;"><span>    want, have <span style="color:#f92672">=</span> split_cpe(m[<span style="color:#e6db74">&#39;criteria&#39;</span>]), split_cpe(installed_cpe)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># compare part, vendor, product (fields 2-4); &#39;*&#39; is ANY</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> (<span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> want[i] <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#39;*&#39;</span> <span style="color:#f92672">and</span> want[i] <span style="color:#f92672">!=</span> have[i]:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    ver <span style="color:#f92672">=</span> have[<span style="color:#ae81ff">5</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> want[<span style="color:#ae81ff">5</span>] <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#39;*&#39;</span>, <span style="color:#e6db74">&#39;-&#39;</span>):            <span style="color:#75715e"># exact version in the criteria</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> want[<span style="color:#ae81ff">5</span>] <span style="color:#f92672">==</span> ver
</span></span><span style="display:flex;"><span>    checks <span style="color:#f92672">=</span> [(<span style="color:#e6db74">&#39;versionStartIncluding&#39;</span>, <span style="color:#66d9ef">lambda</span> c: c <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span>), (<span style="color:#e6db74">&#39;versionStartExcluding&#39;</span>, <span style="color:#66d9ef">lambda</span> c: c <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>),
</span></span><span style="display:flex;"><span>              (<span style="color:#e6db74">&#39;versionEndIncluding&#39;</span>, <span style="color:#66d9ef">lambda</span> c: c <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>), (<span style="color:#e6db74">&#39;versionEndExcluding&#39;</span>, <span style="color:#66d9ef">lambda</span> c: c <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> all(test(cmp(ver, m[k])) <span style="color:#66d9ef">for</span> k, test <span style="color:#f92672">in</span> checks <span style="color:#66d9ef">if</span> k <span style="color:#f92672">in</span> m)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">present</span>(m, inventory):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> any(<span style="color:#66d9ef">match</span>(m, c) <span style="color:#66d9ef">for</span> c <span style="color:#f92672">in</span> inventory)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">node</span>(n, inv):
</span></span><span style="display:flex;"><span>    hits <span style="color:#f92672">=</span> [present(m, inv) <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> n[<span style="color:#e6db74">&#39;cpeMatch&#39;</span>]]
</span></span><span style="display:flex;"><span>    r <span style="color:#f92672">=</span> all(hits) <span style="color:#66d9ef">if</span> n[<span style="color:#e6db74">&#39;operator&#39;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;AND&#39;</span> <span style="color:#66d9ef">else</span> any(hits)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">not</span> r <span style="color:#66d9ef">if</span> n<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;negate&#39;</span>) <span style="color:#66d9ef">else</span> r
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">element</span>(el, inv):
</span></span><span style="display:flex;"><span>    res <span style="color:#f92672">=</span> [node(n, inv) <span style="color:#66d9ef">for</span> n <span style="color:#f92672">in</span> el[<span style="color:#e6db74">&#39;nodes&#39;</span>]]
</span></span><span style="display:flex;"><span>    r <span style="color:#f92672">=</span> all(res) <span style="color:#66d9ef">if</span> el<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;operator&#39;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;AND&#39;</span> <span style="color:#66d9ef">else</span> any(res)  <span style="color:#75715e"># no operator: OR</span>
</span></span><span style="display:flex;"><span>    r <span style="color:#f92672">=</span> <span style="color:#f92672">not</span> r <span style="color:#66d9ef">if</span> el<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;negate&#39;</span>) <span style="color:#66d9ef">else</span> r
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># the statement must also hit at least one entry flagged vulnerable</span>
</span></span><span style="display:flex;"><span>    vuln <span style="color:#f92672">=</span> any(m[<span style="color:#e6db74">&#39;vulnerable&#39;</span>] <span style="color:#f92672">and</span> present(m, inv)
</span></span><span style="display:flex;"><span>               <span style="color:#66d9ef">for</span> n <span style="color:#f92672">in</span> el[<span style="color:#e6db74">&#39;nodes&#39;</span>] <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> n[<span style="color:#e6db74">&#39;cpeMatch&#39;</span>])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> r <span style="color:#f92672">and</span> vuln
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">evaluate</span>(record, inventory):
</span></span><span style="display:flex;"><span>    cna <span style="color:#f92672">=</span> record[<span style="color:#e6db74">&#39;containers&#39;</span>][<span style="color:#e6db74">&#39;cna&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> any(element(el, inventory) <span style="color:#66d9ef">for</span> el <span style="color:#f92672">in</span> cna<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;cpeApplicability&#39;</span>, []))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">ValueError</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;unknown (cannot order version </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">)&#39;</span>
</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">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    rec <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>]))
</span></span><span style="display:flex;"><span>    inventory <span style="color:#f92672">=</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">2</span>:]          <span style="color:#75715e"># every CPE installed on one host</span>
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#39; + &#39;</span><span style="color:#f92672">.</span>join(inventory), <span style="color:#e6db74">&#39;-&gt;&#39;</span>, evaluate(rec, inventory))
</span></span></code></pre></div><p>Saved as <code>cpe_eval.py</code>, we ran it against three records from the sample and the schema&rsquo;s example (output trimmed to the verdict):</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 -I cpe_eval.py CVE-2026-74994.json &#39;cpe:2.3:a:erlang:erlang\/otp:28.5.0.5:*:*:*:*:*:*:*&#39;
</span></span><span style="display:flex;"><span>... -&gt; True
</span></span><span style="display:flex;"><span>$ python3 -I cpe_eval.py CVE-2026-74994.json &#39;cpe:2.3:a:erlang:erlang\/otp:28.5.0.6:*:*:*:*:*:*:*&#39;
</span></span><span style="display:flex;"><span>... -&gt; False
</span></span><span style="display:flex;"><span>$ python3 -I cpe_eval.py full-record-advanced-example.json cpe:2.3:a:example_org:example_enterprise:1.0.5:*:*:*:*:*:*:*
</span></span><span style="display:flex;"><span>... -&gt; False
</span></span><span style="display:flex;"><span>$ python3 -I cpe_eval.py full-record-advanced-example.json cpe:2.3:a:example_org:example_enterprise:1.0.5:*:*:*:*:*:*:* cpe:2.3:o:microsoft:windows:11:*:*:*:*:*:*:*
</span></span><span style="display:flex;"><span>... -&gt; True
</span></span><span style="display:flex;"><span>$ python3 -I cpe_eval.py CVE-2026-115975.json cpe:2.3:a:cervantessec:cervantes:v.1.4.0-beta:*:*:*:*:*:*:*
</span></span><span style="display:flex;"><span>... -&gt; unknown (cannot order version v.1.4.0-beta)
</span></span></code></pre></div><p>The Erlang/OTP record (CVE-2026-74994) is the common case: three ranges in one OR node, one per release line, with <code>versionEndExcluding</code> set to the fixed release. The example record shows the AND rule working: the application version alone is not enough; the platform must be present too. The Cervantes record (CVE-2026-115975) uses version strings like <code>alpha_0.3</code> and <code>1.3-beta</code>, and the honest answer is &ldquo;cannot decide&rdquo;.</p>
<h2 id="where-cpeapplicability-and-affected-disagree">Where cpeApplicability and affected disagree</h2>
<p>CVE-2026-23122 is a Linux kernel record (published February 14, 2026). Its <code>cpeApplicability</code> block has one OR node with two ranges on <code>cpe:2.3:o:linux:linux_kernel</code>: 6.16 up to (excluding) 6.18.8, and 6.16 up to (excluding) 6.19. Its <code>affected</code> block for the semver product says 6.16 is affected, below 6.16 is unaffected, <strong>6.18.8 through 6.18.* is unaffected</strong>, and 6.19 onwards is unaffected.</p>
<p>Our evaluator, run on kernel versions (output trimmed to the version and verdict):</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>6.15.4 -&gt; False
</span></span><span style="display:flex;"><span>6.18.7 -&gt; True
</span></span><span style="display:flex;"><span>6.18.9 -&gt; True     # affected block: unaffected (6.18.8 to 6.18.*)
</span></span><span style="display:flex;"><span>6.19   -&gt; False
</span></span></code></pre></div><p>The second CPE range (6.16 to before 6.19) swallows the stable-branch fix, because a CPE range has no way to say &ldquo;fixed in the 6.18 branch at 6.18.8 but not in mainline until 6.19&rdquo;. The <code>affected</code> block can, through ordered entries and a <code>6.18.*</code> upper limit. A scanner that trusts only CPE here raises a false positive on every patched 6.18 kernel.</p>
<p>That is the general lesson: CPE ranges are a lossy projection of branch-aware version data. Use them for discovery (which records mention a product I run?) and as a cross-check, then decide with the <code>affected</code> block.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul>
<li><strong>Treating every matched CPE as vulnerable.</strong> Entries with <code>vulnerable: false</code> are context. In our sample they were rare (10 of 2,667), which is exactly why code that ignores the flag passes tests and fails later.</li>
<li><strong>Applying bounds to an exact-version criteria string.</strong> If the CPE&rsquo;s version field is not <code>*</code>, compare it directly.</li>
<li><strong>Assuming the element operator.</strong> 84% of sampled elements omit it. With a single node it does not matter; with several, decide and document what your tool does. Our script treats a missing element operator as OR.</li>
<li><strong>Comparing versions as strings.</strong> <code>&quot;6.9&quot; &gt; &quot;6.18&quot;</code> is true as text. Use a scheme-aware comparator and return &ldquo;unknown&rdquo; when the scheme is unknown.</li>
<li><strong>Mixing sources silently.</strong> CNA <code>cpeApplicability</code>, the older <code>affected[].cpes</code>, and NVD&rsquo;s configurations are three different statements. Store which one produced a match.</li>
</ul>
<h2 id="where-this-fits-in-triage">Where this fits in triage</h2>
<p>CPE matching answers &ldquo;does this CVE apply to an asset I run?&rdquo;. It does not answer &ldquo;how urgently?&rdquo;. Once a record matches, the next questions are whether it is in CISA KEV, its EPSS probability and its SSVC decision values; our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">vulnerability prioritization workflow</a> walks that path from record to ticket, and <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">the KEV JSON feed guide</a> covers the exploitation check.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<ul>
<li>The counts come from one random sample of 2026 records at one commit; CNA behaviour changes, so rerun the count on your own snapshot before relying on a share.</li>
<li>The evaluator is illustrative. It does not implement the full CPE 2.3 name-matching specification (NISTIR 7696), wildcard characters inside fields, or version schemes beyond dotted integers.</li>
<li>We describe NVD configurations only by the schema&rsquo;s own statement that this block was forked from the NVD CVE API v2.0 schema; we did not compare NVD&rsquo;s published configurations for these CVEs.</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>Field names, required fields, enums, &ldquo;mostly copied/forked from the NIST NVD CVE API v2.0 schema&rdquo;, alignment recommendation</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE Record Format schema (cve-schema main)</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>cpeApplicability</code> allowed in CNA and ADP containers; <code>cpes</code> description recommends the newer block</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE Record Format schema</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Added in 5.1.1; tag dated October 2024</td>
          <td><a href="https://github.com/CVEProject/cve-schema/releases">cve-schema releases</a>; <code>v5.1.1</code> tag commit date</td>
          <td>Verified (release-note text read via a mirror; qualified)</td>
      </tr>
      <tr>
          <td>AND/OR example with vulnerable: false platforms</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/docs/full-record-advanced-example.json">full-record-advanced-example.json</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CISA stopped adding CPE strings on December 10, 2024</td>
          <td><a href="https://github.com/cisagov/vulnrichment">cisagov/vulnrichment README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Sample counts and shares</td>
          <td><a href="https://github.com/CVEProject/cvelistV5">cvelistV5</a> at commit f0b25a9, our script</td>
          <td>Verified (computed)</td>
      </tr>
      <tr>
          <td>CVE-2026-23122 cpeApplicability vs affected disagreement</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/23xxx/CVE-2026-23122.json">CVE-2026-23122 record</a></td>
          <td>Verified (tested)</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>CVE ADP Containers: CISA-ADP, CVE Program and Supplier ADPs</title><link>https://firewallsync.com/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/</link><guid>https://firewallsync.com/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/</guid><pubDate>Tue, 03 Nov 2026 04:30:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Security Operations</category><description>A CVE record has one CNA container and possibly several ADP containers. Here is what CISA-ADP, the CVE Program Container and supplier ADPs such as redhat-SADP and siemens-SADP add, how often each appears in a sample of 2,000 2026 records, and a tested script that shows who said what.</description><enclosure url="https://firewallsync.com/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/cover_hu6338534624026322280.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/cover_hu6338534624026322280.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/cover_hu6338534624026322280.jpg" alt="Grid of dark wooden card catalog drawers, each with a brass label holder showing an alphabetical range and a brass pull" width="1200" height="630"></p><p><strong>Scope.</strong> This is a specification explainer with measurements from public data. We read the CVE JSON schema (<code>CVE_Record_Format.json</code>, default <code>dataVersion</code> 5.2.0, CVEProject/cve-schema main branch), the cvelistV5 README and the CISA Vulnrichment README, then sampled 2,000 random 2026 record files from cvelistV5 at commit <code>1a44d59</code> (November 3, 2026). The script below was run with Python 3.13.</p>
<h2 id="the-short-answer">The short answer</h2>
<p>A CVE record is not one author&rsquo;s statement. Under <code>containers</code> there is exactly one <code>cna</code> object, written by the CVE Numbering Authority that assigned the ID, and an optional <code>adp</code> array. Each <code>adp</code> entry is written by an Authorized Data Publisher and names its author in <code>providerMetadata.shortName</code>.</p>
<p>In 2026 records you will mostly meet three kinds:</p>
<table>
  <thead>
      <tr>
          <th><code>shortName</code></th>
          <th>Title in the container</th>
          <th>What it adds</th>
          <th>Share of 1,963 published records in our sample</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>CISA-ADP</code></td>
          <td>CISA ADP Vulnrichment</td>
          <td>SSVC decision points; sometimes CVSS, CWE, KEV date</td>
          <td>93.9%</td>
      </tr>
      <tr>
          <td><code>CVE</code></td>
          <td>CVE Program Container</td>
          <td>References added by the CVE Program</td>
          <td>2.2%</td>
      </tr>
      <tr>
          <td><code>redhat-SADP</code>, <code>siemens-SADP</code></td>
          <td>Supplier&rsquo;s own titles (or none)</td>
          <td>The supplier&rsquo;s affected products, versions, fixes, severity</td>
          <td>1.2% combined</td>
      </tr>
  </tbody>
</table>
<p>The rule that matters for tooling: read the CNA container and the CVE Program Container every time, read CISA-ADP for prioritization signals, and treat supplier ADPs as extra affected-product data for that supplier&rsquo;s builds. Keep every value tagged with who published it.</p>
<h2 id="what-the-schema-says-about-adp-containers">What the schema says about ADP containers</h2>
<p>The schema describes <code>containers</code> as &ldquo;a set of structures (called containers) used to store vulnerability information related to a specific CVE ID provided by a specific organization participating in the CVE program.&rdquo; It goes on: &ldquo;There can only be one &lsquo;cna&rsquo; container, as there can only be one assigning CNA. However, there can be multiple &lsquo;adp&rsquo; containers.&rdquo;</p>
<p>Three details shape how you parse them:</p>
<ul>
<li><strong>Only <code>providerMetadata</code> is required.</strong> The <code>adpContainer</code> definition lists <code>providerMetadata</code> as required with <code>minProperties: 2</code>, so an ADP container can be just a publisher plus one other field. Code that assumes <code>affected</code>, <code>descriptions</code> or <code>metrics</code> will exist breaks.</li>
<li><strong>The fields mirror the CNA container.</strong> An ADP container can carry <code>affected</code>, <code>cpeApplicability</code>, <code>problemTypes</code>, <code>metrics</code>, <code>references</code>, <code>solutions</code>, <code>workarounds</code>, <code>exploits</code>, <code>timeline</code> and more. The schema notes that &ldquo;for the most part, the &lsquo;cna&rsquo; and &lsquo;adp&rsquo; containers contain the same properties. The main differences are the source of the information.&rdquo;</li>
<li><strong>Extensions are allowed.</strong> <code>adpContainer</code> permits properties matching <code>^x_[^.]*$</code>. Supplier ADPs use this: every redhat-SADP and siemens-SADP container in our sample had <code>&quot;x_adpType&quot;: &quot;supplier&quot;</code>, and Red Hat&rsquo;s also carried <code>x_generator</code>. These are not schema-defined fields, so treat them as hints.</li>
</ul>
<p><code>providerMetadata</code> holds <code>orgId</code> (a UUID, required), <code>shortName</code> and <code>dateUpdated</code>. Match publishers on <code>orgId</code> if you need certainty; <code>shortName</code> is what humans read.</p>
<h2 id="the-three-kinds-you-will-see">The three kinds you will see</h2>
<h3 id="cisa-adp-prioritization-signals">CISA-ADP: prioritization signals</h3>
<p>The cvelistV5 README says the CISA ADP provides three things: SSVC, KEV catalog data, and &ldquo;Vulnrichment&rdquo; updates such as &ldquo;missing CVSS, CWE, CPE information for CVE Records that meet specific threat characteristics, and for when CNAs do not provide it themselves.&rdquo;</p>
<p>What that looked like in our sample of 1,963 published records:</p>
<table>
  <thead>
      <tr>
          <th>CISA-ADP content</th>
          <th>Records</th>
          <th>Share of published</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SSVC block (<code>metrics[].other.type = &quot;ssvc&quot;</code>)</td>
          <td>1,844</td>
          <td>93.9%</td>
      </tr>
      <tr>
          <td>CVSS (v3.1 in all but one)</td>
          <td>389</td>
          <td>19.8%</td>
      </tr>
      <tr>
          <td>CWE in <code>problemTypes</code></td>
          <td>336</td>
          <td>17.1%</td>
      </tr>
      <tr>
          <td>KEV block (<code>metrics[].other.type = &quot;kev&quot;</code>)</td>
          <td>5</td>
          <td>0.3%</td>
      </tr>
  </tbody>
</table>
<p>Every CISA-ADP container in the sample carried SSVC. CVSS and CWE were mostly gap-fills: 381 of the 389 CVSS entries (97.9%) and 326 of the 336 CWE entries (97.0%) sat on records whose CNA container had none. That matches the Vulnrichment README: &ldquo;For those CVEs that do not already have these fields populated by the originating CNA, CISA will populate the associated ADP container with those values when there is enough supporting evidence to do so. At no point will CISA overwrite the originating CNA&rsquo;s data in the original CNA container.&rdquo;</p>
<p>The exceptions matter. In 8 records both the CNA and CISA-ADP carried a CVSS score, and they did not always agree: in CVE-2026-70373 the CNA (TuranSec) published <code>CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H</code> (8.8) while CISA-ADP published <code>.../C:H/I:N/A:N</code> (6.5). We did not check which came first. The point is that a parser which picks &ldquo;the first CVSS it finds&rdquo; will produce different answers depending on container order.</p>
<p>The KEV block is small: <code>dateAdded</code> and a link to the catalog. Treat it as a pointer, and use the KEV catalog itself for <code>dueDate</code>, <code>requiredAction</code> and ransomware use. Our <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">KEV JSON feed field guide</a> covers those fields, and the <a href="/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/">Vulnrichment SSVC guide</a> covers the three SSVC values.</p>
<h3 id="the-cve-program-container-references-you-must-not-skip">The CVE Program Container: references you must not skip</h3>
<p>The cvelistV5 README is explicit. The container has title &ldquo;CVE Program Container&rdquo; and <code>shortName</code> &ldquo;CVE&rdquo;, and &ldquo;all CVE Program-added references after 7/31/2024 for a CVE Record will be stored in the CVE Program Container of that Record. CNA-provided references will continue to be stored in the CNA Container.&rdquo;</p>
<p>It also says what tools must do: &ldquo;to retrieve all information about a reported vulnerability in the CVE Repository, tool vendors and community users will need to examine the CVE Record CNA Container and the CVE Program Container (if one exists). These two containers are minimally required to obtain the core information required by the Program. All other ADP containers remain optional from a Program perspective.&rdquo;</p>
<p>Two practical consequences:</p>
<ul>
<li>References tagged <code>x_transferred</code> were copied from the CNA container on July 31, 2024, so expect duplicates. The README says downstream users &ldquo;will have to determine the appropriate way to resolve potential reference duplications.&rdquo; Deduplicate on URL.</li>
<li>New records only get this container if the Program adds something. In our 2026 sample, 43 records (2.2%) had one.</li>
</ul>
<h3 id="supplier-adps-a-vendors-view-of-someone-elses-cve">Supplier ADPs: a vendor&rsquo;s view of someone else&rsquo;s CVE</h3>
<p>Supplier ADP containers let a vendor that ships an upstream component add its own product data to a CVE that a different CNA assigned. Siemens describes its SADP as useful when &ldquo;a supplier like Siemens wants to add information to a vulnerability&rdquo; in an upstream dependency, and says it was introduced in April 2026 after a pilot. Its stated aim is better scanner matching for affected Siemens products, with &ldquo;known-not-affected&rdquo; product data planned later.</p>
<p>In our sample, 24 published records (1.2%) carried one: 18 from <code>redhat-SADP</code>, 6 from <code>siemens-SADP</code>. Their shapes differ:</p>
<ul>
<li><strong>redhat-SADP</strong> containers looked like a full advisory: <code>affected</code> entries per Red Hat product stream with CPEs, package names and RPM <code>versionType</code>, plus Red Hat&rsquo;s severity rating, CVSS, <code>solutions</code>, <code>workarounds</code> and references.</li>
<li><strong>siemens-SADP</strong> containers were lean: <code>affected</code> entries for Siemens products, references to the Siemens advisory, and nothing else. All had <code>defaultStatus</code> set to <code>unknown</code>.</li>
</ul>
<p>Two traps. First, supplier <code>affected</code> data describes the supplier&rsquo;s product names and version strings, not the upstream project&rsquo;s. A Red Hat entry such as <code>defaultStatus: affected</code> with <code>lessThan: &quot;*&quot;</code> and <code>status: unaffected</code> from a fixed RPM version is a statement about that Red Hat package, which is how our <a href="/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/">affected-versions guide</a> explains these encodings. Second, supplier severity ratings and CVSS are the supplier&rsquo;s assessment for its builds and can legitimately differ from the CNA&rsquo;s.</p>
<h2 id="one-record-four-publishers">One record, four publishers</h2>
<p>CVE-2026-46300 is a Linux kernel flaw assigned by the <code>Linux</code> CNA and added to the KEV catalog on October 28, 2026. Its record shows why you need a per-provider view. This is the output of the script in the next section against the cvelistV5 file at commit <code>1a44d59</code>:</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>CVE-2026-46300 PUBLISHED dataVersion 5.2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[cna] Linux (updated 2026-08-05)
</span></span><span style="display:flex;"><span>  cvss: [&#39;3.1 7.8 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&#39;]
</span></span><span style="display:flex;"><span>  affected_products: 2
</span></span><span style="display:flex;"><span>  references: 8
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[adp] CVE (updated 2026-05-23)
</span></span><span style="display:flex;"><span>  references: 4
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[adp] redhat-SADP (updated 2026-09-01)
</span></span><span style="display:flex;"><span>  other: [&#39;Red Hat severity rating: Important&#39;]
</span></span><span style="display:flex;"><span>  cvss: [&#39;3.1 7.8 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&#39;]
</span></span><span style="display:flex;"><span>  cwe: [&#39;CWE-123&#39;]
</span></span><span style="display:flex;"><span>  affected_products: 39
</span></span><span style="display:flex;"><span>  references: 36
</span></span><span style="display:flex;"><span>  solutions: 33
</span></span><span style="display:flex;"><span>  workarounds: 1
</span></span><span style="display:flex;"><span>  x_adpType: supplier
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[adp] siemens-SADP (updated 2026-10-15)
</span></span><span style="display:flex;"><span>  affected_products: 10
</span></span><span style="display:flex;"><span>  references: 2
</span></span><span style="display:flex;"><span>  x_adpType: supplier
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[adp] CISA-ADP (updated 2026-10-29)
</span></span><span style="display:flex;"><span>  ssvc: Exploitation=active, Automatable=no, Technical Impact=total
</span></span><span style="display:flex;"><span>  kev: dateAdded=2026-10-28
</span></span><span style="display:flex;"><span>  cwe: [&#39;CWE-456&#39;]
</span></span><span style="display:flex;"><span>  references: 1
</span></span></code></pre></div><p>Read it as five separate statements:</p>
<ul>
<li>The <strong>CNA</strong> says what the kernel project considers affected and scores it 7.8. It gives no CWE.</li>
<li>The <strong>CVE Program</strong> added four references a tool reading only the CNA container would miss.</li>
<li><strong>Red Hat</strong> lists 39 affected product entries for its own streams with 33 solutions, rates it Important, and maps it to CWE-123.</li>
<li><strong>Siemens</strong> lists 10 affected product entries for its own devices.</li>
<li><strong>CISA</strong> says exploitation is active, records the KEV date, and maps it to CWE-456, a different CWE from Red Hat&rsquo;s.</li>
</ul>
<p>None of these overrides the others. A kernel team, a RHEL fleet owner and an OT engineer running Siemens PLCs each need a different slice of the same record.</p>
<h2 id="a-tested-script-who-said-what">A tested script: who said what</h2>
<p>This Python script prints one block per container. It uses only the standard library and was run with Python 3.13 against the record above and against the 2,000-record sample.</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;Print who said what in a CVE JSON 5 record: one line per container and field.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json<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">summarize</span>(container):
</span></span><span style="display:flex;"><span>    out <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> container<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;metrics&#34;</span>, []):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> k, v <span style="color:#f92672">in</span> m<span style="color:#f92672">.</span>items():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> k<span style="color:#f92672">.</span>startswith(<span style="color:#e6db74">&#34;cvss&#34;</span>):
</span></span><span style="display:flex;"><span>                out<span style="color:#f92672">.</span>setdefault(<span style="color:#e6db74">&#34;cvss&#34;</span>, [])<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>v[<span style="color:#e6db74">&#39;version&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>v[<span style="color:#e6db74">&#39;baseScore&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>v[<span style="color:#e6db74">&#39;vectorString&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">elif</span> k <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;other&#34;</span>:
</span></span><span style="display:flex;"><span>                t, c <span style="color:#f92672">=</span> v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;type&#34;</span>), v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;content&#34;</span>, {})
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> t <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;ssvc&#34;</span>:
</span></span><span style="display:flex;"><span>                    out[<span style="color:#e6db74">&#34;ssvc&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;, &#34;</span><span style="color:#f92672">.</span>join(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>k2<span style="color:#e6db74">}</span><span style="color:#e6db74">=</span><span style="color:#e6db74">{</span>v2<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <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> k2, v2 <span style="color:#f92672">in</span> o<span style="color:#f92672">.</span>items())
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">elif</span> t <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;kev&#34;</span>:
</span></span><span style="display:flex;"><span>                    out[<span style="color:#e6db74">&#34;kev&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;dateAdded=</span><span style="color:#e6db74">{</span>c<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;dateAdded&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                    out<span style="color:#f92672">.</span>setdefault(<span style="color:#e6db74">&#34;other&#34;</span>, [])<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>t<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>c<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;value&#39;</span>, c)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    cwes <span style="color:#f92672">=</span> [d[<span style="color:#e6db74">&#34;cweId&#34;</span>] <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> container<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;problemTypes&#34;</span>, []) <span style="color:#66d9ef">for</span> d <span style="color:#f92672">in</span> p<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;descriptions&#34;</span>, []) <span style="color:#66d9ef">if</span> d<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;cweId&#34;</span>)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> cwes: out[<span style="color:#e6db74">&#34;cwe&#34;</span>] <span style="color:#f92672">=</span> cwes
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> container<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;affected&#34;</span>): out[<span style="color:#e6db74">&#34;affected_products&#34;</span>] <span style="color:#f92672">=</span> len(container[<span style="color:#e6db74">&#34;affected&#34;</span>])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> container<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;references&#34;</span>): out[<span style="color:#e6db74">&#34;references&#34;</span>] <span style="color:#f92672">=</span> len(container[<span style="color:#e6db74">&#34;references&#34;</span>])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;solutions&#34;</span>, <span style="color:#e6db74">&#34;workarounds&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> container<span style="color:#f92672">.</span>get(k): out[k] <span style="color:#f92672">=</span> len(container[k])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;x_adpType&#34;</span> <span style="color:#f92672">in</span> container: out[<span style="color:#e6db74">&#34;x_adpType&#34;</span>] <span style="color:#f92672">=</span> container[<span style="color:#e6db74">&#34;x_adpType&#34;</span>]
</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>rec <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>]))
</span></span><span style="display:flex;"><span>c <span style="color:#f92672">=</span> rec[<span style="color:#e6db74">&#34;containers&#34;</span>]
</span></span><span style="display:flex;"><span>rows <span style="color:#f92672">=</span> [(<span style="color:#e6db74">&#34;cna&#34;</span>, c[<span style="color:#e6db74">&#34;cna&#34;</span>])] <span style="color:#f92672">+</span> [(<span style="color:#e6db74">&#34;adp&#34;</span>, a) <span style="color:#66d9ef">for</span> a <span style="color:#f92672">in</span> c<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;adp&#34;</span>, [])]
</span></span><span style="display:flex;"><span>print(rec[<span style="color:#e6db74">&#34;cveMetadata&#34;</span>][<span style="color:#e6db74">&#34;cveId&#34;</span>], rec[<span style="color:#e6db74">&#34;cveMetadata&#34;</span>][<span style="color:#e6db74">&#34;state&#34;</span>], <span style="color:#e6db74">&#34;dataVersion&#34;</span>, rec<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;dataVersion&#34;</span>))
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> kind, cont <span style="color:#f92672">in</span> rows:
</span></span><span style="display:flex;"><span>    pm <span style="color:#f92672">=</span> cont[<span style="color:#e6db74">&#34;providerMetadata&#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:#ae81ff">\n</span><span style="color:#e6db74">[</span><span style="color:#e6db74">{</span>kind<span style="color:#e6db74">}</span><span style="color:#e6db74">] </span><span style="color:#e6db74">{</span>pm<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;shortName&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74"> (updated </span><span style="color:#e6db74">{</span>pm<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;dateUpdated&#39;</span>, <span style="color:#e6db74">&#39;?&#39;</span>)[:<span style="color:#ae81ff">10</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">)&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> k, v <span style="color:#f92672">in</span> summarize(cont)<span style="color:#f92672">.</span>items():
</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>k<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>v<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span></code></pre></div><p>Run it with <code>python3 containers.py CVE-2026-46300.json</code>, using a record file from the cvelistV5 repository (<code>cves/2026/46xxx/CVE-2026-46300.json</code>). It is a reading aid, not a merge engine; the next section is about merging.</p>
<h2 id="how-to-merge-adp-data-without-lying-to-yourself">How to merge ADP data without lying to yourself</h2>
<p>There is no Program-defined precedence between ADPs. The following policy is our editorial recommendation, built on the rules quoted above:</p>
<ol>
<li><strong>Always read <code>cna</code> and the <code>CVE</code> container.</strong> The README calls them the minimum. Union and deduplicate references across both.</li>
<li><strong>Store values with their provider.</strong> Keep <code>cvss_cna</code>, <code>cvss_cisa_adp</code> and <code>cvss_supplier</code> side by side rather than one <code>cvss</code> column. The same goes for CWE.</li>
<li><strong>Prefer the CNA for scoring, fall back to CISA-ADP.</strong> Use the CNA&rsquo;s CVSS when present; use CISA-ADP&rsquo;s when the CNA gave none (the common case in our sample); flag disagreements for a human when both exist.</li>
<li><strong>Take SSVC and the KEV date from CISA-ADP.</strong> No other container in our sample carried them. Then confirm KEV details against the catalog.</li>
<li><strong>Scope supplier data to the supplier.</strong> Use redhat-SADP <code>affected</code> entries only to match Red Hat packages, and siemens-SADP entries only for Siemens products. Never use them to clear or confirm an upstream version.</li>
<li><strong>Do not ignore unknown publishers.</strong> Log any new <code>shortName</code> so a person can decide how to treat it, rather than dropping it silently.</li>
<li><strong>Check the record state first.</strong> A rejected record has no ADP data to act on; see our guide to <a href="/posts/cve-record-states-published-rejected-and-disputed-what-each-means-for-vulnerability-triage/">CVE record states</a>.</li>
</ol>
<p>These steps fit into the record-reading stage of our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">vulnerability prioritization workflow</a>, which takes a CVE from record to patch ticket.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul>
<li><strong>Reading only <code>containers.cna</code>.</strong> You lose CISA&rsquo;s SSVC and KEV signals, about one in five CVSS scores in our 2026 sample, and Program-added references.</li>
<li><strong>Taking <code>adp[0]</code>.</strong> Container order is not a precedence order. In CVE-2026-46300, CISA-ADP came last.</li>
<li><strong>Merging CWE lists without provenance.</strong> Red Hat and CISA mapped the same CVE to different CWEs. A merged list hides that they disagree.</li>
<li><strong>Matching upstream versions against supplier entries.</strong> RPM release strings and Siemens firmware versions are not kernel versions.</li>
<li><strong>Hard-coding today&rsquo;s publisher list.</strong> Supplier ADP is new. More publishers will appear.</li>
</ul>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The sample covers 2,000 of 84,066 2026 record files in cvelistV5 on November 3, 2026, chosen at random, and 2026 records only. Shares will differ for older years, since the README says the CISA-ADP container was launched on June 4 (2024) and applied back to February 2024. We could not load cve.org&rsquo;s ADP pages (they render with JavaScript), so the description of supplier ADPs relies on Siemens&rsquo; own page and on the records themselves. <code>x_adpType</code> is an extension field, not part of the schema. We did not check how NVD or commercial scanners consume ADP data.</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>One cna container, multiple adp containers; ADP requires only providerMetadata; x_ extensions allowed</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE JSON schema, CVE_Record_Format.json</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE Program Container: shortName CVE, Program references after 7/31/2024, CNA + Program container are the minimum</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/README.md">cvelistV5 README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CISA-ADP provides SSVC, KEV data and Vulnrichment; launched June 4</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/README.md">cvelistV5 README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CISA fills CVSS/CWE where the CNA has not and does not overwrite CNA data</td>
          <td><a href="https://github.com/cisagov/vulnrichment/blob/develop/README.md">CISA Vulnrichment README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Supplier ADP purpose and April 2026 introduction</td>
          <td><a href="https://www.siemens.com/en-gb/content/cert-services-sadp/">Siemens: CERT services, SADP</a></td>
          <td>Qualified (vendor page)</td>
      </tr>
      <tr>
          <td>Sample shares (93.9%, 2.2%, 1.2%, 19.8%, 17.1%, 5 KEV blocks; 381/389, 326/336)</td>
          <td>Our count over 2,000 random files from <a href="https://github.com/CVEProject/cvelistV5">cvelistV5</a> at commit 1a44d59</td>
          <td>Verified (code)</td>
      </tr>
      <tr>
          <td>CVE-2026-46300 container contents</td>
          <td><a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/46xxx/CVE-2026-46300.json">cvelistV5 record</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE-2026-46300 KEV dateAdded 2026-10-28</td>
          <td><a href="https://github.com/cisagov/kev-data">CISA KEV data</a></td>
          <td>Verified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>CVE Affected Versions: lessThan, defaultStatus, versionType</title><link>https://firewallsync.com/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/</link><guid>https://firewallsync.com/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/</guid><pubDate>Mon, 02 Nov 2026 04:40:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>The affected block in a CVE record is a small algorithm, not a list. Here is how version, lessThan, lessThanOrEqual, changes, versionType and defaultStatus combine, what a sample of 1,000 2026 CVE records actually contains, and a tested Python evaluator that refuses version types it cannot order.</description><enclosure url="https://firewallsync.com/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/cover_hu16366087011500977870.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/cover_hu16366087011500977870.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/cover_hu16366087011500977870.jpg" alt="Macro close-up of the steel scales of a vernier caliper, with inch and centimetre graduations and a sliding vernier scale" width="1200" height="630"></p><p><strong>Scope.</strong> This is a specification explainer with measurements from public data. We read the CVE JSON schema (version 5.2.0, CVEProject/cve-schema main branch) and its version-encoding guide, sampled 1,000 random 2026 records from the cvelistV5 repository at commit 451e97a (November 2, 2026), and ran the Python script below against real records. We did not test commercial scanners.</p>
<h2 id="the-short-answer">The short answer</h2>
<p>A CVE record&rsquo;s <code>affected</code> block does not hand you a list of bad versions. For each product it gives a small decision procedure:</p>
<ol>
<li>Walk the <code>versions</code> list in order.</li>
<li>An entry with only <code>version</code> and <code>status</code> matches that exact version.</li>
<li>An entry with <code>versionType</code> plus <code>lessThan</code> or <code>lessThanOrEqual</code> matches a range, and an optional <code>changes</code> list can flip the status partway through it.</li>
<li>The first matching entry wins. If nothing matches, the answer is <code>defaultStatus</code>, and if that is missing, the answer is <code>unknown</code>.</li>
</ol>
<p>The possible answers are <code>affected</code>, <code>unaffected</code> and <code>unknown</code>. Your tooling needs a fourth, &ldquo;I cannot order these versions&rdquo;, because the record does not always give you enough to decide.</p>
<h2 id="where-the-fields-live">Where the fields live</h2>
<p>The CNA (CVE Numbering Authority) writes <code>containers.cna.affected</code>, an array of product objects. The schema requires each product to be identified by either <code>vendor</code> and <code>product</code> or <code>collectionURL</code> and <code>packageName</code>, and to carry <code>versions</code>, <code>defaultStatus</code> or both. A 5.2 record can also carry a <code>packageURL</code> (a purl, which the schema says must not include a version).</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;vendor&#34;</span>: <span style="color:#e6db74">&#34;GitoxideLabs&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;product&#34;</span>: <span style="color:#e6db74">&#34;gitoxide&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;defaultStatus&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;packageURL&#34;</span>: <span style="color:#e6db74">&#34;pkg:cargo/gix-fs&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;versions&#34;</span>: [
</span></span><span style="display:flex;"><span>    {<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;0.23.0&#34;</span>},
</span></span><span style="display:flex;"><span>    {<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0.23.0&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is the product entry from CVE-2026-100419 (a gix-fs symlink worktree escape), shown in full. Read it as: semantic versions from 0 up to but not including 0.23.0 are affected, 0.23.0 is unaffected, and anything else is unaffected by default.</p>
<h2 id="the-fields-one-by-one">The fields, one by one</h2>
<table>
  <thead>
      <tr>
          <th>Field</th>
          <th>What the schema says</th>
          <th>What it means in practice</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>version</code></td>
          <td>The single version, or the start of a range. &ldquo;By convention, typically 0 denotes the earliest possible version.&rdquo;</td>
          <td><code>&quot;version&quot;: &quot;0&quot;</code> means &ldquo;no lower bound&rdquo;</td>
      </tr>
      <tr>
          <td><code>status</code></td>
          <td>One of <code>affected</code>, <code>unaffected</code>, <code>unknown</code></td>
          <td><code>unknown</code> can mean nobody investigated or the vendor did not disclose</td>
      </tr>
      <tr>
          <td><code>versionType</code></td>
          <td>&ldquo;The version numbering system used for specifying the range&rdquo;; examples include <code>semver</code>, <code>maven</code>, <code>python</code>, <code>rpm</code>, <code>git</code>, <code>custom</code></td>
          <td>Picks the comparison rules. Required for a range</td>
      </tr>
      <tr>
          <td><code>lessThan</code></td>
          <td>&ldquo;The non-inclusive upper limit of the range. This is the least version NOT in the range.&rdquo;</td>
          <td>Usually the first fixed release</td>
      </tr>
      <tr>
          <td><code>lessThanOrEqual</code></td>
          <td>&ldquo;The inclusive upper limit of the range&rdquo;</td>
          <td>Usually the last vulnerable release</td>
      </tr>
      <tr>
          <td><code>changes</code></td>
          <td>Status changes inside a range, each with <code>at</code> and <code>status</code>; &ldquo;clients must re-sort the list themselves rather than assume it is sorted&rdquo;</td>
          <td>Lets one entry say &ldquo;affected, then fixed from X&rdquo;</td>
      </tr>
      <tr>
          <td><code>defaultStatus</code></td>
          <td>&ldquo;The default status for versions that are not otherwise listed&hellip; If not specified, defaultStatus defaults to &lsquo;unknown&rsquo;.&rdquo;</td>
          <td>The answer for every version no entry matched</td>
      </tr>
  </tbody>
</table>
<p>The upper limit can end in <code>*</code>, which the schema defines as &ldquo;an arbitrarily large number in the version ordering&rdquo;. <code>{version: 1.0, lessThan: 1.*}</code> describes the whole 1.x branch, and <code>lessThan: *</code> means &ldquo;and everything after&rdquo;.</p>
<h3 id="why-versiontype-is-not-optional">Why versionType is not optional</h3>
<p>The schema guide explains that &ldquo;there is no single definition of &rsquo;less than&rsquo; for versions&rdquo; and gives the example that in semantic versioning <code>1.0.0-cr1</code> sorts before <code>1.0.0-m1</code>, while in Maven the opposite is true. If your code compares every version string with one generic routine, it will be wrong for some ecosystems.</p>
<p>The guide is blunt about <code>custom</code>: it &ldquo;means that the version status algorithm cannot be executed, so its use is discouraged&rdquo;. It exists to convert older data.</p>
<h2 id="three-records-three-encodings">Three records, three encodings</h2>
<p><strong>Allow-list style (gix-fs, CVE-2026-100419).</strong> The range is affected and <code>defaultStatus</code> is <code>unaffected</code>. Version 0.22.1 is affected, 0.23.0 is unaffected by exact match, and 0.25.4 is unaffected by default.</p>
<p><strong>List without a default (quickjs, CVE-2026-0821).</strong> The CNA lists eleven single versions from 0.1 to 0.11.0 as affected and sets no <code>defaultStatus</code>. Version 0.10 is affected. Version 0.12.0 is <code>unknown</code>: the record makes no claim about it. A naive matcher that treats &ldquo;not listed&rdquo; as &ldquo;not affected&rdquo; would clear it.</p>
<p><strong>Deny-list style (Linux, CVE-2026-100075).</strong> The second product entry sets <code>&quot;defaultStatus&quot;: &quot;affected&quot;</code> and lists unaffected ranges per stable branch:</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-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;4.7&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;0&#34;</span>, <span style="color:#f92672">&#34;lessThan&#34;</span>: <span style="color:#e6db74">&#34;4.7&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;6.6.157&#34;</span>, <span style="color:#f92672">&#34;lessThanOrEqual&#34;</span>: <span style="color:#e6db74">&#34;6.6.*&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;semver&#34;</span>}<span style="color:#960050;background-color:#1e0010">,</span>
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;7.3&#34;</span>, <span style="color:#f92672">&#34;lessThanOrEqual&#34;</span>: <span style="color:#e6db74">&#34;*&#34;</span>, <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;unaffected&#34;</span>, <span style="color:#f92672">&#34;versionType&#34;</span>: <span style="color:#e6db74">&#34;original_commit_for_fix&#34;</span>}
</span></span></code></pre></div><p>(Excerpt; the record also lists 5.10, 5.15, 6.1, 6.12, 6.18 and 7.2 branches.) Kernels before 4.7 are unaffected, 6.6.157 and later 6.6 releases are unaffected, and anything not covered, such as 6.6.150 or a 5.4 kernel, falls through to <code>affected</code>. The first product entry in the same record describes the same flaw with <code>versionType: git</code> commit ranges, which you cannot evaluate without the kernel&rsquo;s commit history.</p>
<h2 id="what-a-sample-of-2026-records-contains">What a sample of 2026 records contains</h2>
<p>We drew 1,000 CVE IDs at random (seeded) from the 83,665 record files under <code>cves/2026</code> at cvelistV5 commit 451e97a. 16 were <code>REJECTED</code>; the 984 published records held 2,061 product entries, 4,555 version entries and 2,831 ranges. Counts were computed with a Python script over the JSON.</p>
<table>
  <thead>
      <tr>
          <th>Measure</th>
          <th>Count</th>
          <th>Share</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Product entries with no <code>defaultStatus</code> (so <code>unknown</code> applies)</td>
          <td>1,130 of 2,061</td>
          <td>54.8%</td>
      </tr>
      <tr>
          <td><code>defaultStatus: unaffected</code></td>
          <td>585</td>
          <td>28.4%</td>
      </tr>
      <tr>
          <td><code>defaultStatus: affected</code></td>
          <td>311</td>
          <td>15.1%</td>
      </tr>
      <tr>
          <td>Product entries with only single versions and no <code>defaultStatus</code></td>
          <td>315</td>
          <td>15.3%</td>
      </tr>
      <tr>
          <td>Version entries that are single versions / <code>lessThan</code> ranges / <code>lessThanOrEqual</code> ranges</td>
          <td>1,724 / 1,929 / 902 of 4,555</td>
          <td>37.8% / 42.3% / 19.8%</td>
      </tr>
      <tr>
          <td>Ranges with <code>versionType: custom</code></td>
          <td>1,006 of 2,831</td>
          <td>35.5%</td>
      </tr>
      <tr>
          <td>Ranges with <code>semver</code> / <code>git</code> / <code>rpm</code></td>
          <td>971 / 560 / 171</td>
          <td>34.3% / 19.8% / 6.0%</td>
      </tr>
      <tr>
          <td>Ranges with a <code>*</code> in the upper limit</td>
          <td>717</td>
          <td>25.3%</td>
      </tr>
      <tr>
          <td>Version entries using <code>changes</code></td>
          <td>48</td>
          <td>1.1%</td>
      </tr>
      <tr>
          <td>Published records with a <code>cpeApplicability</code> block</td>
          <td>290 of 984</td>
          <td>29.5%</td>
      </tr>
      <tr>
          <td>Published records with a <code>packageURL</code> on any product</td>
          <td>61</td>
          <td>6.2%</td>
      </tr>
  </tbody>
</table>
<p>Two numbers matter most for automation. More than half of product entries leave unlisted versions as <code>unknown</code>, and about a third of ranges use <code>custom</code>, so a generic comparator cannot decide them. This is one sample at one point in time; the mix varies by CNA, and a different year or sample would give different shares.</p>
<h2 id="a-tested-evaluator">A tested evaluator</h2>
<p>The script implements the schema&rsquo;s algorithm. It handles dotted numeric versions (so 1.2.10 sorts after 1.2.9) and <code>*</code>, and it refuses any range whose <code>versionType</code> it has not been told how to order. It does not implement semver pre-release ordering, RPM epochs or Maven qualifiers; treat it as a reference, not a drop-in matcher. 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;Decide affected / unaffected / unknown for one product version from a CVE record,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">following the algorithm in the CVE JSON 5 schema (product.versions + defaultStatus).
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Usage: affected_status.py RECORD.json VERSION [vendor/product filter] [--numeric TYPE,...]
</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> json<span style="color:#f92672">,</span> sys
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>SUPPORTED <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;semver&#34;</span>}  <span style="color:#75715e"># extend with real comparators (rpm, maven, python...) before use</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">key</span>(v):
</span></span><span style="display:flex;"><span>    parts <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> v<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;.&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> p <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;*&#34;</span>:
</span></span><span style="display:flex;"><span>            parts<span style="color:#f92672">.</span>append(float(<span style="color:#e6db74">&#34;inf&#34;</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> p<span style="color:#f92672">.</span>isdigit():
</span></span><span style="display:flex;"><span>            parts<span style="color:#f92672">.</span>append(int(p))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;non-numeric version part </span><span style="color:#e6db74">{</span>p<span style="color:#e6db74">!r}</span><span style="color:#e6db74"> in </span><span style="color:#e6db74">{</span>v<span style="color:#e6db74">!r}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> parts
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">cmp</span>(a, b):
</span></span><span style="display:flex;"><span>    ka, kb <span style="color:#f92672">=</span> key(a), key(b)
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> max(len(ka), len(kb))
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># pad: a missing part is 0, except after a wildcard, which stays infinite</span>
</span></span><span style="display:flex;"><span>    ka <span style="color:#f92672">+=</span> [float(<span style="color:#e6db74">&#34;inf&#34;</span>) <span style="color:#66d9ef">if</span> ka <span style="color:#f92672">and</span> ka[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> float(<span style="color:#e6db74">&#34;inf&#34;</span>) <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> (n <span style="color:#f92672">-</span> len(ka))
</span></span><span style="display:flex;"><span>    kb <span style="color:#f92672">+=</span> [float(<span style="color:#e6db74">&#34;inf&#34;</span>) <span style="color:#66d9ef">if</span> kb <span style="color:#f92672">and</span> kb[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>] <span style="color:#f92672">==</span> float(<span style="color:#e6db74">&#34;inf&#34;</span>) <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> (n <span style="color:#f92672">-</span> len(kb))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (ka <span style="color:#f92672">&gt;</span> kb) <span style="color:#f92672">-</span> (ka <span style="color:#f92672">&lt;</span> kb)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">status</span>(product, v):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> e <span style="color:#f92672">in</span> product<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;versions&#34;</span>, []):
</span></span><span style="display:flex;"><span>        is_range <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;lessThan&#34;</span> <span style="color:#f92672">in</span> e <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span> <span style="color:#f92672">in</span> e
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> is_range:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> v <span style="color:#f92672">==</span> e[<span style="color:#e6db74">&#34;version&#34;</span>]:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> e[<span style="color:#e6db74">&#34;status&#34;</span>], <span style="color:#e6db74">&#34;exact match&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        vt <span style="color:#f92672">=</span> e<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;versionType&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> vt <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> SUPPORTED:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;undetermined&#34;</span>, <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;range uses versionType </span><span style="color:#e6db74">{</span>vt<span style="color:#e6db74">!r}</span><span style="color:#e6db74">; needs that ordering&#34;</span>
</span></span><span style="display:flex;"><span>        lo_ok <span style="color:#f92672">=</span> cmp(e[<span style="color:#e6db74">&#34;version&#34;</span>], v) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>        hi_ok <span style="color:#f92672">=</span> (cmp(v, e[<span style="color:#e6db74">&#34;lessThan&#34;</span>]) <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;lessThan&#34;</span> <span style="color:#f92672">in</span> e <span style="color:#66d9ef">else</span> (cmp(v, e[<span style="color:#e6db74">&#34;lessThanOrEqual&#34;</span>]) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> lo_ok <span style="color:#f92672">and</span> hi_ok:
</span></span><span style="display:flex;"><span>            s <span style="color:#f92672">=</span> e[<span style="color:#e6db74">&#34;status&#34;</span>]
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> c <span style="color:#f92672">in</span> sorted(e<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;changes&#34;</span>, []), key<span style="color:#f92672">=</span><span style="color:#66d9ef">lambda</span> c: key(c[<span style="color:#e6db74">&#34;at&#34;</span>])):  <span style="color:#75715e"># schema: re-sort yourself</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> cmp(c[<span style="color:#e6db74">&#34;at&#34;</span>], v) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>                    s <span style="color:#f92672">=</span> c[<span style="color:#e6db74">&#34;status&#34;</span>]
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> s, <span style="color:#e6db74">&#34;range match&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> product<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;defaultStatus&#34;</span>, <span style="color:#e6db74">&#34;unknown&#34;</span>), <span style="color:#e6db74">&#34;defaultStatus&#34;</span> <span style="color:#f92672">+</span> (<span style="color:#e6db74">&#34;&#34;</span> <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;defaultStatus&#34;</span> <span style="color:#f92672">in</span> product <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34; (absent, so unknown)&#34;</span>)
</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>    args <span style="color:#f92672">=</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>:]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;--numeric&#34;</span> <span style="color:#f92672">in</span> args:  <span style="color:#75715e"># opt-in: treat these extra versionTypes as dotted numeric</span>
</span></span><span style="display:flex;"><span>        i <span style="color:#f92672">=</span> args<span style="color:#f92672">.</span>index(<span style="color:#e6db74">&#34;--numeric&#34;</span>)
</span></span><span style="display:flex;"><span>        SUPPORTED<span style="color:#f92672">.</span>update(args[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;,&#34;</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">del</span> args[i:i <span style="color:#f92672">+</span> <span style="color:#ae81ff">2</span>]
</span></span><span style="display:flex;"><span>    sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>:] <span style="color:#f92672">=</span> args
</span></span><span style="display:flex;"><span>    rec <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>load(open(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>]))
</span></span><span style="display:flex;"><span>    version <span style="color:#f92672">=</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">2</span>]
</span></span><span style="display:flex;"><span>    want <span style="color:#f92672">=</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">3</span>]<span style="color:#f92672">.</span>lower() <span style="color:#66d9ef">if</span> len(sys<span style="color:#f92672">.</span>argv) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">3</span> <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> rec[<span style="color:#e6db74">&#34;cveMetadata&#34;</span>][<span style="color:#e6db74">&#34;state&#34;</span>] <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;PUBLISHED&#34;</span>:
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>rec[<span style="color:#e6db74">&#39;cveMetadata&#39;</span>][<span style="color:#e6db74">&#39;cveId&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> is </span><span style="color:#e6db74">{</span>rec[<span style="color:#e6db74">&#39;cveMetadata&#39;</span>][<span style="color:#e6db74">&#39;state&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i, p <span style="color:#f92672">in</span> enumerate(rec[<span style="color:#e6db74">&#34;containers&#34;</span>][<span style="color:#e6db74">&#34;cna&#34;</span>]<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;affected&#34;</span>, [])):
</span></span><span style="display:flex;"><span>        name <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>p<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;vendor&#39;</span>, <span style="color:#e6db74">&#39;&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>p<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;product&#39;</span>, p<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;packageName&#39;</span>, <span style="color:#e6db74">&#39;&#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> want <span style="color:#f92672">and</span> want <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> name<span style="color:#f92672">.</span>lower():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</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>rec[<span style="color:#e6db74">&#39;cveMetadata&#39;</span>][<span style="color:#e6db74">&#39;cveId&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> [</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">] </span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">{</span>version<span style="color:#e6db74">}</span><span style="color:#e6db74">: %s (%s)&#34;</span> <span style="color:#f92672">%</span> status(p, version))
</span></span></code></pre></div><p>We ran it with Python 3.13 on November 2, 2026 against records downloaded from cvelistV5 (<code>raw.githubusercontent.com/CVEProject/cvelistV5/&lt;commit&gt;/cves/2026/&lt;block&gt;/&lt;CVE&gt;.json</code>). Selected output:</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>CVE-2026-100419 [0] GitoxideLabs/gitoxide 0.22.1: affected (range match)
</span></span><span style="display:flex;"><span>CVE-2026-100419 [0] GitoxideLabs/gitoxide 0.23.0: unaffected (exact match)
</span></span><span style="display:flex;"><span>CVE-2026-100419 [0] GitoxideLabs/gitoxide 0.25.4: unaffected (defaultStatus)
</span></span><span style="display:flex;"><span>CVE-2026-0821 [0] quickjs-ng/quickjs 0.12.0: unknown (defaultStatus (absent, so unknown))
</span></span><span style="display:flex;"><span>CVE-2026-100075 [0] Linux/Linux 6.6.150: undetermined (range uses versionType &#39;git&#39;; needs that ordering)
</span></span><span style="display:flex;"><span>CVE-2026-100075 [1] Linux/Linux 6.6.150: undetermined (range uses versionType &#39;original_commit_for_fix&#39;; needs that ordering)
</span></span><span style="display:flex;"><span>CVE-2026-103345 [0] Shamim Rajani/Pie Register 3.8.4.13: undetermined (range uses versionType &#39;custom&#39;; needs that ordering)
</span></span><span style="display:flex;"><span>CVE-2026-23915 is REJECTED
</span></span></code></pre></div><p>The Linux result is the instructive one. For 6.6.150 the evaluator reaches the <code>7.3 ... *</code> entry, whose <code>versionType</code> is <code>original_commit_for_fix</code>, a value that is not among the schema&rsquo;s examples. A strict implementation stops there. If you decide, for this CNA&rsquo;s data, that those values are ordinary kernel version numbers, you can opt in with <code>--numeric original_commit_for_fix</code>, and the second product entry then gives 4.4.302 unaffected, 4.7 affected, 6.6.150 affected (by <code>defaultStatus</code>), 6.6.157 unaffected and 7.3 unaffected. That opt-in is our judgment about one CNA&rsquo;s encoding, not something the schema states.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul>
<li><strong>Treating &ldquo;not listed&rdquo; as &ldquo;not affected&rdquo;.</strong> Without <code>defaultStatus: unaffected</code>, unlisted versions are <code>unknown</code>.</li>
<li><strong>Off-by-one on the upper bound.</strong> <code>lessThan: 0.23.0</code> excludes 0.23.0; <code>lessThanOrEqual: 3.8.4.13</code> includes 3.8.4.13.</li>
<li><strong>Comparing strings.</strong> As text, &ldquo;0.10&rdquo; sorts before &ldquo;0.9&rdquo;. Parse versions with the ecosystem&rsquo;s rules.</li>
<li><strong>Assuming <code>changes</code> is sorted.</strong> The schema tells clients to re-sort.</li>
<li><strong>Reading only the first product entry.</strong> The same record can describe a product twice, as the Linux record does with git ranges and version ranges.</li>
<li><strong>Ignoring the record state.</strong> A <code>REJECTED</code> record has no <code>affected</code> block to evaluate; see <a href="/posts/cve-record-states-published-rejected-and-disputed-what-each-means-for-vulnerability-triage/">CVE record states</a>.</li>
</ul>
<h2 id="where-this-fits-in-triage">Where this fits in triage</h2>
<p>Version matching answers &ldquo;do we run an affected version?&rdquo;, which comes before any priority signal. If the answer is <code>unknown</code> or undetermined, check the vendor advisory, or a supplier VEX statement that says whether the product is affected (see <a href="/posts/vex-explained-what-a-not-affected-statement-means-and-how-to-read-one/">how to read a VEX not-affected statement</a>). An SBOM with accurate component versions is what makes this lookup possible at scale; our summary of the <a href="/posts/sbom-minimum-elements-2026-what-cisa-and-partners-changed-from-the-2021-ntia-baseline/">2026 SBOM minimum elements</a> covers what to ask suppliers for. Once a finding is confirmed as affected, our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">vulnerability prioritization workflow</a> takes it from record to ticket using KEV, EPSS and SSVC.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The sample covers 2026 records only and reflects the repository on November 2, 2026; CNAs update records, so shares will drift. Our evaluator is a reference implementation of the schema&rsquo;s algorithm for numeric versions, not a full comparator for semver pre-releases, RPM, Maven, Python or git ranges. We did not examine ADP containers, which can carry their own <code>affected</code> data (15 of the 984 sampled records had one), or NVD&rsquo;s separate CPE configurations.</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>Field definitions, the status algorithm, <code>defaultStatus</code> defaults to unknown, <code>*</code> wildcard, <code>changes</code> must be re-sorted, <code>packageURL</code> must not include a version</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json">CVE JSON schema, CVE_Record_Format.json (v5.2.0)</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>version: 0</code> convention, Maven vs semver ordering example, <code>custom</code> means the algorithm cannot be executed</td>
          <td><a href="https://github.com/CVEProject/cve-schema/blob/main/schema/docs/versions.md">CVE schema version-encoding guide (versions.md)</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Example records CVE-2026-100419, CVE-2026-0821, CVE-2026-100075, CVE-2026-103345, CVE-2026-23915</td>
          <td><a href="https://github.com/CVEProject/cvelistV5">cvelistV5 repository</a>, commit 451e97a</td>
          <td>Verified (read November 2, 2026)</td>
      </tr>
      <tr>
          <td>Sample counts and percentages</td>
          <td>Our analysis of 1,000 randomly sampled 2026 records from cvelistV5 at commit 451e97a</td>
          <td>Verified (computed with code); a sample, not a census</td>
      </tr>
      <tr>
          <td>Evaluator output</td>
          <td>Script above, run with Python 3.13</td>
          <td>Verified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>