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
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
branches: [main]
pull_request:

permissions:
contents: read
pull-requests: write

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
Expand Down Expand Up @@ -42,6 +46,56 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features

# Informational coverage report. Emits lcov.info as an artifact and
# posts a sticky PR comment with the line-coverage %. Intentionally
# NOT in the required-status-checks list and NOT using
# `--fail-under-lines` — let the number be visible across 2-3
# releases before ratcheting (see CONTRIBUTING.md "Coverage").
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.88
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
run: cargo install cargo-llvm-cov --locked
- name: Run coverage
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
- name: Upload lcov artifact
uses: actions/upload-artifact@v4
with:
name: lcov.info
path: lcov.info
retention-days: 30
- name: Compute coverage %
id: compute
run: |
covered=$(grep "^DA:" lcov.info | awk -F, '$2>0' | wc -l)
total=$(grep -c "^DA:" lcov.info)
if [ "$total" -gt 0 ]; then
pct=$(awk "BEGIN{printf \"%.1f\", ${covered}*100/${total}}")
else
pct="N/A"
fi
echo "pct=$pct" >> "$GITHUB_OUTPUT"
echo "covered=$covered" >> "$GITHUB_OUTPUT"
echo "total=$total" >> "$GITHUB_OUTPUT"
- name: Post coverage summary as PR comment
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: bomdrift-coverage
message: |
## Coverage report

Line coverage: **${{ steps.compute.outputs.pct }}%** (${{ steps.compute.outputs.covered }} / ${{ steps.compute.outputs.total }} lines)

Full lcov report available as workflow artifact `lcov.info`.

<sub>v0.9.8 introduces this report; `--fail-under-lines` will be added once coverage is visible across 2–3 releases.</sub>

audit:
runs-on: ubuntu-latest
steps:
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Fuzz

# cargo-fuzz requires nightly Rust, so this lives in its own workflow
# rather than bolting onto ci.yml's stable-pinned matrix. PR runs use
# a 60s budget per target (signal: did the seed corpus crash?); the
# weekly schedule extends to 600s for deeper coverage.
on:
pull_request:
paths:
- 'src/parse/**'
- 'fuzz/**'
- '.github/workflows/fuzz.yml'
schedule:
- cron: '17 3 * * 0' # Sundays 03:17 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
fuzz:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [parse_cyclonedx, parse_spdx, parse_syft]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
workspaces: fuzz
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Run fuzz target
run: |
BUDGET=60
if [ "${{ github.event_name }}" = "schedule" ]; then
BUDGET=600
fi
cd fuzz
cargo +nightly fuzz run ${{ matrix.target }} -- -max_total_time=$BUDGET
- name: Upload artifacts on crash
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-${{ matrix.target }}-artifacts
path: fuzz/artifacts/
80 changes: 80 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,86 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.9.8] - 2026-04-30

The "code-review-driven hardening" milestone. External agent review surfaced
nine recommendations across P1/P2/P3; v0.9.8 takes five of the six high-leverage
items, defers the rest as v1.0+ candidates with explicit rationale.

### Added

- **Continuous parser fuzzing.** New `fuzz/` standalone sub-workspace with
three `cargo-fuzz` libfuzzer targets (`parse_cyclonedx`, `parse_spdx`,
`parse_syft`). Each target two-stages JSON validation before invoking the
bomdrift parser, scoping the fuzz to bomdrift-side parsing rather than
serde_json's well-tested layer. Seed corpus from `tests/fixtures/`.
New `.github/workflows/fuzz.yml` runs each target for 60s on PRs touching
`src/parse/**` or `fuzz/**`, and 600s weekly on a Sunday cron schedule.
Nightly Rust toolchain pinned per cargo-fuzz convention. Closes the
textbook "untrusted-input parser" gap for a security tool.
- **CI coverage report.** New `coverage` job in `.github/workflows/ci.yml`
runs `cargo-llvm-cov`, emits `lcov.info` as a workflow artifact, and posts
a sticky PR comment via `marocchino/sticky-pull-request-comment@v2` showing
line-coverage percentage. **No `--fail-under-lines` gate yet** — coverage
is informational for v0.9.8/v0.9.9 to establish a stable baseline before
ratcheting in a later release. CONTRIBUTING.md gains a "Coverage" subsection
describing the policy.

### Changed

- **`unwrap`/`expect`/`panic`/`todo`/`unimplemented` lints now warn** at
crate root via `#![warn(clippy::unwrap_used, clippy::expect_used,
clippy::panic, clippy::todo, clippy::unimplemented)]`. Production code
audited; the four remaining `.expect()` sites
(`baseline.rs:389`, `render/json.rs:42`, `render/sarif.rs:84`,
`vex.rs:932`) are true invariants and gain explicit
`#[allow(clippy::expect_used, reason = "...")]` annotations citing the
why. Test modules opt-out via inner `#![allow(clippy::unwrap_used,
clippy::expect_used)]` (28 modules touched). Zero production `.unwrap()`
remain.
- **Every `unsafe` block now carries a `// SAFETY:` comment.**
16 of 23 unsafe sites (the Rust 2024 `env::set_var` wrappers + a few
test helpers) lacked annotation. Added rationale to each, and enforce
going forward via `#![warn(clippy::undocumented_unsafe_blocks)]` at
crate root.

### Refactored

- **`src/lib.rs` 47 KB → 31 lines.** Extracted the 1,300-line `run_diff`
orchestration into a new `src/run.rs` module. `lib.rs` is now pure
re-exports + module declarations. Public API surface preserved
byte-for-byte: external consumers calling `bomdrift::run_diff(...)` get
the same function via re-export. Behavior-preserving — all 432 tests
pass without modification beyond import-path updates.

### Documentation

- README.md gains a "Continuous fuzzing (v0.9.8+)" subsection.
- CONTRIBUTING.md gains a "Coverage" subsection.

### Tests

- 432 → 432 (no net change). Refactor commits are behavior-preserving.

### Scope notes — what's deferred to v1.0

The external review surfaced four other recommendations explicitly deferred:

- **Remaining file splits** for `vex.rs` (50 KB), `render/markdown.rs`
(58 KB), `render/sarif.rs` (48 KB), `baseline.rs` (42 KB),
`enrich/typosquat.rs` (42 KB), `enrich/license.rs` (34 KB). The lib.rs
split was the highest-ROI single split; the others can land
organically as future PRs touch those files.
- **Mutation testing audit** via `cargo-mutants`. High-signal but slow;
use as v1.0 audit tool, not a CI gate.
- **Calibration FPR docs** — running bomdrift on top-1000 npm + PyPI for
12 months of releases needs data-collection infrastructure that
doesn't exist yet. Tracked separately.
- **Coverage `--fail-under-lines` ratchet** — flip on after 2-3 releases
of visibility.
- **WASM-sandboxed plugin model** — carryover from v0.9.7; conflicts with
single-binary tenet at current toolchain costs.

## [0.9.7] - 2026-04-29

The "v0.9.6 follow-up backlog" milestone. Five concrete items from the
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ Network-touching enrichers should have a unit test for the network-
failure path (fake fetcher returns `Err`) — the best-effort contract
matters and silently breaking it would be an easy regression.

### Coverage (v0.9.8+)

CI runs `cargo llvm-cov` on every PR and posts a sticky comment with
the overall line coverage % (the full `lcov.info` is uploaded as a
workflow artifact). The job is informational for now — there is no
`--fail-under-lines` threshold yet. The plan is to add a ratchet in
v0.9.9 once 2–3 releases have made the baseline visible. Until then,
the report is a nudge, not a gate; PRs that move coverage in the
wrong direction without justification will get a review comment, not
a red check.

### Test conventions (v0.9.5+)

Tests that mutate `SOURCE_DATE_EPOCH` (directly or indirectly via
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.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bomdrift"
version = "0.9.7"
version = "0.9.8"
edition = "2024"
rust-version = "1.88"
description = "SBOM diff with supply-chain risk signals (CVEs, typosquats, maintainer-age)."
Expand Down
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Recent incidents bomdrift would have surfaced:

The dimensions adopters actually filter on. Sourced from
[`files/competitor-research-v0.7-v0.9.md`](./files/competitor-research-v0.7-v0.9.md);
correct as of v0.9.7.
correct as of v0.9.8.

| | bomdrift | Socket | Snyk | Trivy | OSV-Scanner | Grype |
|------------------------------------------|:---:|:---:|:---:|:---:|:---:|:---:|
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
# verify-signatures: true (set false on trusted mirrors)
```

Pin to `@v1` for the latest v0.x; pin to `@v0.9.7` 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 — including `upload-to-code-scanning`, `verify-signatures`, `comment-size-limit`, and the `before-sbom`/`after-sbom` escape hatch.
Pin to `@v1` for the latest v0.x; pin to `@v0.9.8` 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 — including `upload-to-code-scanning`, `verify-signatures`, `comment-size-limit`, and the `before-sbom`/`after-sbom` escape hatch.

**Other forges:** GitLab CI, Bitbucket Pipelines, and Azure DevOps Pipelines all have ready-to-copy templates under [`examples/`](./examples/) and dedicated docs chapters: [GitLab CI](https://metbcy.github.io/bomdrift/gitlab-ci.html), [Bitbucket](https://metbcy.github.io/bomdrift/bitbucket.html), [Azure DevOps](https://metbcy.github.io/bomdrift/azure-devops.html). Comment-driven `/bomdrift suppress` works on all four SCMs via the Cloudflare Worker bridges added in v0.9.5.

Expand Down Expand Up @@ -127,7 +127,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.7
VERSION=v0.9.8
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 @@ -143,7 +143,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.7 bomdrift
cargo install --locked --git https://github.com/Metbcy/bomdrift --tag v0.9.8 bomdrift
```

Requires Rust 1.85+ (the project uses edition 2024).
Expand Down Expand Up @@ -279,7 +279,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.7
VERSION=v0.9.8
TARGET=x86_64-unknown-linux-gnu
ARCHIVE=bomdrift-${VERSION}-${TARGET}.tar.gz

Expand All @@ -293,6 +293,16 @@ cosign verify-blob \

The Action verifies signatures automatically by default. Set `verify-signatures: false` on trusted mirrors to skip the cosign install step (~15s saved per run).

### Continuous fuzzing (v0.9.8+)

The CycloneDX, SPDX, and Syft JSON parsers are continuously fuzzed
via [`cargo-fuzz`](https://rust-fuzz.github.io/book/cargo-fuzz/).
Pull requests touching `src/parse/**` get a short fuzz pass per
target on Linux nightly; a longer scheduled run executes weekly on
`main`. Crash artifacts are uploaded for triage.
See [`.github/workflows/fuzz.yml`](./.github/workflows/fuzz.yml) and
[`fuzz/fuzz_targets/`](./fuzz/fuzz_targets/).

## Documentation

- **[Docs site (mdBook)](https://metbcy.github.io/bomdrift/)** — full reference: CLI flags, every action input, output-format anatomy, per-enricher deep dives, architecture notes, roadmap.
Expand Down
6 changes: 3 additions & 3 deletions docs/src/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
```

The `@v1` mutable tag tracks the latest v0.x release. Pin to a specific
version (`@v0.9.7`) if you prefer reproducible builds. See
version (`@v0.9.8`) if you prefer reproducible builds. See
[GitHub Action](./github-action.md) for every input.

If you prefer a checked-in policy file, install the binary and run
Expand All @@ -39,7 +39,7 @@ 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.7
VERSION=v0.9.8
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 @@ -60,7 +60,7 @@ To verify the archive's signature before you trust the binary, see
## From source

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

Requires Rust 1.85+ (the project uses edition 2024).
Expand Down
5 changes: 5 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# cargo-fuzz / libFuzzer build + crash artifacts. The seed corpus
# under corpus/ IS checked in (those are inputs, not outputs).
/target
/artifacts
Cargo.lock
43 changes: 43 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[package]
name = "bomdrift-fuzz"
version = "0.0.0"
publish = false
edition = "2024"
rust-version = "1.88"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
serde_json = "1"

[dependencies.bomdrift]
path = ".."

# Prevent cargo from treating this directory as part of the parent
# workspace. cargo-fuzz expects fuzz/ to be its own workspace so the
# nightly toolchain (required for libfuzzer instrumentation) doesn't
# leak into the main build.
[workspace]

[[bin]]
name = "parse_cyclonedx"
path = "fuzz_targets/parse_cyclonedx.rs"
test = false
doc = false
bench = false

[[bin]]
name = "parse_spdx"
path = "fuzz_targets/parse_spdx.rs"
test = false
doc = false
bench = false

[[bin]]
name = "parse_syft"
path = "fuzz_targets/parse_syft.rs"
test = false
doc = false
bench = false
Loading
Loading