skip to content

How do you feed VEX statements to a Trivy 0.74 scan so not_affected findings drop out, and how do you audit what they suppressed?

level: seniorimportance: nice to knowfreq 12%

answer

  1. one repeatable flag, four kinds of source
  2. PURL is the product identifier
  3. format support differs by target
  4. --show-suppressed
  5. a versionless PURL matches every version

basics

~20 s

Pass --vex with a local OpenVEX, CSAF or CycloneDX file, or with repo, oci or sbom-ref; a statement whose PURL matches a found package and says not_affected or fixed removes that finding. Adding --show-suppressed lists what VEX removed and why.

solid answer

~40 s

`--vex`, still experimental in 0.74, is repeatable and ordered by priority: a file path, `repo` (VEX Hub or a configured VEX repository), `oci` (VEX attestations discovered beside an image) or `sbom-ref` (VEX URLs in a CycloneDX SBOM's `externalReferences`). OpenVEX and CSAF work on every target; CycloneDX VEX applies only when scanning a CycloneDX SBOM, as a separate document that references it. Trivy matches statements by PURL, so take PURLs from Trivy's own JSON report; a matching `not_affected` or `fixed` statement removes the finding, after severity, status and ignore-file filtering. To audit, add `--show-suppressed`: the table lists each suppressed CVE with its status, statement and source, and JSON exports them as `ExperimentalModifiedFindings`. Watch scope — a PURL without a version matches every version.

code

json · 17 lines
json
{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://vex.example.internal/payments-api/2026-09",
  "author": "Platform Security, example.internal",
  "timestamp": "2026-09-14T09:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": {"name": "CVE-2019-8457"},
      "products": [
        {"@id": "pkg:deb/debian/[email protected]+dfsg1-0.8"}
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path"
    }
  ]
}

go deeper

for a junior

Recall that --vex feeds a VEX document to Trivy and that a matching not_affected statement removes the finding from the report.

for a middle

Explain the four source kinds, which formats work on which targets, PURL matching, and that VEX runs after severity, status and ignore-file filtering.

for a senior

Show you audit suppressions with --show-suppressed and the JSON records, avoid versionless products, and know when CycloneDX VEX silently does nothing.

for a principal

Decide whose VEX statements may delete findings from your reports, how they are reviewed and expired, and how suppressions stay visible to auditors.

## What --vex does in the pipeline **VEX** (Vulnerability Exploitability eXchange) is a document in which a supplier or your own team states, per vulnerability and product, whether that product is affected. What each status and justification claims, and who may assert it, is a supply-chain subject of its own; this answer is about how Trivy **consumes** such statements. Trivy applies VEX as the **last** filtering step, after severity, status, the ignore file and any Rego policy. A finding that matches a statement with status `not_affected` or `fixed` is removed from the result; statements saying `affected` or `under_investigation` remove nothing. Each removal is recorded, and a log line such as `Filtered out the detected vulnerability` names the format, the CVE, the status and the justification. The feature is marked **experimental** in 0.74, so its behaviour may change between releases. ## The four kinds of source `--vex` can be repeated, and the order you give is the priority order. | Value | Where statements come from | |---|---| | a file path | a local OpenVEX, CSAF or CycloneDX document | | `repo` | VEX repositories configured in `repository.yaml`, by default Aqua's VEX Hub on GitHub | | `oci` | VEX attestations attached to the scanned image in its registry | | `sbom-ref` | VEX URLs listed as `exploitability-statement` external references in a CycloneDX SBOM | The flag's own help text in 0.74 names only `repo`, `oci` and a file path; the documentation and the source also accept `sbom-ref`. ## Format support by target - **OpenVEX** and **CSAF** apply to every target: image, filesystem, repository, VM image, Kubernetes and SBOM. - **CycloneDX VEX** applies only when the target is a CycloneDX SBOM, and only in the independent form — a separate VEX document whose `affects` entries point into that SBOM with BOM-Links. A CycloneDX VEX file passed to `trivy image` filters nothing. ## How a statement matches 1. The vulnerability ID in the statement must match the finding's ID. 2. The product must be a **PURL** that matches the package Trivy found; the reliable way to get it right is to copy the `PURL` from Trivy's JSON report. 3. Matching follows a community convention: a PURL **without a version** matches every version, a PURL without qualifiers matches every qualifier variant, and a PURL with qualifiers matches only packages carrying the same values. 4. OpenVEX **subcomponents** narrow a statement about an image to the specific package inside it, which reduces the chance of over-filtering. Trivy applies statements over its dependency graph: a statement that an intermediate module is not affected by a CVE in a subcomponent suppresses the finding only when every path from the root to the vulnerable package runs through a not-affected parent; one uncovered path keeps it. ## Auditing what was suppressed - `--show-suppressed` adds a *Suppressed Vulnerabilities* table listing package, CVE, severity, status, statement and source, such as `CSAF VEX` or `.trivyignore.yaml`. - In JSON the same records appear as `ExperimentalModifiedFindings`. - Findings dropped by `--severity` or `--ignore-status` never appear there; only ignore-file and VEX suppressions are recorded. ## Pitfalls - **Versionless products.** A `not_affected` statement on `pkg:deb/debian/libdb5.3` silences the CVE for every future version of that package, including one where the vulnerable path becomes reachable. - **Wrong format for the target.** CycloneDX VEX with an image target does nothing and raises no alarm. - **Network sources offline.** `repo` fetches from GitHub; in a restricted network, self-host the repository or use `--skip-vex-repo-update`. - **Partial package lists.** `--pkg-relationships` cannot be combined with `--vex`, because a filtered package list could make VEX misapply. - **Trust.** `--vex repo` and `--vex oci` accept other parties' statements; decide whose statements you are willing to let delete findings from your report. ## A minimal workflow 1. Scan once with `--format json` and copy the exact `PURL` of the package the assessment covers. 2. Write the statement — OpenVEX for an image, filesystem or repository target — with a **versioned** PURL and the justification your assessment supports. 3. Keep the document in version control beside the service, so a reviewer sees who asserted what and when. 4. Scan with `--vex <file> --show-suppressed` and confirm the finding moved to the suppressed table with the expected source. 5. When the package version changes, the versioned PURL stops matching and the finding returns, which forces a fresh assessment instead of an inherited one.

  • Why does a CycloneDX VEX file have no effect on trivy image?
    Trivy supports CycloneDX VEX only in the independent BOM-plus-VEX form and only when the scan target is a CycloneDX SBOM, because the statements reference components by BOM-Link. For an image, write the statements as OpenVEX or CSAF, or scan the image's CycloneDX SBOM with `trivy sbom` and pass the VEX there.
  • What happens to --vex repo on a runner with no route to GitHub?
    The default VEX repository is VEX Hub, fetched over HTTPS from GitHub, so the update fails. Self-host a copy, point `repository.yaml` (created by `trivy vex repo init`) at the internal server with the default repository disabled, or pass `--skip-vex-repo-update` to use what is already cached.
  • Can --vex be combined with --pkg-relationships root,direct?
    No. Trivy's documentation states that `--pkg-relationships` cannot be used with `--vex`, because filtering packages by relationship may leave an incomplete package list, and VEX is applied across the dependency graph.

saying these in an interview costs you the question

  • --vex marks findings not_affected but leaves them in the report
  • Any VEX format works with any Trivy target
  • A statement saying under_investigation also hides the finding
  • Once VEX filters a finding, there is no way to see it again
  • Trivy matches VEX statements by CVE ID alone, whatever the package