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

GitHub Action

The Metbcy/bomdrift action is a composite action (no Docker), which keeps PR-comment latency low — typically 5–10s on a warm runner versus 30s+ for a Docker container action.

Quick start (zero-config, v0.5+)

On a pull_request workflow, the action defaults to comparing the PR’s base branch against the PR’s head SHA — no checkout step, no Syft step, no SBOM-path wiring needed:

on: pull_request
permissions:
  contents: read
  pull-requests: write
jobs:
  diff:
    runs-on: ubuntu-latest
    steps:
      - uses: Metbcy/bomdrift@v1

That’s it. The action checks out both refs into opaque sibling paths, generates CycloneDX-JSON SBOMs via Syft (installed automatically and cached across job runs), and posts the rendered diff as an upserted PR comment.

For a repo-owned policy, run bomdrift init once and commit the generated .bomdrift.toml plus workflows. The action auto-loads .bomdrift.toml from the repo root when present, or you can pass config: .bomdrift.toml explicitly.

If you already produce SBOMs through a non-Syft toolchain — Trivy, SPDX-tools, an in-house generator — supply the file paths via the before-sbom / after-sbom inputs instead. The advanced flow below documents that path; both flows continue to be supported in v1.

Inputs

The action exposes the full bomdrift CLI surface as inputs (v0.9.7+ input parity, current as of v0.9.9). For the canonical flag semantics see CLI reference; the tables below document only the action-side wrapper. Empty defaults mean “don’t pass the flag” — bomdrift then uses its own CLI/config defaults.

What’s new in v0.9.7

These inputs are newly exposed (the underlying CLI flags shipped earlier):

  • VEX: vex, emit-vex, vex-author, vex-default-justification
  • License policy: allow-licenses, deny-licenses, allow-exception, deny-exception, allow-ambiguous-licenses
  • Enrichment toggles: no-epss, no-kev, no-registry
  • Failure thresholds: fail-on-epss
  • Calibration knobs: recently-published-days, typosquat-similarity-threshold, young-maintainer-days, cache-ttl-hours, multi-major-delta (new CLI flag in v0.9.7)
  • Attestation: before-attestation, after-attestation, cosign-identity, cosign-issuer, require-attestation
  • Plugins: plugin

Before v0.9.7 these had to be driven through .bomdrift.toml or a direct cargo install invocation. The config-file path remains supported and is still preferred for repo-wide policy.

Core: refs, paths, SBOMs

InputTypeDefaultDescription
before-refstring${{ github.event.pull_request.base.ref }}Git ref / SHA to check out as the “before” side. Default works on pull_request events.
after-refstring${{ github.event.pull_request.head.sha }}Git ref / SHA for the “after” side.
pathstring.Subdirectory of the checked-out ref to scan with Syft (monorepos: path: services/api).
before-sbomstring (path)''Pre-generated “before” SBOM. Bypasses the in-action Syft invocation.
after-sbomstring (path)''Pre-generated “after” SBOM.
formatenumautoForce input format: auto/cdx/spdx/syft. Maps to --format.

Output

InputTypeDefaultDescription
outputenummarkdownOutput format: terminal/markdown/json/sarif. PR comments require markdown. Maps to --output.
comment-on-prbooltruePost the rendered diff as a PR comment on pull_request events.
comment-size-limitnumber60000Bytes. Above this size, the PR-comment body is re-rendered with --summary-only. 0 disables the fallback.
findings-onlyboolfalseMarkdown-only. Maps to --findings-only.
upload-to-code-scanningboolfalseUpload SARIF to GitHub Code Scanning. Requires output: sarif.
github-tokenstring${{ github.token }}Token used to post PR comments.

Suppression and policy

InputTypeDefaultDescription
configstring (path)''Path to .bomdrift.toml. Empty auto-loads from the repo root when present. Maps to --config.
baselinestring (path)''Pre-captured bomdrift diff --output json snapshot to suppress against. Maps to --baseline.
vexstring (multi-line paths)''OpenVEX documents to consume; one path per line, each becomes a repeated --vex.
emit-vexstring (path)''Path to write a freshly emitted OpenVEX document. Maps to --emit-vex.
vex-authorstring''Author identity for the emitted OpenVEX. Maps to --vex-author.
vex-default-justificationstring''OpenVEX not_affected justification ID applied by default. Maps to --vex-default-justification.

License policy

InputTypeDefaultDescription
allow-licensesstring (comma list)''SPDX expressions to allow. Maps to --allow-licenses.
deny-licensesstring (comma list)''SPDX expressions to deny. Maps to --deny-licenses.
allow-exceptionstring (comma list)''SPDX exception identifiers to allow inside WITH clauses. v0.9.7 refines compound-expression inheritance. Maps to --allow-exception.
deny-exceptionstring (comma list)''SPDX exception identifiers to deny. Maps to --deny-exception.
allow-ambiguous-licensesboolfalseTreat unresolved license expressions as allowed. Maps to --allow-ambiguous-licenses.

Enrichment toggles

InputTypeDefaultDescription
no-epssboolfalseDisable EPSS exploit-likelihood enrichment. Maps to --no-epss.
no-kevboolfalseDisable CISA KEV enrichment. Maps to --no-kev.
no-registryboolfalseDisable registry / maintainer-age enrichment (no network calls to package registries). Maps to --no-registry.

Calibration knobs

InputTypeDefaultDescription
recently-published-daysnumber''“Recently published” maintainer-age window. Maps to --recently-published-days.
typosquat-similarity-thresholdnumber (0.0–1.0)''Damerau-Levenshtein threshold for typosquat detection. Maps to --typosquat-similarity-threshold.
young-maintainer-daysnumber''Age below which a maintainer is flagged as “young”. Maps to --young-maintainer-days.
cache-ttl-hoursnumber''TTL for the on-disk enrichment cache. Maps to --cache-ttl-hours.
multi-major-deltanumber (≥1)''Major-version delta at or above which an upgrade is flagged as multi-major (CLI default 2). Maps to --multi-major-delta. New in v0.9.7.

Failure thresholds

InputTypeDefaultDescription
fail-onenumnoneTrip exit 2 on findings of the configured kind: none/cve/critical-cve/typosquat/license-change/any. The PR comment is still posted on a tripped run.
fail-on-epssnumber (0.0–1.0)''Trip exit 2 when any new advisory has an EPSS score at or above this value. Maps to --fail-on-epss.
max-addednumber''Exit 2 when more than this many dependencies are added.
max-removednumber''Exit 2 when more than this many dependencies are removed.
max-version-changednumber''Exit 2 when more than this many dependencies change version.

OCI attestation

InputTypeDefaultDescription
before-attestationstring (OCI ref)''OCI reference for the cosign attestation covering the “before” SBOM. Maps to --before-attestation.
after-attestationstring (OCI ref)''OCI reference for the “after” SBOM attestation. Maps to --after-attestation.
cosign-identitystring (regex)''Regex matched against the cosign certificate identity (--certificate-identity-regexp). Maps to --cosign-identity.
cosign-issuerstring (URL)''OIDC issuer URL for keyless cosign verification. Maps to --cosign-issuer.
require-attestationboolfalseFail when either side is missing a verified attestation. Maps to --require-attestation.

For air-gapped / self-hosted Sigstore deployments, see Air-gapped / self-hosted Sigstore.

Plugins

InputTypeDefaultDescription
pluginstring (multi-line paths)''Plugin manifests to load; one path per line, each becomes a repeated --plugin. See Plugins.

Release verification

InputTypeDefaultDescription
verify-signaturesbooltrueInstall cosign and verify the bomdrift release archive’s Sigstore signature. Set false on trusted mirrors / cached runners (saves ~15s). When true and cosign is missing, the action fails loudly.

Outputs

The action does not declare formal outputs. Its side effects are:

  1. The rendered diff is written to stdout (visible in the workflow run log under the Run bomdrift step).
  2. When output == markdown and GITHUB_STEP_SUMMARY is set, the rendered diff is appended to the step summary so reviewers can see it without a PR-comment posting permission.
  3. On pull_request events with comment-on-pr: true, the rendered diff is upserted into a single PR comment marked <!-- bomdrift:diff -->. Subsequent pushes update the same comment instead of accumulating new ones (peter-evans/create-or-update-comment-style upsert).
  4. When fail-on or a diff budget trips, the action exits with code 2 — but only after the PR comment has been posted, so reviewers see the findings even when the workflow step fails.

Common patterns

Repo policy file

Use .bomdrift.toml when you want the policy in version control instead of repeated YAML inputs:

[diff]
fail_on = "critical-cve"
baseline = ".bomdrift/baseline.json"
findings_only = true
max_added = 25
max_version_changed = 10
- uses: Metbcy/bomdrift@v1
  with:
    config: .bomdrift.toml

Explicit action inputs still override the config-backed defaults for one-off workflows.

Bring your own SBOMs (advanced / pre-v0.5 flow)

When the SBOMs come from a non-Syft toolchain (Trivy, SPDX-tools, proprietary scanners) or you already generate them in an earlier job step, supply both paths explicitly. The action skips the in-action Syft invocation entirely:

- uses: actions/checkout@v4
- uses: anchore/sbom-action@v0
  with: { path: ., output-file: after.json }
- uses: actions/checkout@v4
  with: { ref: ${{ github.event.pull_request.base.ref }}, path: base }
- uses: anchore/sbom-action@v0
  with: { path: base, output-file: before.json }
- uses: Metbcy/bomdrift@v1
  with: { before-sbom: before.json, after-sbom: after.json }

This is the v0.4-era “manual” pattern. It still works in v0.5 — the before-sbom / after-sbom inputs were required: true in v0.4 and became required: false in v0.5; nothing else changed about how they behave. Existing v0.4 workflows continue to function unchanged after a @v1 tag bump.

Block the merge on critical findings

- uses: Metbcy/bomdrift@v1
  with:
    before-sbom: before.json
    after-sbom:  after.json
    fail-on:     critical-cve

critical-cve filters on severity >= High per the OSV-fetched severity (see OSV.dev CVE lookup). typosquat, license-change, and any are also accepted thresholds — see --fail-on.

Self-hosted / trusted-mirror runners

- uses: Metbcy/bomdrift@v1
  with:
    before-sbom: before.json
    after-sbom:  after.json
    verify-signatures: false   # ~15s faster, skips cosign-installer

This is appropriate when:

  • You’re running on self-hosted runners with a hardened image you control.
  • You’ve pre-pinned the bomdrift archive in your Nexus/Artifactory mirror and verified its signature once at mirror time.
  • You’re running in a network-restricted environment where the public Sigstore endpoints aren’t reachable.

When verify-signatures: true and cosign isn’t installed (or the .sig/ .pem aren’t on the release), the action fails loudly rather than silently degrading — that’s the whole point of the explicit opt-out.

Big monorepo with massive SBOMs

If bomdrift diff rendered output exceeds GitHub’s 65,536-char comment-body cap, the v0.3 size fallback re-renders with --summary-only for the PR comment and keeps the full body in the workflow step summary:

- uses: Metbcy/bomdrift@v1
  with:
    before-sbom: before.json
    after-sbom:  after.json
    comment-size-limit: 60000   # default; tune for GHE with raised limits

Set comment-size-limit: 0 to disable the fallback entirely and let GitHub return a 422 on oversized comments (rarely what you want).

Diff-only (no PR comment)

Useful for SARIF uploads, third-party comment posting, or when you just want the diff in the step summary:

- uses: Metbcy/bomdrift@v1
  with:
    before-sbom:    before.json
    after-sbom:     after.json
    output:         sarif
    comment-on-pr:  false

- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: bomdrift.sarif }

The output: sarif produces SARIF v2.1.0 with stable rule IDs (see Output formats).

Comment-driven suppression bridges (other forges)

The comment-suppress companion sub-action is GitHub-only — it relies on the issue_comment workflow event. For GitLab, Bitbucket Cloud, and Azure DevOps, bomdrift ships parallel Cloudflare Worker bridges that listen on each forge’s webhook, validate the trigger, and dispatch the equivalent bomdrift baseline add --from-comment "<body>" run on the underlying CI:

Each bridge enforces the same five guards: webhook secret / HMAC verification, event-type filter, repo / project allowlist, commenter-permission check, and a PR-context guard. The /bomdrift suppress <ID> [reason: …] grammar is identical across all four SCMs and shares a single regex (scripts/parse-suppress-comment.sh) so behavior cannot drift. See the per-forge chapters GitLab CI · Bitbucket · Azure DevOps for setup.

Action permissions

pull-requests: write is required when comment-on-pr: true (the default). Without it, the comment-upsert step fails with a 403; the action’s exit code remains the bomdrift exit (so a fail-on or budget trip still fails the workflow correctly).

contents: read is required so the action’s internal actions/checkout steps (zero-config flow) can fetch both refs. In the bring-your-own-SBOMs flow it’s still required by whichever step generates the SBOMs upstream.

What the action does (v0.5+)

When the zero-config flow runs (no explicit before-sbom / after-sbom):

  1. Two sibling checkouts of before-ref and after-ref into ${{ github.workspace }}/__bomdrift_before and __bomdrift_after. Both with fetch-depth: 1 and persist-credentials: false. Skipped for whichever side has a pre-supplied SBOM path.
  2. Syft installed via anchore/sbom-action/download-syft@v0. Cached across job runs in the runner’s tool cache.
  3. syft scan dir:... against each checkout’s ${path} subtree, producing CycloneDX-JSON into a tempfile under $RUNNER_TEMP. The bomdrift parser drops Ecosystem::Other("file") pseudo-components that Syft’s directory cataloger emits — set --include-file-components (CLI) or pass a pre-generated SBOM via before-sbom / after-sbom to bypass.
  4. bomdrift diff runs as in the v0.4 flow, and the upsert + step summary plumbing is unchanged.

The new behavior costs about 30 MB of one-time tool cache and 3–5s of cold-cache wall time per first invocation. Subsequent runs in the same job (or in repos that share the runner’s tool cache) reuse Syft.

Monorepo setup

When a single repo owns N services with independent dependency trees (services/api, services/worker, apps/web, …), running one bomdrift job per service gives each PR a focused, per-service comment without merging unrelated diff churn into a single 65k-char wall.

Pattern A — path: per matrix entry

The simplest setup uses a job matrix and the action’s path input:

on: pull_request
permissions:
  contents: read
  pull-requests: write
jobs:
  diff:
    strategy:
      fail-fast: false
      matrix:
        service: [api, worker, web]
    runs-on: ubuntu-latest
    steps:
      - uses: Metbcy/bomdrift@v1
        with:
          path: services/${{ matrix.service }}
          fail-on: critical-cve

Each matrix leg posts (or upserts) its own PR comment, distinguished by the rendered title (e.g. “SBOM diff — services/api”). The <!-- bomdrift:diff --> upsert marker is namespaced internally by path:, so leg N’s comment doesn’t clobber leg N-1’s.

fail-fast: false is recommended: a vulnerability in worker shouldn’t hide an emergent api finding from the same PR.

Pattern B — share a baseline across services

Most monorepos do want one shared exception list (the same false positive will show up in any service that depends on the same package). Point each leg at the same file:

- uses: Metbcy/bomdrift@v1
  with:
    path: services/${{ matrix.service }}
    baseline: .bomdrift/baseline.json

The baseline file is keyed by (purl_with_version, advisory_id) — see Match keys — so a suppression for pkg:npm/colour-print@2.1.0 covers every service that pulls in that exact version. New versions still surface (intentional; that’s the point of the version-pinned key).

When services pin different versions of the same dep, you’ll get per-version baseline entries. That’s working-as-intended — a known-fine finding at v1.0.0 should still get a fresh review at v1.1.0.

Pattern C — per-service .bomdrift.toml

When the policy itself differs (worker has a stricter fail-on, docs-site has a generous max-added), drop a .bomdrift.toml per service:

- uses: Metbcy/bomdrift@v1
  with:
    path:   services/${{ matrix.service }}
    config: services/${{ matrix.service }}/.bomdrift.toml

The auto-discovery only checks the repo root, so an explicit config: is required for nested files.

What to scope per service vs. globally

SettingScopeWhy
fail-on, max-* budgetsPer-serviceWorker’s risk surface ≠ web’s
baselineSharedSame false positives across services
comment-on-pr, outputPer-serviceDiff-only legs vs. PR-comment legs
verify-signaturesGlobalRunner-image property, not service property

Action-broke troubleshooting checklist

When a previously-working bomdrift action job starts failing — typically right after a merge to your default branch, a token rotation, or a runner-image upgrade — work through these in order. Each row is one symptom, one fix so you can grep your job log for the symptom and land on the recipe.

Symptom (in the job log)Likely causeFix
403 Resource not accessible by integration on the comment-upsert steppull-requests: write permission missing on the workflow / jobAdd permissions: { pull-requests: write, contents: read } at the workflow or job level. PR comments need pull-requests: write; the action’s internal checkouts need contents: read.
Forks cannot post PR comments warning, exit 0PR is from a fork; default GITHUB_TOKEN on pull_request events is read-onlySwitch the trigger to pull_request_target (and harden — see GitHub’s guidance), or accept that fork PRs only get the workflow step summary, not a PR comment.
Could not find SBOM at services/api after a green earlier runDefault branch protection bumped the merge-base; before-ref now points at a commit that predates the services/api directoryEither move the path: value to match the new layout, or pin before-ref explicitly to a known-good commit (before-ref: main).
cosign: signature verification failed after a release-archive rotationCached release archive in the runner’s tool cache is stale and predates a rotationBump to the latest patch tag (e.g. Metbcy/bomdrift@v1 re-resolves to the floating tag), or set verify-signatures: false on a self-hosted runner you’ve pinned manually.
path: services/api warning + empty SBOMThe path doesn’t exist post-checkout — typo, or the directory was renamed in before-ref onlybomdrift v0.7+ surfaces an actionable error pointing at this exact case. See the monorepo section for the matrix recipe; double-check ${{ matrix.service }} substitution.
“Comment exceeds 65,536 characters” 422 from GitHubA massive diff blew past the size cap; the v0.3 fallback to --summary-only was disabled (comment-size-limit: 0)Re-enable the fallback (drop comment-size-limit to use the default, or set it to 60000). The full body is preserved in the workflow step summary.
Action runs, no PR comment appears, exit 0Workflow event isn’t pull_request (the comment path is gated on PR events), or comment-on-pr: false was set explicitlyFor push/schedule events, the comment path is intentionally skipped — use the step summary or upload the markdown as an artifact.

If you hit a failure mode not in the table above, please open an issue with the failing job log — the troubleshooting table grows from real reports.