← All articles

The VEX analysis your supplier already did

August 18, 2026 · Wilson Santos · updated October 30, 2026
On this page

A supplier ships you a component. Your scanner finds forty CVEs in it. Thirty-one of them are in code paths their build never compiles, and they know this, because somebody on their side spent a week working it out.

So they tell you. Sometimes in a PDF, sometimes in a spreadsheet, increasingly in a machine-readable document, a VEX file listing each vulnerability and whether it affects the product.

Supplier does not have to mean vendor. The most common version of this is the platform team two floors up who publishes the base image a dozen services build on, and that is the case where all of this already works, because both ends of it are yours.

And then the interesting part. You take their thirty-one, and you type them into your own suppression config, because that is where your pipeline looks.

What you just lost

Not the week. The week was theirs and they already spent it. What you lost is the answer to the only question anybody asks about a suppressed finding, which is not did the scanner run. It ran, that is why you have forty findings. The question is who decided this one was acceptable, and when.

Before you retyped it

Your supplier, in a document, on a date, with a technical justification and somebody's name on it. An answer that stands on its own and belongs to whoever did the work.

After

You, in a config file, with a reason that reads like your reason because it is in your file, under your commit, signed by whoever opened the pull request.

The next person to audit that finding asks your team to defend an analysis your team did not do.

The document was already the right shape

A VEX statement is a claim rather than a suppression rule, and a claim has parts a rule does not:

{
  "vulnerability": { "name": "CVE-2016-2781" },
  "products": [{
    "@id": "pkg:oci/example/base-python",
    "subcomponents": [{ "@id": "pkg:deb/debian/coreutils@9.1-1" }]
  }],
  "status": "not_affected",
  "justification": "vulnerable_code_not_in_execute_path",
  "impact_statement": "chroot(1) is only exploitable when invoked by a privileged process that then drops privileges. Nothing in this image runs it, and the image runs as an unprivileged user."
}

An author, a date, a vocabulary term a machine can act on, and prose a person can argue with. A package identifier precise enough to match one library inside one image.

Copying that into a suppression keeps the prose and throws away everything else.

So read it instead

Point at the document:

components:
  - name: payments-api
    images:
      - image: registry.example.com/acme/api:1.4.0
    vex:
      - path: vendor/acme-api.openvex.json

Or at a URL they publish, or a path inside a repository of theirs, cloned with whatever credentials git already has on the machine, so a private supplier repository works without a token in your descriptor.

The findings they excused stop counting against your gate, exactly as an exclusion would. What is different is the record:

ACCEPTED
  VEX  1 finding excused

and, under --evidence, whose analysis it was:

ACCEPTED
  VEX  1 finding excused · 1 asserted by Example Base Images <security@example.com>

That row is the whole argument. Every finding it covers carries the author, the document it came from, and the date the claim was asserted, named in the terminal under --evidence and always present in report.json and the SARIF property bag your platform reads. A year-old not_affected about a package that has moved on reads as old, rather than as current.

What a supplier is not allowed to do

This is where consuming somebody else's claims stops being obviously a good idea, so it is worth being precise about the limits.

A supplier cannotBecause
turn off a finding by telling you it is badonly not_affected and fixed excuse anything. A statement of affected is recorded, reported, and changes nothing, or a vendor's honesty about their own exposure would quietly reduce your count
overrule youwhere your own exclusion already covers a finding, your reason stands. Theirs is evidence rather than an override, and the reason whose author you can walk down the hall and ask is the more useful one to keep
reach past their own componenta document declared on one component applies to that component's findings. One vendor excusing something in a service they have never seen is the failure this design exists to prevent
talk you into the safer of two storieswhere two statements disagree, the one conceding more exposure wins. Reporting affected for something fine costs an afternoon; the other direction tells a customer they are safe when they are not
do nothing quietlya statement matching no finding is reported, because a document that excused nothing looks exactly like one that is working. Usually your scanner and their tooling name the same package differently

The honest state of VEX

VEX is not a solved technology, and it would be easy to write this post as though it were.

The OpenSSF's own survey of the field puts it bluntly: VEX "feels more like a promise than a practice". There is no common way to discover a supplier's document. Signing and verification conventions are unsettled. Different tools identify packages by different schemes, so a correct statement can match nothing at all. Some vendors publish meticulous analysis; others publish bulk not_affected with no supporting evidence, which is worth exactly what it costs to produce.

All true. Here is why it does not change the conclusion.

The consuming side is the leverage

Publishing is the half everyone measures and the half nobody can use alone. A document nothing reads changes no verdict, so the tooling that reads one is what makes publishing worth doing.

A bad claim is visible, not laundered

Distribution and quality are real problems, and they are problems about a named author with a date attached. That is a better position than an unattributed suppression in somebody's ignore file.

The pressure arrives on a date

Under the EU Cyber Resilience Act, obligations land on a schedule rather than when the ecosystem feels ready, and the teams that can already read a supplier's analysis will not be rebuilding for it.

The supply side is two lines away

The complaint about VEX is always that too few people publish, and the reason is assumed to be effort. It is not. An exclusion you wrote for your own gate is already the analysis a statement carries, so the document is a rendering of your descriptor rather than a second thing to maintain:

draugr scan draugr.saga.yaml -o out/ --report vex     # out/openvex.json

What each exclusion becomes is decided by what you put in it, and nothing is inferred:

What the descriptor saysWhat the document publishes
nothing, the finding is untriagedunder_investigation. Open, and said so, rather than omitted
an exclusion with a reasonaffected, carrying that reason. True: you found it and decided to live with it
an exclusion with a vex: blocknot_affected, with a justification a consumer's tooling can act on without reading English

The third is the one worth the two extra lines:

config:
  exclude:
    - rules: ["CVE-2018-18074"]
      reason: "The redirect path that leaks the header is never taken; we pin the host."
      vex:
        status: not_affected
        justification: vulnerable_code_not_in_execute_path

justification comes from a closed list of five, which is the entire point of the field: a consumer matches on it rather than parsing your sentence. Omit the block and your reason is published as VEX's prose alternative, which is valid and simply less useful to a machine.

Draugr will not read your reason and decide. Prose that sounds like not reachable stays affected until you say otherwise in the block, because not_affected is a claim of safety published over your name, and inferring one from English is how a document stops being worth trusting.

Publish that beside your SBOM and the twelve teams consuming your component stop retyping your analysis into twelve suppression files, which is the problem this post opened with, seen from the other end. An ecosystem where too few publish is not fixed by more people agreeing that too few publish.

The case that does not wait for the ecosystem

This is the arrangement promised at the top, and none of the adoption problems above apply to it: your suppliers are other teams in your own company.

A platform team publishes a base image. Twelve product teams consume it. The platform team already knows which CVEs in that image are reachable, and if they run a scanner that produces VEX, they already have the document. Nobody is waiting on an industry convention, because both ends are yours.

config:
  vexSources:
    - url: https://platform.internal/vex/current.openvex.json

Said once, applied to every component. The platform team's analysis lands in twelve pipelines with their name on it, and when somebody asks why a finding stopped counting in the payments service, the answer names the team who assessed it rather than the person who happened to edit the config.

What this does not do

It does not decide whether a claim is true. Draugr records whose it was and when they made it; the judgement stays with you, which is the only place it can honestly sit. Naming a source is the decision to accept it.

And it is not a substitute for reading what you accepted. A supplier's document is a description of their product as they understand it on the day they wrote it. Everything above is machinery for keeping that fact attached to the finding, not for making it go away.

The scanner found forty. Somebody did the work on thirty-one of them. The least you can do is keep their name on it.