---
key: "vex-decisions"
title: "Use VEX without losing vulnerability evidence"
url: "https://softwaresecurity.com/domains/supply-chain/vex/producer-and-consumer-decisions/"
api: "https://softwaresecurity.com/api/v1/articles/vex-decisions.json"
domain: "supply-chain"
topic: "Software supply chain"
type: "Guide"
identifiers: ["A03:2025"]
tags: ["SBOM","VEX and vulnerability status","Vulnerability management","Vulnerability disclosure"]
stages: ["release","respond"]
roles: ["appsec-engineer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Use VEX without losing vulnerability evidence

Produce and consume precise vulnerability status statements tied to the product, version, justification and evidence.

Canonical URL: https://softwaresecurity.com/domains/supply-chain/vex/producer-and-consumer-decisions/

## In short

A VEX statement says whether one product is affected by one vulnerability, and why; it does not remove the component from the SBOM. Issue `not_affected` only for an exact product version or digest, with a justification your build evidence supports, such as `vulnerable_code_not_present`. As a consumer, accept statements only from issuers you trust and for the exact product you run, and keep the original finding so that a build or configuration change can reopen it.

- VEX states impact, not presence: a `not_affected` component still belongs in the SBOM.
- OpenVEX v0.2.0 was still a draft on 11 October 2026; CSAF 2.0 and CycloneDX 1.7 express the same four states under different names.
- Trivy and Grype match OpenVEX products by package URL, and a product purl without a version or digest suppresses the finding in every version of the image.
- `vulnerable_code_not_present` and `vulnerable_code_not_in_execute_path` are different claims; a default-off feature a customer can enable supports neither.
- A valid signature proves who issued a statement, not that it is still true for the build you deploy.

## Verify it

1. Scan with `trivy image --vex <file> --show-suppressed` and confirm that only the named package and vulnerability move to the suppressed list.
2. Scan a different image digest with the same VEX file and confirm that nothing is suppressed.
3. Reject a statement whose product purl has no version or digest unless the issuer means it to cover every version.
4. Confirm that every `not_affected` statement carries a justification label or an impact statement, as OpenVEX requires.
5. Rebuild with the vulnerable code enabled and confirm that your process reopens the finding.

## What you will learn

- Distinguish component presence from product impact.
- Select a justification supported by build evidence.
- Reject stale or contradictory VEX despite a valid signature.

Audience: Product security teams who publish VEX statements and engineers who apply supplier VEX to scanner results

Before you start:

- Distinguish component presence, vulnerability status, affected product and supporting justification.

Time: About 28 minutes: 6 to read and 22 for the exercise

## Keep the detection and the impact assessment separate

An SBOM states component presence under its declared scope. A vulnerability match connects a component to an advisory. VEX supplies a product-specific assessment of that vulnerability's impact. These are different records. A `not_affected` statement does not mean the component was absent from the inventory or that the advisory is false. It means the issuer asserts a particular product is not affected, with a reason whose validity depends on scope and evidence.

OpenVEX provides the statuses `affected`, `not_affected`, `fixed` and `under_investigation`, and requires a `not_affected` statement to carry a justification label or an impact statement. Its repository describes the specification as draft; the example here pins the v0.2.0 context and should not be described as a final universal standard. A consumer must know the issuer it trusts and what product identifiers mean. A claim about one product version, platform or feature configuration does not automatically apply to another merely because they share a component name.

## Map statuses across OpenVEX, CSAF and CycloneDX

Suppliers publish VEX in three formats. CSAF 2.0, an OASIS Standard also published as ISO/IEC 20153:2025, has a VEX profile used in vendor security advisories; CSAF 2.1 is still a committee draft. CycloneDX carries VEX in its `vulnerabilities` array, either inside an SBOM or as a separate document. The four states line up, but the justification vocabularies differ: OpenVEX and CSAF share five labels, while CycloneDX has nine of its own. Only two map exactly, `vulnerable_code_not_present` to `code_not_present` and `vulnerable_code_not_in_execute_path` to `code_not_reachable`, so keep the original label when you convert.

| Meaning | OpenVEX v0.2.0 | CSAF 2.0 VEX profile | CycloneDX 1.7 analysis |
| --- | --- | --- | --- |
| The product is affected | `affected`, with an `action_statement` | `known_affected`, with a remediation | `exploitable`, with a `response` |
| The product is not affected | `not_affected`, with a justification or `impact_statement` | `known_not_affected`, with a flag label or impact statement | `not_affected`, with a `justification` |
| The product is fixed | `fixed` | `fixed` | `resolved`, or `resolved_with_pedigree` |
| Not yet known | `under_investigation` | `under_investigation` | `in_triage` |
| The advisory does not apply to the component at all | No status | No status | `false_positive` |

## Make the reason falsifiable for one product

The fictional Larch 5.0 includes codec 2.4, but its compiled build excludes the vulnerable legacy decoder. Evidence E17 identifies the exact product digest and symbol/call analysis supporting that exclusion. A defensible not_affected statement binds the product version, vulnerability and justification, with an impact explanation that a reviewer can challenge. Record the evidence reference and limitations outside or alongside the exchange document as appropriate to the chosen format.

The distinction between vulnerable code absent and vulnerable code unreachable matters. The former claims the relevant code is not in the artifact; the latter claims it cannot execute through the product's paths. A disabled default option is weaker if customers can turn it on. The illustrative document below uses `vulnerable_code_not_present` because the fixture explicitly supplies compiled exclusion evidence. If that evidence were missing, `under_investigation` would be the honest state. Never select a convenient justification merely to silence a scanner.

```json
{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://security.example/vex/larch-5-adv8",
  "author": "https://security.example/larch-team",
  "timestamp": "2026-10-07T12:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": {
        "name": "ADV-8-FICTIONAL"
      },
      "products": [
        {
          "@id": "pkg:generic/larch@5.0.0"
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_present",
      "impact_statement": "For artifact L50 only, build evidence E17 shows legacy decoder excluded; no claim for plugins or L51."
    }
  ]
}
```

## Treat authority, scope and freshness as separate checks

A consumer should authenticate the issuer, match the product identity and vulnerability, inspect the stated rationale and reconcile contradictory local evidence. A signed supplier statement can still be out of scope or stale. Maintain a local acceptance policy that defines when VEX suppresses an alert, when it only changes priority and when further review is required; /domains/supply-chain/dependencies/evidence-based-triage/ routes each finding to one such outcome. The underlying detection should remain recoverable, including the original component identity and advisory data.

In the fixture, the issuer is trusted for Larch, but the deployed product is L51, whose build now includes the decoder. The previous L50 statement cannot suppress L51's finding. A new statement claiming all Larch 5.x products are not affected conflicts with E18 and must be investigated even if its signature verifies. Timestamps and version numbers help sequence statements; the most recently received document is not necessarily the newest authoritative revision. Consumers need a policy for supersession and rollback of incorrect assertions.

## Apply VEX in Trivy and Grype without hiding other findings

Scanners match OpenVEX statements by package URL, so the purl rules in /domains/supply-chain/sbom/formats-and-identifiers/ decide what a statement covers. Name the product as the image, bound to its digest, and the package the statement is about as a subcomponent. The script below writes such a statement with vexctl and applies it with Trivy and Grype. Tested on the `alpine:3.22.4` image, CVE-2026-14456 was reported for both `libcrypto3` and `libssl3`; with the statement, both scanners suppressed only the `libcrypto3` finding and still reported `libssl3`. A statement that named the `alpine:3.22.6` digest instead suppressed nothing in the same scan.

The near miss is the product identifier. When the product purl had no digest, `pkg:oci/alpine?repository_url=...`, both scanners suppressed the finding as well, because a purl without a version matches every version of the image. That is the broad suppression the next section warns against. `--show-suppressed` keeps suppressed findings visible with the statement that suppressed them. Trivy applies CycloneDX VEX only when scanning an SBOM, matching components by BOM-Link rather than by purl.

```bash
# Product: the released image by digest. Subcomponent: the package the statement is about.
PRODUCT="pkg:oci/cedar@${DIGEST}?repository_url=ghcr.io%2Facme%2Fcedar"
vexctl create --author "Acme Product Security" \
  --product "$PRODUCT" --subcomponents "pkg:apk/alpine/libcrypto3@3.5.6-r0" \
  --vuln CVE-2026-14456 --status not_affected \
  --justification vulnerable_code_not_in_execute_path \
  --impact-statement "Call-graph evidence E31: Cedar never reaches the affected code." \
  --file cedar-2.3.openvex.json

# Consumers apply it; every other package and vulnerability is still reported
trivy image --vex cedar-2.3.openvex.json --show-suppressed "ghcr.io/acme/cedar@$DIGEST"
grype "ghcr.io/acme/cedar@$DIGEST" --vex cedar-2.3.openvex.json --show-suppressed
```

## Test the events that invalidate the conclusion

A VEX decision should name reopening triggers: product digest changes, affected code or feature becomes available, advisory scope changes, issuer trust is withdrawn, or local evidence contradicts the rationale. For `not_affected` due to code absence, a build change enabling that code is decisive. For a `fixed` statement, verify the actual patched product rather than accepting a future release announcement. Uncertainty should produce review, not silent permanent suppression.

There is an operational tradeoff between reducing noise and preserving accountability. Broad wildcard suppression is easy to operate but can hide real exposure after a configuration change, and a product purl without a version is such a wildcard. Very narrow artifact-specific statements cost more to maintain but can be joined to precise release evidence. Use the granularity required by the claim and automate re-evaluation on relevant changes. The exercise is fully self-contained: the JSON is illustrative VEX data, the evidence is fictional, and no scanner or production suppression is changed.

## Guided practice

A consumer has a VEX statement for Larch 5.0 and must process a new release and contradictory update.

### Supplied material

```text
ADV-8-FICTIONAL: affects codec 2.4 legacy decoder when it executes.
L50 = Larch 5.0.0, codec 2.4; E17 compiled legacy decoder excluded; product artifact L50.
V1 = printed statement, version 1, trusted Larch issuer, covers pkg:generic/larch@5.0.0 only.
L51 = Larch 5.1.0, codec 2.4; E18 legacy decoder included; customer can enable legacy mode.
V2 = trusted signature, version 2, claims not_affected for all Larch 5.x because legacy mode is disabled by default; no additional evidence.
Current deployment: L51, legacy mode enabled.
Consumer policy: accept only authenticated, exact-scope statements with rationale consistent with local evidence; contradictions enter review; retain original alert.
```

### 1. Interpret V1

Explain why presence of codec 2.4 does not contradict V1 for L50.

Worked answer: The component can be present while its vulnerable code is excluded from this product artifact. E17 supports that specific justification for L50; the SBOM entry should remain.

### 2. Match deployment scope

Decide whether V1 can suppress the L51 alert.

Worked answer: No. L51 is a different product version and E18 changes the relevant build fact. Exact-scope policy rejects reuse of V1 for L51.

### 3. Evaluate the signed update

Assess V2 against the current deployment and name the contradiction.

Worked answer: Do not suppress. Disabled by default does not establish unreachability when the customer enabled legacy mode. E18 plus current configuration contradict the broad rationale despite trusted authentication.

### 4. Choose the local state

Write a local response without rewriting the supplier evidence.

Worked answer: Retain the component/advisory alert, V1 and V2 as received, and record V2 disputed for L51. Treat current affected-path evidence as requiring remediation review; obtain corrected supplier scope and plan disabling or patching the decoder.

### 5. Specify the regression

Give an exact future re-evaluation trigger and expected outcome.

Worked answer: Any build enabling the decoder invalidates an E17-style absence claim. For L51 with legacy enabled, policy must keep the alert active; for exact L50 with authenticated V1/E17, it may apply the bounded `not_affected` decision.

Deliverable: A VEX acceptance record with one bounded acceptance, one scope rejection and one evidence contradiction.

Scope: The JSON is a teaching document with fictional vulnerability/product identifiers. It is not a real supplier assertion, schema-validation result or suppression instruction.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OpenVEX specification](https://github.com/openvex/spec). v0.2.0 (draft).
- [CycloneDX specification overview, version 1.7](https://cyclonedx.org/specification/overview/). Version 1.7 (October 2025).
- [OSV: vulnerability data and package queries](https://google.github.io/osv.dev/)
- [OASIS: Common Security Advisory Framework Version 2.0](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html). OASIS Standard, 18 November 2022.
- [CycloneDX: VEX capabilities](https://cyclonedx.org/capabilities/vex/)
- [Trivy: applying VEX from a local file](https://trivy.dev/docs/latest/supply-chain/vex/file/)
- [Grype: vulnerability scanner (VEX support)](https://github.com/anchore/grype)
- [vexctl](https://github.com/openvex/vexctl)
- [CISA: Minimum Requirements for Vulnerability Exploitability eXchange (VEX)](https://www.cisa.gov/resources-tools/resources/minimum-requirements-vulnerability-exploitability-exchange-vex). 21 April 2023.