Output formats
vulkro scan, vulkro discover, and most other commands take a
--format flag (alias -f) that selects the emitted payload. Every
format is built from the same in-memory ScanReport so they're all
consistent - the only thing that changes is the rendering.
Quick reference
| Format | What it's for | File extension |
|---|---|---|
table | Default. Colourised summary on the terminal. | - |
json | Machine-readable. Pipe to jq, save to disk. | .json |
sarif | GitHub Code Scanning, VS Code, Azure DevOps. SARIF 2.1.0 with partialFingerprints + codeFlows + per-rule helpUri. | .sarif |
gh-pr | One PR summary comment. Markdown. | .md |
gh-pr-inline-comments | One per-finding inline review comment per line. NDJSON ready to pipe to gh api. | .ndjson |
github-annotations | One GitHub Actions / GitLab CI workflow-command line per finding (::error file=...,line=...,title=<rule id>::<message>). Printed to a CI job's stdout, each line becomes an inline PR annotation on the diff. | - |
junit | GitLab MR test report, Jenkins, etc. | .xml |
csv | Spreadsheet hand-off. | .csv |
cyclonedx | CycloneDX 1.6 SBOM (JSON). Package inventory (purl, version, scope) plus a vulnerabilities[] block and a root dependencies edge. | .json |
cbom | CycloneDX 1.6 Cryptographic Bill of Materials (JSON). | .json |
spdx | SPDX 2.3 SBOM (JSON). Package inventory with DESCRIBES relationships. | .json |
pdf | Executive HTML rendered to PDF. Requires wkhtmltopdf on PATH. | .pdf |
ropa-md | GDPR Art. 30 Records of Processing - Markdown. | .md |
ropa-html | GDPR Art. 30 Records of Processing - HTML. | .html |
evidence-graph | Stable, versioned (evidence-graph/1.0), AI-consumable JSON view of the repo's attack surface. Pairs with vulkro aggregate. | .json |
Specification versions
The standards-based formats emit a specific spec version. The table below is
generated from the format catalogue via vulkro formats --format markdown,
so it stays in sync with the binary: if a version bumps in code and this table
is not regenerated, the release check fails. Tooling can read the same catalogue
as JSON with vulkro formats --format json.
| 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 |
| 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 |
Examples
vulkro scan . --format json | jq '.findings[] | select(.severity == "Critical")'
vulkro scan . --format sarif > vulkro.sarif
vulkro scan . --format gh-pr > comment.md
gh pr comment "$PR" --body-file comment.md
vulkro scan . --format cyclonedx > sbom.json
vulkro scan . --format ropa-md > ropa.md
Per-finding fields: exploitability and reachability_verdict
Each finding in the json output can carry two deterministic grading fields.
Both are computed with no model and no network, from signals the scan already
produced. Each is omitted from the JSON when the scan made no claim, so a
consumer can tell "no claim" apart from a concrete value.
reachability_verdict
Whether the vulnerable or tainted symbol is actually reached from the project's entry points (the call-graph forward closure). Promotes a verdict that used to live only inside evidence prose to a headline value a CI gate, VEX emitter, or console can read directly.
| Value | Meaning |
|---|---|
reachable | The vulnerable / tainted symbol IS reached from an entry-point forward closure. The most actionable verdict. |
unreachable | The symbol is known but is NOT reached from any entry point. The finding was degraded on this basis, never dropped. |
unknown | No reachability claim either way (no symbol map, or insufficient signal). The conservative default. |
exploitability
A deterministic confidence grade for whether this specific finding can be
exploited as written. This is not a re-labelling of severity: severity
answers "how bad is the bug class if it is exploited", exploitability answers
"how confident are we that this finding is exploitable as written". It is
composed from the taint source certainty and sanitizer-gap trace, the
reachability_verdict above, and the finding's own severity and confidence.
| Value | Meaning |
|---|---|
exploitable | A tainted, unsanitised, user-reachable path to the sink is present: exploitable as written. The most actionable grade. |
likely | Strong signal the finding is real, but at least one exploit precondition is unproven (e.g. reachability is unknown, or a sanitizer may cover the gap). Worth fixing, not a confirmed exploit. |
theoretical | The pattern is present but exploitation is not demonstrated (the sink is unreachable, a sanitizer covers the gap, or confidence is low). Keep it visible for audit, deprioritise for triage. |
exploitability is also surfaced as a SARIF property. Both fields serialize in
lowercase, matching the values above.
SARIF specifics
- Spec version 2.1.0. The emitted
$schemapoints at the immutable OASIS-published standard (.../sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json), not a mutable Git branch. - Each finding maps to one
resultwithruleId,ruleIndex,level,rank(0..100 mapped fromSeverity),message,locations, andproperties(carryingconfidence,confidence_reason,compliance_controls, plus thevulkro.fix/vulkro.fixesremediation strings for autofix-aware consumers). tool.driver.rulesenumerates every detector that contributed to this scan, with descriptions.partialFingerprintspopulated per result with two versioned keys:vulkro/v1: SHA-256 of{rule_id}|{owasp_category}|{file}| {message_head}. Stable across line drift; lets GitHub Code Scanning / Sonar / DefectDojo dedupe a finding across runs even when unrelated edits shift its line number.vulkro/locHash: SHA-256 of{file}:{line}. Exact identity; useful for joining against a fresh scan of the same commit.
relatedLocations[]materialised fromSecurityFinding.trace. Every trace hop becomes a labelled location in the related- locations panel of the SARIF consumer (sanitizer gap, missing auth middleware, the source of the taint, etc.).codeFlows[].threadFlows[].locations[]built from trace items whosekind == Taint. Renders as the data-flow ladder view in GitHub Code Scanning and matches how Semgrep / CodeQL show taint findings today. Cross-method Python and JavaScript / TS findings from the language-neutral taint engine populate this automatically.- Compatible with GitHub Code Scanning's SARIF uploader; the
vulkro/v1fingerprint replaces GitHub's auto-derived fingerprint so dedup is deterministic.
gh-pr-inline-comments specifics
NDJSON, one object per line. Each line is the shape
gh api repos/:owner/:repo/pulls/:N/comments wants:
{
"path": "src/auth.ts",
"line": 42,
"side": "RIGHT",
"severity": "high",
"rule_id": "OWASP-API1",
"fingerprint": "<sha256>",
"body": "**vulkro [High]** (OWASP-API1): ...\n\n**Fix.** ...\n\n<!-- vulkro:fingerprint:HASH -->"
}
Because the stream is one JSON object per line, a short shell loop reads it
and POSTs each comment via gh api, with no GitHub App middleman:
vulkro gate --base "origin/${GITHUB_BASE_REF}" \
--format gh-pr-inline-comments \
| while IFS= read -r comment; do
path=$(echo "$comment" | jq -r .path)
line=$(echo "$comment" | jq -r .line)
body=$(echo "$comment" | jq -r .body)
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/comments" \
-f path="$path" -F line="$line" -f side=RIGHT -f body="$body" \
-f commit_id="${GITHUB_SHA}"
done
The only GitHub scope required is pull_requests: write, available on the
default GITHUB_TOKEN that actions/checkout mints.
Behaviour when piped from vulkro gate:
- Only new findings (vs the baseline) are emitted. Pre-existing tech debt does not flood a PR with first-time inline comments.
- Findings without a concrete
fileor withline == 0are silently skipped: GitHub's review-comment API rejects them and a failed POST would noise the Actions log. - The body carries an HTML-comment fingerprint footer
(
<!-- vulkro:fingerprint:HASH -->) so the poster can dedupe comments across runs. The fingerprint matches the SARIFvulkro/v1value so an inline comment can be joined back to its SARIF row by id.
github-annotations specifics
One GitHub Actions workflow command per finding, newline-terminated:
::error file=src/auth.ts,line=42,endLine=44,col=1,title=OWASP-API1::IDOR on /users/:id - rebind the lookup (https://vulkro.com/docs/rules/broken-object-level-auth)
When these lines are printed to a GitHub Actions job's stdout, GitHub turns
each into an inline annotation pinned to file:line on the Files Changed tab
of the PR (and on the job summary). ::error / ::warning / ::notice set
the annotation severity (Critical / High become error, Medium becomes
warning, Low / Info become notice). GitLab CI's annotation parser reads
the same grammar, so one --format github-annotations lane drives PR
annotations on both hosts.
CI wiring (minimal GitHub Actions step):
- name: vulkro PR annotations
run: vulkro scan . --format github-annotations
Region precision mirrors SARIF: line is the 1-based start line, endLine
is emitted when the finding spans a block, and the message tail carries the
rule's helpUri so a reviewer can click through to the rule docs. Findings
without a concrete file or with line == 0 are skipped (GitHub drops
annotations that lack a location). When piped from vulkro gate only new
findings (vs the baseline) are annotated, so a first run does not paper the PR.
CycloneDX specifics
- Spec 1.6 JSON (schema). The 1.6 bump
lets the
cbomformat reuse the same emitter forcryptographic-assetcomponents. components[]lists every statically resolved dependency as alibrarycomponent with a stablebom-ref,purl,name,version, andscope(required/optional, derived from dev-dependency status).vulnerabilities[]carries one entry per matched (package, CVE) pair, sourced to OSV, with aratings[]severity and anaffects[].refpointing back at the vulnerable component'sbom-ref. The array is present-but-empty when the scan found no dependency CVEs (the "scanned, none found" signal), so consumers can tell that apart from "not scanned".dependencies[]records the one edge Vulkro can prove - the root application depends on every resolved package. Vulkro does not resolve the transitive dependency tree, so leaf sub-graphs are intentionally left unstated rather than fabricated.- Licences emit the SPDX sentinel
NOASSERTION: the dependency model does not yet carry per-package licence data. cbomis a separate CycloneDX 1.6 document ofcryptographic-assetcomponents only (MD5, SHA-1, ECB, RC4, DES, static IV, insecure RNG) with file:line occurrences. Use it for FedRAMP / post-quantum reviews alongside the library SBOM.
SPDX specifics
- Spec version SPDX-2.3 (schema), JSON.
- One
SPDXRef-Package-Nper resolved dependency, each with apurlexternal reference. - The document declares a top-level
DESCRIBESrelationship to every package (both the canonicalrelationships[]form and thedocumentDescribesarray), so strict validators (the SPDX online tool,pyspdxtools) accept it. licenseConcluded,licenseDeclared,copyrightText, anddownloadLocationareNOASSERTIONfor the same model-fidelity reason as CycloneDX above.
RoPA (GDPR Art. 30)
The Record of Processing Activities format is intended as a starting point for a GDPR audit pack. It enumerates:
- Each endpoint that handles personal data (PII detected by the privacy engine - see Privacy).
- The detected categories of personal data per endpoint.
- The stated purpose, retention, and lawful basis (you fill these in).
- The controls Vulkro detected as in place / missing.
ropa-md is best for Git review; ropa-html is best for emailing to a
DPO who doesn't read Markdown.
PDF
PDF rendering shells out to wkhtmltopdf. If the binary isn't on PATH,
the format errors out cleanly with a hint. The PDF source is the same
executive HTML report vulkro report produces.
evidence-graph
A stable, versioned, machine-readable JSON view of everything a scan already
knows, shaped for an external AI agent to consume as deterministic ground
truth. Emitted by vulkro scan <repo> --format evidence-graph and
vulkro discover <repo> --format evidence-graph.
vulkro scan . --format evidence-graph > evidence.json
The document does not reason, rank, or guess. It composes what the scanner produced into one connected graph and stamps its own provenance so an agent never mistakes it for AI-generated content:
schema-evidence-graph/1.0. An agent keys its parser on this. New fields are added, never renamed or removed (a stable contract).engine- a provenance block withtool: "vulkro",deterministic: true,uses_llm: false. Always present.endpoints[]- exposed HTTP routes with method, path, normalisedroute_shape, auth tier, handler, file, line, and reachability.outbound_calls[]- outbound HTTP client calls this repo makes, each with a normalisedroute_shapeand anis_orphanflag (no matching endpoint in the same repo, so a strong cross-repo candidate).taint_flows[]- taint source -> sink flows lifted from the data-flow graph, withreaches_sink,unauthenticated,protection_gap, a deterministicriskscore, and data classes.findings[]- findings flattened to id, severity, OWASP category, message, file, line, optional endpoint id, and confidence.dependencies[]- SBOM entries (name, version, ecosystem, vuln count).summary- rollup counts so an agent can budget attention without walking the full arrays.
Feed several of these documents (one per repo) to
vulkro aggregate to surface candidate cross-repo flows.
The same two steps are exposed as the evidence_graph and aggregate MCP
tools on vulkro mcp serve.