Output format versions
Vulkro emits every machine-readable format below at a pinned specification
version. The table is generated from the emitter constants in
src/output/ so it can never claim a version the binary does not actually
write.
- Source of truth:
src/output/format_spec.rs, fed bysrc/output/sarif.rsandsrc/output/sbom.rs. - Regenerate:
vulkro formats --format markdown, then paste the output between the twoGENERATED FORMAT VERSIONSmarkers below. - Guard:
tests/format_docs_drift.rs::output_format_version_table_matches_code
The website repository mirrors this table into its own output-formats.md
page, which carries the surrounding prose. If the two differ, this one is
canonical: it is the one CI checks against the code.
| Format | --format | Spec version | Specification | Extension |
|---|---|---|---|---|
| SARIF | sarif | 2.1.0 | 2.1.0 | .sarif |
| CycloneDX SBOM | cyclonedx | 1.6 | 1.6 | .json |
| CycloneDX SBOM | cyclonedx-1.7 | 1.7 | 1.7 | .json |
| CycloneDX CBOM | cbom | 1.6 | 1.6 | .json |
| CycloneDX CBOM | cbom-1.7 | 1.7 | 1.7 | .json |
| CycloneDX ML-BOM (AIBOM) | aibom | 1.6 | 1.6 | .json |
| SPDX SBOM | spdx | SPDX-2.3 | SPDX-2.3 | .json |
| SPDX SBOM | spdx3 | SPDX-3.0.1 | SPDX-3.0.1 | .json |
| OpenVEX | openvex | 0.2.0 | 0.2.0 | .json |
| CycloneDX VEX | cyclonedx-vex | 1.6 | 1.6 | .json |
| JUnit XML | junit | Ant/Surefire (de-facto) | Ant/Surefire (de-facto) | .xml |
Vulkro's own documents: format_version
The Core --format json document and the --format ndjson summary line carry
a top-level "format_version": 1. It is the version of Vulkro's own output
shape (the SARIF and SBOM formats above carry their standard's version
instead). It changes only when an existing field is renamed, removed or
changes type, which also needs a deprecation release (CLAUDE.md rule 11).
Adding a key does not change it, so a consumer should ignore keys it does not
know. The schema key in the JSON document is something else: the database
schema map the scan found in the project.
vulkro-sf JSON documents carry their own schema_version (currently 1),
unchanged.
Stable output: the one volatile field
Two scans of the same tree with the same binary produce the same JSON, NDJSON
and SARIF bytes except for one field: scanned_at (RFC 3339), which the NDJSON
summary also uses as scan_id. Ids are content hashes and every array has a
fixed order. To make the whole document byte-identical, for a cache key or a
baseline diff, pin the clock:
vulkro scan . --format json --no-timestamp # or VULKRO_DETERMINISTIC=1
SOURCE_DATE_EPOCH=1700000000 vulkro scan . --format json # pin to an instant
--no-timestamp sets scanned_at to 1970-01-01T00:00:00+00:00.
Deprecated: reachability.graph_built_at is no longer written (0.37.0). It was
an optional debugging field serialised as {"secs_since_epoch", "nanos_since_epoch"}
and duplicated scanned_at. It was always optional, so a consumer that read it
already had to handle its absence; read scanned_at instead.
Error documents
When a command is asked for --format json, ndjson or sarif (or --json)
and fails, stdout carries a machine-readable error document instead of being
empty, so a CI step that parses the output gets a reason, not a parse error.
The human message is still on stderr, and the exit code is unchanged.
| Exit | error.reason | When |
|---|---|---|
| 1 | license_required | no licence on this machine (stderr ends with vulkro: license_required) |
| 2 | bad_arguments | an unknown flag, a bad value, a missing argument |
| 2 | path_not_found | a path the command needs does not exist |
| 2 | permission_denied | a path exists but cannot be read or written |
| 2 | config_invalid | vulkro.toml or a rule file could not be parsed |
| 2 | io_error | any other read or write failure |
| 2 | network_error | a refused or failed network call (for example under --offline) |
| 2 | internal_error | a crash inside the scanner (a bug: please report it) |
| 2 | error | any other operational error |
Shapes:
{"format_version": 1, "error": {"reason": "path_not_found", "message": "...", "product": "vulkro", "exit_code": 2}}
ndjson: the same object on one line with"type": "error".sarif: a valid SARIF 2.1.0 log with no results;runs[0].invocations[0]hasexecutionSuccessful: false,exitCode: 2and onetoolExecutionNotificationsentry whosedescriptor.idis the reason.- The licence refusal (
license_required) keeps its existing shape, which also carriesmachine_idandbilling_url.
SARIF tool identity
runs[0].tool.driver names the product that ran: name is vulkro or
vulkro-sf, version is that binary's version, and informationUri points at
its documentation. (Before 0.37.0 / 0.29.0 a vulkro-sf log said vulkro at
the engine version.) GitHub code scanning keys alerts by tool name, so the first
vulkro-sf upload after the upgrade opens its alerts under the new name and
closes the old ones.
The licence marker
Every licence refusal from either binary ends its stderr with the line
vulkro: license_required, so one CI check (grep -q '^vulkro: license_required')
tells a licence block from findings. vulkro-sf also prints its older
vulkro-sf: license_required line after it for one more release.
Proof tier values
Every finding carries a proof tier: the finding card's certainty.tier (JSON
card, the SARIF message), SARIF properties["vulkro.proofTier"], the editor's
data.proofTier, the MCP prove tool's proof_tier, guard check's
per-finding proof_tier, notifications and the exposure report.
| Value | Meaning |
|---|---|
proven | A traced source-to-sink flow, or an equally established fact: a value-verified secret, an advisory match on the exact installed version, a Salesforce metadata proof chain. |
attributed | Admitted on a guard-oracle verdict or a dispositive syntactic shape alone: the rule matched at a named sink, and no source-to-sink flow was traced. |
unproven | A real heuristic hit with no proof (disposition demoted). |
not-checked | A class Vulkro cannot decide offline (disposition not-examined). |
attributed is new in 0.37.0 (decision D4, audit CLI-06). Through 0.36 those
findings were called proven. Deprecation path, one release (CLAUDE.md rule 11):
card.certainty.legacy_tiercarries the 0.36 value (provenfor anattributedfinding) beside the newtier. It is removed in 0.38.- The OCSF export keeps
unmapped.dispositionandunmapped.provenwith their 0.36 meaning (admitted) and addsunmapped.proof_tier. - MCP
provekeeps the 0.36 meaning underadmitted;provenis now true only for theproventier, as its description always said. guard checkkeepsproven(the findings that block on proof, admitted) and addsproof_tier.- Readers that parse a tier value accept
provenfrom either release (ProofTier::parse); it simply means the stricter tier from 0.37.
The disposition field (admitted / demoted / not-examined) is unchanged:
proven plus attributed is exactly the admitted set.