Automated SBOM VEX in 2026: CI Workflow [How-To]
Bottom Line
In 2026, the practical path is to treat VEX as a release artifact beside your SBOM, not as an afterthought. Generate a machine-readable SBOM, attach a small OpenVEX document for reviewed exceptions, and make your scanner consume both in CI.
Key Takeaways
- ›CycloneDX 1.7 is the current CycloneDX spec; SPDX 3.0 is the current SPDX spec.
- ›CISA VEX still centers on four statuses: affected, not_affected, fixed, under_investigation.
- ›Trivy can consume VEX from a local file, OCI attestation, repository, or SBOM reference.
- ›Start with local-file VEX in CI before moving to repository-based distribution.
False positives are still one of the fastest ways to make SBOM programs lose credibility. The fix is not to suppress scanner findings blindly, but to publish exploitability decisions in a machine-readable format that travels with the release. As of May 01, 2026, CycloneDX 1.7 is the current CycloneDX specification, SPDX 3.0 is the current SPDX specification, and modern scanners such as Trivy can already consume VEX as part of a normal CI/CD run.
Why VEX Belongs in the Pipeline
Bottom Line
Ship your SBOM and your exploitability decisions together. The operational win is simple: fewer noisy alerts, better auditability, and a repeatable path from human triage to automated enforcement.
VEX is the missing link between “this package exists” and “this vulnerability actually matters for this product.” CISA’s minimum VEX requirements still revolve around four statuses, and that is enough for most engineering teams to automate the common path.
- affected: the product is impacted and remediation is needed.
- not_affected: the vulnerable component is present, but the product is not actually exploitable in context.
- fixed: the product version contains a remediation.
- under_investigation: review is still in progress and the pipeline should not treat the issue as cleared.
The practical 2026 pattern is to keep your main SBOM generation fully automated, and keep VEX generation policy-driven. In other words, tools produce inventory; your review process produces exploitability assertions. That separation keeps the system defensible during audits.
Why this workflow ages well
- CycloneDX already models vulnerabilities and exploitability cleanly.
- OpenVEX stays intentionally small, which makes it easier to generate and review.
- Trivy supports multiple VEX input methods, so you can start local and scale later.
- OCI attestation support lets you move from “file in CI” to “artifact attached to release” without redesigning the process.
Prerequisites
Prerequisites Box
- syft available in CI to generate SBOMs.
- vexctl available in CI to emit OpenVEX.
- trivy available in CI to validate the effect of VEX.
- jq for extracting PURLs and asserting expected output.
- An immutable build target such as an OCI image digest, not only a floating tag.
- A review owner allowed to approve not_affected or fixed statements.
Two process requirements matter more than the tooling itself.
- Your SBOM must include stable identifiers such as PURLs.
- Your VEX decision must be traceable to a ticket, advisory, or triage record.
If you store reviewer notes alongside VEX, redact customer names, internal paths, or secrets before committing them. A lightweight option is TechBytes’ Data Masking Tool for sanitizing evidence shared across teams.
Implement the Workflow
The sequence below is intentionally boring. That is a feature. Boring pipelines survive audits and handoffs better than clever ones.
Generate a CycloneDX SBOM
Use syft to emit a CycloneDX JSON artifact for the image you are about to ship.
IMAGE_REF="ghcr.io/example/api@sha256:replace-with-your-digest" mkdir -p dist syft scan "$IMAGE_REF" -o cyclonedx-json=dist/sbom.cdx.jsonThis gives you the inventory artifact that downstream scanners and VEX statements will key off.
Extract the exact product PURL from the SBOM
VEX only works if the product identifier matches what your consumer expects. Pull the real PURL from the generated SBOM instead of hand-typing it.
PRODUCT_PURL="$(jq -r '.components[] | select(.purl != null) | .purl' dist/sbom.cdx.json | head -n 1)" printf 'product=%s\n' "$PRODUCT_PURL"For larger systems, replace the
head -n 1shortcut with a package selection rule that targets the specific component you reviewed.Create a minimal OpenVEX document
Once triage confirms a finding is truly non-exploitable, generate a VEX statement with vexctl create. Replace the CVE and justification with your reviewed values.
VULN_ID="CVE-2024-0000" AUTHOR="security@your-company.example" vexctl create \ --author "$AUTHOR" \ --product "$PRODUCT_PURL" \ --vuln "$VULN_ID" \ --status "not_affected" \ --justification "vulnerable_code_not_in_execute_path" \ > dist/app.vex.jsonFor not_affected, do not treat justification as optional in practice. CISA permits either justification or impact statement, but pipelines are easier to review when every exception carries a specific reason code.
Watch out: The most common failure is a PURL mismatch. If the PURL inapp.vex.jsondoes not line up with the package identity the scanner resolved, your VEX file will be valid but ineffective.Run the baseline and VEX-aware scans
Trivy can consume VEX from several sources. For a first production rollout, use a local file because it is easy to debug and does not depend on repository distribution. Then compare the before and after results.
trivy image --format json --output dist/trivy-no-vex.json "$IMAGE_REF" trivy image --vex dist/app.vex.json --format json --output dist/trivy-with-vex.json "$IMAGE_REF" BASELINE="$(jq '[.Results[]?.Vulnerabilities[]?] | length' dist/trivy-no-vex.json)" FILTERED="$(jq '[.Results[]?.Vulnerabilities[]?] | length' dist/trivy-with-vex.json)" printf 'baseline=%s filtered=%s\n' "$BASELINE" "$FILTERED"If the VEX statement matches a reported package-vulnerability pair, the filtered count should drop or remain the same. It should never increase.
Publish SBOM and VEX as release artifacts
At minimum, archive both JSON files in CI. If you distribute OCI images, add an attestation step so the VEX document travels with the image.
# Optional OCI-attached VEX attestation vexctl attest --attach --sign dist/app.vex.json "$IMAGE_REF"This is the clean handoff point from internal CI to customer-facing distribution.
A simple CI shell wrapper
Most teams eventually hide the commands above behind one script so the workflow stays stable while policies evolve.
#!/usr/bin/env bash
set -euo pipefail
IMAGE_REF="$1"
VULN_ID="$2"
JUSTIFICATION="$3"
AUTHOR="security@your-company.example"
mkdir -p dist
syft scan "$IMAGE_REF" -o cyclonedx-json=dist/sbom.cdx.json
PRODUCT_PURL="$(jq -r '.components[] | select(.purl != null) | .purl' dist/sbom.cdx.json | head -n 1)"
vexctl create \
--author "$AUTHOR" \
--product "$PRODUCT_PURL" \
--vuln "$VULN_ID" \
--status "not_affected" \
--justification "$JUSTIFICATION" \
> dist/app.vex.json
trivy image --vex dist/app.vex.json --format json --output dist/trivy-with-vex.json "$IMAGE_REF"If you want the generated JSON cleaned up for code review, running it through a formatter before commit is worthwhile; TechBytes’ Code Formatter is a convenient quick pass for shared snippets and examples.
Verification and Expected Output
Do not stop at “the command succeeded.” Verify identity alignment, VEX content, and scanner effect.
jq -r '.components[] | select(.purl != null) | .purl' dist/sbom.cdx.json | head
jq '.statements[] | {vulnerability: .vulnerability.name, status: .status, products: [.products[]."@id"]}' dist/app.vex.json
jq '[.Results[]?.Vulnerabilities[]?] | length' dist/trivy-no-vex.json
jq '[.Results[]?.Vulnerabilities[]?] | length' dist/trivy-with-vex.jsonExpected outcome:
- The SBOM prints at least one concrete PURL.
- The VEX file shows the reviewed vulnerability, the intended status, and the same product identifier.
- The VEX-aware scan count is less than or equal to the baseline count.
- If the count does not change, the usual cause is identity mismatch rather than a malformed VEX document.
Troubleshooting Top 3
1. The VEX file is valid, but nothing gets filtered
- Check that the VEX product
@idmatches the package identity found by the scanner. - Pull the identifier from the SBOM programmatically instead of typing it by hand.
- Confirm the vulnerability ID in VEX matches the scanner’s reported ID exactly.
2. Your team is overusing not_affected
- Reserve not_affected for reviewed cases with a defensible reason code.
- Use under_investigation when analysis is incomplete.
- Store the ticket or advisory reference next to the VEX statement so reviewers can reconstruct the decision later.
3. Format conversion strips useful data
- Keep the source-of-truth SBOM in the producer’s native format when possible.
- With syft, generate native JSON if you need maximum fidelity, then convert outward.
- Do not bounce between formats repeatedly in CI; generate once, convert once, publish once.
What's Next
Once the local-file pattern is stable, move up the maturity curve deliberately.
- Attach VEX as an OCI attestation so downstream consumers can discover it automatically.
- Evaluate repository-based distribution after your local-file workflow is trusted; note that Trivy documents repository mode as experimental.
- Add policy gates that block merges when a reviewed exception lacks justification, owner, or timestamp.
- Expand from one-off statements to a release process that emits VEX from approved triage records, not ad hoc shell history.
The core design principle stays the same: inventory is automated, exploitability is reviewed, and CI enforces the connection between the two.
Frequently Asked Questions
What is the difference between an SBOM and a VEX document? +
Can Trivy consume VEX directly in CI? +
--vex /path/to/file.json is the easiest option to debug and audit.When should I use notaffected versus underinvestigation in VEX? +
vulnerable_code_not_in_execute_path. Use under_investigation when triage is still open and you are not ready to suppress the finding in automated workflows.Should I publish OpenVEX as a file or as an OCI attestation? +
Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.