← All guides

VEX: Vulnerability Exploitability eXchange

Build & artifactsDraugr: available today

In plain terms#

An SBOM says what your software is made of. A scanner says which known vulnerabilities touch those components. Neither answers the question your customers are asking, which is whether any of it matters.

VEX (Vulnerability Exploitability eXchange) is that answer, written down. For each vulnerability, a short machine-readable statement of whether it affects the product, and on what grounds.

{
  "vulnerability": { "name": "CVE-2021-44228" },
  "products": [ { "@id": "pkg:oci/acme/api@2.4.0" } ],
  "status": "not_affected",
  "justification": "vulnerable_code_not_in_execute_path"
}

Why it matters#

Ship an SBOM without a VEX and you have published a list of every CVE anyone can match against your components. Most of them will not affect you, because the vulnerable function is never called, the feature is compiled out, the component is a build-time dependency that never reaches production. But nobody downstream can tell which.

So they ask. Every customer, separately, by email, about the same CVE, and somebody on your side answers from memory, inconsistently, for as long as that release is supported. VEX replaces that with one document their tooling applies without a human reading it.

The reverse case matters as much. A vulnerability that does affect you, stated plainly with what you are doing about it, is far better received than silence, and silence is what a bare SBOM communicates about everything in it.

The four statuses#

StatusMeans
not_affectedThe vulnerability is present but cannot affect this product.
affectedIt does affect the product. An action statement says what is being done.
fixedA remediation has been applied.
under_investigationKnown, not yet concluded.

not_affected is the load-bearing one, and the specification does not let you assert it. You must give either a justification from a fixed vocabulary, component_not_present, vulnerable_code_not_present, vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary, inline_mitigations_already_exist, or a prose impact statement. The closed list is the entire point, because a consumer can act on it without reading English.

Where it fits#

VEX travels alongside an SBOM, not inside it. The SBOM describes the product; the VEX describes your assessment of it, and the two are updated on different rhythms. A new build regenerates the inventory, while a triage decision changes only the assessment.

Consumers apply it at scan time. Both Trivy and Grype accept a VEX document and drop the statements you have marked not_affected from their results, which is what makes the whole exchange worth doing. The noise disappears on their side, without anybody asking.

How it works#

The matching is by product identifier. A statement names a product, conventionally a package URL, and a consumer applies it when that identifier matches what their own SBOM calls the thing they are scanning. Get the identifier wrong and the document is read, understood, and applied to nothing.

Three dialects exist and all express the same idea:

  • OpenVEX. Standalone JSON, small, from the OpenSSF. Works whether or not you ship an SBOM.
  • CycloneDX VEX. Embedded in, or paired with, a CycloneDX BOM.
  • CSAF VEX. The OASIS standard, used by large vendors publishing security advisories.

Common pitfalls#

Asserting not_affected without evidence. It is a claim of safety, published over your name, that a customer will act on. Reachability analysis, a build flag, a configuration you enforce. The justification should point at something someone could check.

Generating it from triage prose after the fact. "Not reachable" and "we are living with this until Q3" are both perfectly good reasons for setting a finding aside, and they are opposite VEX statuses. Anything that infers the status from free text will eventually publish the wrong one.

Letting it go stale. A VEX describes a product version. When you ship a new one, the assessment has to be re-made, because a not_affected that was true in 2.3 says nothing about 2.4.

Overstating scope. A VEX statement covers what you assessed. If you have only reviewed the components you ship in the container, say so; a document implying you reviewed the whole dependency tree when you reviewed the top level is worse than a narrower one.

How Draugr fits. VEX generation ships today: --report vex emits an OpenVEX document from the run. Most of it is already in your descriptor, because a suppression in Draugr is not a delete. config.exclude requires a reason and records who accepted it and when that acceptance lapses, which is most of a VEX statement and the part most producers never captured.

Draugr will not guess the status from your prose. A finding nobody has triaged is published as under_investigation; a suppressed one as affected, carrying your reason. To claim not_affected you say so, in a vex: block on the exclusion. Overstating your exposure costs a reader some wasted triage; understating it tells a customer they are safe when they are not, so the default sits on the side you can survive being wrong on. See publish a VEX document.

Draugr also reads one. Point a component at a document a supplier published, whether a file, a URL, or a path inside their repository, and their analysis is applied to your findings, with their name on it. That is the part worth having, because a supplier’s not_affected retyped into your own config becomes indistinguishable from a decision you made and are answerable for, while an imported one keeps the author, the document and the date it was asserted. Only not_affected and fixed excuse anything; a supplier saying you are affected changes nothing, and your own exclusions still win. See read a supplier’s VEX.

Keep learning#