Skip to main content

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 by src/output/sarif.rs and src/output/sbom.rs.
  • Regenerate: vulkro formats --format markdown, then paste the output between the two GENERATED FORMAT VERSIONS markers 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--formatSpec versionSpecificationExtension
SARIFsarif2.1.02.1.0.sarif
CycloneDX SBOMcyclonedx1.61.6.json
CycloneDX SBOMcyclonedx-1.71.71.7.json
CycloneDX CBOMcbom1.61.6.json
CycloneDX CBOMcbom-1.71.71.7.json
CycloneDX ML-BOM (AIBOM)aibom1.61.6.json
SPDX SBOMspdxSPDX-2.3SPDX-2.3.json
SPDX SBOMspdx3SPDX-3.0.1SPDX-3.0.1.json
OpenVEXopenvex0.2.00.2.0.json
CycloneDX VEXcyclonedx-vex1.61.6.json
JUnit XMLjunitAnt/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.

Exiterror.reasonWhen
1license_requiredno licence on this machine (stderr ends with vulkro: license_required)
2bad_argumentsan unknown flag, a bad value, a missing argument
2path_not_founda path the command needs does not exist
2permission_denieda path exists but cannot be read or written
2config_invalidvulkro.toml or a rule file could not be parsed
2io_errorany other read or write failure
2network_errora refused or failed network call (for example under --offline)
2internal_errora crash inside the scanner (a bug: please report it)
2errorany 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] has executionSuccessful: false, exitCode: 2 and one toolExecutionNotifications entry whose descriptor.id is the reason.
  • The licence refusal (license_required) keeps its existing shape, which also carries machine_id and billing_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.

ValueMeaning
provenA 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.
attributedAdmitted 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.
unprovenA real heuristic hit with no proof (disposition demoted).
not-checkedA 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_tier carries the 0.36 value (proven for an attributed finding) beside the new tier. It is removed in 0.38.
  • The OCSF export keeps unmapped.disposition and unmapped.proven with their 0.36 meaning (admitted) and adds unmapped.proof_tier.
  • MCP prove keeps the 0.36 meaning under admitted; proven is now true only for the proven tier, as its description always said.
  • guard check keeps proven (the findings that block on proof, admitted) and adds proof_tier.
  • Readers that parse a tier value accept proven from 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.