<?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>Cpe | FirewallSync</title><link>https://firewallsync.com/tags/cpe/</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>Wed, 04 Nov 2026 04:30:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/cpe/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>