The VEX analysis your supplier already did
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 excusedand, 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 cannot | Because |
|---|---|
| turn off a finding by telling you it is bad | only 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 you | where 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 component | a 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 stories | where 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 quietly | a 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 says | What the document publishes |
|---|---|
| nothing, the finding is untriaged | under_investigation. Open, and said so, rather than omitted |
an exclusion with a reason | affected, carrying that reason. True: you found it and decided to live with it |
an exclusion with a vex: block | not_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.