VEX — Vulnerability Exploitability eXchange
What is VEX?#
When a vulnerability scanner (grype, trivy, snyk) finds a CVE in our image, it's reporting that a vulnerable package is present — not that the vulnerable code path is reachable. VEX (Vulnerability Exploitability eXchange) is the standardized way to tell a scanner "we know about CVE-X and it does not affect this image, because we don't compile that subsystem / don't expose that endpoint / patched it ourselves / etc."
We use OpenVEX v0.2.0 — the format SPDX, CycloneDX, and
grype all consume natively. Every image in this repo has its own
vex/<image>.openvex.json document; build CI passes it to grype via --vex,
so the published dashboard shows both the raw CVE count and the
effective (VEX-adjusted) count.
When to add a statement#
Only when all four are true:
- Grype reports a CVE against one of our images.
- We have a defensible reason the CVE is not exploitable here.
- The reason is durable (not "we'll fix it next week" — use a fix PR for that).
- We can write the justification in one sentence a stranger would believe.
If we can't meet #4, don't write the statement. An unbacked not_affected is
worse than no statement at all.
How to add a statement#
- Identify the image and the CVE: e.g. nginx, CVE-2024-12345.
- Open
vex/nginx.openvex.json. - Append to
statements[]:
{
"vulnerability": { "name": "CVE-2024-12345" },
"products": [
{ "@id": "pkg:oci/minimal-nginx?repository_url=ghcr.io/rtvkiz" }
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "The bug is in nginx's mail proxy module; we build with --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module."
}- Bump the document's top-level
versioninteger (start at 1, increment on every edit — readers use this to detect changes). - Update the top-level
timestampto today (date -u +%Y-%m-%dT%H:%M:%SZ). - Run
tools/vex/validate.sh vex/nginx.openvex.jsonlocally to confirm shape. - Commit on a branch named
vex/<image>-<cve>, open a PR. CI re-runs grype with--vex, so the dashboard count drops on merge.
Valid status values#
| Status | Meaning |
|---|---|
not_affected |
The CVE exists in the package but cannot affect this image. Requires justification. |
affected |
The CVE affects this image and we have not patched it. Should be rare — usually we'd ship a fix. |
fixed |
We backported a patch (in melange.yaml) and the vulnerable code is no longer present. |
under_investigation |
We're still triaging. Time-bound — don't leave a CVE in this state for weeks. |
Standard justification values for not_affected#
These are the OpenVEX-standardized strings — use them verbatim:
component_not_present— the vulnerable component isn't in our image at allvulnerable_code_not_present— the package is present but our build excluded the vulnerable codevulnerable_code_not_in_execute_path— the code is present but unreachable in normal operationvulnerable_code_cannot_be_controlled_by_adversary— reachable but only via a trusted pathinline_mitigations_already_exist— a runtime mitigation (e.g. seccomp, AppArmor) blocks exploitation
The free-form impact_statement field is where you explain which of these
applies and how, in one or two sentences.
Validating locally#
# all images
tools/vex/validate.sh
# specific file
tools/vex/validate.sh vex/nginx.openvex.jsonCI runs tools/vex/validate.sh as a standalone job on every PR. A schema
failure blocks merge — same gate as the image build itself.