Generation execution report¶
vexcalibur generate can write a versioned JSON report for downstream
automation on Linux and macOS. The report describes what Vexcalibur processed
without repeating package names and URLs, vulnerability IDs, repository names,
filesystem paths, provider URLs, credentials, or exception text.
Counts, source categories, the package version, and a document digest can still be sensitive in some environments. Apply the same access policy you use for other build metadata.
Request the report with --execution-report PATH:
work_dir="$(mktemp -d)"
trap 'rm -rf -- "$work_dir"' EXIT
vexcalibur generate \
sbom.json \
--findings-file findings.json \
--offline \
--output "$work_dir/vex.json" \
--execution-report "$work_dir/execution-report.json"
This example creates a private temporary directory and removes it when the
shell exits. --execution-report needs v0.6.0 or later. For the complete
setup and validation sequence, including the schema validator that ships in a
source checkout, see the
consumption guide.
Success writes both files and exits with status 0. The report describes the
exact UTF-8 bytes in vex.json.
Schema¶
Schema version 1 has this structure:
{
"analysis_state_counts": {
"in_triage": 4
},
"command": "generate",
"component_count": 122,
"document": {
"bytes": 824,
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
},
"finding_count": 4,
"finding_source": "local_file",
"inventory_source": "sbom_file",
"output_format": "cyclonedx",
"schema_version": 1,
"vexcalibur_version": "EXAMPLE"
}
EXAMPLE stands for the installed package version. Vexcalibur reads that
value from Python package metadata; the source tree does not contain a release
version constant. In an editable Git checkout, Vexcalibur also requires the
generated commit identifier to match the checkout’s HEAD. The all-zero digest
is a placeholder; a real report contains the SHA-256 of its generated
document.
The file is canonical minified JSON with one trailing newline. Object key
order is stable, but consumers should use JSON keys rather than byte positions.
The JSON Schema is the structural
machine-readable contract. It uses JSON Schema Draft 2020-12 and rejects
unknown properties. JSON Schema treats a number such as 1.0 as an integer
when it has no fractional part, but Vexcalibur’s canonical report contract
requires an integer JSON token. Use
parse_generation_execution_report or the tested
consumer validator to apply those
exact-type checks before schema validation.
Field |
Type |
Meaning |
|---|---|---|
|
exact integer token |
Exactly |
|
string |
Exactly |
|
string |
Installed Vexcalibur distribution version loaded by the process. It must match package metadata; an editable Git checkout must also identify its current |
|
string |
One inventory category from the table below. |
|
string |
One finding category from the table below. |
|
string |
|
|
exact integer token from 0 through 10,000,000 |
Normalized components sent to the finding source. |
|
exact integer token from 0 through 10,000,000 |
Normalized findings sent to the renderer. |
|
object |
Exact positive integer tokens through 10,000,000, keyed by |
|
string |
64-character lowercase hexadecimal SHA-256 digest of the exact rendered UTF-8 document. |
|
exact integer token from 0 through 26,214,400 |
Length of the exact rendered document in UTF-8 bytes. The maximum is the 25 MiB generation limit. |
component_count is not the number of raw entries in an SBOM.
finding_count is not a severity threshold, policy decision, or proof that the
inventory is safe. A zero-finding report says only that the selected source
returned no normalized findings for this operation.
The sum of analysis_state_counts always equals finding_count. With no
findings, the object is empty.
Source categories¶
inventory_source uses one of these values:
Value |
Input |
|---|---|
|
Local CycloneDX JSON or XML, or SPDX 3 JSON-LD |
|
GitHub Dependency Graph SBOM API |
|
Inventory supplied by an embedding through the Python API |
finding_source uses one of these values:
Value |
Input |
|---|---|
|
|
|
The canonical |
|
A caller-selected, noncanonical OSV-compatible endpoint, whether privately or publicly reachable |
|
Another finding source classified by an embedding through the Python API |
The categories distinguish the official public OSV service from a caller-chosen
endpoint without exposing the source path, repository, or service URL. A
non-root-equivalent path or nonstandard port on the api.osv.dev host is
custom_osv, not public_osv. OSV endpoint URLs with queries or fragments are
invalid and fail before generation.
The CLI emits only its concrete inventory, finding, and output categories. A
Python embedding that injects an OSV client records custom, even when that
client contacts an OSV-compatible endpoint. Reserved OSV categories describe
only the exact built-in client selected by Vexcalibur. Other custom sources and
renderers also use custom. The value records the extension boundary without
pretending the extension is CycloneDX, OSV, or a built-in VEX format.
Write behavior¶
Vexcalibur removes a stale report before generation. When the command parser
rejects an unknown option or missing input before the generate callback
starts, recovery uses the group and command Click parsers to identify the
--execution-report and --output values. Parsed path parameters and
unconsumed operands remain protected as possible inputs. Vexcalibur then runs
the same path-binding and alias checks used by a successful parse. It removes
the candidate only when those checks prove that the path is not an input, VEX
output, or redirected standard stream.
After the callback starts, only the prepared transaction may remove a report. Its rollback guard checks the published file identity, so a later writer’s replacement remains in place. Parser recovery does not rebind the report path after a runtime failure.
Help and completion exit successfully, so they leave the candidate path unchanged. An unsafe or invalid report destination also remains unchanged; the command cannot remove a path that it cannot bind safely. Once parsing succeeds, the prepared transaction removes an existing report before it validates timestamps, source combinations, or document metadata.
Stale-report cleanup briefly takes the report directory lock while Vexcalibur prepares the destination. The locks that coordinate the document and report replacements begin during commit, after generation. The replacements are individually atomic, but the pair is not an atomic two-file commit. A failure after Vexcalibur replaces the document can leave that document in place without a report.
Another process can publish a newer report after the failed transaction removes the old one. It can also replace a successful report after this command exits. Give each job its own output directory when consumers must retain a one-to-one mapping between a VEX document and its report.
After generation, Vexcalibur publishes in this order:
Construct and validate the complete report.
Stage the report and any file-based VEX output in private mode-
0600files, then flush them.Keep the staged files open, verify their filesystem identities, and acquire destination-directory locks in stable order.
Remove any report created after the initial cleanup. If removal fails, publish nothing.
Publish the VEX document and check the destination paths again for aliases.
Publish the report last, then release the locks.
Standard output follows a related sequence, but its bytes cannot be rolled back after a partial write. Vexcalibur first takes a lock for the report name. While holding that lock, it briefly takes the directory lock and removes an intervening report. If cleanup fails, standard output remains unchanged.
Vexcalibur releases the directory lock before it writes and flushes standard output. It then takes the directory lock again, removes any report created by a process that didn’t use the sequence lock, and publishes the new report. The per-report lock remains held through the complete sequence. A blocked stream therefore serializes another writer for the same report path without blocking a different report path in that directory.
Each atomic replacement is followed by a directory fsync. A failed file or
directory flush makes the command fail. Vexcalibur removes an unpublished
temporary file when it can, but callers must not treat leftover private
temporary files as completed reports.
If Python handles an interruption, including SIGINT, while the rollback guard
still has complete removal authority, Vexcalibur removes the report and the
document may remain. A concurrent replacement at the report path is left alone.
Finalization then crosses an explicit point of no return as it releases the
guard’s descriptors. A cleanup failure after that point does not turn a valid
publication into a failed command beside a retained success marker. The
transaction remains in its internal FINALIZING state so cleanup can be
retried, while the CLI completes successfully. Consumers must still require
exit status 0 and validate the report against the document.
Abrupt termination does not run that cleanup. SIGKILL always stops the
process immediately, and the default SIGTERM handler also bypasses Python
unwinding. Either signal can leave a report that was already published even
though the process did not exit successfully. Consumers must require exit
status 0 and validate the report against the document.
A parent-directory change stops the operation instead of redirecting either
write. The parent must already exist and must be readable, writable, and
searchable by the process. Its filesystem must support descriptor-relative
operations, advisory flock, and fsync.
Each destination parent contains a persistent .vexcalibur-locks directory.
Vexcalibur requires that directory to be owned by the current user and sets its
mode to 0700. One directory.lock file coordinates every report-aware
publication in that parent. Standard-output transactions also use a
lock whose name starts with stdout-, continues with the 64-character
lowercase SHA-256 digest of the report leaf name, and ends with .lock. Writers
for that report therefore keep their stream and report order.
Vexcalibur requires each lock to be a regular file owned by the current user
with one link, and sets its mode to 0600. The fixed directory lock name gives
processes the same publication point even when they use different filesystem
encodings.
Do not use a destination parent that another user can write. A different user can create the fixed coordination directory first and prevent publication. Directly shared multi-user output directories are not supported.
Vexcalibur retries a contended directory or per-report sequence lock for up to
10 seconds. If the lock remains busy, publication fails without leaving a
completed report. The coordination directory and lock files remain in place
for later runs.
Vexcalibur reserves the complete .vexcalibur-locks directory namespace.
Neither an output nor a report may name the directory, a file inside it, or a
hard link to an acquired coordination lock.
The report path:
must be absent or name a regular file or symbolic link, not a directory, FIFO, socket, or device;
must not alias the inventory, local findings, or VEX output path;
must not alias redirected standard output or standard error;
must have an existing parent directory.
When --output is used, its path has the same leaf-type and parent-directory
requirements.
Alias checks cover existing filesystem identity, including hard links, and conservatively treat Unicode-normalized, case-folded names in one parent directory as equal. Vexcalibur checks again after it publishes file-based VEX output.
Publishing replaces the report path and, in report mode, the VEX output path.
The resulting files have mode 0600. Their owner and group follow the process
and filesystem rules for a new file, including any set-group-ID parent
directory behavior. Previous ownership and mode are not preserved. A symbolic
link or hard-link path is replaced as a directory entry. Vexcalibur does not
write through that link to the old target.
Use a new destination when existing output must survive a failed operation. Once replacement starts, a failed directory flush or path verification does not restore the previous file. Vexcalibur removes its replacement when it can, so the path may be absent even though it held valid output before the command.
The --execution-report CLI option and its coordinated publication transaction
are not supported on Windows. A Windows CLI request fails before Vexcalibur
removes the candidate path, reads an inventory, or writes VEX output. Calls
that omit --execution-report keep the existing Windows behavior.
Python embeddings can construct and serialize an execution report on every supported platform. The caller writes the VEX document and report separately, so those writes do not provide the CLI’s transaction guarantees.
The report is limited to 16 KiB. A missing or stale package version, invalid report value, size violation, or write failure makes the command fail.
The VEX file and report are separate atomic writes, not one two-file
transaction. A later report-write failure can leave the replaced VEX file, or
the document in captured standard output, even though the command exits
nonzero. Automation must treat exit status 0 and a valid report as the
success condition.
The directory locks prevent cooperating Vexcalibur processes from interleaving their file commits. The per-report sequence lock preserves stdout and report ordering when those processes use the same report path. These locks do not coordinate every program that can write the directory. The path checks and advisory locks also cannot protect against a process that ignores the lock, runs as the same user and changes the coordination files, or changes an output after publication. Use a per-job directory that is isolated from untrusted processes, especially processes that share the same user ID.
An embedding must also serialize concurrent transactions that share one writable standard-output stream but use different report destinations. The directory lock coordinates the report destination; it cannot identify an arbitrary shared stream as another transaction’s output.
Compatibility¶
The option is additive on supported systems. Calls that omit
--execution-report retain the existing text-mode output and Python API
behavior.
--execution-report is available in Vexcalibur v0.6.0 and later. Before you
depend on the option, verify the installed command:
vexcalibur generate --help | grep -- --execution-report
An exit status of 0 means the installed command exposes the option.
Consumers should reject an unknown schema_version. They should not infer a
schema from vexcalibur_version.
The 1.x compatibility policy preserves the report contract for existing invocations. New report schemas must be opt-in during 1.x; new opt-in feature categories require an updated reviewed schema copy.
Schema version 1 added spdx3 to the output_format values when SPDX 3
output shipped. A report can only carry that value when the operator selects
--format spdx3, so reports for the other formats validate unchanged against
earlier copies of the schema. A consumer that pins a reviewed schema copy, as
the consumption guide recommends, must adopt the updated copy before its
pipeline adopts --format spdx3; until then it rejects those reports and
fails closed.
Validate the closed-world JSON Schema, check that state counts sum to
finding_count, and verify the digest and byte count before trusting a report.
The consumption guide shows the complete
sequence. Python embeddings can use parse_generation_execution_report to
enforce Vexcalibur’s canonical serialization contract before applying their
own document and policy checks.