Security Engineering · Supply-Chain Security

VEX: Encoding Exploitability So a Scanner Can Stop Guessing

A CVE in an SBOM is not a finding. VEX records whether a product is affected and why, with a justification token that a machine can act on and a human can audit.

An SBOM lists components; a vulnerability database lists advisories; and the intersection of the two produces a list of packages that contain code with a known flaw. That list is not a list of things that need fixing. It routinely contains entries where the vulnerable function is never called, where the vulnerable code is not compiled into the binary at all, where a configuration must be enabled and is not, or where the version in the product already contains the fix backported under a vendor-specific version string. Treating those entries as findings produces a fixed ratio of noise to signal that stays constant no matter how good the scanner gets, and the predictable organizational response is to stop reading the output.

VEX, Vulnerability Exploitability eXchange, is the format for recording the judgment that separates the two. Its design idea is that the party who knows the answer is the party that built the product, and that their answer should be expressed in a form that is machine-actionable, attributable, and revisable.

The four states and why three would not do

VEX defines four statuses for a product/ vulnerability pair.

Not affected asserts that the product does not contain the vulnerability. The justification matters because “not affected” has several distinct causes and a consumer needs to know which one. The predefined justifications include: the vulnerable code is not present; the vulnerable code is present but not in the execution path; the vulnerability requires a configuration the product does not use; the vulnerability requires a feature that was never enabled at compile time; and the product already has a fix for it, whether by upgrade or backport.

Affected asserts that the product is known to be exploitable as shipped, and the statement should carry the action to take. This is the only status that consumes remediation capacity.

Fixed asserts that a specific product version contains the fix. This is a stronger claim than “not affected”, since it makes a statement about remediation rather than about exposure.

Under investigation asserts that no determination has been made. Its value is that it is honest and it is countable: a product with a hundred under-investigation entries has a measurable backlog, whereas the same product with no VEX at all looks identical to a product with no vulnerabilities.

The reason four states are necessary is that the consumer’s action differs. For not affected, ignore for now but re-check when the component version changes. For fixed, no action; record the version boundary. For affected, remediate. For under investigation, the work is triage, not patching. Collapsing not-affected and fixed into a single “not vulnerable” loses the distinction between exposure and remediation, which is the distinction a consumer needs to plan.

Justification tokens are the load-bearing part

A status alone is insufficient. “Not affected” without a justification is an assertion that cannot be audited, cannot be aggregated, and cannot be invalidated when conditions change. Two statements can both be true — the code is absent, and the code is present but unreachable — but the second has an expiration condition that the first does not: if the dependency graph changes so that the function becomes reachable, the statement must be revisited.

This is why the justification vocabulary is enumerated rather than free text. A consumer can aggregate tokens across a portfolio and see which of them dominate; an auditor can review the reasoning behind each class; and a rule engine can invalidate specific classes automatically when the product changes. Free text is not aggregable and not automatically revisable, which makes it useless except as a note attached to a token.

The reachability justification deserves particular attention because it is the most frequently wrong. “Not in the execution path” requires knowing which functions are reachable from entry points in the shipped artifact, and that determination is either made by static call-graph analysis, by runtime observation, or by reasoning about the product’s design. Static analysis over-approximates and under-approximates differently depending on whether the analysis is whole-program and whether the artifact includes reflection, dynamic dispatch, or plugin loading. A reachability claim made by a tool that analyzed only direct calls is weaker than one made from a call graph built over the linked binary. Since the statement is attributed, the consumer can at least see who asserted it and apply their own confidence.

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.invalid/vex/2025-10-image",
  "author": "Example Product Security",
  "timestamp": "2025-10-20T12:00:00Z",
  "version": 3,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2024-00000" },
      "products": [
        { "@id": "pkg:oci/app@sha256:9f2c...", "identifiers": { "purl": "pkg:oci/app@sha256:9f2c..." } }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "The affected parser is only reachable from the disabled legacy import path."
    },
    {
      "vulnerability": { "name": "CVE-2024-11111" },
      "products": [ { "@id": "pkg:oci/app@sha256:9f2c..." } ],
      "status": "affected",
      "action_statement": "Upgrade the bundled compression library in the next patch release."
    }
  ]
}

Two structural details in that document carry most of the weight. The product reference is an image digest or a PURL, not a product name, which ties the statement to bytes for the same reason provenance binds to a digest; component identity in SBOMs is the catalogue of why names are insufficient. And the version field is incremented, which makes the document a sequence rather than a point: revision 3 supersedes revision 2, and a consumer that has cached revision 1 must fetch a newer one. Statements about exploitability age because the product, the vulnerability knowledge, and the attacker’s capability all change.

Three formats and the interoperability cost

VEX exists in at least three concrete containers. CycloneDX carries VEX in its vulnerabilities array, which means a single BOM can list components and their exploitability status together. CSAF, the OASIS Common Security Advisory Framework, carries VEX as a profile of a broader advisory format and is the shape most familiar to vendors who already publish advisories. OpenVEX is a minimal standalone document designed so that its statements can be composed and reasoned over without a full advisory.

The formats differ in expressiveness and in what they assume about the publisher. A CSAF document is an advisory with a product tree, a document-level tracking identifier, and revision history; it can express a product’s whole vulnerability posture. An OpenVEX document is a set of independent statements, which makes it suitable for incremental publication: a vendor can issue a new document per advisory without regenerating a full advisory. CycloneDX attaches status to a component inventory and inherits that inventory’s identity model.

The cost is that a consumer must implement more than one. The mitigation is to normalize on the common core — a vulnerability identifier, a product identifier, a status, a justification, and a timestamp — and treat everything else as provenance metadata. That core is small enough that normalization is tractable, and the parts that resist normalization are the parts that vary in quality anyway.

Composition, staleness, and the direction of trust

The hard problem with VEX is not the format but the composition rule when several statements apply to one product. A product has its own VEX, its components have theirs, and the platform it runs on has another. If the product’s VEX says affected and a component’s says fixed, which governs? The sane rule is that the more specific and more recent statement about the exact artifact wins, and that a product-level statement must not silently downgrade a component-level affected claim without justification. A composition rule that lets an aggregator take the most favourable statement produces a document that nobody is accountable for.

Staleness is the second problem, and it has two sources. A statement is stale when the artifact it describes has changed, which is why the product identifier should be a digest and why a rebuilt image invalidates every statement about the previous one. And a statement is stale when the vulnerability record itself changes, which happens when a CVE is revised, when a disputed severity is resolved, or when a new exploitation technique is published. The revision counter and timestamp exist to make staleness visible; a consumer that ignores them treats an old judgment as current.

The direction of trust is the third. A VEX statement from the vendor says the vendor believes the product is not affected. A consumer who must defend the decision needs to know whether the vendor’s statement was the result of analysis or of a default, and vendor statements vary widely in that respect. The remediation is the same as elsewhere in this cluster: attribution plus evidence. A statement that names the analyzing tool, the artifact digest, and the justification token is auditable; one that says “not affected” for a thousand products at once is a claim about a policy, not about the software.

What VEX cannot do is assert that a product is free of vulnerabilities. It records judgments about specific known vulnerabilities. Vulnerabilities with no CVE, defects that are not classified as vulnerabilities, and malicious code in a dependency are all outside its scope, and the last of those is the domain of the detection and response side of the problem rather than the exploitability side. VEX reduces the noise floor of vulnerability management; it does not raise the ceiling of assurance.

References

  1. CycloneDX specification: Vulnerability Exploitability eXchange — OWASP CycloneDX project
  2. CSAF Common Vulnerability Reporting Framework — OASIS Open
  3. OpenVEX specification — OpenVEX project
  4. SPDX Specification — SPDX project, Linux Foundation