Generate your first VEX document¶
In this tutorial, we’ll turn two committed example files into CycloneDX 1.6 VEX JSON. The generation step uses only local inputs, so no package inventory leaves the repository.
We work from a source checkout here so that you get the same inputs and the same output we describe. To run Vexcalibur against your own SBOM instead, install a release and follow the CycloneDX generation guide.
Set up Vexcalibur¶
You need:
Git.
Python 3.10 or newer.
uv sync --frozenprovisions the interpreter pinned in.python-version(currently 3.14) and downloads it if your system lacks it.uv.A POSIX-style shell.
Clone the source and enter its root:
git clone https://github.com/vexcalibur-dev/vexcalibur.git
cd vexcalibur
The checkout’s .tool-versions records the tested uv version. Activate that
version with your version manager before you continue.
Install the locked dependencies:
uv sync --frozen
This setup step may contact your configured package index. Once the dependencies are installed, the rest of the tutorial does not need a network finding source.
Check that the command starts:
uv run --frozen vexcalibur --help
You should see the query-osv and generate commands.
Generate the document¶
Run Vexcalibur with the example SBOM and findings file:
uv run --frozen vexcalibur generate \
tests/fixtures/sbom/cyclonedx-json-simple.json \
--offline \
--findings-file tests/fixtures/findings/all-analysis-states.json \
--timestamp 2026-06-23T00:00:00Z \
--output /tmp/vexcalibur-vex.json
The command should exit without output and create /tmp/vexcalibur-vex.json.
Inspect the result¶
Print the first part of the document:
uv run --frozen python -m json.tool /tmp/vexcalibur-vex.json | sed -n '1,80p'
Now check the fields this tutorial expects:
uv run --frozen python - <<'PY'
import json
from pathlib import Path
vex = json.loads(Path("/tmp/vexcalibur-vex.json").read_text())
assert vex["bomFormat"] == "CycloneDX"
assert vex["specVersion"] == "1.6"
assert vex["metadata"]["timestamp"] == "2026-06-23T00:00:00+00:00"
assert len(vex["vulnerabilities"]) == 5
print("generated offline CycloneDX VEX")
PY
You should see generated offline CycloneDX VEX.
Vexcalibur read component identities from the SBOM. It matched the local findings to those components, then rendered the result. --offline prevented network finding sources. The fixed timestamp made the metadata and generated identifiers stable.
Keep going¶
Next, write your own local findings file. When you need a network source, use the generation how-to and choose either a private OSV mirror or an explicitly approved public OSV query.