Wiring VEX Into CI: Authoring OpenVEX Statements That Auditors Trust
Generate, review, and attach OpenVEX statements in a pipeline so scanner suppressions carry a real justification, survive re-verification, and never silence a finding on vibes.
- #security
- #ai
- #vex
- #sbom
- #supply-chain
You can write a single VEX statement by hand. What you cannot do by hand, sustainably, is keep VEX statements accurate across hundreds of images that rebuild daily, dependencies that shift under you, and a not_affected claim that was true last sprint and quietly became false when someone added a call site. The hard part of VEX was never the JSON. It is the lifecycle — generating statements where triage actually happened, reviewing them so a justification means something, attaching them to the right artifact, and re-checking the fragile ones before they lie to your scanner. This guide is about turning that lifecycle into a pipeline.
Why a One-Off VEX File Rots
The first VEX statement a team writes is usually a JSON file checked into a repo, hand-edited whenever a scanner complains. It works for a week. Then the image rebuilds with a new base layer, the component the statement referenced moves or disappears, and the statement now applies to a product version that no longer exists — or worse, suppresses a CVE in a context where it has become reachable. Nothing alerts you, because a stale VEX statement looks exactly like a fresh one.
The fix is to treat VEX like any other build artifact: generated as close as possible to the evidence, versioned against a specific image digest, and re-evaluated on every build. A statement that says vulnerable_code_not_in_execute_path is a reachability claim, and reachability is the first thing that changes when code changes. If the statement is not regenerated and re-reviewed alongside the build that produced the artifact, it is a liability wearing the costume of a control.
The Pipeline Shape
A workable VEX pipeline has four stages, and each maps to a distinct owner:
- Scan — generate the SBOM and run the matcher (Grype, Trivy) to get the raw CVE set for this exact digest.
- Triage — for each finding, a human (assisted, not replaced) decides status and justification.
- Author — emit a valid OpenVEX document bound to the digest.
- Apply + verify — attach the VEX as an OCI attestation and re-run the scanner with the VEX so the report reflects reality.
The point of separating triage from authoring is that the decision is the expensive, human part, and the JSON is the cheap, mechanical part. If you let the mechanical part drive — “the scanner is red, write a suppression” — you get rubber stamps. The triage decision has to come first and stand on its own.
Generating the Statement From a Real Decision
Here is a complete OpenVEX statement, the kind your author stage should emit, bound to a digest and carrying a justification from the fixed vocabulary:
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://example.com/vex/checkout-2026-06-23",
"author": "Security Team <secops@example.com>",
"timestamp": "2026-06-23T10:00:00Z",
"version": 1,
"statements": [
{
"vulnerability": { "name": "CVE-2025-12345" },
"products": [
{ "@id": "pkg:oci/checkout@sha256:abc123..." }
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
]
}
Two details carry the weight. The products entry pins to a specific digest, not a floating tag — so the statement cannot accidentally apply to a future rebuild. And the justification comes from the closed set the spec defines (component_not_present, vulnerable_code_not_present, vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary, inline_mitigations_already_exist). If a triage decision cannot map to one of those, the correct status is not not_affected — it is under_investigation.
Using AI to Draft the Triage, Not Decide It
The slowest part of this loop is reasoning about each CVE: is the component even loaded, is the vulnerable function reachable, is there a config that already neutralizes it. An LLM is genuinely useful here as a drafting partner — it can read the SBOM, the CVE description, and a call-graph excerpt and propose a status with a candidate justification. What it must never do is finalize a suppression.
Prompt: “You are a product-security engineer authoring OpenVEX. For each CVE below, propose a VEX status. If you propose
not_affected, you MUST map it to one of the five spec justifications; if you cannot, returnunder_investigationinstead and say what evidence is missing. Output a table: CVE, proposed status, justification, the single deciding fact, and a re-verify cadence. Do not assume any suppression is approved.”Output (excerpt): “CVE-2025-12345 — proposed
not_affected—vulnerable_code_not_in_execute_path— deciding fact: the vulnerablexmlParseEntitypath is only reached via the legacy importer, which is compiled out in this build (-DNO_LEGACY_IMPORT). Re-verify cadence: every release, because adding the importer back silently invalidates this. Flag: confirm the build flag in the actual Dockerfile, not assumed.”
That output is a draft. A human confirms the build flag, accepts or rejects the justification, and then it becomes a statement. The AI compresses the triage; it does not own the call. This is the same AI-drafts, human-verifies pattern that makes SBOM and signing workflows safe to automate — the machine handles volume, the engineer owns the claim.
Attaching and Verifying in the Pipeline
Once a statement is approved, attach it to the image as an attestation and re-run the scanner so the report tells the truth:
# Attach the VEX document as an OCI attestation
cosign attest --predicate checkout-vex.json \
--type openvex \
ghcr.io/example/checkout@sha256:abc123...
# Re-scan with the VEX applied so suppressed CVEs drop out of the report
grype ghcr.io/example/checkout@sha256:abc123... \
--vex checkout-vex.json \
--fail-on critical
The --fail-on critical gate now reflects exploitable criticals, not raw matches — which is the entire point. A finding only disappears from the gate because a justified, attached VEX statement said it should, and that statement is bound to the digest the gate is evaluating.
Keeping It Honest Over Time
Two practices keep the pipeline from drifting back into rubber-stamp territory.
First, re-evaluate reachability claims on every build. Tag any statement with vulnerable_code_not_in_execute_path and surface it in the triage stage of the next build for re-confirmation, rather than carrying it forward silently. A reachability claim that is never re-checked is the most dangerous artifact in the whole system.
Pro Tip: Track a “VEX age” metric — how long since each active
not_affectedstatement was last human-confirmed. A statement older than a couple of release cycles that no one has re-touched is not a suppression, it’s a guess that stopped being supervised.
Second, make under_investigation a real, common status. Teams treat it as a failure state and rush every finding to not_affected or affected. But “we’ve seen it, we don’t yet know, and we’re saying so out loud” is an honest and defensible position. A pipeline that produces some under_investigation statements is one where the not_affected statements actually mean something.
Where to Go Next
VEX is one link in a supply-chain story that only holds if the other links are sound — the SBOM has to be accurate, the image has to be signed, and the attestation has to be verified at admission. If you are wiring VEX into CI, pair it with keyless image signing and admission verification so the VEX attestation you attach rides the same trust chain as the signature. And if your scanner output still feels like noise before VEX even enters the picture, start with disciplined triage using the VEX statement authoring prompt to turn each decision into a defensible, re-verifiable record rather than a one-off file that rots.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.