Skip to content

VEX policy — Vulnerability Exploitability eXchange

How TLSStress.Art communicates "yes there's a CVE in this dependency, but it doesn't apply to us because…" in a machine-readable format that scanners (Trivy, Grype, OSV) can consume.

What VEX is

VEX (Vulnerability Exploitability eXchange) is a NIST + CISA- backed format for asserting the exploitability status of a known CVE against a specific product. Two formats are widely supported:

  • OpenVEX — JSON, minimal, human-readable
  • CSAF VEX — XML profile of the broader CSAF advisory format

We publish in OpenVEX. Trivy + Grype consume it natively as of v0.45+.

When we file a VEX statement

A VEX statement is appropriate when one of these is true:

Status Meaning
not_affected The vulnerable code path is unreachable in our usage (e.g. the function is dead code under our build flags)
affected The CVE applies and we don't have a fix yet (this is an advisory we publish to be transparent)
fixed The CVE applied to a prior release; we've patched in a release ≥ vX.Y.Z
under_investigation We've been notified and are still triaging

The first one — not_affected with a clear justification — is the most common. Trivy / Grype scanning the release-shipped SBOM will otherwise flag the CVE every time, drowning real signal in noise.

Required not_affected justifications

OpenVEX defines five canonical justifications. We use these verbatim — never invent new ones, because scanners only honour the canonical strings:

Justification When to use
component_not_present The dependency listed in the SBOM is not actually built into the shipping image (build-time-only dep)
vulnerable_code_not_present The CVE applies to a function / module / feature flag that we don't compile in
vulnerable_code_not_in_execute_path The vulnerable function is compiled in but no code path reaches it in our runtime
vulnerable_code_cannot_be_controlled_by_adversary The input that triggers the CVE cannot be reached by an external attacker (e.g. only our test fixtures hit it)
inline_mitigations_already_exist A WAF / NetworkPolicy / OPA gate / etc. prevents exploitation in our deployment

Pick the most specific one that's actually true. Auditors read these.

Where VEX documents live

docs/security/vex/
  ├── README.md
  ├── 2026-XX-XX-CVE-YYYY-NNNN.openvex.json
  └── ...

File naming: <advisory-date>-<CVE-id>.openvex.json. One file per CVE per product version we're attesting against. The docs/security/vex/ directory is populated lazily — when a real CVE drops, we add a file.

Each VEX file is also attached to the affected release(s) as a release asset, so a scanner pointing at the GitHub release page picks it up automatically.

VEX statement template

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://github.com/nollagluiz/AI_forSE/security/vex/2026-05-12-CVE-2026-XXXX",
  "author": "André Luiz Gallon <agallon@Cisco.com>",
  "timestamp": "2026-05-12T15:00:00.000Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": {
        "@id": "https://nvd.nist.gov/vuln/detail/CVE-2026-XXXX",
        "name": "CVE-2026-XXXX"
      },
      "products": [
        {
          "@id": "pkg:oci/web-agent-dashboard@sha256:XXXXXXXX?repository_url=ghcr.io/nollagluiz",
          "identifiers": {
            "purl": "pkg:oci/web-agent-dashboard@sha256:XXXXXXXX?repository_url=ghcr.io/nollagluiz"
          }
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "The vulnerable function `parseLegacyHeader` is exposed only when the HTTP/1.1 path is enabled. Our Caddy persona configuration forces `protocols h2 h3` (see ADR 0001 + docs/help-center/primers/), so the affected code is loaded but never reached by any agent or persona traffic."
    }
  ]
}

Process for filing a VEX

When a Trivy / Grype scan against a release SBOM flags a CVE:

  1. Triage the CVE — read the upstream advisory + the vulnerable function's source. Determine which OpenVEX status applies.
  2. Pick the justification — must be one of the canonical five for not_affected, or skip if affected / fixed / under_investigation.
  3. Write the impact_statement — plain English, one paragraph, citing the specific code path or config flag that makes the justification true. This is what an auditor reads first.
  4. Create the file at docs/security/vex/<date>-<cve>.openvex.json.
  5. Open a PR — uses the standard PR template; CODEOWNERS routes to the maintainer.
  6. Attach to releases — after merge, the PR author or maintainer attaches the JSON file to every affected release on GitHub's release-edit UI. The release-feed branch is also updated with a vex/ directory pointer.

Verifying VEX is consumed correctly

After publishing a VEX file, re-scan with Trivy in VEX mode:

trivy sbom dashboard-sbom.spdx.json \
  --vex docs/security/vex/2026-05-12-CVE-2026-XXXX.openvex.json

The CVE the VEX file references should now appear with status "not_affected" instead of "HIGH" / "CRITICAL". Confirm before shipping the VEX in production.

What VEX is NOT

  • ❌ Not a fix — the vulnerable code is still there; the VEX just documents why it doesn't matter in our usage
  • ❌ Not eternal — if our usage changes (e.g. we enable HTTP/1.1), the VEX becomes stale and must be revoked or amended
  • ❌ Not a substitute for a security advisory — if the CVE does apply, we file a GitHub Security Advisory + ship a fix per SECURITY.md, then file a status: fixed VEX for the release that contains the fix

Revoking or amending a VEX

If a VEX becomes incorrect (usage changed, new evidence emerged):

  1. Open a PR that either deletes the file or bumps its version field and updates status / justification.
  2. Mark the release-page asset as superseded.
  3. Add an entry to CHANGELOG.md under the release that bundles the correction.

Why this matters

For audit and valuation, the existence of a VEX policy says two things:

  1. We understand the difference between a CVE being present and being exploitable — many noncommercial projects don't
  2. We respond in a machine-readable way that downstream integrators can consume automatically

A scanner-clean release where every "present-but-not-exploitable" CVE has a paired VEX statement is materially less audit risk for a procurement reviewer than a scanner-noisy release with no VEX.


Last verified against shipping code: v3.7.0 (2026-05-12).