Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Output formats

bomdrift writes one rendered representation of a diff per invocation. The shape is deterministic — identical inputs produce byte-identical output — which is what the PR-comment upsert mechanism in the action relies on.

--output selects the format. Default is terminal when stdout is a TTY, markdown otherwise.

Markdown

The default for piped/redirected output, designed to drop into a GitHub PR comment. Renders the diff as a summary table at the top, followed by per-section tables for each change category and finding type.

## SBOM diff

| Change | Count |
|---|---:|
| Added | 1 |
| Removed | 1 |
| Version changed | 1 |
| License changed | 0 |
| Possible typosquats | 1 |

### Added
| Ecosystem | Name | Version |
|---|---|---|
| npm | plain-crypto-js | 4.2.1 |

### Possible typosquats
| Ecosystem | Name | Version | Similar to | Similarity |
|---|---|---|---|---:|
| npm | plain-crypto-js | 4.2.1 | crypto-js | 0.95 |

When OSV.dev enrichment is enabled, an additional Vulnerabilities section lists each affected component with its advisory IDs sorted highest-severity-first within a component (ties broken by ID, so output stays byte-deterministic).

--summary-only emits only the summary table + a footer line. Used by the action’s comment-size fallback for big-PR survival.

Terminal

ANSI-colored, tree-style output. Default when stdout is a TTY. Falls back to markdown when stdout is piped/redirected (so action workflows that capture stdout always see safe markdown). Honors NO_COLOR (skip ANSI) and CLICOLOR_FORCE (force ANSI even on a non-TTY).

Findings are rendered with bracketed prefixes:

PrefixMeaning
[ADD]Added component
[REM]Removed component
[VER]Version changed
[LIC]License changed (same version)
[CVE]OSV.dev advisory
[SQT]Typosquat
[JMP]Multi-major version jump
[YNG]Young maintainer

No emojis — bomdrift’s renderers stay strictly bracketed-prefix per project convention, both for terminal accessibility and for grepability of CI logs.

JSON

Pretty-printed {"changes": ChangeSet, "enrichment": Enrichment} graph for downstream tooling, baselines, debugging.

{
  "changes": {
    "added":           [ ... Component objects ... ],
    "removed":         [ ... ],
    "version_changed": [[ before, after ], ... ],
    "license_changed": [[ before, after ], ... ]
  },
  "enrichment": {
    "vulns":          { "<purl>": [{ "id": "...", "severity": "..." }, ...] },
    "typosquats":     [ ... ],
    "version_jumps":  [ ... ],
    "maintainer_age": [ ... ]
  }
}

The Enrichment.vulns shape is per-purl, per-advisory severity-tagged as of v0.3. v0.2 emitted a flat Vec<String> of advisory IDs without severity — consumers parsing v0.2 output need to migrate. See the CHANGELOG for the migration note.

JSON output is the canonical format for --baseline snapshots: capture once with bomdrift diff --output json > baseline.json, replay with bomdrift diff --baseline baseline.json on subsequent runs to suppress already-triaged findings.

SARIF v2.1.0

Suitable for ingestion by GitHub Code Scanning, GitLab Vulnerability Reports, and any other consumer that speaks SARIF.

Stable rule IDs

These IDs surface in Code Scanning’s UI and are the join key for suppressions, so they’re load-bearing public API once any consumer has seen a finding. Renaming any of them is a breaking change.

Rule IDSourceMaps to
bomdrift.cveenrichment.vulnsone result per (component, advisory_id)
bomdrift.typosquatenrichment.typosquatsone per typosquat finding
bomdrift.version-jumpenrichment.version_jumpsone per multi-major bump
bomdrift.young-maintainerenrichment.maintainer_ageone per young-maintainer finding
bomdrift.license-changecs.license_changedone per license-changed-without-version-bump

All five rules are always emitted in tool.driver.rules, even when the current diff has zero findings of that kind — Code Scanning consumers enumerate rules independently of results, so omitting unused rules confuses the suppression UI.

Severity mapping

result.level maps from the OSV-fetched severity:

  • Critical / Highlevel: "error"
  • Medium / Low / Nonelevel: "warning"

This is intentionally separate from --fail-on critical-cve’s threshold (which also fires on High); SARIF’s three-level model (error/warning/note) doesn’t map 1:1 to OSV’s four severity labels, so the renderer collapses High+Critical into error and everything else into warning.

Locations

SARIF requires locations on every result. Since SBOM-derived findings have no source line numbers, all results project onto a synthetic physicalLocation.artifactLocation.uri = "sbom", matching the convention used by trivy.

Determinism

Enrichment.vulns is a HashMap and its iteration order is non-deterministic. The SARIF renderer sorts the keys before emission. Other finding collections are already deterministically ordered Vecs (their enrichers iterate the BTreeMap-derived ChangeSet order), so they need no extra sorting. The render-twice-byte-equal regression test in src/render/sarif.rs::tests::render_is_pure_byte_deterministic guards against future regressions of this contract.