Skip to content

CycloneDX VEX: only the last vulnerability object is used when several share the same ID #11396

Open

Description

@DmitriyLewen

Description

When a CycloneDX VEX contains several vulnerabilities[] objects with the same id, Trivy only uses the last one.
Statements from the earlier objects are silently lost: they aren't applied, and no warning is shown.

CycloneDX allows this shape.
The vulnerabilities array has uniqueItems in the schema, but that is uniqueness of whole objects, not of id.
The analysis (analysis.state, analysis.justification) is defined per object and applies to all of its affects, so different verdicts for different components of the same vulnerability can only be expressed with several objects sharing the same id.
The official examples (CycloneDX/bom-examples) contain such documents too: for example, VEX/CISA-Use-Cases/Case-7/vex.json has four CVE-2021-44228 objects with the exploitable, not_affected and resolved states.

Reproduced on v0.75.0 and on main.
The document is both the SBOM and the VEX:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
  "version": 1,
  "components": [
    {
      "type": "library",
      "bom-ref": "pkg:maven/org.springframework/spring-web@5.3.0",
      "group": "org.springframework",
      "name": "spring-web",
      "version": "5.3.0",
      "purl": "pkg:maven/org.springframework/spring-web@5.3.0"
    },
    {
      "type": "library",
      "bom-ref": "pkg:maven/org.springframework/spring-web@5.3.18",
      "group": "org.springframework",
      "name": "spring-web",
      "version": "5.3.18",
      "purl": "pkg:maven/org.springframework/spring-web@5.3.18"
    }
  ],
  "vulnerabilities": [
    {
      "id": "CVE-2016-1000027",
      "affects": [{ "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/org.springframework/spring-web@5.3.0" }],
      "analysis": { "state": "not_affected", "justification": "code_not_reachable" }
    },
    {
      "id": "CVE-2016-1000027",
      "affects": [{ "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/org.springframework/spring-web@5.3.18" }],
      "analysis": { "state": "not_affected", "justification": "code_not_reachable" }
    }
  ]
}
trivy sbom --show-suppressed --vex two-objects.cdx.json two-objects.cdx.json

CVE-2016-1000027 is expected to be suppressed for both spring-web versions, the same as when both refs are listed in the affects of a single object.
In fact only 5.3.18 is suppressed, and 5.3.0 is reported.
Swapping the objects gives the opposite result, so the outcome depends on the order of objects in the document.

Discussed in #11392

Root cause

newCycloneDX (pkg/vex/cyclonedx.go) stores statements in a map[string]Statement keyed by vuln.ID, so each subsequent object with the same id overwrites the previous one.
NotAffected gets a single statement from the map and only checks its Affects.

Implementation challenges

1. Conflicting statements for the same ref

Once this is fixed, a single vulnerability and component pair can match several statements with different states (for example, exploitable and not_affected).
OpenVEX settles this in its spec: the latest statement for a product wins, and OpenVEX.NotAffected follows that.
CycloneDX has no such rule, so the expected behavior has to be defined, along with whether the user should be told about the conflict.

2. affects[].versions is ignored

In the official examples, objects with the same id and the same ref often differ only in affects[].versions: in CISA Case-7, product-ABC is exploitable for some version ranges and not_affected for others.
Trivy doesn't read versions at all today.
While the document keeps one object per id, this is hardly noticeable, but once all objects are taken into account, such a document could suppress the finding for every version, including the vulnerable ones.
We need to decide whether versions support is in scope, and if not, how to avoid wrong suppression for such documents.

3. Tests

At a minimum we need cases for: several objects with the same id and different refs, several objects with the same ref and different states, and different object order.
The existing CycloneDX VEX tests are in pkg/vex/vex_test.go, with fixtures in pkg/vex/testdata.

Before implementation

Before implementing, the approach (primarily items 1 and 2) should be discussed and agreed on in this issue, and only then should the implementation start.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug.
    scan/sbomIssues relating to SBOM
    component/vexIssues and PRs relating to VEX support
    on Oct 13, 2026
  2. choksi2212 commented on Oct 13, 2026

    @choksi2212

    I'd like to take this one. The statement map in pkg/vex/cyclonedx.go keeps only the last object per vulnerability ID, which matches what you're seeing. I'll send a PR with a test for the multi-object case shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

component/vexIssues and PRs relating to VEX supportkind/bugCategorizes issue or PR as related to a bug.scan/sbomIssues relating to SBOM

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions