SBOM and VEX
Generate SBOM and VEX documents offline.
An inventory of everything you ship, and a statement of which advisories actually reach your code. Both are built on your machine, and their contents never leave it.
CycloneDX 1.6 and 1.7 · SPDX 2.3 and 3.0.1 · OpenVEX 0.2.0
- 01 · readYour lockfilesThe versions they resolved to, not the ranges declared.npmPyPIGo modulescrates.ioMavenRubyGemsPackagistNuGet
- 02 · inventorySBOMEvery component, its version, its package URL and its licence.CycloneDX 1.6SPDX 2.3CBOM
- 03 · reachabilityDoes your code call it?From each entry point to the function an advisory names.
- 04 · statementVEXOne verdict per component and advisory.affectednot_affectedunder_investigation
01SBOM formats
An inventory of what you ship.
When an advisory lands, the question is whether you ship that component, at that version, anywhere. A current SBOM makes it a lookup, in the format whoever asked for it reads.
The sbom reference- CycloneDX SBOM1.6 default · 1.7 opt-inOne entry per resolved package, with matched advisories in the same document.
- SPDX SBOM2.3 default · 3.0.1 opt-inThe same inventory in the SPDX shape, for a pipeline that reads SPDX.
- CBOMCycloneDX 1.6 · 1.7 opt-inWeak cryptography in your own code, each with the file and line it was seen at.
$ vulkro sbom . --format cyclonedx{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{
"type": "library",
"name": "lodash",
"version": "4.17.20",
"purl": "pkg:npm/lodash@4.17.20",
"licenses": [ { "license": { "id": "MIT" } } ],
"scope": "required"
}
]
}A licence that could not be resolved is written as unknown, never guessed. A dependency declared in a format the scanner does not read is reported as unread, not as clean. These sit in a catalogue of 25 output formats in all.
02VEX
“Not affected”, only when your code cannot reach it.
A VEX tells a reviewer which advisories actually reach your code. Vulkro writes not affected only when no path from your code reaches the vulnerable function, and never when it is unsure.
How reachability works- reachableaffected
Your code calls the vulnerable function from a real entry point. Severity is kept, and the statement cites the call chain.
- unreachablenot_affected
The advisory names its vulnerable functions and nothing your code runs calls one. The row stays in the report, ranked down.
- unknownunder_investigation
The advisory does not say which functions are vulnerable, the common case. No claim is made either way.
$ vulkro sbom . --format openvex{
"@context": "https://openvex.dev/ns/v0.2.0",
"statements": [
{
"vulnerability": { "name": "CVE-2023-29401" },
"products": [ { "@id": "pkg:golang/github.com/gin-gonic/gin@1.7.7" } ],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
},
{
"vulnerability": { "name": "CVE-2021-23337" },
"products": [ { "@id": "pkg:npm/lodash@4.17.20" } ],
"status": "affected",
"status_notes": "Reachability evidence for lodash: call chain: POST /invoices (routes/invoice.ts:12) -> buildInvoice (routes/invoice.ts:47) ..."
}
]
}Written as OpenVEX 0.2.0 or CycloneDX VEX 1.6. Where signals conflict, affected wins. The reachability check is on by default and only labels: it never changes a finding's severity, so the scan and the SBOM agree on the grade. VULKRO_SCA_REACHABLE=0 turns it off. An affected statement is static reachability, not a confirmed exploit. Eight manifest formats are parsed. The default published bundle currently ships npm and PyPI; the wider signed bundle covers the other six.
03The evidence pack
One zip a reviewer can open.
The SBOM and the VEX are usually part of a larger request. The pack answers it as files: both SBOMs, the VEX, and every finding mapped to the controls of a framework.
- Traceable
- Each control file cites the findings it was built from, down to a file and a line.
- Honest about itself
- Its own front page says what it is: readiness evidence, not a conformance attestation.
Asked about the EU Cyber Resilience Act? The bundle reference lists the frameworks it can be built against.
- cra-readiness.zip
- index.htmlreadiness one-pager
- compliance/
- manifest.jsonframework, scan id, control summary
- summary.mdevery control with its status
- findings.csvfinding to control mapping, flat
- controls/one JSON file per control
- sbom/
- cyclonedx.jsonCycloneDX 1.6
- spdx.jsonSPDX 2.3
- vex/
- openvex.jsonOpenVEX 0.2.0
04Generating them
Four steps, in the order you run them.
Generate the inventory once, match it again as advisories land, and package the full set when someone asks for it.
# the inventory, in whichever shape was asked for $ vulkro sbom . --format cyclonedx > sbom.cdx.json $ vulkro sbom . --format spdx > sbom.spdx.json $ vulkro sbom . --format cbom > cbom.json # the VEX, backed by reachability (on by default) $ vulkro sbom . --format openvex > vex.json # re-match an SBOM you already have, as advisories land $ vulkro match-cve --sbom sbom.cdx.json --format table # SBOM + VEX + evidence pack, in one zip $ vulkro cra-bundle . --framework iso27001 -o cra-readiness.zip
Read next
- Every flag on the generatorFormats, spec versions, and what happens when a lockfile is missing.Reference
- Re-matching an SBOM laterMatch it again as advisories land, without a full rescan.Guide
- Running with no networkCarrying the signed advisory archive in by hand.Guide
- The compliance mappingsWhich control each finding maps to, and why.Reference