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

schema_version

exact integer token

Exactly 1. Booleans and decimal forms such as 1.0 are rejected. The report schema changes independently of the package version.

command

string

Exactly generate.

vexcalibur_version

string

Installed Vexcalibur distribution version loaded by the process. It must match package metadata; an editable Git checkout must also identify its current HEAD. The value has 1–128 characters from [0-9A-Za-z.!+_-], with an alphanumeric first character.

inventory_source

string

One inventory category from the table below.

finding_source

string

One finding category from the table below.

output_format

string

cyclonedx, openvex, csaf, spdx3, or custom.

component_count

exact integer token from 0 through 10,000,000

Normalized components sent to the finding source.

finding_count

exact integer token from 0 through 10,000,000

Normalized findings sent to the renderer.

analysis_state_counts

object

Exact positive integer tokens through 10,000,000, keyed by resolved, exploitable, in_triage, false_positive, or not_affected. States with zero findings are omitted.

document.sha256

string

64-character lowercase hexadecimal SHA-256 digest of the exact rendered UTF-8 document.

document.bytes

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

sbom_file

Local CycloneDX JSON or XML, or SPDX 3 JSON-LD

github_dependency_graph

GitHub Dependency Graph SBOM API

custom

Inventory supplied by an embedding through the Python API

finding_source uses one of these values:

Value

Input

local_file

--findings-file

public_osv

The canonical https://api.osv.dev endpoint, with explicit consent

custom_osv

A caller-selected, noncanonical OSV-compatible endpoint, whether privately or publicly reachable

custom

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:

  1. Construct and validate the complete report.

  2. Stage the report and any file-based VEX output in private mode-0600 files, then flush them.

  3. Keep the staged files open, verify their filesystem identities, and acquire destination-directory locks in stable order.

  4. Remove any report created after the initial cleanup. If removal fails, publish nothing.

  5. Publish the VEX document and check the destination paths again for aliases.

  6. 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.