# CVE cpeApplicability: CPE Match Nodes, Operators, Ranges

> 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.

- URL: https://firewallsync.com/posts/cve-cpeapplicability-cpe-match-nodes-operators-version-ranges/
- Site: FirewallSync (https://firewallsync.com/)
- Topic: AppSec & APIs
- Tags: vulnerability management, cve, cve json 5, cpe, cpeApplicability, version matching, appsec
- Published: 2026-11-04
- Author: Craig Leslie

## Key takeaways

- cpeApplicability is an optional array in the CNA container (and allowed in ADP containers) of a CVE JSON record. The schema says it is "mostly copied/forked from the NIST NVD CVE API v2.0 schema", so it reads like an NVD configuration.
- Each element holds nodes; each node has an operator (AND or OR, required), an optional negate flag and a cpeMatch list. Each cpeMatch needs criteria (a CPE 2.3 name) and vulnerable, plus up to four versionStart/End bounds.
- vulnerable: false marks a platform or context CPE, such as the operating system a vulnerable application must run on. A match that only hits non-vulnerable entries does not mean the host is affected.
- In a random sample of 2,000 records from 2026 (1,969 published), 27.3% carried a CNA cpeApplicability block; four CNAs (Linux, VulnCheck, Microsoft, Oracle) produced 88.7% of them, and no ADP container carried one.
- CPE ranges cannot express stable-branch fixes. In a Linux kernel record we tested, cpeApplicability marks 6.18.9 vulnerable while the same record's affected block marks 6.18.8 to 6.18.* unaffected. Prefer the affected block and use CPE as a cross-check.



**Scope.** 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 `dataVersion` 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's own matching engine.

## The short answer

`cpeApplicability` is where a CNA can say, in CPE terms, which configurations a CVE applies to. Each array element is one statement:

1. A statement has `nodes`, and optionally an `operator` and `negate` for combining them.
2. Each node has a required `operator` (`AND` or `OR`), an optional `negate`, and a `cpeMatch` list.
3. Each `cpeMatch` has a CPE 2.3 `criteria` string, a required `vulnerable` boolean, and optional bounds: `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, `versionEndExcluding`.
4. A host is in scope when the logic evaluates true against its CPE inventory **and** at least one matched entry has `vulnerable: true`.

If you already parse NVD configurations, the shape is familiar. The schema says so directly: the block is "mostly copied/forked from the NIST NVD CVE API v2.0 schema".

## Where the block lives

The schema defines `cpeApplicability` 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 `v5.1.1` tag in CVEProject/cve-schema dates from October 2024), which the release notes describe as adding "support for Common Platform Enumeration (CPE) Identifiers using the CPE Applicability Language".

It is separate from the older `cpes` array inside each `affected[]` product. That array is a flat list of CPE names; its own schema description now ends: "Consider using the newer cpeApplicability block for defining CPE data using the CPE Applicability Language which includes more options for defining CPE Names."

The schema adds one instruction that matters for consumers: "it is recommended that it align with (as much as possible) the product data provided within the affected block." Recommended, not required. If you already read the `affected` block with the rules in [our guide to lessThan, defaultStatus and versionType](/posts/cve-affected-versions-lessthan-defaultstatus-versiontype/), treat `cpeApplicability` as a second encoding of the same claim, and expect the two to disagree sometimes.

## The fields, one by one

| Level | Field | Required | Meaning |
|---|---|---|---|
| Element | `nodes` | yes | Array of nodes |
| Element | `operator` | no | `AND` or `OR` between nodes |
| Element | `negate` | no | Invert the element's result |
| Node | `operator` | yes | `AND` or `OR` between the node's cpeMatch entries |
| Node | `negate` | no | Invert the node's result |
| Node | `cpeMatch` | yes | Array of match entries |
| cpeMatch | `criteria` | yes | CPE 2.3 formatted string, e.g. `cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*` |
| cpeMatch | `vulnerable` | yes | `true` for the vulnerable product, `false` for a platform or context CPE |
| cpeMatch | `versionStartIncluding` / `versionStartExcluding` | no | Lower bound, inclusive or exclusive |
| cpeMatch | `versionEndIncluding` / `versionEndExcluding` | no | Upper bound, inclusive or exclusive |
| cpeMatch | `matchCriteriaId` | no | UUID, carried over from NVD's schema |

Two details trip people up.

**Version bounds apply only when the CPE's version field is a wildcard.** A criteria string such as `cpe:2.3:a:ibm:mq_operator:3.4.1:*:...` names one exact version; a string with `*` 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.

**There is no `versionType`.** The `affected` block tells you whether versions are semver, git commits, or a custom scheme; `cpeMatch` does not. Whatever compares `6.18.9` with `6.19` has to guess the ordering, and for strings like `alpha_0.3` or `1.3-beta` there may be no safe guess.

## AND, OR and vulnerable: false

The schema's advanced example record shows the pattern that makes this block more expressive than a flat list. Its single element has `"operator": "AND"` and two nodes:

- Node 1 (`OR`): `example_org:example_enterprise` from 1.0.0 up to (excluding) 1.0.6, or from 2.1.6 up to 2.1.9, each with `vulnerable: true`.
- Node 2 (`OR`): Windows, macOS or a hardware model `xt-4500`, each with `vulnerable: false`.

Read it as: the application is vulnerable in those ranges **when running on** one of those platforms. The platform CPEs are conditions, not affected products. A tool that flags every host whose inventory contains `cpe:2.3:o:microsoft:windows:...` has misread the record.

## What a sample of 2026 records contains

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's CNA and ADP containers.

| Measure | Count | Share |
|---|---|---|
| Published records with a CNA `cpeApplicability` block | 538 of 1,969 | 27.3% |
| ...of which from Linux, VulnCheck, Microsoft or Oracle (186, 110, 97, 84) | 477 of 538 | 88.7% |
| ADP containers carrying `cpeApplicability` | 0 | 0% |
| Records still using the flat `affected[].cpes` list | 195 of 1,969 | 9.9% |
| Elements with no element-level `operator` | 489 of 582 | 84.0% |
| Nodes, all with `operator: OR` | 598 | 100% |
| cpeMatch entries | 2,667 | |
| ...with `vulnerable: false` | 10 | 0.4% |
| ...with a specific version in `criteria` rather than `*` | 170 | 6.4% |
| ...with `versionStartIncluding` / `versionEndExcluding` / `versionEndIncluding` | 2,397 / 2,333 / 113 | 89.9% / 87.5% / 4.2% |
| ...with `matchCriteriaId` | 0 | 0% |

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 `AND` 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.

The absence in ADP containers fits CISA's stated policy. The Vulnrichment README says: "as of December 10, 2024, CISA will no longer be adding CPE strings to the enriched dataset." See [our explainer on CVE ADP containers](/posts/cve-adp-containers-cisa-adp-cve-program-supplier-sadp/) for what CISA-ADP does still add.

## A tested evaluator

The script takes one CVE record and every CPE installed on one host, and answers `True`, `False` or `unknown`. It is illustrative: it compares only dotted numeric versions, handles the `\:`-style escaping in CPE 2.3 names only for splitting, and matches part, vendor and product but ignores the remaining CPE fields.

```python
"""Evaluate a CVE record's cna.cpeApplicability against one host's CPE inventory.
Illustrative: numeric-dotted versions only; anything else is reported as 'unknown'."""
import json, re, sys

def split_cpe(cpe):
    # split on ':' not preceded by a backslash (CPE 2.3 formatted-string escaping)
    return re.split(r'(?<!\\):', cpe)

def vkey(v):
    if not re.fullmatch(r'\d+(\.\d+)*', v):
        raise ValueError(v)
    return tuple(int(x) for x in v.split('.'))

def cmp(a, b):
    a, b = vkey(a), vkey(b)
    n = max(len(a), len(b))
    a, b = a + (0,) * (n - len(a)), b + (0,) * (n - len(b))
    return (a > b) - (a < b)

def match(m, installed_cpe):
    want, have = split_cpe(m['criteria']), split_cpe(installed_cpe)
    # compare part, vendor, product (fields 2-4); '*' is ANY
    for i in (2, 3, 4):
        if want[i] != '*' and want[i] != have[i]:
            return False
    ver = have[5]
    if want[5] not in ('*', '-'):            # exact version in the criteria
        return want[5] == ver
    checks = [('versionStartIncluding', lambda c: c >= 0), ('versionStartExcluding', lambda c: c > 0),
              ('versionEndIncluding', lambda c: c <= 0), ('versionEndExcluding', lambda c: c < 0)]
    return all(test(cmp(ver, m[k])) for k, test in checks if k in m)

def present(m, inventory):
    return any(match(m, c) for c in inventory)

def node(n, inv):
    hits = [present(m, inv) for m in n['cpeMatch']]
    r = all(hits) if n['operator'] == 'AND' else any(hits)
    return not r if n.get('negate') else r

def element(el, inv):
    res = [node(n, inv) for n in el['nodes']]
    r = all(res) if el.get('operator') == 'AND' else any(res)  # no operator: OR
    r = not r if el.get('negate') else r
    # the statement must also hit at least one entry flagged vulnerable
    vuln = any(m['vulnerable'] and present(m, inv)
               for n in el['nodes'] for m in n['cpeMatch'])
    return r and vuln

def evaluate(record, inventory):
    cna = record['containers']['cna']
    try:
        return any(element(el, inventory) for el in cna.get('cpeApplicability', []))
    except ValueError as e:
        return f'unknown (cannot order version {e})'

if __name__ == '__main__':
    rec = json.load(open(sys.argv[1]))
    inventory = sys.argv[2:]          # every CPE installed on one host
    print(' + '.join(inventory), '->', evaluate(rec, inventory))
```

Saved as `cpe_eval.py`, we ran it against three records from the sample and the schema's example (output trimmed to the verdict):

```text
$ python3 -I cpe_eval.py CVE-2026-74994.json 'cpe:2.3:a:erlang:erlang\/otp:28.5.0.5:*:*:*:*:*:*:*'
... -> True
$ python3 -I cpe_eval.py CVE-2026-74994.json 'cpe:2.3:a:erlang:erlang\/otp:28.5.0.6:*:*:*:*:*:*:*'
... -> False
$ python3 -I cpe_eval.py full-record-advanced-example.json cpe:2.3:a:example_org:example_enterprise:1.0.5:*:*:*:*:*:*:*
... -> False
$ 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:*:*:*:*:*:*:*
... -> True
$ python3 -I cpe_eval.py CVE-2026-115975.json cpe:2.3:a:cervantessec:cervantes:v.1.4.0-beta:*:*:*:*:*:*:*
... -> unknown (cannot order version v.1.4.0-beta)
```

The Erlang/OTP record (CVE-2026-74994) is the common case: three ranges in one OR node, one per release line, with `versionEndExcluding` 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 `alpha_0.3` and `1.3-beta`, and the honest answer is "cannot decide".

## Where cpeApplicability and affected disagree

CVE-2026-23122 is a Linux kernel record (published February 14, 2026). Its `cpeApplicability` block has one OR node with two ranges on `cpe:2.3:o:linux:linux_kernel`: 6.16 up to (excluding) 6.18.8, and 6.16 up to (excluding) 6.19. Its `affected` block for the semver product says 6.16 is affected, below 6.16 is unaffected, **6.18.8 through 6.18.\* is unaffected**, and 6.19 onwards is unaffected.

Our evaluator, run on kernel versions (output trimmed to the version and verdict):

```text
6.15.4 -> False
6.18.7 -> True
6.18.9 -> True     # affected block: unaffected (6.18.8 to 6.18.*)
6.19   -> False
```

The second CPE range (6.16 to before 6.19) swallows the stable-branch fix, because a CPE range has no way to say "fixed in the 6.18 branch at 6.18.8 but not in mainline until 6.19". The `affected` block can, through ordered entries and a `6.18.*` upper limit. A scanner that trusts only CPE here raises a false positive on every patched 6.18 kernel.

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 `affected` block.

## Common mistakes

- **Treating every matched CPE as vulnerable.** Entries with `vulnerable: false` 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.
- **Applying bounds to an exact-version criteria string.** If the CPE's version field is not `*`, compare it directly.
- **Assuming the element operator.** 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.
- **Comparing versions as strings.** `"6.9" > "6.18"` is true as text. Use a scheme-aware comparator and return "unknown" when the scheme is unknown.
- **Mixing sources silently.** CNA `cpeApplicability`, the older `affected[].cpes`, and NVD's configurations are three different statements. Store which one produced a match.

## Where this fits in triage

CPE matching answers "does this CVE apply to an asset I run?". It does not answer "how urgently?". Once a record matches, the next questions are whether it is in CISA KEV, its EPSS probability and its SSVC decision values; our [vulnerability prioritization workflow](/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/) walks that path from record to ticket, and [the KEV JSON feed guide](/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/) covers the exploitation check.

## Limits of this article

- 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.
- 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.
- We describe NVD configurations only by the schema's own statement that this block was forked from the NVD CVE API v2.0 schema; we did not compare NVD's published configurations for these CVEs.

## Sources and verification

| Important claim | Source | Status |
|---|---|---|
| Field names, required fields, enums, "mostly copied/forked from the NIST NVD CVE API v2.0 schema", alignment recommendation | [CVE Record Format schema (cve-schema main)](https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json) | Verified |
| `cpeApplicability` allowed in CNA and ADP containers; `cpes` description recommends the newer block | [CVE Record Format schema](https://github.com/CVEProject/cve-schema/blob/main/schema/CVE_Record_Format.json) | Verified |
| Added in 5.1.1; tag dated October 2024 | [cve-schema releases](https://github.com/CVEProject/cve-schema/releases); `v5.1.1` tag commit date | Verified (release-note text read via a mirror; qualified) |
| AND/OR example with vulnerable: false platforms | [full-record-advanced-example.json](https://github.com/CVEProject/cve-schema/blob/main/schema/docs/full-record-advanced-example.json) | Verified |
| CISA stopped adding CPE strings on December 10, 2024 | [cisagov/vulnrichment README](https://github.com/cisagov/vulnrichment) | Verified |
| Sample counts and shares | [cvelistV5](https://github.com/CVEProject/cvelistV5) at commit f0b25a9, our script | Verified (computed) |
| CVE-2026-23122 cpeApplicability vs affected disagreement | [CVE-2026-23122 record](https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/23xxx/CVE-2026-23122.json) | Verified (tested) |

## Frequently asked questions

### What is cpeApplicability in a CVE record?

It is an optional array in a CVE JSON 5 record, added in CVE Record Format 5.1.1, where the CNA (or an ADP) describes affected products with the CPE Applicability Language: CPE 2.3 match strings grouped into nodes joined by AND or OR, each marked vulnerable or not, with optional version bounds.

### What is the difference between affected.cpes and cpeApplicability?

affected[].cpes is a flat list of CPE names attached to one product entry. cpeApplicability is a separate block that can combine CPEs with AND/OR logic, mark platform CPEs as not vulnerable, negate nodes and express version ranges with versionStartIncluding, versionStartExcluding, versionEndIncluding and versionEndExcluding. The schema's cpes description itself recommends the newer block.

### Is cpeApplicability the same as the NVD configuration?

No. It uses nearly the same structure, but it is written by the CNA inside the CVE record. NVD configurations are NVD's own analysis published through the NVD API. The two can differ, so record which source a match came from.

### Should a scanner trust cpeApplicability over the affected versions list?

Not by default. The schema recommends that the block align with the affected block as much as possible, but CPE version ranges cannot say "fixed in this stable branch". Evaluate both and send disagreements to a human or the vendor advisory.


Image credit: Photo by [Hans-Peter Gauster](https://unsplash.com/photos/stack-of-jigsaw-puzzle-pieces-3y1zF4hIPCg) on Unsplash
