Skip to main content

VEX documents — export & import

A VEX (Vulnerability Exploitability eXchange) document records which CVEs actually affect your product. Inside the portal that record lives on each finding as its VEX status; this page covers moving it across the boundary as a standards document — exporting your triage for downstream consumers, and importing someone else's (or a previously exported) document back onto your findings.

Audience

Engineers exchanging triage with suppliers or downstream consumers. Export requires developer or higher; import is a bulk-triage action and requires team_admin.

Export a VEX document

Beyond the VEX state embedded in a CycloneDX SBOM, the portal can export a standalone VEX document built purely from the project's current finding triage. A VEX (Vulnerability Exploitability eXchange) document tells downstream consumers which CVEs actually affect your product — so a consumer can suppress the noise from CVEs you have already analyzed as not_affected or fixed.

Two formats are supported:

FormatQuery value (format=)MIMEUse case
OpenVEX 0.2.0openvexapplication/jsonThe minimal, vendor-neutral OpenVEX schema. Default.
CycloneDX 1.5 VEXcyclonedxapplication/jsonA CycloneDX BOM carrying only vulnerabilities[] + analysis — pairs with a CycloneDX SBOM.

The document is built from the latest succeeded scan's findings. A project with no succeeded scan (or no findings) still exports a valid, empty VEX document (HTTP 200) so downstream tooling can parse it.

Status mapping

Each internal VEX state maps to the target format's status vocabulary. The free-text justification you entered during triage is carried verbatim into a free-text field — it is never force-fit onto the OpenVEX justification enum (whose members have precise legal meaning the portal cannot infer from arbitrary analyst prose).

Portal stateOpenVEX statusCycloneDX analysis.state
Newunder_investigationin_triage
Analyzingunder_investigationin_triage
Exploitableaffectedexploitable
Not affectednot_affectednot_affected
False positivenot_affectedfalse_positive
Suppressednot_affectednot_affected
Fixedfixedresolved

The justification text lands in OpenVEX impact_statement and in CycloneDX analysis.detail.

Byte-stable output

Like the SBOM export, the VEX export is byte-stable: re-exporting the same scan produces identical bytes, so the document can be signed, cached, and diffed across releases. Statements are sorted by (CVE id, purl), the document id is derived deterministically from the scan id, and the timestamp reflects the scan's persisted completion time (not the moment of export).

Download from the API

Export the VEX document over the API:

# OpenVEX (default)
curl -sS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/vex?format=openvex"

# CycloneDX VEX
curl -sS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/vex?format=cyclonedx"

format accepts openvex or cyclonedx. The file name is vex-<project-slug>.<ext>.

StatusMeaning
200VEX document download.
401Not authenticated — supply a valid token.
404Project does not exist, or the caller is not a member of its team (existence-hidden, same posture as the SBOM export).
422Unknown format — use openvex or cyclonedx.
Access

Downloading the VEX document requires developer or higher. Cross-team callers receive 404, not 403, so a non-member cannot tell whether the project exists.

Import a VEX document (consume)

The portal can also import an external VEX document (OpenVEX or CycloneDX VEX) and auto-apply its statements to your findings, suppressing triage noise. This is the inverse of exporting a VEX document: export captures your triage as a standards document; import applies someone else's (or a previously-exported) document back onto your findings.

Typical uses:

  • A vendor or upstream maintainer publishes a VEX document saying a CVE is not affected in their package — import it instead of re-triaging by hand.
  • You exported a VEX document, edited it in another tool, and want the decisions back in the portal.
  • A CI step generated a VEX document you want to consume on the next sync.

Permissions

VEX import is a bulk-triage action — a single upload can transition many findings — so it requires team_admin within the project's team (the same bar as moving a finding into Suppressed). A developer who is a team member receives 403; a non-member receives 404 (existence-hidden, same posture as export).

How matching works

Each VEX statement is matched to a finding by vulnerability id (CVE/GHSA/OSV name) + component purl against your project's latest succeeded scan. A statement that resolves to no finding (the CVE isn't in this scan, or the purl doesn't match) is skipped with a reason — it never errors the whole import.

Status mapping (VEX → portal)

The import reverse-maps each VEX status to a single canonical portal state:

OpenVEX statusCycloneDX analysis.statePortal state
not_affectednot_affectedNot affected
false_positiveFalse positive
affectedexploitableExploitable
fixedresolvedFixed
under_investigationin_triageAnalyzing

under_investigation / in_triage map to Analyzing (not New): New is the discovery inbox and nothing transitions into it.

Import obeys the same VEX state machine as the manual workflow. Because every verdict routes through Analyzing, importing (say) not_affected onto a finding that is still New applies the legal two-step path New → Analyzing → Not affected automatically, and the audit log records both steps. The justification from the VEX document (impact_statement / analysis.detail) is preserved on the finding.

Idempotency & round-trip

Importing the same document twice is safe: a finding already in the target state is skipped (already_at_target), not re-written. Exporting your triage and immediately re-importing it is a no-op — the portal's export/import round-trip is status-stable.

Import from the API

curl -sS -X POST \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
-F "upload=@vex.openvex.json;type=application/json" \
"https://trustedoss.example.com/v1/projects/${PROJECT_ID}/vex/import"

The response is a JSON summary:

{
"format": "openvex",
"matched": 12,
"applied": 9,
"skipped": 3,
"errors": [
{
"vulnerability": "CVE-2024-0001",
"product": "pkg:npm/left-pad@1.0.0",
"reason": "unknown_component",
"detail": "CVE-2024-0001 has no finding on pkg:npm/left-pad@1.0.0 in the latest scan"
}
]
}
  • matched — findings a statement resolved to.
  • applied — findings whose status actually changed.
  • skipped — findings/statements deliberately not applied (no-op, unknown vuln/purl, …).
  • errors[].reason — one of unknown_vulnerability, unknown_component, ambiguous_match, unmapped_status, illegal_transition, already_at_target, forbidden_transition, malformed_statement.
StatusMeaning
200Import ran — see the summary (even when 0 applied).
401Not authenticated.
403Authenticated, member of the team, but not team_admin.
404Project missing or caller not a team member (existence-hidden).
413The uploaded document exceeds the size limit (VEX_IMPORT_MAX_BYTES, default 8 MiB).
422The document is not valid JSON, or is neither OpenVEX nor CycloneDX VEX. Body is application/problem+json.

VEX in the UI

Everything above is also available without the API, from the Vulnerabilities tab toolbar.

Export and import buttons

  • Export VEX — two buttons, OpenVEX and CycloneDX VEX. Click either to download the project's current triage as a standalone VEX document. The download goes through your authenticated session (the token never appears in the URL), the same as the SBOM and PDF report downloads. Export is a read, so any developer (or higher) can use it.
  • Import VEX — opens a dialog where you choose an OpenVEX or CycloneDX VEX JSON file and upload it. The format is auto-detected. After the import runs the dialog shows a summary panel with three counts — Matched (findings a statement resolved to), Applied (findings whose status actually changed), and Skipped — plus a per-statement list of skip reasons for anything that did not apply (unknown CVE/component, illegal transition, already at target, …). Import is a bulk-triage action: the button is only enabled for team_admin (and super_admin). A developer sees it disabled with a tooltip explaining the requirement. A 403, 413, or 422 from the server is shown inline as a plain-language message — the dialog never leaves you guessing.

Filter: VEX-suppressed only

The toolbar has a VEX-suppressed only checkbox. Turn it on to keep only the findings on the current page whose status was set by a VEX import (analysis_source = vex_import) — handy for eyeballing exactly what a document you just imported changed. The toggle is mirrored into the URL (?vex_suppressed=1) so it survives a reload and can be shared as a deep link. Rows set by a VEX import also carry a small VEX badge next to their status, paired with the label (not color alone) so the provenance is visible at a glance.

Provenance badge in the drawer

Open a finding whose status came from an import and the drawer shows a VEX provenance panel: the consuming document's author, id (@id / serialNumber), timestamp, the VEX status the matching statement carried, when the import ran, and the imported justification. All of these fields come from the uploaded document and are rendered strictly as text — the portal never interprets them as HTML, so a justification or author containing markup is shown verbatim and is inert.