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.9.0` / `@<sha>`
- **bomdrift version pin**: `@v1` / `@v0.9.5` / `@<sha>`
- **Runner**: <ubuntu-latest / self-hosted / etc.>
- **Trigger event**: <pull_request / push / workflow_dispatch / etc.>
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Pinned to MSRV (1.88) intentionally; bump deliberately when updating Cargo.toml rust-version.
name: CI

on:
Expand All @@ -14,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.88
with:
components: rustfmt
- run: cargo fmt --all --check
Expand All @@ -23,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.88
with:
components: clippy
- uses: Swatinem/rust-cache@v2
Expand All @@ -37,7 +38,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.88
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features

Expand All @@ -56,3 +57,25 @@ jobs:
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check

shell-bridges:
name: shell-bridges (parser tests + regex sync)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: comment-suppress parser unit tests
run: bash comment-suppress/test.sh
- name: suppress-regex sync (shell ↔ bridge JS)
run: bash scripts/check-suppress-regex-sync.sh
- name: bash syntax check (scripts/, comment-suppress/)
run: |
for f in scripts/*.sh comment-suppress/*.sh; do
[ -f "$f" ] || continue
bash -n "$f"
done
- name: node syntax check (bridge workers)
run: |
for f in examples/*/comment-bridge/worker.js; do
[ -f "$f" ] || continue
node --check "$f"
done
3 changes: 2 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Pinned to MSRV (1.88) intentionally; bump deliberately when updating Cargo.toml rust-version.
name: docs

on:
Expand Down Expand Up @@ -27,7 +28,7 @@ jobs:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@1.88

- uses: Swatinem/rust-cache@v2
with:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Pinned to MSRV (1.88) intentionally; bump deliberately when updating Cargo.toml rust-version.
name: release

# Triggered when a SemVer tag (e.g. v0.1.0) is pushed. Builds release binaries
Expand Down Expand Up @@ -100,7 +101,7 @@ jobs:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@1.88
with:
targets: ${{ matrix.target }}

Expand Down
106 changes: 106 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,112 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.9.5] - 2026-04-29

The "polish + multi-SCM parity" milestone. v0.9.5 ships the v0.9 follow-up
backlog that was deferred to v1.0 in the v0.9 changelog: per-exception
SPDX allow/deny granularity, exact-pinning of license-data dependencies,
a public synthetic-id parser for VEX consumers, and — most visibly —
**Bitbucket Cloud and Azure DevOps comment-driven suppression bridges**,
giving bomdrift comment-driven suppression parity across all four major
SCMs (GitHub, GitLab, Bitbucket, Azure DevOps).

### Added

- **Per-exception SPDX allow/deny.** New `[license] allow_exceptions` /
`deny_exceptions` arrays in `.bomdrift.toml` plus matching
`--allow-exception` / `--deny-exception` CLI flags. License expressions
using the `WITH` operator (e.g. `Apache-2.0 WITH LLVM-exception`) are
now evaluated at the exception level too, not just the base license.
Fail-closed semantics carry over: if `allow_exceptions` is non-empty
and the exception is not in it, the package is denied; if
`deny_exceptions` is non-empty and the exception IS in it, denied.
Empty lists preserve v0.9 behavior (exception treated as informational).
Surfaces in `LicenseViolation::matched_rule` as
`"exception:<id> denied"` / `"exception:<id> not in allow list"` and
is fingerprinted distinctly from base-license violations in SARIF.
- **Bitbucket Cloud comment-suppress bridge.** New
`examples/bitbucket-pipelines/comment-bridge/` (Cloudflare Worker
reference) implementing the same five guards as the GitLab bridge:
webhook UUID/HMAC verification, event-type filter
(`pullrequest:comment_created` only), repo allowlist, commenter-
permission check (Bitbucket Cloud REST API workspace permissions ≥
`write`), and PR-context guard (rejects fork-PR comment-suppress to
prevent untrusted forks suppressing findings on upstream).
- **Azure DevOps comment-suppress bridge.** New
`examples/azure-devops/comment-bridge/` covering the
`ms.vss-code.git-pullrequest-comment-event` Service Hook with
parallel guards: header-secret verification, event-type filter,
project-UUID allowlist, commenter-permission check (Azure DevOps
identities API), and protected-target-branch guard. Triggers an
Azure Pipeline run with `BOMDRIFT_NOTE_BODY` template parameter.
- **Public `bomdrift::vex::parse_synthetic_id` helper.** Round-trips
bomdrift's synthetic finding IDs (`bomdrift.typosquat:<purl>:<closest>`,
etc.) back to a structured `SyntheticFindingKind` enum. Lets external
VEX tooling identify which bomdrift finding a VEX statement targets
without string-splitting. Re-exported from `lib.rs` for downstream
Rust consumers.

### Changed

- **`spdx` crate exact-pinned to `=0.10.9`.** SPDX list updates can
shift `LicenseId.is_gnu()` / `is_osi_approved()` membership and
silently change license-policy semantics. v0.9.5 pins exactly so
bumps are deliberate; the v0.9 caret pin was changed to exact in
this release.
- **`BaselineEntry` and `ExpiredEntry` unified.** They overlapped on
`id`, `purl`, `expires`, `reason`. The two are now backed by a single
internal type with a status enum; the public `ExpiredEntry` alias is
preserved for back-compat. No behavior change; warning text on
expired entries unchanged.
- **CI Rust toolchain pinned to MSRV 1.88.** `dtolnay/rust-toolchain@1.88`
across all CI workflow jobs. Avoids surprises from newer clippy lints
(`cloned_ref_to_slice_refs`, `useless_vec`, `is_multiple_of` came in
Rust 1.94 and broke the v0.8 build until handled). Bump deliberately
via `Cargo.toml`'s `rust-version` field in lockstep.

### Refactored

- **Single source of truth for the `/bomdrift suppress <ID> [reason: ...]`
comment grammar.** New `scripts/parse-suppress-comment.sh` is the
canonical regex; `comment-suppress/entrypoint.sh` sources it,
`examples/gitlab-ci/comment-bridge/worker.js` mirrors it with a
pointer comment, and `scripts/check-suppress-regex-sync.sh` is wired
into CI to fail the build if the shell + JS copies drift. The Rust
`--from-comment` parser keeps its native regex but now carries a
doc-comment pointing at the canonical grammar.

### Documentation

- **GitLab note upsert + threading semantics** documented in
`docs/src/gitlab-ci.md` "How notes are upserted" section. Closes the
v0.7 plan's open question about whether the `PUT
/merge_requests/:id/notes/:note_id` upsert preserves reviewer-reply
threading (it does — note ID stays stable, replies survive, no
re-fired note hooks for unchanged content).
- **`docs/src/license-policy.md`** extended with the SPDX `WITH`
exceptions section + worked examples.
- **`docs/src/vex.md`** extended with the synthetic-id grammar and
`parse_synthetic_id` reference for external tooling authors.

### Tests

- 369 → 389 (+20) — covering exception allow/deny semantics,
synthetic-id round-trip, baseline-entry-unify regression guards, and
bridge regex sync checks.

### Scope notes

- The Rust `--from-comment` parser remains its own regex (Rust regex
syntax differs from POSIX/JS); CI guards the shell+JS copies but the
Rust copy is doc-linked, not auto-synced.
- PyPI/crates.io maintainer-set-changed enrichment (a v0.9 follow-up
for parity with the npm enricher) stayed deferred — neither PyPI's
nor crates.io's REST API exposes maintainer history cleanly.
- Bridge worker.js files stay user-deployed (Cloudflare Worker /
Vercel / Netlify / AWS Lambda Edge); bomdrift the binary still does
not run a webhook server.

## [0.9.0] - 2026-05-01

The "interoperability + breadth" milestone. v0.9 adds VEX (Vulnerability
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bomdrift"
version = "0.9.0"
version = "0.9.5"
edition = "2024"
rust-version = "1.88"
description = "SBOM diff with supply-chain risk signals (CVEs, typosquats, maintainer-age)."
Expand Down Expand Up @@ -33,7 +33,8 @@ 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 }
# Exact-pinned: SPDX list updates can shift LicenseId.is_gnu() / is_osi_approved membership and silently change license-policy semantics. Bump deliberately.
spdx = { version = "=0.10.9", default-features = false }

[dev-dependencies]
criterion = { version = "0.5", default-features = false, features = ["html_reports"] }
Expand Down
8 changes: 4 additions & 4 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.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.
Pin to `@v1` for the latest v0.x; pin to `@v0.9.5` 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.9.0
VERSION=v0.9.5
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.9.0 bomdrift
cargo install --locked --git https://github.com/Metbcy/bomdrift --tag v0.9.5 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.9.0
VERSION=v0.9.5
TARGET=x86_64-unknown-linux-gnu
ARCHIVE=bomdrift-${VERSION}-${TARGET}.tar.gz

Expand Down
4 changes: 2 additions & 2 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ keeping the project OSS-first: no hosted dashboard, no account, no telemetry.
| Suppression expiry (`expires` + `reason`) | Supported (v0.8+) — time-boxed risk acceptance |
| 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 Pipelines | Supported (v0.9+) — `examples/bitbucket-pipelines/` |
| Azure DevOps Pipelines | Supported (v0.9+) — `examples/azure-devops/` |
| Bitbucket Pipelines | Supported (v0.9+) — `examples/bitbucket-pipelines/`; comment-driven suppression via Cloudflare Worker bridge (v0.9.5+) |
| Azure DevOps Pipelines | Supported (v0.9+) — `examples/azure-devops/`; comment-driven suppression via Cloudflare Worker bridge (v0.9.5+) |
| 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 |
Expand Down
49 changes: 23 additions & 26 deletions comment-suppress/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@

set -euo pipefail

# Suppress-directive grammar lives in scripts/parse-suppress-comment.sh
# (single source of truth shared with the GitLab/Bitbucket/Azure DevOps
# Cloudflare Worker bridges). CI guard: scripts/check-suppress-regex-sync.sh.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../scripts/parse-suppress-comment.sh
source "$SCRIPT_DIR/../scripts/parse-suppress-comment.sh"

REPO="Metbcy/bomdrift"
GH_DL="https://github.com/${REPO}/releases/download"
# REPO above is the bomdrift project (where the release archives live).
Expand Down Expand Up @@ -56,39 +63,29 @@ if [ "$is_pr_comment" != "true" ]; then
fi

comment_body="$(jq -r '.comment.body' "$event_path")"
if [[ ! "$comment_body" =~ ^/bomdrift[[:space:]]+suppress[[:space:]]+ ]]; then
notice "skipping: comment body does not start with '/bomdrift suppress'"
if ! grep -qE '^[[:space:]]*/bomdrift[[:space:]]+suppress[[:space:]]+' <<< "$comment_body"; then
notice "skipping: comment body does not contain '/bomdrift suppress'"
exit 0
fi

# Parse out the advisory ID. Allow an optional trailing newline / extra args.
advisory_id="$(printf '%s\n' "$comment_body" | awk 'NR==1 { print $3 }')"
# Single source of truth: scripts/parse-suppress-comment.sh.
# rc=0 → matched, rc=1 → no directive, rc=2 → matched but malformed ID.
set +e
parse_bomdrift_suppress "$comment_body"
parse_rc=$?
set -e
case "$parse_rc" in
0) advisory_id="$BOMDRIFT_PARSED_ID"; reason="$BOMDRIFT_PARSED_REASON" ;;
1) notice "skipping: comment body does not contain a /bomdrift suppress directive"
exit 0 ;;
2) fail "advisory id does not match (GHSA|CVE|MAL|OSV)-... shape: ${BOMDRIFT_PARSED_ID}" ;;
*) fail "internal error: parse_bomdrift_suppress returned $parse_rc" ;;
esac

if [ -z "$advisory_id" ]; then
fail "could not parse advisory id from comment body: $comment_body"
fi

# Optional `reason: <free text>` line in the comment body. v0.8+: when
# present, the entry is recorded in the v0.8 object form so the reason
# 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 \
| sed -E 's/^\s*[Rr]eason:\s*//')"

# Validate it looks like an advisory id we'd expect from OSV.dev. Reject
# anything else early so a typo doesn't turn into a no-op suppress with no
# user-visible feedback.
if [[ ! "$advisory_id" =~ ^(GHSA-[a-z0-9-]+|CVE-[0-9]{4}-[0-9]+|MAL-[0-9]{4}-[0-9]+)$ ]]; then
fail "advisory id does not match GHSA-/CVE-/MAL- shape: $advisory_id"
fi

pr_number="$(jq -r '.issue.number' "$event_path")"
comment_id="$(jq -r '.comment.id' "$event_path")"
commenter="$(jq -r '.comment.user.login' "$event_path")"
Expand Down
Loading
Loading