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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/action-broke.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ failure is usually obvious if you expand all groups. -->

## Environment

- **bomdrift version pin**: `@v1` / `@v0.8.0` / `@<sha>`
- **bomdrift version pin**: `@v1` / `@v0.9.0` / `@<sha>`
- **Runner**: <ubuntu-latest / self-hosted / etc.>
- **Trigger event**: <pull_request / push / workflow_dispatch / etc.>
101 changes: 101 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,107 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.9.0] - 2026-05-01

The "interoperability + breadth" milestone. v0.9 adds VEX (Vulnerability
Exploitability eXchange) consumption + emission, full SPDX expression
evaluation, multi-SCM templates (Bitbucket Pipelines + Azure DevOps),
registry-metadata enrichers (npm/PyPI/crates.io), and a security-reviewed
GitLab comment-driven suppression bridge.

### Added

- **VEX consume (`--vex <path>`, repeatable).** Auto-detects OpenVEX
0.2.0 vs CycloneDX VEX 1.6 per file. Statements with status
`not_affected` / `fixed` suppress matching findings (counted in the
new "Suppressed by VEX" markdown summary row); `under_investigation`
annotates with a `VEX:` badge in markdown and
`properties.vexStatus` in SARIF; `affected` annotates as a no-op.
Match keys are `(VulnRef.id OR alias, purl_with_version)` with a
documented synthetic-id convention for non-CVE finding kinds
(`bomdrift.<kind>:<purl>:<discriminator>`).
- **VEX emit (`--emit-vex <path>`).** Writes a single OpenVEX 0.2.0
document covering baseline-suppressed entries (status from the
per-entry `vex_status`, defaulting to `under_investigation` —
baseline ≠ "not affected", never auto-promoted) and un-suppressed
findings (status `affected` with `status_notes` describing the
finding kind). New baseline fields `vex_status` and
`vex_justification`. New `[diff] vex_author` and
`[diff] vex_default_justification` config keys.
- **Full SPDX expression evaluator** via the `spdx = "0.10"` crate.
Replaces v0.8's atomic+glob matcher: `(MIT OR Apache-2.0)` with
`allow=[MIT]` permits; `(MIT AND GPL-3.0-only)` with
`deny=[GPL-3.0-only]` violates; `Apache-2.0 WITH LLVM-exception`
parses cleanly (base license checked, exception identity
informational only). Non-SPDX strings fall back to the v0.8
atomic+glob path. `allow_ambiguous` is deprecated with a one-time
stderr warning.
- **Bitbucket Pipelines + Azure DevOps Pipelines** support.
`Platform::Bitbucket` and `Platform::AzureDevOps` variants on the
CLI; auto-detection via `BITBUCKET_BUILD_NUMBER` and `TF_BUILD`
envs; `BITBUCKET_GIT_HTTP_ORIGIN` and `BUILD_REPOSITORY_URI`
honored as `--repo-url` fallbacks. Per-platform footer URL shapes
(`/issues/new` for Bitbucket; `/_workitems/create` for Azure
DevOps). Drop-in templates with READMEs in
`examples/bitbucket-pipelines/` and `examples/azure-devops/`.
- **Registry-metadata enrichers (`src/enrich/registry.rs`).** Three
best-effort fetchers — npm, PyPI, crates.io — with disk cache at
`<XDG_CACHE>/bomdrift/registry/<eco>/<pkg>.json` (24h TTL,
atomic temp-file + rename). Three new finding kinds:
`RecentlyPublished` (default <14d threshold, tunable via
`--recently-published-days`), `Deprecated` (npm
`versions[].deprecated`, PyPI `info.yanked` + Inactive
classifiers, crates.io `versions[].yanked`), and
`MaintainerSetChanged` (npm only — PyPI/crates.io don't expose
per-version maintainers cleanly). New `--no-registry` flag and
`[diff] no_registry = true` config key. New `--fail-on
recently-published` and `--fail-on deprecated` thresholds. New
SARIF rules `bomdrift.recently-published`, `bomdrift.deprecated`,
`bomdrift.maintainer-set-changed` with stable
`partialFingerprints.primaryHash/v1`.
- **GitLab comment-driven suppress.** `bomdrift baseline add
--from-comment <BODY>` parses the raw note body, extracts the
first `/bomdrift suppress <ID>[ reason: <text>]` directive, and
fails non-zero with a clear stderr message when no directive is
found (so a misconfigured webhook bridge fails loudly).
`examples/gitlab-ci/comment-bridge/` ships a Cloudflare Worker
reference implementation enforcing five guards: webhook secret
(constant-time compare), event-type filter, project-ID
allowlist, commenter access_level >= 30, and an MR-context
guard that rejects fork-MR exfiltration. Vercel/Netlify/Lambda
port note included.
- **Explicit non-goals + pair-with recommendations** in README,
STATUS, and the roadmap. Reachability, tarball static analysis,
auto-fix PR generation, continuous monitoring, container
scanning, SAST/secrets, risk-score dashboards, and closed-source
advisory feeds are deliberately out of scope; each is paired with
a recommended complementary tool.

### Changed

- OSV cache schema extended with `aliases: Vec<String>` so cache
hits no longer drop alias data. Old cache entries without the
field deserialize with an empty vec (graceful migration).
- `Command::Diff` argument is now boxed (`Box<DiffArgs>`) to satisfy
clippy's large-enum-variant lint after the v0.9 flag growth.
Internal-only change; no user-facing impact.
- `BaselineAddArgs.id` becomes optional (`Option<String>`) to allow
`--from-comment` invocations without a positional ID. Existing
positional callers are unchanged.

### Scope notes (deferred)

- **Per-exception SPDX allow/deny** — the WITH-exception identifier
is currently informational only; allow/deny narrows to the base
license.
- **PyPI / crates.io maintainer-set-changed** — blocked on per-version
maintainer data in upstream APIs.
- **Bitbucket / Azure DevOps comment-driven suppression** — only the
diff path ships in v0.9; comment-suppress is GitHub-only (and
GitLab via the bridge).
- **VEX vocabulary beyond OpenVEX's 8 justifications** — bomdrift
uses the spec enum verbatim; no extension yet.

## [0.8.0] - 2026-04-29

The "supply-chain hardening" milestone. v0.8 finishes SARIF for GitHub
Expand Down
12 changes: 11 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bomdrift"
version = "0.8.0"
version = "0.9.0"
edition = "2024"
rust-version = "1.88"
description = "SBOM diff with supply-chain risk signals (CVEs, typosquats, maintainer-age)."
Expand Down Expand Up @@ -33,6 +33,7 @@ directories = "6"
toml = "0.8"
time = { version = "0.3", default-features = false, features = ["serde", "parsing", "formatting", "macros", "std"] }
sha2 = { version = "0.10", default-features = false }
spdx = { version = "0.10", default-features = false }

[dev-dependencies]
criterion = { version = "0.5", default-features = false, features = ["html_reports"] }
Expand Down
63 changes: 56 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
# verify-signatures: true (set false on trusted mirrors)
```

Pin to `@v1` for the latest v0.x; pin to `@v0.8.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.
Pin to `@v1` for the latest v0.x; pin to `@v0.9.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 @@ -112,7 +112,7 @@ Comment `/bomdrift suppress GHSA-xxxx` on any PR; the sub-action appends to `.bo
Pre-built binaries cover Linux x86_64 + aarch64, macOS aarch64, and Windows x86_64. Each archive is cosign-signed via Sigstore + GitHub OIDC.

```bash
VERSION=v0.8.0
VERSION=v0.9.0
TARGET=x86_64-unknown-linux-gnu
curl -sSL -o bomdrift.tar.gz \
"https://github.com/Metbcy/bomdrift/releases/download/${VERSION}/bomdrift-${VERSION}-${TARGET}.tar.gz"
Expand All @@ -128,7 +128,7 @@ Verify the archive's signature before you trust the binary — see [Release sign
### From source

```bash
cargo install --locked --git https://github.com/Metbcy/bomdrift --tag v0.8.0 bomdrift
cargo install --locked --git https://github.com/Metbcy/bomdrift --tag v0.9.0 bomdrift
```

Requires Rust 1.85+ (the project uses edition 2024).
Expand Down Expand Up @@ -230,7 +230,7 @@ Every release archive is signed with cosign keyless via Sigstore (GitHub OIDC).

```bash
# Replace VERSION + TARGET with your downloaded archive's pair
VERSION=v0.8.0
VERSION=v0.9.0
TARGET=x86_64-unknown-linux-gnu
ARCHIVE=bomdrift-${VERSION}-${TARGET}.tar.gz

Expand All @@ -257,9 +257,58 @@ PRs welcome. The `good first issue` label tracks focused asks for new contributo

## Non-goals

- **SBOM generation.** Use [Syft](https://github.com/anchore/syft) — it's already great. bomdrift only consumes SBOMs (and as of v0.5 invokes Syft itself inside the Action so consumers don't have to).
- **Dependency-tree visualization.** [`cargo tree`](https://doc.rust-lang.org/cargo/commands/cargo-tree.html), [`pnpm why`](https://pnpm.io/cli/why), and friends do this well.
- **Replacing your SCA scanner.** OSV-scanner, Grype, Trivy all have richer vulnerability databases. bomdrift's CVE enrichment is *change-focused*: only on what's new in this diff.
bomdrift's design constraints (OSS-first, single-binary, no
telemetry, change-focused) put a number of capabilities deliberately
out of scope. We don't ship them, but we recommend pairing bomdrift
with tools that do.

- **SBOM generation.** Use [Syft](https://github.com/anchore/syft) —
it's already great. bomdrift only consumes SBOMs (and as of v0.5
invokes Syft itself inside the Action so consumers don't have to).
- **Dependency-tree visualization.**
[`cargo tree`](https://doc.rust-lang.org/cargo/commands/cargo-tree.html),
[`pnpm why`](https://pnpm.io/cli/why), and friends do this well.
- **Reachability / call-graph analysis.** "Is this CVE reachable
from my code's entry points?" requires AST + call-graph
infrastructure orthogonal to SBOM diffing. *Pair with Endor Labs
or Snyk Reachability.*
- **Static analysis of registry tarballs.** Detecting malicious code
inside a published package needs a sandbox + behavior heuristics.
*Pair with [Socket](https://socket.dev/).*
- **Auto-fix PR generation.** bomdrift surfaces findings; it doesn't
open follow-up PRs. *Pair with Renovate or Dependabot.*
- **Continuous monitoring / always-on agent.** bomdrift is a
one-shot CLI invoked from CI. There's no daemon, no telemetry, no
scheduled background polling. *Run bomdrift in a scheduled CI
workflow if you want periodic re-checks.*
- **Container / OCI image scanning.** SBOM + image-layer scanning is
Trivy / Grype's lane. Use them; bomdrift focuses on
application-dependency drift between two SBOMs.
- **SAST / secrets scanning.** Different problem space; well
served by GitHub Advanced Security, Semgrep, or gitleaks.
- **Risk-score dashboards / asset-context aggregation.** Cross-repo
dashboards inevitably require telemetry, which violates bomdrift's
no-telemetry tenet. *Pair with Endor / Snyk if your org needs
centralized risk reporting.*
- **Closed-source advisory databases.** bomdrift uses OSV.dev (the
open advisory aggregator). Closed proprietary feeds aren't
consumed in the OSS distribution.
- **Replacing your SCA scanner.** OSV-scanner, Grype, Trivy all
have richer vulnerability databases for *full-tree* scans.
bomdrift's CVE enrichment is **change-focused**: only on what's
new in this diff.

### Pair with…

| Need | Recommended tool |
|---|---|
| Reachability analysis | Endor Labs, Snyk Reachability |
| Tarball / behavior analysis | Socket |
| Auto-fix PRs | Renovate, Dependabot |
| Container image scans | Trivy, Grype |
| SAST / secrets | GitHub Advanced Security, Semgrep, gitleaks |
| Cross-repo risk dashboards | Endor, Snyk |
| SBOM generation | Syft (bomdrift bundles this in the Action) |

## License

Expand Down
29 changes: 26 additions & 3 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,35 @@ keeping the project OSS-first: no hosted dashboard, no account, no telemetry.
| CISA KEV (known-exploited) flagging | Supported (v0.8+) — auto, opt-out via `--no-kev` |
| License allow/deny policy | Supported (v0.8+) — `[license]` block / CLI flags |
| Suppression expiry (`expires` + `reason`) | Supported (v0.8+) — time-boxed risk acceptance |
| GitLab CI merge requests | Supported through the `examples/gitlab-ci/` template (v0.7+); in-comment suppression deferred to v0.9 |
| GitLab CI merge requests | Supported through the `examples/gitlab-ci/` template (v0.7+); comment-driven suppression supported via Cloudflare Worker bridge (v0.9+) |
| GitHub Enterprise / self-hosted runners | Expected to work, not broadly tested yet |
| Bitbucket / Azure DevOps | Planned for v0.9 |
| VEX consume / emit | Planned for v0.9 |
| Bitbucket Pipelines | Supported (v0.9+) — `examples/bitbucket-pipelines/` |
| Azure DevOps Pipelines | Supported (v0.9+) — `examples/azure-devops/` |
| VEX consume / emit | Supported (v0.9+) — OpenVEX 0.2.0 + CycloneDX VEX 1.6 |
| SPDX expression evaluation | Supported (v0.9+) — full `Expression::evaluate` via `spdx` crate |
| Registry-metadata enrichers (npm/PyPI/crates.io) | Supported (v0.9+) — recently-published, deprecated, maintainer-set-changed |
| Hosted dashboard / SaaS | Not planned |

## Out-of-scope by design

bomdrift's design constraints (OSS-first, single-binary, no
telemetry, change-focused) put a number of capabilities deliberately
out of scope. Pair bomdrift with the suggested complementary tools
when you need them — see the README's
[Non-goals](https://github.com/Metbcy/bomdrift#non-goals) section
for the rationale.

| Out-of-scope | Pair with |
|---|---|
| Reachability / call-graph analysis | Endor Labs, Snyk Reachability |
| Tarball / behavior analysis | Socket |
| Auto-fix PR generation | Renovate, Dependabot |
| Container / OCI image scanning | Trivy, Grype |
| SAST / secrets scanning | GitHub Advanced Security, Semgrep, gitleaks |
| Risk-score dashboards (cross-repo) | Endor, Snyk |
| Continuous monitoring / always-on agent | Run bomdrift in scheduled CI |
| Closed-source advisory feeds | bomdrift uses OSV.dev only |

## Known limitations

- The zero-config Action path is built for `pull_request` workflows. For
Expand Down
5 changes: 5 additions & 0 deletions comment-suppress/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ fi
# is preserved alongside the advisory id. Pattern matches the start of
# any line (case-insensitive) so reviewers can write
# `reason: awaiting upstream patch (issue #42)` on a continuation line.
#
# This shell parser MUST stay in lockstep with the Rust
# `baseline::parse_comment_directive` parser used by the GitLab
# webhook bridge (`bomdrift baseline add --from-comment`). Any
# grammar change has to land in both places.
reason="$(printf '%s\n' "$comment_body" \
| grep -iE '^\s*reason:\s*' \
| head -n1 \
Expand Down
4 changes: 4 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
- [Quickstart](./quickstart.md)
- [GitHub Action](./github-action.md)
- [GitLab CI](./gitlab-ci.md)
- [Bitbucket Pipelines](./bitbucket.md)
- [Azure DevOps Pipelines](./azure-devops.md)
- [CLI reference](./cli-reference.md)

# Output

- [Output formats](./output-formats.md)
- [SARIF + Code Scanning](./sarif.md)
- [VEX](./vex.md)
- [License policy](./license-policy.md)
- [Baseline & suppression](./baseline.md)

Expand All @@ -25,6 +28,7 @@
- [Typosquat detection](./enrichers/typosquat.md)
- [Multi-major version jumps](./enrichers/version-jump.md)
- [Maintainer age signal](./enrichers/maintainer-age.md)
- [Registry metadata (npm/PyPI/crates.io)](./enrichers/registry.md)

# Operations

Expand Down
49 changes: 49 additions & 0 deletions docs/src/azure-devops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Azure DevOps Pipelines

bomdrift runs in Azure Pipelines and posts a single upserted PR
thread per pull request.

## Quickstart

Copy [`examples/azure-devops/azure-pipelines.yml`](https://github.com/Metbcy/bomdrift/blob/main/examples/azure-devops/azure-pipelines.yml)
to your repo root and add a secret pipeline variable named
`BOMDRIFT_API_TOKEN` containing a PAT with the `Code (Read & Write)`
scope.

## What the job does

1. Installs Rust + bomdrift + Syft on the `ubuntu-latest` agent.
2. Generates a CycloneDX SBOM for the PR target branch and the PR
head.
3. Renders the diff to markdown with `bomdrift diff --platform
azure-devops`.
4. Looks up the existing bomdrift PR thread (by the
`<!-- bomdrift:diff -->` marker) and either creates a new thread
or updates the existing comment.

## Tokens & permissions

| Variable | Scope | Why |
|---|---|---|
| `BOMDRIFT_API_TOKEN` | PAT, `Code (Read & Write)` | Creating / updating PR threads. |

The default `System.AccessToken` is **not** used because most
organizations don't grant it permission to create PR threads.

## CLI auto-detection

Setting `TF_BUILD=true` (Azure Pipelines sets this on every job)
auto-selects `--platform azure-devops` when the flag is omitted.

`BUILD_REPOSITORY_URI` is honored as a `--repo-url` fallback. Note
that this variable is empty for some local debug runs; passing
`--repo-url` explicitly is fine.

## Suppressions

Comment-driven suppression is not wired up for Azure DevOps in v0.9.
Use `bomdrift baseline add` and commit the result.

## Troubleshooting

See [`examples/azure-devops/README.md`](https://github.com/Metbcy/bomdrift/blob/main/examples/azure-devops/README.md).
Loading
Loading