Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Repository policy config (`.bomdrift.toml`).** `bomdrift diff`
auto-loads `.bomdrift.toml` from the current working directory when
present, or an explicit file via `--config`. Config can set defaults
for output format, fail thresholds, baseline path, markdown focus
mode, and dependency-churn budgets while leaving CLI flags as the
one-off override path.

- **`bomdrift init` scaffolding.** `bomdrift init` writes a starter
`.bomdrift.toml`, SBOM-diff workflow, and comment-suppression workflow.
`--config-only` writes just the policy file; `--force` overwrites
existing generated files.

- **Diff-budget gates.** `--max-added`, `--max-removed`, and
`--max-version-changed` exit 2 after rendering when a PR changes more
dependencies than the configured budget allows.

- **Focused markdown comments.** `--findings-only` keeps the summary and
risk-bearing sections but omits raw Added / Removed / Version changed
detail rows for high-churn PRs.

- **License-change threshold.** `--fail-on license-change` exits 2 on
same-version license drift without also requiring `--fail-on any`.

- **GitHub Action inputs for policy controls.** The action now accepts
`config`, `findings-only`, `max-added`, `max-removed`, and
`max-version-changed` and passes them through to the CLI.

## [0.5.0] - 2026-04-29

The adoption milestone: bomdrift now works as a copy-paste GitHub Action,
Expand Down
60 changes: 60 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ strsim = "0.11"
owo-colors = { version = "4", features = ["supports-colors"] }
supports-color = "3"
directories = "6"
toml = "0.8"

[dev-dependencies]
criterion = { version = "0.5", default-features = false, features = ["html_reports"] }
Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ jobs:
steps:
- uses: Metbcy/bomdrift@v1
# Optional inputs (all have sensible defaults):
# fail-on: critical-cve | cve | typosquat | any | none
# fail-on: critical-cve | cve | typosquat | license-change | any | none
# baseline: .bomdrift/baseline.json
# findings-only: true
# verify-signatures: true (set false on trusted mirrors)
```

Pin to `@v1` for the latest v0.x; pin to `@v0.5.0` for reproducible builds. See the [Action reference](https://metbcy.github.io/bomdrift/github-action.html) for every input.
Pin to `@v1` for the latest v0.x; pin to `@v0.5.0` for reproducible builds. Run `bomdrift init` if you want a checked-in `.bomdrift.toml` policy and both workflows scaffolded locally. See the [Action reference](https://metbcy.github.io/bomdrift/github-action.html) for every input.

#### Optional: in-comment suppression (v0.5+)

Expand Down Expand Up @@ -148,9 +149,18 @@ bomdrift diff before.json after.json --output sarif
# Exit 2 on findings (the action wraps this for PR-comment workflows)
bomdrift diff before.json after.json --fail-on critical-cve

# Keep raw churn out of PR comments while preserving risk sections
bomdrift diff before.json after.json --findings-only

# Block unusually large dependency churn
bomdrift diff before.json after.json --max-added 25 --max-version-changed 10

# Suppress findings already present in a baseline snapshot
bomdrift diff before.json after.json --baseline .bomdrift/baseline.json

# Scaffold .bomdrift.toml and GitHub Action workflows
bomdrift init

# Hand-curate a baseline (or let the comment-suppress sub-action do it)
bomdrift baseline add GHSA-xxxx-yyyy-zzzz

Expand All @@ -159,7 +169,7 @@ bomdrift refresh-typosquat # all ecosystems
bomdrift refresh-typosquat --ecosystem pypi # one specific list
```

`bomdrift diff` exits 0 on success regardless of findings unless `--fail-on` is set — then it exits 2 when the threshold trips. Stdout is Markdown by default when piped/redirected (the PR-comment path) and ANSI-colored when stdout is a TTY. `--output markdown|json|terminal|sarif` overrides detection.
`bomdrift diff` exits 0 on success regardless of findings unless `--fail-on` or a diff budget is set — then it exits 2 when the policy trips. Stdout is Markdown by default when piped/redirected (the PR-comment path) and ANSI-colored when stdout is a TTY. `--output markdown|json|terminal|sarif` overrides detection.

See the [`examples/`](./examples/) directory for end-to-end scenarios (axios incident, multi-ecosystem typosquats, version jumps, baseline suppression).

Expand Down Expand Up @@ -206,10 +216,11 @@ With network access, an additional Vulnerabilities section lists each advisory I
- Flag deps whose **top GitHub maintainer joined the project recently** (the xz-style takeover signal). Honors `GITHUB_TOKEN`, rate-limit-aware, skipped when the repo has > 50 contributors.
- Flag **multi-major version jumps** (≥ 2 majors) in a single diff — often correlates with takeover swaps and namespace reuse.
- **Output formats**: terminal (colored, TTY-aware), Markdown (PR comment, with collapsible sections + severity sort), **JSON**, and **SARIF v2.1.0** for GitHub Code Scanning ingestion.
- **`--fail-on`** thresholds (`cve` / `critical-cve` / `typosquat` / `any`) exit code 2 on trip while still emitting the comment body, so the PR comment posts even when the workflow step fails.
- **`--fail-on`** thresholds (`cve` / `critical-cve` / `typosquat` / `license-change` / `any`) and diff budgets (`--max-added`, `--max-removed`, `--max-version-changed`) exit code 2 on trip while still emitting the comment body, so the PR comment posts even when the workflow step fails.
- **`.bomdrift.toml` + `bomdrift init`** let repos keep policy in version control instead of repeating inputs in workflow YAML.
- **`/bomdrift suppress <id>`** in-comment suppression (v0.5+) via a companion sub-action.
- **`--baseline <path.json>`** suppresses findings already captured in a previously stored `bomdrift diff --output json` snapshot.
- **`--summary-only`** + automatic comment-size fallback (default 60 KB) keeps big SBOM diffs under GitHub's 65,536-char comment-body cap.
- **`--summary-only`**, **`--findings-only`**, and automatic comment-size fallback (default 60 KB) keep big SBOM diffs under GitHub's 65,536-char comment-body cap.
- Ships as a **single Rust binary** (~3.4 MB, stripped + LTO) **and** a composite GitHub Action — no Docker.
- Releases are **cosign-signed** keyless via Sigstore + GitHub OIDC — eat-your-own-supply-chain-dogfood.

Expand Down
31 changes: 30 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ inputs:
fail-on:
description: |
Threshold to fail the action with exit code 2 when findings of the
configured kind surface. Accepted: none|cve|critical-cve|typosquat|any.
configured kind surface. Accepted: none|cve|critical-cve|typosquat|
license-change|any.
`critical-cve` filters on real OSV severity (HIGH or above per GHSA's
`database_specific.severity`); advisories with no resolvable severity
surface in the diff but don't trip this threshold. The PR comment is
Expand All @@ -79,6 +80,29 @@ inputs:
to 0 to disable the fallback (a too-large comment will then 422
from the GitHub API and the action posts nothing).
default: '60000'
config:
description: |
Path to a `.bomdrift.toml` repo policy file. When set, the CLI loads
defaults such as fail_on, baseline, findings_only, and diff budgets
before applying explicit action inputs. Leave empty to auto-load
`.bomdrift.toml` when present.
default: ''
findings-only:
description: |
Markdown-only. Keep the summary table and risk-bearing sections, but
omit raw Added / Removed / Version changed detail tables from the PR
comment. Useful for high-churn repos where reviewers only want the
actionable findings inline.
default: 'false'
max-added:
description: Exit 2 when more than this many dependencies are added.
default: ''
max-removed:
description: Exit 2 when more than this many dependencies are removed.
default: ''
max-version-changed:
description: Exit 2 when more than this many dependencies change version.
default: ''
baseline:
description: |
Path to a previously captured `bomdrift diff --output json` snapshot.
Expand Down Expand Up @@ -158,6 +182,11 @@ runs:
OUTPUT_FORMAT: ${{ inputs.output }}
COMMENT_ON_PR: ${{ inputs.comment-on-pr }}
COMMENT_SIZE_LIMIT: ${{ inputs.comment-size-limit }}
CONFIG_PATH: ${{ inputs.config }}
FINDINGS_ONLY: ${{ inputs.findings-only }}
MAX_ADDED: ${{ inputs.max-added }}
MAX_REMOVED: ${{ inputs.max-removed }}
MAX_VERSION_CHANGED: ${{ inputs.max-version-changed }}
FAIL_ON: ${{ inputs.fail-on }}
BASELINE: ${{ inputs.baseline }}
VERIFY_SIGNATURES: ${{ inputs.verify-signatures }}
Expand Down
73 changes: 72 additions & 1 deletion docs/src/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ groups the same information by behavior so it's easier to look up.

```text
bomdrift diff <BEFORE> <AFTER> [OPTIONS]
bomdrift init [--config-only] [--force]
bomdrift baseline add <ID> [--path <PATH>]
bomdrift refresh-typosquat [--ecosystem <ECOSYSTEM>]
```

Expand Down Expand Up @@ -48,6 +50,46 @@ Markdown-only. Emits just the summary table + a footer pointing at the
full output. Used by the action's comment-size fallback when the full
diff exceeds GitHub's 65,536-char comment-body cap.

#### `--findings-only`

Markdown-only. Keeps the summary table and risk-bearing sections
(vulnerabilities, typosquats, version jumps, young maintainers, license
changes) but omits raw Added / Removed / Version changed detail tables.
This is useful when a PR intentionally updates a large lockfile and
reviewers only want the actionable findings inline.

The counts still appear in the summary table, so churn is visible even
when the long per-dependency rows are hidden.

### Repo policy config

#### `--config <PATH>`

Load defaults from a `.bomdrift.toml` policy file. When omitted,
`bomdrift diff` auto-loads `.bomdrift.toml` from the current working
directory if it exists; missing default config is ignored. An explicit
`--config` path must exist and parse.

CLI flags override config values for one-off runs. Positive booleans in
config, such as `findings_only = true`, turn the behavior on; v0.6 does
not add parallel `--no-*` flags to turn those booleans off from the CLI.

Example:

```toml
[diff]
fail_on = "critical-cve"
baseline = ".bomdrift/baseline.json"
findings_only = true
max_added = 25
max_version_changed = 10
```

Supported `[diff]` keys map to the CLI flags: `output`, `format`,
`no_osv`, `no_osv_cache`, `baseline`, `no_maintainer_age`, `fail_on`,
`summary_only`, `findings_only`, `include_file_components`, `repo_url`,
`max_added`, `max_removed`, and `max_version_changed`.

### Enrichment flags

#### `--no-osv`
Expand Down Expand Up @@ -83,13 +125,22 @@ Exit with code 2 when findings of the configured threshold surface. One of:
many actively-exploited advisories ship as HIGH.
- `typosquat` — trips on any typosquat finding (always `severity = none`,
but the threshold lets you gate on the structural signal).
- `license-change` — trips on same-version license changes.
- `any` — trips on any finding (CVE, typosquat, version-jump,
maintainer-age) OR any license-changed-without-version-bump.

The PR-comment body is written to stdout **before** exit-2 — the action's
`tee` + `PIPESTATUS` wrapper relies on this so the comment posts even
when the workflow step fails.

#### Diff budgets

`--max-added <N>`, `--max-removed <N>`, and
`--max-version-changed <N>` fail the run with exit code 2 when a diff
exceeds the configured dependency-churn budget. The rendered body is
still written before exit, just like `--fail-on`, so GitHub Actions can
post the PR comment and then block the merge.

#### `--baseline <PATH>`

Path to a previously captured `bomdrift diff --output json` snapshot.
Expand All @@ -98,6 +149,26 @@ and from the `--fail-on` trip-evaluation. Match keys are conservative —
a finding at a different version than baseline still surfaces. See
[Baseline & suppression](./baseline.md) for full match-key semantics.

## `bomdrift init`

Scaffold a copy-paste adoption setup in the current repository:

```bash
bomdrift init
```

This writes:

- `.bomdrift.toml`
- `.github/workflows/sbom-diff.yml`
- `.github/workflows/bomdrift-suppress.yml`

Flags:

- `--config-only` — write only `.bomdrift.toml`.
- `--force` — overwrite existing generated files. Without `--force`,
existing files are preserved and the command fails loudly.

## `bomdrift refresh-typosquat`

Refresh the bundled typosquat top-package lists from upstream sources.
Expand Down Expand Up @@ -131,7 +202,7 @@ over the embedded snapshot when present and parseable.
|---|---|
| 0 | Success. |
| 1 | bomdrift internal error (parse failure, network mishap not gated by best-effort path, etc.). |
| 2 | `--fail-on` threshold tripped. The body is still on stdout — the action posts it before propagating the exit code. |
| 2 | `--fail-on` threshold or diff budget tripped. The body is still on stdout — the action posts it before propagating the exit code. |
| (clap 2) | Usage error from clap (unknown flag, missing required argument). Distinguishable from exit-2 from `--fail-on` by stderr containing `error: ...` rather than the v0.2 caveat warning. |

## Environment variables
Expand Down
2 changes: 2 additions & 0 deletions docs/src/enrichers/osv-cve.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ bomdrift diff before.json after.json --no-osv
| `none` | Never. |
| `cve` | Any vuln finding present (regardless of severity). |
| `critical-cve` | Any finding with `severity >= High` (covers HIGH and CRITICAL). |
| `typosquat` | Any typosquat finding; OSV findings do not trip it. |
| `license-change` | Any same-version license change; OSV findings do not trip it. |
| `any` | Any finding of any kind, plus license-changed-without-version-bump. |

The `critical-cve` name covers HIGH-or-CRITICAL because CRITICAL alone
Expand Down
Loading
Loading