SBOM

SBOM vs. VEX: What's the Difference and When Do You Need Each?

SBOMs tell you what is in your software. VEX tells you which of those components are actually exploitable. Here is how to use both without drowning in noise.

Shadab Khan
Security Engineer
8 min read

An SBOM tells you which components are in your software. A VEX tells you which of those components, with which vulnerabilities, actually affect your software. They are two different artifacts that solve two different problems, and they work best together.

Teams routinely confuse the two because both deal with components and vulnerabilities and both are distributed as JSON. But the confusion costs real money — treat a VEX-able scenario as an SBOM-only problem and your team ends up patching things that were never exploitable in the first place.

What is an SBOM, in one paragraph?

A Software Bill of Materials is a machine-readable inventory of every component that makes up a piece of software, along with enough identifying metadata to match each component against a vulnerability database. At minimum, each component has a name, version, and either a PURL (package URL) or a CPE that lets a scanner check it against NVD, OSV, GHSA, or your chosen feed. CycloneDX and SPDX are the two formats that matter; both encode the same core idea.

SBOMs are static. You generate one per build, and it describes that build — nothing about intent, configuration, or whether any of the included components are actually reachable at runtime. An SBOM for a Spring Boot application lists log4j-core-2.14.1 either way, regardless of whether your application ever routes user input into a logger.

What is a VEX, in one paragraph?

A Vulnerability Exploitability eXchange (VEX) document is a statement from a vendor or internal team about whether a given vulnerability in a given product is actually exploitable. The document answers the question "yes, CVE-2021-44228 is in our SBOM — does it affect us?" with one of a handful of well-defined statuses: not_affected, affected, fixed, or under_investigation. When the status is not_affected, the VEX author is required to specify a justification, such as vulnerable_code_not_in_execute_path or component_not_present.

There are two practical VEX formats in 2026. CycloneDX VEX embeds the statements inside a CycloneDX document alongside the components they reference. OpenVEX is a standalone JSON schema maintained under the OpenSSF umbrella; it is designed to be lightweight, portable, and to travel independently of any SBOM. Both encode the same semantic content and both are supported by mainstream scanners.

When do you need only an SBOM?

You need only an SBOM when your job is to know what you ship, not to communicate exploitability judgments about it. That covers most internal use cases.

Examples where SBOM alone is sufficient: generating a build manifest for CI, meeting baseline U.S. federal attestation requirements under EO 14028, feeding a vulnerability scanner that produces alerts for your own team, participating in coordinated disclosure as an affected downstream. In each case the SBOM is the raw material; any exploitability judgment stays inside your own ticketing system.

The common trap is generating SBOMs for everything and then treating the output as a finished product. A 2,400-component SBOM for a single microservice, matched against NVD, will typically produce 180–400 CVE hits, of which the overwhelming majority are non-reachable, non-exploitable, or already patched upstream. Without a VEX layer or a reachability filter, that list is noise.

When does VEX actually help?

VEX helps when you are shipping software to customers, regulators, or auditors who will run their own scanners against your product and expect answers about each finding. The classic failure mode without VEX: a customer runs Trivy or Grype against your container image, files a critical-severity ticket with your enterprise support team about CVE-2023-Something, and the incident is closed three days later with "we are not affected because we never invoke that code path." VEX lets you ship that answer in the box.

Three concrete scenarios where VEX pays for itself:

Vendor-to-customer communication. You publish a VEX alongside each release telling customers which CVEs do and do not apply. This is the intended flow for CISA's VEX work and increasingly what enterprise procurement teams ask for during SBOM exchange.

Long-tail component CVEs. Your product bundles a library that has a published CVE, but the vulnerable function is not called in your configuration. VEX with a vulnerable_code_not_in_execute_path justification closes the loop for anyone scanning your artifact.

Regulated industries. Medical devices (FDA premarket guidance), automotive (UNECE R155), and the EU CRA all expect some form of per-vulnerability exploitability disposition. VEX is the canonical answer.

Can you walk through a real example with Log4Shell?

A Log4Shell-style scenario is the clearest way to see SBOM and VEX split duties. Imagine a Spring Boot service built in late 2021 that bundles log4j-core-2.14.1. The SBOM for this service, generated from the Maven dependency tree, includes a component entry like this:

{
  "type": "library",
  "group": "org.apache.logging.log4j",
  "name": "log4j-core",
  "version": "2.14.1",
  "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
  "hashes": [{ "alg": "SHA-256", "content": "..." }]
}

A vulnerability scanner matches that PURL against NVD and fires on CVE-2021-44228 (Log4Shell, CVSS 10.0). The SBOM's job ends there — it correctly reports "yes, the vulnerable component is in this artifact."

Now the exploitability question. Log4Shell requires (a) a JNDI lookup to be invoked, which requires (b) attacker-controlled input to flow into a Logger.info/Logger.error/etc. call, and (c) the formatMsgNoLookups flag to be at its default. If your application only logs statically formatted strings through a custom wrapper that pre-sanitizes input, the CVE is present but not exploitable in your configuration.

A CycloneDX VEX statement embedded in the release artifact encodes that judgment:

{
  "vulnerabilities": [{
    "id": "CVE-2021-44228",
    "source": { "name": "NVD" },
    "affects": [{ "ref": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1" }],
    "analysis": {
      "state": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "detail": "User input is sanitized before reaching any log invocation; JNDI lookups are disabled via -Dlog4j2.formatMsgNoLookups=true."
    }
  }]
}

The SBOM plus VEX together give a downstream consumer the full picture: the component is present, but the vendor has analyzed exploitability and documented why it does not apply. An OpenVEX document would encode the same statement in its own standalone schema, referencing the product and vulnerability by identifier rather than embedding inside an SBOM.

What is the difference between CycloneDX VEX and OpenVEX?

CycloneDX VEX and OpenVEX solve the same problem with different packaging tradeoffs. CycloneDX VEX colocates the VEX statements with the SBOM, which is convenient when you control both and want a single artifact per release. OpenVEX is standalone, which is convenient when you want to update exploitability statements without regenerating the SBOM, or when the VEX author is a different party than the SBOM author.

In practice, most vendors in 2026 publish CycloneDX SBOMs for every build and layer OpenVEX statements on top as their analysis progresses. The two formats converge on the same status vocabulary (not_affected, affected, fixed, under_investigation) and the same justification enum, so conversion between them is mechanical.

If you are starting from scratch, pick CycloneDX VEX when the same team owns the build and the vulnerability analysis. Pick OpenVEX when PSIRT or security is a separate function from engineering and needs to publish exploitability statements independently of the release cadence.

How do you operationalize both without drowning?

Automate SBOM generation, automate VEX authoring for the common cases, and reserve human review for the rest. Generating an SBOM on every build is cheap — seconds of CI time with syft, cdxgen, or a build-plugin — and there is no reason not to.

VEX is harder because it asserts an exploitability judgment, which requires analysis. The efficient pattern is to let a reachability-analysis engine produce draft VEX statements automatically: every CVE whose vulnerable function is not reachable in your call graph gets a draft not_affected with vulnerable_code_not_in_execute_path. A human reviews and signs off (or downgrades the confidence) before publication. That workflow covers 60–80% of the typical CVE backlog with minimal analyst time, and the remaining 20–40% get proper human analysis.

Republish VEX whenever the underlying judgment changes — a new exploit technique, a config default change, a patch applied. VEX is not a one-shot document; it is a versioned statement that travels with the product over its lifecycle.

How Safeguard.sh Helps

Safeguard.sh generates CycloneDX and SPDX SBOMs on every build and ingests vendor SBOMs into the TPRM module so you can assess third-party risk before procurement closes. Reachability analysis produces draft VEX statements for every component CVE that is not actually invoked, collapsing the patch queue by 60–80% and handing your PSIRT a pre-populated analysis to sign off on. Griffin AI can autonomously remediate the reachable remainder — opening, testing, and proposing patches without a human in the loop for low-risk upgrades. The scanner follows dependencies to 100 levels deep so hidden transitive components are not missed, and the container self-healing layer keeps SBOMs and VEX statements current across base image drift between releases.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.