Supply Chain · Jul 12, 2026 · 7 min read

SBOM and VEX: from component list to exploitability proof

An SBOM lists every component you ship. VEX declares which CVEs from those components do not affect you - and why. The gap between them is where supply-chain noise lives.

An SBOM tells you what components are in your application. VEX tells a machine which CVEs from those components do not affect you - and why. The gap between "this CVE exists in a component we import" and "this CVE is reachable and exploitable in our product" is where most supply-chain noise lives. Closing it requires proof, not a reviewer's note.

01What an SBOM is and what it is not

A Software Bill of Materials is a structured, machine-readable inventory of every component in a release artifact: name, version, origin, content hash, license, and relationship to the parent. Formats converge around CycloneDX and SPDX. An SBOM generated at build time from the resolved dependency graph captures the actual transitive tree - not the declared tree, which can diverge under version pinning and workspaces.

What an SBOM does not tell you is whether any of those components matter. A transitive dependency that sits in node_modules but is never imported by any reachable code path appears in the SBOM identical to a dependency your critical payment handler calls on every request. Every CVE scanner that takes the SBOM at face value conflates them - which is where the noise originates.

build artifact ├── lodash@4.17.20 direct import, merge() called in 3 handlers ├── moment@2.29.4 transitive, date formatting reached └── unused-lib@2.1.0 transitive, zero call-graph paths from any entry └── CVE-2024-31234 CVSS 9.8 ──► reachability: no entry point reaches vulnerable function ──► VEX status: not_affected justification: vulnerable_code_not_in_execute_path
A CVSS 9.8 CVE in a transitive dependency earns a VEX not_affected assertion only when the call graph proves no entry point reaches the vulnerable function - not when a reviewer says so.

02VEX: from silence to machine-readable assertion

Without VEX, a CVE in a component creates a binary: the CVE is present or it is not. Every tool downstream treats "present" as "exploitable" unless a human says otherwise, and that human comment is not machine-readable, not signed, not attached to the artifact, and not carried into the next pipeline run. The reviewer's verdict disappears the moment the ticket closes.

VEX (Vulnerability Exploitability eXchange) replaces that silence with a signed, traceable assertion per CVE per artifact version. The four VEX statuses are not_affected, affected, fixed, and under_investigation. A not_affected assertion requires a justification that a machine can validate: component_not_present, vulnerable_code_not_present, vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary, or inline_mitigations_already_exist. Each justification class carries a different proof requirement.

vex-statement.cdx.jsonjson
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "vulnerabilities": [{
    "id": "CVE-2024-31234",
    "affects": [{ "ref": "pkg:npm/unused-lib@2.1.0" }],
    "analysis": {
      "state": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "detail": "Call-graph analysis: no entry point reaches parseUntrustedXml(). "
               "Confirmed by apPosture reachability pass on commit a3f9b12."
    }
  }]
}

The statement is co-located with the SBOM, versioned to the artifact, and consumable by downstream tooling without a human in the loop. A VEX consumer can suppress the CVE from a report automatically - but only for the specific version and justification class declared. A new version of the artifact needs a new analysis pass; the assertion does not carry forward silently.

03Reachability is the proof behind the assertion

A VEX assertion backed by "we reviewed it and it is fine" is an opinion. An assertion backed by call-graph reachability analysis is a fact - and the difference matters when an auditor asks you to substantiate a not_affected claim six months after the release.

Reachability analysis builds the static call graph for the resolved dependency tree: every function import, every dynamic dispatch that can be resolved statically, every entry point (HTTP handler, CLI command, event listener). The question for each vulnerable function is whether any path from any entry point can reach it. "Can reach" is the conservative bound - if the path exists even if guarded by a runtime condition that looks impossible, the finding stays open. Only a path that structurally cannot exist justifies vulnerable_code_not_in_execute_path.

Reachable - Highlodash CVE-2021-23337 - prototype pollution via merge() - payment-handler.js
call graph path: POST /api/orders ──► orderController.create() ──► utils/merge.js:14 ──► lodash.merge() [CVE-2021-23337 vulnerable code] user-controlled key reaches merge path: confirmed via taint trace VEX status: affected - not eligible for not_affected assertion
Oracle: static call-graph + taint trace. Severity high - prototype pollution via a user-controlled key in a reachable merge path. Fix: upgrade lodash to >= 4.17.21 or replace recursive merge with structured clone at the entry point.

The contrast is what makes the system defensible. A CVE in a component that is structurally unreachable gets a machine-readable not_affected assertion with a traceable justification. A CVE in a component that sits on a taint path from a user-controlled entry point gets a finding, a severity, and an SLA - not a note saying "we will look at it."

04The operational workflow

Generating an SBOM once, at release, is a starting point. The posture that survives an audit is one where the SBOM is generated per build artifact, enriched against live CVE feeds (OSV, NVD, vendor advisories), reachability is run on the same commit, and a VEX document is emitted with every not_affected assertion traceable to the reachability result that proved it. The chain must be unbroken: artifact hash -> SBOM -> VEX -> reachability evidence -> commit.

FormatPrimary useVEX supportTooling fit
CycloneDXSecurity-first; vulnerability metadata nativeFirst-class in spec v1.4+Strong: grype, trivy, apPosture
SPDXLicense compliance, NTIA minimum elementsVia external VEX doc (OpenVEX)Broad: GitHub, FOSSA, FOSSology
SWIDEnterprise software inventory (ISO 19770-2)Not native; requires tooling bridgeNarrow: mostly government/enterprise

An air-gapped pipeline does not need to phone out to OSV at scan time. A curated CVE cache updated on a controlled schedule carries the same enrichment without an outbound connection - which is both a data-sovereignty requirement and a supply-chain integrity point. The tool that enriches your SBOM must not itself be a soft target.

5
VEX justification classes
0
assertions without a reachability trace
1
VEX doc per artifact version, not per repo

The posture question an SBOM answers is "what is here." The posture question a VEX-backed reachability pass answers is "which of what is here can actually hurt you." Both are required; neither replaces the other. Together they close the loop between a CVE advisory and an evidence-backed decision about whether your specific artifact is affected - and they do it in a form a machine can consume, an auditor can verify, and a pipeline gate can enforce.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →