Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VEX (Vulnerability Exploitability eXchange)

bomdrift consumes and emits VEX statements so reviewers can record exploitability decisions next to their SBOMs and have those decisions suppress noise on subsequent diffs.

Two formats are supported on input (auto-detected per file):

  • OpenVEX 0.2.0 — see https://github.com/openvex/spec.
  • CycloneDX VEX 1.6analysis.state is mapped onto the OpenVEX vocabulary (not_affected / resolvednot_affected, exploitableaffected, in_triageunder_investigation).

OpenVEX is bomdrift’s preferred output format on emission (--emit-vex) because the standalone JSON-LD doc is the smallest interop surface.

Consuming VEX (--vex <path>)

The flag is repeatable. Each file is auto-detected by its top-level shape. Statements match findings by (vuln_id_or_alias, product_purl).

VEX statusEffect on the matching finding
not_affectedSuppresses (counted in “Suppressed by VEX”)
fixedSuppresses
under_investigationAnnotates with VEX:under_investigation
affectedAnnotates with VEX:affected

A VEX statement’s products[] may be either purl strings or {"@id": "pkg:..."} objects. A versionless statement (pkg:npm/foo) matches every versioned finding-product (pkg:npm/foo@1.2.3); a versioned statement only matches the exact purl.

Synthetic finding IDs

bomdrift emits non-CVE findings (typosquats, version-jumps, maintainer-age, license-violations). To author VEX statements that suppress them, use the synthetic ID convention:

Finding kindSynthetic ID format
Typosquatbomdrift.typosquat:<purl>:<closest>
Version-jumpbomdrift.version-jump:<purl>:<before_major>-><after_major>
Maintainer-agebomdrift.young-maintainer:<purl>:<top_contributor>
License-violationbomdrift.license-violation:<purl>:<license_string>

Example OpenVEX statement suppressing a typosquat finding:

{
  "vulnerability": { "name": "bomdrift.typosquat:pkg:npm/plain-crypto-js@4.2.1:crypto-js" },
  "products": [ { "@id": "pkg:npm/plain-crypto-js@4.2.1" } ],
  "status": "not_affected",
  "justification": "vulnerable_code_not_present",
  "status_notes": "verified the package is a re-export and not impersonating crypto-js"
}

Multiple files

--vex first.json --vex second.json is processed left-to-right. Statements with the same (vuln_id, product) are first-write-wins — later files do NOT override earlier ones. Layer policy-level VEX first and project-level VEX second so the project-level entries override the defaults. (Or pass them in the reverse order if you want the opposite precedence.)

Verifying with vexctl

If you have vexctl installed:

vexctl filter --vex bomdrift.openvex.json sbom.cdx.json

verifies the VEX doc is well-formed and that statements match a known purl in your SBOM.

Emitting VEX (--emit-vex <path>)

Writes a single OpenVEX 0.2.0 document covering every finding in the post-baseline diff.

  • Baseline-suppressed findings inherit their vex_status from the baseline entry, defaulting to under_investigation. Baseline ≠ “not affected” — baseline often means “accepted in PR review” or “temporarily ignored”, so emitting not_affected by default would publish a false claim. Opt in by adding vex_status: "not_affected" to the baseline entry:

    {
      "id": "GHSA-x-y-z",
      "purl": "pkg:npm/foo",
      "expires": "2026-12-31",
      "reason": "Awaiting upstream patch (issue #42)",
      "vex_status": "not_affected",
      "vex_justification": "vulnerable_code_not_present"
    }
    
  • Un-suppressed findings emit as affected with status_notes describing the bomdrift finding kind. The justification field falls back to the configured [diff] vex_default_justification (default vulnerable_code_not_in_execute_path).

The doc’s timestamp honors SOURCE_DATE_EPOCH, so --emit-vex output is byte-deterministic in CI when the env is set.

Configuration keys

[diff]
vex_author = "https://example.com/security"
vex_default_justification = "vulnerable_code_not_in_execute_path"

vex_author falls back to repo_url when unset; falls back to "bomdrift" when both are missing.

Justification vocabulary

bomdrift uses the OpenVEX 0.2.0 spec’s standard justification values verbatim: component_not_present, vulnerable_code_not_present, vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary, inline_mitigations_already_exist, plus the under_investigation-related justifications the spec defines. Richer justification vocabularies (per-organization tags, custom-reason strings, tool-specific extensions) are out of scope — authoring against a single canonical enum keeps --emit-vex output interoperable with any OpenVEX consumer. If the OpenVEX spec evolves to add new justifications, bomdrift follows the spec; non-spec justifications won’t be invented here.

Worked rotation example

  1. Run a diff that surfaces GHSA-evil on pkg:npm/foo@1.0.0.

  2. Investigate, conclude the vulnerable function is not on your execute path.

  3. Add the entry to .bomdrift/baseline.json with VEX status:

    {
      "schema_version": 1,
      "suppressed_advisories": [
        {
          "id": "GHSA-evil",
          "purl": "pkg:npm/foo@1.0.0",
          "expires": "2027-01-01",
          "reason": "Function is unreachable per audit (PR #123)",
          "vex_status": "not_affected",
          "vex_justification": "vulnerable_code_not_in_execute_path"
        }
      ]
    }
    
  4. Re-run with --emit-vex bomdrift.openvex.json to produce a publishable exploitability statement that downstream consumers can ingest with their own --vex flag.