For engineers
Buyers asking for VEX? CycloneDX, OpenVEX and CSAF compared
By Manish SharmaPublished 7 min read
When a vulnerability makes the news, buyers ask whether your product is affected. CycloneDX, OpenVEX and CSAF all say the same thing, that a product is affected, not affected, fixed or under investigation for a vulnerability. But they name the product, the vulnerability and the reason differently. Map the four statuses one to one, and carry any justification label the target format lacks as text instead of guessing. Identify the product so the customer's tools can match it, and keep non-CVE identifiers out of fields that accept only CVEs.
In short
- CycloneDX, OpenVEX and CSAF all say the same thing: a product is affected, not affected, fixed or under investigation for a vulnerability. The four statuses map one to one.
- They differ in how they name the product and the vulnerability, and in their justification labels. Every problem we hit in our own exports was in those details.
- Never invent a justification label the target format lacks; carry it as text. Name the product so the customer's tools can match it.
Same sentence, different details
All three formats carry the same sentence: this product, for this vulnerability, has this status, for this reason. The four statuses map one to one. What differs is how each names the product and the vulnerability, which justification labels it has and what it insists on. In our own export work, every problem we hit was in those details, not in the status.
This post sets the formats side by side, gives the mappings we use in our own exports and lists the mistakes that cost us time.
What a VEX statement must say
CISA's Minimum Requirements for VEX (April 2023) describe a VEX document as a set of statements. Each ties a product and a vulnerability to one of four statuses: not affected, affected, fixed or under investigation. A not affected statement needs a justification or an impact statement, and an affected one needs an action statement.
CISA's VEX Status Justifications (June 2022) define five machine-readable reasons for not affected:
component_not_presentvulnerable_code_not_presentvulnerable_code_not_in_execute_pathvulnerable_code_cannot_be_controlled_by_adversaryinline_mitigations_already_exist
CERT-In's SBOM guidelines (version 2.0, July 2025) ask suppliers for the same four statuses, and recommend a CSAF advisory after the VEX.
The three formats side by side
| CycloneDX | OpenVEX | CSAF | |
|---|---|---|---|
| Steward and version | OWASP and Ecma TC54; 1.6 is what we export, 1.7 the latest release | OpenVEX project; v0.2.0 | OASIS; 2.0 is the standard, 2.1 a committee draft |
| Where the status lives | vulnerabilities[].analysis.state | statements[].status | vulnerabilities[].product_status lists of product ids |
| How the product is named | affects[].ref: a bom-ref in the same BOM, or a BOM-Link to another | products[].@id: an IRI, usually a purl, with optional subcomponents | product_id, a token local to the document, defined in product_tree with optional identification helpers |
| How the vulnerability is named | id with a source (name and URL) | vulnerability.name with aliases | cve (CVE format only) or ids with system_name and text |
| Not affected needs | A justification "should be specified" | A justification label or an impact statement (MUST) | A flag or a threat of category impact (VEX profile) |
| Affected needs | Nothing mandatory; response and recommendation available | An action statement (MUST) | A remediation as action statement (VEX profile) |
| Granularity | Per component | Per statement, usually per product | Per product id |
Mapping the statuses
The statuses map cleanly, and these are the values we write.
| Status | CycloneDX analysis.state | OpenVEX status | CSAF product_status |
|---|---|---|---|
| Not affected | not_affected | not_affected | known_not_affected |
| Affected | exploitable | affected | known_affected |
| Fixed | resolved | fixed | fixed |
| Under investigation | in_triage | under_investigation | under_investigation |
CycloneDX has two more states. resolved_with_pedigree is a fixed with evidence of the change, and maps to fixed. false_positive says the vulnerability was wrongly associated with the component. The other formats have no word for it, so if you use it, carry the reason as text in a not affected statement.
Mapping the justifications
The justifications do not map cleanly. OpenVEX and CSAF flags share CISA's five labels. CycloneDX has nine of its own, and only two have exact twins.
| CycloneDX justification | CISA label (OpenVEX, CSAF flag) | How we export it |
|---|---|---|
code_not_present | vulnerable_code_not_present | Label to label, both ways |
code_not_reachable | vulnerable_code_not_in_execute_path | Label to label, both ways |
requires_configuration, requires_dependency, requires_environment | No exact twin | Kept as text in the impact statement |
protected_by_compiler, protected_at_runtime, protected_at_perimeter | No exact twin | Kept as text in the impact statement |
code_not_present | Also written for component_not_present | From CISA labels into CycloneDX only |
protected_by_mitigating_control | Written for inline_mitigations_already_exist and vulnerable_code_cannot_be_controlled_by_adversary | From CISA labels into CycloneDX only |
Going into CycloneDX, three CISA labels land on CycloneDX's broader neighbours. Coming out of CycloneDX, we never invent a CISA label: a justification with no exact twin travels as words in the impact statement.
A customer's policy engine may act on vulnerable_code_cannot_be_controlled_by_adversary very differently from requires_environment. Quietly turning one into the other changes your statement.
The mistakes we hit
Product identifiers that nobody can match
A firmware release is not a package, so it has no natural purl. Each format handles that differently, and each has a trap.
In CycloneDX, a VEX entry points at a bom-ref. If the VEX sits in a separate file from the SBOM, a bare bom-ref points at nothing. Use a BOM-Link (urn:cdx: with the SBOM's serial number, version and the reference) so the consumer can join the two.
In OpenVEX, @id should be something the customer can look up. A purl works for a package, but for a firmware product, use an IRI you control and keep it stable across releases.
In CSAF, product_id is only a token inside the document, and CSAFPID-0001 means nothing to an asset database. The product_identification_helper is what a customer matches on: CPE, purl, hashes, model numbers, SKUs, serial numbers or SBOM URLs. Our own CSAF export names the product and the release. If your customers match devices by model number or image hash, add those helpers before you publish.
Identifiers that are not CVEs
Plenty of vulnerabilities in open source packages carry a GitHub, OSV or ecosystem identifier first and a CVE later, or never. CSAF's cve field accepts only the CVE pattern, and a document that puts a GHSA identifier there fails validation. Such identifiers belong in ids with a system_name.
CycloneDX takes any id but needs a source so the reader knows where to look, and we write NVD for CVEs and OSV for the rest. OpenVEX takes any name and lists the others under aliases. When a CVE exists, make it the main name, because that is what most customers' scanners match.
One vulnerability, two components, two statuses
CycloneDX states exploitability per component, so the same CVE can be not affected in one library and affected in another inside the same image.
OpenVEX and CSAF statements in our exports speak for the whole product, and CSAF's mandatory test 6.1.23 forbids one CVE in two vulnerability items. So the product needs one status per vulnerability. We take the most urgent: affected, then under investigation, then fixed, then not affected. Taking the least urgent would be the dangerous mistake.
Statements missing their required parts
The commonest validation failure is a not affected statement with neither a label nor an impact statement, followed by an affected statement with no action. CSAF's VEX profile tests both. Its mandatory tests in section 6.1.27 check for an impact statement on every known not affected product, and an action statement on every known affected one.
Write the impact statement even when you have a label. A sentence a person wrote is what a customer's security reviewer reads.
A CSAF draft that says it is final
CSAF's test 6.1.17 requires the tracking status to be draft when the document version is 0 or a pre-release. Publish version 1 or later as final, and keep the revision history in step with every change.
Status that never moves
Under investigation is a promise. A VEX with statements stuck there for months tells the customer nobody looked. Version your VEX documents (OpenVEX's version, CSAF's revision history, CycloneDX's BOM version) and reissue them when the status changes.
A short checklist before you publish
- Every product named in a way the customer can match: purl, CPE, model number, SKU or hash.
- Every vulnerability under its CVE when one exists, with the other identifiers as aliases.
- One status per vulnerability per product, the most urgent when components disagree.
- Every not affected with a label the format really has, or words; every affected with an action.
- The document validated against its schema, and for CSAF against the VEX profile tests.
- A version number and a date, and a plan to revisit everything under investigation.
Writing VEX once, exporting it three ways
In FDIE by Magdox, your people set the VEX status and justification for each vulnerability in a release, and the app never decides them. From that one record it exports CycloneDX, OpenVEX and CSAF 2.0, with the mappings above, so the three files cannot disagree with each other.

Questions
What is the difference between VEX and an SBOM?
An SBOM lists the components in a product. A VEX states, for a given vulnerability, whether the product is affected, not affected, fixed or under investigation, and why.
Which VEX format should I use?
Use what your customers' tools read. CycloneDX VEX fits teams exchanging CycloneDX SBOMs, OpenVEX is small and easy to generate, and CSAF 2.0 is the OASIS standard for security advisories that many vendors and CERTs publish. Exporting all three from one source of truth avoids the choice.
Can a CSAF document describe a vulnerability that has no CVE?
Yes. The cve field accepts only identifiers of the form CVE-year-number. Anything else, such as a GitHub or OSV identifier, goes in the ids list with a system name and the identifier text.
What must a not affected statement include?
OpenVEX requires a justification label or an impact statement. CSAF's VEX profile requires a flag or a threat of category impact for every known not affected product. CycloneDX says a justification should be given for every not_affected case.
Who should write VEX statements?
The people responsible for the product. A VEX is the manufacturer's word to its customers about exploitability, so tools can propose evidence, but a person should set the status.
Sources
- CISA, Minimum Requirements for Vulnerability Exploitability eXchange (VEX), April 2023 cisa.gov
- CISA, VEX Status Justifications, June 2022 (PDF) cisa.gov
- OpenVEX specification v0.2.0 github.com
- CycloneDX specification overview cyclonedx.org
- CycloneDX 1.6 JSON reference cyclonedx.org
- OASIS Common Security Advisory Framework (CSAF) Version 2.0, OASIS Standard, 18 November 2022 docs.oasis-open.org
- OASIS CSAF Version 2.1, Committee Specification Draft 03, 11 September 2026 docs.oasis-open.org
- CERT-In Technical Guidelines on SBOM, QBOM and CBOM, AIBOM and HBOM, version 2.0, 9 July 2025 (PDF) cert-in.org.in
- Package URL (purl) specification github.com
- OSV schema ossf.github.io
Product and project names are trademarks of their owners, used only to identify the public firmware, software and standards discussed. MAGDOX is not affiliated with or endorsed by them.