CycloneDX VEX: Evidence-Based Exploitability Is Not a Security Waiver
Interpret CycloneDX VEX with scoped component references, accurate analysis states, justified conclusions, authenticated evidence, and fail-closed policy.
A dependency scanner can identify a component associated with an advisory without determining whether the vulnerable behavior is reachable in a particular deployed product. Vulnerability Exploitability eXchange, or VEX, communicates that contextual assessment. It should make a security decision more accountable, not provide a machine-readable way to hide inconvenient findings.
This article uses CycloneDX’s official VEX description and the explicit JSON schema for specification version 1.6, reviewed on October 11, 2026. The examples deliberately target that version rather than claim it is the latest schema. Pin the version supported by your producer, validator, and consumer; validate a different version against its own contract.
Keep inventory, advisory, assessment, and policy separate
An inventory identifies components. An advisory describes a vulnerability. An assessment explains whether that vulnerability affects a specific product or component context. Deployment policy decides what evidence and residual risk are acceptable for a release.
Those stages can be connected but must not be collapsed. Finding a package in an SBOM does not prove that every advisory for a similarly named package applies. A VEX document saying not_affected does not authenticate its issuer. An authenticated supplier assessment does not necessarily cover your modified build or runtime configuration.
Record the artifact identity, component version, advisory identifier, assessment issuer, analysis context, and applicable policy. Prefer immutable release identifiers and content digests in the surrounding evidence chain over a moving label such as “latest.” The VEX record then contributes a scoped conclusion, not an unbounded exception for a package name.
Use analysis states precisely
CycloneDX 1.6 distinguishes six analysis states. in_triage means investigation is ongoing. exploitable means the vulnerability may be directly or indirectly exploitable. false_positive means the vulnerability was wrongly identified or associated with the component or service. not_affected means the component or service is not affected, with justification expected for that conclusion.
resolved means the vulnerability has been remediated. resolved_with_pedigree additionally provides evidence of the changes in the affected component’s pedigree, such as verifiable commit history or differences. A plan to patch next month is not remediation, and an accepted risk is not a false positive.
Distinguish misidentification from non-exploitability. If the scanner matched the wrong product, false_positive can describe that association failure. If the vulnerable component is genuinely present but the product is not affected under documented conditions, that is a different assessment. Choosing whichever state silences a tool more easily corrupts the record.
Bind the affected object through the BOM reference
Within the version 1.6 schema, a vulnerability’s affects entries reference a component or service through ref, typically matching that object’s bom-ref. BOM-Link can support references outside the local document, but a consumer must implement that reference form before relying on it.
The following JSON uses a fictional component and a fictional advisory identifier. It makes no statement about any real vulnerability and intentionally remains in_triage:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": [
{
"type": "library",
"bom-ref": "example-parser-1",
"name": "example-parser",
"version": "1.0.0"
}
],
"vulnerabilities": [
{
"id": "EXAMPLE-001",
"analysis": {
"state": "in_triage",
"detail": "Illustrative record only: runtime reachability is not yet assessed."
},
"affects": [
{ "ref": "example-parser-1" }
]
}
]
}
Schema validation checks the document’s structure, not whether the assessment is true. A separate semantic check should ensure a local reference resolves to exactly one intended object. A typo in ref must not cause a consumer to interpret the assessment as applying to every component or to ignore a remaining finding.
A justification needs an evidence trail
The schema includes justifications such as code_not_present, code_not_reachable, requires_configuration, requires_dependency, and requires_environment. It also includes compiler, runtime, perimeter, and other mitigating-control explanations. These categories summarize reasoning; they do not supply the reasoning themselves.
For code_not_present, record how the delivered artifact excludes the vulnerable implementation and how that exclusion was verified. For code_not_reachable, identify the relevant entry points, feature configuration, and limits of the reachability analysis. A test suite that never triggered a vulnerable path is not necessarily proof that an attacker cannot reach it.
A conclusion dependent on configuration needs that configuration in scope. Changing a feature flag, dependency, network exposure, or execution environment can invalidate the assessment without changing the component version. Build a reassessment trigger around those changes.
The detail field is the place to explain impact and assessment methods. Keep supporting reports, source revisions, build identities, and reviewer approval available through your evidence system. A bare state and a vague comment such as “not used” are insufficient for a demanding release gate.
Response intent is not exploitability state
The analysis response array can contain can_not_fix, will_not_fix, update, rollback, or workaround_available. More than one response is allowed. These values describe a supplier or project’s response to a vulnerability; they do not mean the same thing as the analysis state.
In particular, will_not_fix does not imply not_affected. It can describe an exploitable problem for which the supplier declines to provide a change. Likewise, workaround_available identifies the existence of a mitigation path, not proof that your deployment has applied and verified it.
If an organization explicitly accepts residual risk, record that approval, scope, owner, duration, and conditions separately. Do not rewrite the technical assessment to make the acceptance look like a patch. A scan gate may have an authorized exception policy, but its output should remain truthful about the unresolved finding.
Review recency without inventing expiry semantics
CycloneDX 1.6 provides firstIssued and lastUpdated timestamps for analysis. Those fields can help consumers understand when an assessment was issued or revised. They are not, by themselves, an automatic expiry rule or a guarantee that the document describes the latest artifact.
Set a local review policy for assessment age and context changes. Authenticate the evidence before treating its timestamps as trusted. A current date on an old conclusion should not pass a gate unless the conclusion was actually reevaluated for the required release context.
Keep prior versions for auditability. When a new assessment replaces an old one, retain enough information to explain why the state changed, who authorized it, and which artifact versions were covered. Silent overwriting makes regression investigation needlessly difficult.
Separate schema checks from release authorization
Validate the example against the official version 1.6 JSON schema, using a validator that supports the schema dialect and referenced definitions. Pin or vendor official schemas through your ordinary dependency-integrity process instead of downloading an arbitrary schema at each production gate.
After structural validation, check uniqueness of local bom-ref values and resolution of every applicable affects.ref. Then authenticate the producer, compare artifact and component context, verify the supported advisory namespace, and evaluate the evidence required for the asserted state.
Unknown or pending conclusions should remain unknown or pending under policy. Do not translate a missing analysis.state to approval. If a document contains additional fields, follow the schema and consumer compatibility rules for its declared version rather than inventing behavior from another format’s VEX terminology.
Test policy with adversarial but realistic fixtures
Create test documents for a valid pending assessment, an unsupported state, a dangling local reference, duplicate component identifiers, a correct reference to the wrong build context, and a will_not_fix response accompanying an exploitable state. The last case must not be converted into remediation by a convenient response-to-state mapping.
Also test a not_affected conclusion with absent supporting evidence, an expired local approval, an unauthorized issuer, and a configuration change that invalidates an earlier reachability assessment. Those are policy tests, not solely JSON-schema tests.
When a gate rejects an assessment, preserve its decision reason and evidence. Repair the document, reference binding, trust configuration, or actual product condition as appropriate. Do not suppress the scanner or broaden a waiver just to obtain a green release.
VEX is valuable when it explains why a finding does or does not affect the exact product under review. The production-grade outcome is traceable analysis and explicit residual risk, not merely fewer red entries in a dashboard.
Related:
- Container Image Vulnerabilities: Scanning, CVEs, and Supply Chain Risk
- in-toto Statements: Bind Verified Claims to the Artifact You Actually Deploy
Sources: