OpenVEX and CSAF VEX on Linux: Cutting Trivy & Grype False Positives in 2026

VEX is the layer between your SBOM and your scanner that says this CVE doesn't apply to us, and here's why. Walks through OpenVEX, CSAF VEX, vexctl, Trivy --vex, Grype match exclusions, and the CI pipeline pattern I run on every Linux build.

OpenVEX VEX 2026: Cut Trivy Noise

Updated: June 16, 2026

VEX (Vulnerability Exploitability eXchange) is a machine-readable assertion that a known CVE either does not affect a product, has been fixed, is under investigation, or is exploitable. Honestly, it's the only practical way I've found to keep Trivy and Grype output trustworthy in a busy CI pipeline. In 2026 the two dominant formats are OpenVEX (lightweight, JSON-LD, Sigstore-friendly) and CSAF VEX (CISA's heavyweight format used by major vendors). This guide walks through how to author VEX documents with vexctl, attach them to images with cosign, and consume them in Trivy and Grype so your build only fails on CVEs that actually matter.

  • VEX answers a single question per CVE: does this vulnerability affect my product right now? It complements (but never replaces) an SBOM.
  • OpenVEX 0.2.0 (May 2024 spec, stable through 2026) is the easiest format to generate locally; CSAF VEX 2.0 is what RHEL, SUSE, Cisco, and Siemens publish.
  • Trivy supports both OpenVEX and CSAF natively via --vex since 0.49, and discovers VEX attestations attached to OCI images via Sigstore.
  • Grype consumes OpenVEX via --vex and downgrades matching findings to ignored rather than dropping them, and auditors love this.
  • Treat VEX statements as code: review them in PRs, sign them with Sigstore, and re-validate under_investigation entries on a 30-day cron.
  • The "ignored false positive" comment in a Slack thread is technical debt. A signed VEX statement with a justification is an audit artefact.

What is VEX in cybersecurity?

VEX, short for Vulnerability Exploitability eXchange, is a structured statement that pairs a vulnerability identifier (usually a CVE) with one of five status values for a specific product or component version: not_affected, affected, fixed, or under_investigation. The format originated at the U.S. National Telecommunications and Information Administration (NTIA) in 2022 and is now maintained jointly by OpenVEX and CISA via the Minimum Requirements for VEX publication.

The reason VEX exists is brutal arithmetic. The average container image I scan in a Linux CI pipeline returns 80–150 CVE matches. In my own audits, somewhere between 60 and 85 percent of those are not exploitable. The vulnerable function is never called, the dependency is build-time only, or the CVE got rescored after we'd already pinned the patched version. Without VEX, the only way to suppress that noise is per-tool ignore files (.trivyignore, .grype.yaml) that nobody outside the team can verify and that rot the moment someone bumps a base image.

A VEX statement makes the suppression auditable: it carries the CVE id, the impacted product (typically a package URL or CPE), a status, a justification code (e.g. component_not_present, vulnerable_code_not_in_execute_path), an author, and a timestamp. Drop it next to your SBOM, sign it with Sigstore, and any downstream scanner (yours or your customer's) can replay your reasoning.

How does VEX differ from an SBOM?

An SBOM enumerates what is in your software; a VEX document enumerates which known CVEs in that SBOM actually matter to you. The two are complementary, never substitutes. A useful mental model: SBOM is the bill of materials shipped with the product, VEX is the engineering disposition memo that accompanies a specific recall.

Concretely, if your Linux supply-chain pipeline with Syft and SLSA produces a CycloneDX SBOM containing libxml2 2.12.5, Grype will match every CVE published against libxml2 in that version range. Some of those CVEs require XPath evaluation against attacker-controlled XML, which your product never accepts. A VEX statement says: "for product pkg:oci/[email protected], CVE-2024-34459 is not_affected because vulnerable_code_not_in_execute_path." Grype now ignores the finding, but the auditor sees the chain of reasoning, signed by you.

Three properties make VEX work in practice:

  • It is per-product, not per-package. The same CVE in libxml2 might be exploitable in your web frontend and not in your batch worker. VEX lets you assert different statuses for each.
  • It is time-stamped. A under_investigation from March 2026 should not still be in effect in June. Trivy and Grype both respect last_updated when deduplicating.
  • It is portable. The same document feeds Trivy, Grype, Dependency-Track 4.10+, Anchore Enterprise, and Snyk's CSAF importer.

OpenVEX vs CSAF VEX: which format to pick

The two formats solve the same problem at different altitudes. OpenVEX is a single small JSON-LD file you can hand-write; CSAF VEX is the OASIS standard used by national CSIRTs and large vendors, with rich metadata for distribution, tracking, and remediation. Pick based on who has to consume your statements.

DimensionOpenVEX 0.2.0CSAF VEX 2.0
MaintainerOpenSSF (Chainguard, RedHat)OASIS CSAF TC + CISA
FormatJSON-LD, ~30 lines per statementJSON, ~300+ lines per advisory
Tooling on Linuxvexctl, Trivy, Grype, cosigncsaf-tool, secvisogram, Trivy
Product identifierpurl + Subject hashCPE, purl, vendor-defined
Distribution modelAttach to OCI artefact via cosignCSAF Trusted Provider directory
Best forInternal pipelines, micro-servicesPublished vendor advisories, ICS/regulated
Learning curve30 minutes2 days

For everything I ship inside a company boundary I default to OpenVEX. When we publish images that downstream customers ingest into their own SBOM pipelines (and especially when those customers include public-sector buyers), I additionally emit CSAF VEX from the same source of truth. The vexctl binary can convert OpenVEX to CSAF, so the duplication stays mechanical.

Authoring an OpenVEX document with vexctl

The reference tool is vexctl, the OpenSSF CLI maintained alongside the OpenVEX spec. As of June 2026 the current release is v0.3.1; it ships as a single static binary that runs cleanly on any glibc or musl Linux distro.

# Install vexctl 0.3.1 (Linux amd64)
curl -sSL -o /usr/local/bin/vexctl \
  https://github.com/openvex/vexctl/releases/download/v0.3.1/vexctl-linux-amd64
chmod +x /usr/local/bin/vexctl
vexctl version

Create your first VEX statement for a single CVE. The product is identified by purl; --justification takes one of the five values defined in the OpenVEX specification:

vexctl create \
  --product="pkg:oci/example-api@sha256:9d4a...c1" \
  --vuln="CVE-2024-34459" \
  --status="not_affected" \
  --justification="vulnerable_code_not_in_execute_path" \
  --author="[email protected]" \
  --file=vex/example-api-libxml2.openvex.json

The resulting document is roughly 25 lines of JSON-LD. Commit it to a vex/ directory in the same repository as the service it covers. Treat each file as code: PR review the justification, require a second approver on anything more permissive than under_investigation, and add a CODEOWNERS rule pointing at the security team.

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.com/vex/2026/example-api-libxml2",
  "author": "[email protected]",
  "timestamp": "2026-06-16T09:14:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2024-34459" },
      "products": [
        { "@id": "pkg:oci/example-api@sha256:9d4a...c1" }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path"
    }
  ]
}

Multiple statements can live in one document, which is handy when you're suppressing an entire family of CVEs against a single image. Merge them with vexctl merge before publishing.

Using VEX with Trivy in CI

Trivy 0.55 (April 2026) consumes VEX in three ways: from a local file via --vex path/to/file.json, from a directory of VEX files via --vex vex/, and from a Sigstore attestation discovered on the image itself. All three respect both OpenVEX and CSAF.

# Trivy with a local VEX directory: drops every matched 'not_affected' CVE
trivy image \
  --vex vex/ \
  --severity HIGH,CRITICAL \
  --exit-code 1 \
  --format json \
  --output trivy.json \
  ghcr.io/example/example-api:1.4.0

The exit-code semantics matter. By default Trivy treats not_affected as a true negative and excludes it from the count that drives --exit-code 1. So a finding that you have a signed VEX statement for stops failing your build the moment the statement merges to main, with no .trivyignore drift.

To see what was suppressed (and why), pass --show-suppressed. In my pipelines I emit two reports: a "build-failing" report with VEX applied and a "transparent" report without it, both uploaded as artefacts. Auditors get the full picture without the engineers ever having to look at the noise.

Using VEX with Grype and Syft

Grype's VEX support landed in v0.74 and matured significantly through 2025. Unlike Trivy, Grype defaults to downgrading matched CVEs to an ignored state in its output rather than removing them. For a pipeline-first workflow this is the better default: your machine-readable artefact carries the full story, but your gate logic only inspects the un-ignored matches.

# Generate an SBOM with Syft, then scan with Grype using VEX
syft ghcr.io/example/example-api:1.4.0 -o cyclonedx-json > sbom.json

grype sbom:sbom.json \
  --vex vex/ \
  --fail-on high \
  --output table \
  --file grype.txt

Grype's gate logic respects --vex through its match.exclude configuration. Add this snippet to ~/.grype.yaml or check it in alongside your repo:

# .grype.yaml: pipeline defaults
fail-on-severity: "high"
ignore-states:
  - "not_affected"
  - "fixed"
vex:
  - "./vex/"
match:
  exclude:
    # belt-and-braces: also drop kernel CVEs in distroless images
    - vulnerability: CVE-2024-*
      package:
        type: rpm

If you are tuning Grype's noise floor you should already be reading the matched results into a SARIF file for GitHub code scanning. The automated Linux vulnerability scanning playbook covers that wiring in depth.

Attaching VEX as a Sigstore attestation

The most underrated property of OpenVEX is that it embeds cleanly inside a Sigstore in-toto attestation. Once attached, any consumer who pulls the image can verify both the VEX document and the identity that signed it, with no separate distribution channel to coordinate. cosign handles this end-to-end:

# Sign an OpenVEX document and attach it to an OCI image
cosign attest \
  --predicate vex/example-api-libxml2.openvex.json \
  --type "https://openvex.dev/ns/v0.2.0" \
  --yes \
  ghcr.io/example/example-api:1.4.0

# Downstream verification
cosign verify-attestation \
  --type "https://openvex.dev/ns/v0.2.0" \
  --certificate-identity-regexp ".+@example\.com$" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/example/example-api:1.4.0 | jq '.payload | @base64d | fromjson'

Trivy then auto-discovers the attestation on trivy image with no extra flag, provided --registry-token and the keyless verification certificate identity are configured. This is how I close the loop for cross-team consumers: they never need to clone our repo or fetch our VEX directory, the policy travels with the artefact.

A pipeline-first VEX workflow for Linux builds

Here is the workflow I now run on every container build. It assumes GitHub Actions on a hardened self-hosted runner (see the Linux CI/CD pipeline hardening guide), but the shape translates cleanly to GitLab CI, Argo Workflows, or Tekton.

# .github/workflows/build-scan-vex.yml
name: build-scan-vex
on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write
  id-token: write   # required for keyless cosign

jobs:
  build:
    runs-on: [self-hosted, linux, hardened]
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        id: build
        run: |
          IMAGE=ghcr.io/example/example-api:${{ github.sha }}
          docker buildx build --push -t "$IMAGE" .
          echo "image=$IMAGE" >> "$GITHUB_OUTPUT"

      - name: Generate SBOM (Syft)
        run: syft "${{ steps.build.outputs.image }}" -o cyclonedx-json > sbom.json

      - name: Validate VEX directory
        run: vexctl validate vex/

      - name: Sign & attach VEX statements
        run: |
          for f in vex/*.openvex.json; do
            cosign attest --yes \
              --predicate "$f" \
              --type "https://openvex.dev/ns/v0.2.0" \
              "${{ steps.build.outputs.image }}"
          done

      - name: Trivy scan (VEX-aware, fails build on real HIGH/CRITICAL)
        run: |
          trivy image \
            --vex vex/ \
            --severity HIGH,CRITICAL \
            --exit-code 1 \
            --ignore-unfixed \
            --format sarif \
            --output trivy.sarif \
            "${{ steps.build.outputs.image }}"

      - name: Grype scan (transparent report, never fails the build)
        run: |
          grype "${{ steps.build.outputs.image }}" \
            --vex vex/ \
            --output table \
            --file grype-full.txt || true

      - uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: trivy.sarif }
      - uses: actions/upload-artifact@v4
        with: { name: scan-reports, path: '*.txt' }

The split between "Trivy fails the build" and "Grype runs in observe-only mode" is deliberate. One scanner owns the gate; the other catches anything the gate-scanner missed. VEX feeds both, from a single repository of statements, which is the property that makes the whole approach scale to dozens of services.

Pitfalls I've hit (and how to avoid them)

VEX is a young ecosystem and the failure modes are not yet folklore. Five that have cost me time:

  • Stale under_investigation entries. Add a scheduled job that lists all VEX statements whose last_updated is older than 30 days and opens a PR demanding a real status. Otherwise under_investigation becomes a permanent suppression by accident.
  • Product purl drift. When the base image bumps from distroless/static@sha256:abc to distroless/static@sha256:xyz, every VEX statement that pinned the OCI digest stops matching. Pin to tags or use floating subject identifiers for long-lived suppressions.
  • Justification misuse. vulnerable_code_not_in_execute_path is the most common justification and the easiest to misuse. Require a code reference in the PR description that proves the call path is dead. A Semgrep query result is fine; "I checked" is not.
  • Mixing VEX with .trivyignore. If both files suppress the same CVE, debugging which one fired becomes a nightmare. Pick one mechanism per repo. I delete the ignore file the moment a VEX statement replaces it.
  • Forgetting to sign. An unsigned VEX statement is just a JSON file. If a downstream consumer can rewrite it on the registry, your security stance is fiction. Always attach via cosign attest --yes, always verify identity, never accept anonymous VEX.

Frequently Asked Questions

Can VEX replace ignore files like .trivyignore?

Yes, and it should. Ignore files are unsigned, opaque, and per-tool. A VEX statement carries the CVE, product, status, justification, author, and timestamp in a portable format that both Trivy and Grype consume. Pick one mechanism per repo and delete the other.

How do I create a VEX document for a Docker image?

Install vexctl, identify the image with a purl such as pkg:oci/myapp@sha256:..., and run vexctl create --product=<purl> --vuln=CVE-XXXX-YYYY --status=not_affected --justification=<reason>. Commit the resulting JSON-LD file to a vex/ directory and attach it via cosign attest --type https://openvex.dev/ns/v0.2.0.

What are the five VEX status values?

OpenVEX defines exactly four statuses: not_affected, affected, fixed, and under_investigation. CSAF VEX adds a fifth dimension via remediation states (known_affected, known_not_affected, fixed, first_fixed, under_investigation). The not_affected status requires a justification code.

Does VEX work with Kubernetes admission controllers?

Yes. Kyverno 1.12+ and Sigstore Policy Controller 0.10+ can require a verified OpenVEX attestation on admitted images. Combine that with a Trivy operator scan and you get cluster-wide enforcement that respects your VEX statements without redeploying.

Is OpenVEX or CSAF VEX better for federal compliance?

CSAF VEX 2.0 is the format CISA and U.S. federal acquisition guidance reference as of the September 2025 update. If you sell to civilian agencies, publish CSAF; for internal pipelines OpenVEX is faster to author and convertible to CSAF via vexctl when you need it.

Raj Patel
About the Author Raj Patel

DevSecOps engineer who's gradually turning every CI pipeline he sees into a security-checking machine.