VEX and SBOMs: Triaging Vulnerabilities That Matter

SecuritySupply ChainDevSecOps

If you've run a container scanner against a real production image lately, you know the feeling: 300 CVEs reported, red everywhere, and a nagging certainty that maybe five of them actually matter. VEX — Vulnerability Exploitability eXchange — is the discipline and the data format for separating those five from the other 295. It's a machine-readable way to assert, per vulnerability, whether a CVE is genuinely exploitable in your product, so you stop treating "present in the bill of materials" as identical to "dangerous."

I got religious about this after watching a team burn a full sprint chasing scanner findings, only to discover that most were in code paths their service never executed. SBOMs told them what was inside; nothing told them what to fear. VEX is that missing layer. Here's how the two fit together and how to run triage that respects your engineers' time.

The noise problem VEX solves

A CVE is filed against a component and version. It says nothing about how you use that component. Your image might contain a vulnerable libxml2, but if your service never parses untrusted XML through the affected function, the exploit has no path to reach. The scanner can't know that — it matches versions against a vulnerability database, full stop.

The result is a signal-to-noise disaster. When 95% of findings are non-exploitable, engineers learn to ignore the scanner entirely, which means the 5% that do matter get ignored too. Alert fatigue in vulnerability management isn't a nuisance; it's a security failure, because it trains your team to dismiss the tool that would have caught the real breach.

SBOM first, then VEX on top

The two artifacts have distinct jobs:

A VEX status is one of a small set: not affected, affected, fixed, or under investigation. The interesting one is not affected, which should come with a machine-readable justification — for example, vulnerable_code_not_in_execute_path or component_not_present. That justification is what lets a downstream consumer trust your assertion instead of re-triaging everything themselves.

If you're not yet producing SBOMs at build time, start there — the practices in container image security and SBOMs are the prerequisite. VEX without an SBOM has nothing to annotate.

What a VEX assertion looks like

VEX can be expressed in several formats — CycloneDX has native VEX support, and there's the OpenVEX spec for a minimal standalone format. An OpenVEX statement is refreshingly small:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.com/vex/2026-0001",
  "author": "Security Team <[email protected]>",
  "timestamp": "2026-02-01T09:00:00Z",
  "statements": [
    {
      "vulnerability": { "name": "CVE-2025-12345" },
      "products": [
        { "@id": "pkg:oci/payments-api@sha256:abc123" }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path"
    }
  ]
}

That's the whole idea: a signed, timestamped, machine-readable claim that CVE-2025-12345 is present but not exploitable in this specific artifact, with a reason a tool can act on. Your scanner or policy engine reads the VEX and suppresses that finding automatically instead of asking a human to re-investigate it every scan.

Triage that scales

The manual version of triage — a person reading each CVE and deciding — does not scale past a handful of services. The workflow that does looks like this:

  1. Generate the SBOM at build time, attached to the artifact so it's always current.
  2. Scan the SBOM, not the running system, so results are reproducible.
  3. Triage each new finding once, recording the decision as a VEX statement.
  4. Re-apply VEX on every subsequent scan so previously-triaged findings are suppressed automatically.
  5. Re-investigate only when the SBOM or the CVE changes.

The leverage is in step 4. A finding you've marked not_affected with a solid justification never bothers a human again unless the underlying facts change. Over a few months, your scanner output collapses from hundreds of lines to a short list of genuinely new, genuinely relevant issues.

Without VEX With VEX
Every scan re-surfaces all CVEs Triaged CVEs stay suppressed
Humans re-triage the same findings Decide once, record, automate
Alert fatigue → scanner ignored Short, trustworthy signal
No shared context downstream Consumers inherit your assertions

The honest tradeoffs

VEX is not free, and I've seen it misused. The biggest risk is a rubber-stamp culture — teams marking everything not_affected to make the dashboard green without doing the reachability analysis. A VEX statement is a security claim; if you can't articulate the justification, you haven't triaged, you've lied to your future self. Require a real justification code and, ideally, a link to the analysis.

The second issue is staleness. A not_affected verdict is true given the current code. Refactor a service so it now calls the vulnerable path, and your VEX is silently wrong. Tie VEX validity to the artifact digest (as in the example above) so a rebuild forces re-evaluation rather than carrying a stale assertion forward.

And VEX is a complement to remediation, not a substitute. "Not exploitable" is a reason to deprioritize, not to never patch — an unpatched dependency is a future exploit path when your code changes around it. This all sits inside the broader provenance and integrity story covered in supply chain security with SLSA and SBOMs; VEX handles "which known issues matter," while SLSA handles "can I trust how this was built."

Where to start

If you're drowning in scanner output today, the highest-leverage move is not buying a fancier scanner — it's adding the VEX layer to the SBOMs you (hopefully) already produce. Start with your most-scanned, noisiest service. Triage its current findings once, record the decisions as VEX, wire the scanner to consume them, and watch the daily noise drop. Then make VEX generation part of the build so new services inherit the discipline.

The payoff isn't a prettier dashboard. It's that when a real, reachable, critical vulnerability lands, your team actually sees it — because you've spent the effort making sure the scanner only shouts when it should.

Resources

Frequently asked questions

What is VEX (Vulnerability Exploitability eXchange)?

VEX is a machine-readable format for asserting whether a specific vulnerability actually affects a specific product. Instead of just listing that a CVE exists in a dependency, a VEX document states a status — affected, not affected, fixed, or under investigation — often with a justification. It lets a vendor tell downstream consumers 'yes this CVE is in our bill of materials, but it's not exploitable here, and here's why.'

How do VEX and SBOMs work together?

An SBOM (software bill of materials) inventories every component in your software; a scanner cross-references that inventory against vulnerability databases and produces a list of CVEs. VEX is the layer on top that annotates which of those CVEs are actually exploitable in your context. The SBOM tells you what's inside; VEX tells you what to worry about.

Why do most scanner findings turn out to be non-exploitable?

A CVE is filed against a component version regardless of how that component is used. Your product may never call the vulnerable function, may not expose the affected code path, or may run the dependency in a context the exploit can't reach. Scanners match versions, not reachability, so a large share of findings are present-but-not-exploitable — which is exactly the noise VEX is designed to suppress.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →