Releases: Metbcy/bomdrift
v0.9.9
[0.9.9] - 2026-04-30
The "distribution release." Phase 1 of the bomdrift adoption plan: every
plausible install path now works in one command. The release pipeline
gains four new outputs — crates.io, ghcr.io, SLSA build provenance, and
an automated v1 major-tag retag — alongside the existing GitHub
Release + cosign-signed archives. No source-code feature work; the
v0.9.9 binary is functionally identical to the v0.9.8 binary.
Added
cargo install bomdriftworks. The crate is now published to
crates.io. Cargo.toml gainsdocumentation = "https://docs.rs/bomdrift"
and a[package.metadata.docs.rs]block so the auto-built docs.rs
page renders cleanly. The published-crate footprint is 54 files /
220 KiB compressed, achieved via anexcludelist trimmingtests/
(2.7 MB of fixtures),docs/(mdbook source),examples/,
benches/,fuzz/,comment-suppress/,scripts/,.github/, the
GitHub Action manifests, and the CONTRIBUTING / CODE_OF_CONDUCT /
STATUS files.data/(typosquat reference lists,include_str!'d
at compile time) stays in.docker run ghcr.io/metbcy/bomdrift:latestworks. A new
multi-arch (linux/amd64, linux/arm64) image is published to
GitHub Container Registry on every release. Single-stage Dockerfile;
the image consumes the cosign-signed binaries already produced by the
release matrix — nocargo buildruns in the image. Distroless cc
base, runs as the distrolessnonrootuser, ~28 MB. Tag matrix:
:vX.Y.Z,:vX.Y,:vX,:latest. The image carries an inline
SLSA build provenance attestation (verify with
gh attestation verify --owner Metbcy oci://ghcr.io/metbcy/bomdrift:vX.Y.Z).- SLSA build provenance attestations. Every release archive AND
the multi-arch ghcr.io image are now covered by
actions/attest-build-provenance@v2. SLSA proves "this artifact
was produced by the public release.yml workflow on tag X in this
repo"; the existing cosign signatures continue to prove "the
bomdrift maintainer's GitHub OIDC identity signed this artifact."
Both verifications must pass for the release to be trustworthy. See
docs/src/release-signing.md for the
full threat-model framing and the
gh attestation verify/slsa-verifierrecipes. publish-dry-runPR-time CI guard. Newci.ymljob runs
cargo publish --dry-run --lockedon every PR. Catches
crate-metadata regressions (oversized package, missing required
fields, anexcludelist change that drops a build-time
include_str!source) before they reach a release tag.
Changed
- README install section ships three new install paths.
cargo install --locked bomdrift,docker run ghcr.io/metbcy/bomdrift,
and the existing release-archive curl path are now equally
prominent. The from-sourcecargo install --gitpath stays as a
fourth option for adopters who want to track main. - README badges expanded. crates.io, docs.rs, and GitHub
Marketplace badges added at the top of the README. The CI / Release
/ Docs / License badges are kept. - GitHub Marketplace listing description rewritten. Lead with the
axios narrative and the maintainer-age heuristic, drop generic
copy. (Marketplace dashboard edit, not a repo-file change.) - Workflows opt into the Node.js 24 runner ahead of GitHub's
2026-06-02 forced-default. Every workflow now sets
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'at the top-level
env:block, silencing the runner's "Node.js 20 actions are
deprecated" warning that was firing on everyactions/checkout,
upload-artifact, and sticky-pull-request-comment step. Covers
ci.yml,docs.yml,fuzz.yml,release.yml, and
sbom-diff.yml. Remove the env var once Node.js 24 is the default
runtime (after 2026-09-16 per GitHub's deprecation timeline). v1major-version tag is now automated. A newretag-major
job inrelease.ymlforce-pushesv1to point at the latest
release on every tag. Until v1.0.0 ships, the floating tag stays
v1(Marketplace already references it); from v1.0.0 onward, the
job switches tov${major}.
Fixed
- Coverage-report PR comment no longer autolinks to a fake parked
domain. v0.9.8's coverage job uploaded the report under an
artifact name that ended in a real TLD and rendered the same string
in its sticky PR comment. Inside GitHub's web UI the backticks made
it inline code, but email and feed clients re-render comments with
their own autolinker that ignores backticks, so the bare
TLD-shaped token got linked to a real, unrelated parked domain and
produced exactly the kind of supply-chain-credibility hit a security
tool can't afford. The artifact is now namedcoverage-lcov(the
on-disk filename is still the standard one, so lcov-consuming tools
work unchanged), and the comment links directly to the workflow
run's artifacts tab so users have a real destination to follow.
Documentation
docs/src/release-signing.mdgains a SLSA provenance section.
Frames cosign + SLSA as complementary, with the threat model where
each catches what the other misses. Includes
gh attestation verify(recommended) andslsa-verifier
(air-gapped) recipes, and the ghcr.io image attestation verification
path.- Rustdoc cleanup. Eight broken intra-doc links in module-level
docstrings (MAX_QUERIES_PER_BATCH,SUFFIX_BOOST_SCORE,
run_with,eval_leaf,LeafOutcome,VulnRef,Cache::with_root,
crate::run_diff) were either demoted to backtick-quoted code spans
(private items not visible to public docs) or fixed to use the
correct path after the v0.9.8 lib.rs split. Plus one redundant
explicit link target.RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-featuresnow passes; docs.rs auto-build is clean. No public
API change. - README and STATUS.md drift fixed. Removed STATUS.md's stale
Marketplace-publication-pending line (the listing has been live
since v0.9.8). README from-source bumped from v0.9.8 to v0.9.9.
Tests
- 444 → 444 (no net change). Distribution-release plumbing only.
Scope notes — what's in v0.9.9 vs deferred
In v0.9.9: crates.io, ghcr.io, SLSA, docs.rs, Marketplace polish,
README badges + new install paths.
Deferred (separate plans, not version-coupled):
- Homebrew tap (
Metbcy/homebrew-tap+bomdrift.rbformula). - nix flake, AUR PKGBUILD, winget + Scoop manifests.
- README diet (the wall-of-text comparison table moves to
docs/src/compare.md). - Asciinema demo recorded against
examples/axios-incident/.
Deferred from v0.9.8 (held for the next code-hygiene release):
- File splits for
vex.rs,render/markdown.rs,render/sarif.rs,
baseline.rs,enrich/typosquat.rs,enrich/license.rs. - Mutation-testing audit via
cargo-mutants. - Coverage
--fail-under-linesratchet (planned for "after 2-3
releases of visibility" — v0.9.9 is the second).
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.9.9' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.9.9-<target>.<ext>.pem \
--signature bomdrift-v0.9.9-<target>.<ext>.sig \
bomdrift-v0.9.9-<target>.<ext>v0.9.8
[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
threecargo-fuzzlibfuzzer 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 fromtests/fixtures/.
New.github/workflows/fuzz.ymlruns each target for 60s on PRs touching
src/parse/**orfuzz/**, 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
coveragejob in.github/workflows/ci.yml
runscargo-llvm-cov, emitslcov.infoas a workflow artifact, and posts
a sticky PR comment viamarocchino/sticky-pull-request-comment@v2showing
line-coverage percentage. No--fail-under-linesgate 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/unimplementedlints 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#. Zero production.unwrap()
remain.- Every
unsafeblock now carries a// SAFETY:comment.
16 of 23 unsafe sites (the Rust 2024env::set_varwrappers + 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.rs47 KB → 31 lines. Extracted the 1,300-linerun_diff
orchestration into a newsrc/run.rsmodule.lib.rsis now pure
re-exports + module declarations. Public API surface preserved
byte-for-byte: external consumers callingbomdrift::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-linesratchet — 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.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.9.8' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.9.8-<target>.<ext>.pem \
--signature bomdrift-v0.9.8-<target>.<ext>.sig \
bomdrift-v0.9.8-<target>.<ext>v0.9.7
[0.9.7] - 2026-04-29
The "v0.9.6 follow-up backlog" milestone. Five concrete items from the
v0.9.6 release notes' "Suggested next milestone candidates" list shipped
in one polish release: per-exception SPDX inheritance through compound
expressions, the last hardcoded calibration threshold lifted, proper
Windows plugin process timeouts, full action.yml parity with the CLI
flag surface, and air-gapped Sigstore documentation.
Added
--multi-major-delta <u32>CLI flag and matching
[diff] multi_major_deltaconfig key (default2, validated>= 1).
Lifts the last hardcoded calibration threshold:
version_jump::MIN_MAJOR_DELTA. Raise to reduce false positives on
legitimate major-version-bump-heavy ecosystems; lower to flag any
major bump (1.x → 2.x).--debug-calibrationrows now emit the
active delta rather than the const default.action.ymlinput parity with the v0.7-v0.9.7 CLI surface.
Twenty-five new inputs now map to their corresponding CLI flags:
vex,emit-vex,vex-author,vex-default-justification,
allow-licenses,deny-licenses,allow-exception,
deny-exception,allow-ambiguous-licenses,no-epss,no-kev,
no-registry,fail-on-epss,recently-published-days,
typosquat-similarity-threshold,young-maintainer-days,
cache-ttl-hours,multi-major-delta,before-attestation,
after-attestation,cosign-identity,cosign-issuer,
require-attestation,plugin. Multi-line inputs (vex,plugin)
iterate one flag per non-empty line. Empty inputs contribute no
CLI args, so existing workflows are byte-identical without changes.- Air-gapped / self-hosted Sigstore documentation in
docs/src/attestation.md. Documents env vars cosign respects and
bomdrift inherits unchanged:SIGSTORE_REKOR_URL,
COSIGN_REKOR_URL,SIGSTORE_FULCIO_URL,COSIGN_FULCIO_URL,
SIGSTORE_OIDC_ISSUER,COSIGN_OIDC_ISSUER,SIGSTORE_ROOT_FILE,
COSIGN_REPOSITORY,TUF_ROOT. Includes a worked GitHub Actions
example with internal Sigstore endpoints, key-based attestation
fallback notes (for true air-gap where keyless OIDC isn't
reachable), and a 6-item troubleshooting checklist.
Changed
- SPDX
WITH-chain exception inheritance through compound
expressions. v0.9.5 added per-exception allow/deny via
[license] allow_exceptions/deny_exceptions, but the evaluator
only checked the immediate atom. v0.9.7 evaluates each leaf in the
expression tree and combines outcomes via the SPDX crate's native
AND/OR semantics:(Apache-2.0 WITH LLVM-exception) AND (BSD-3-Clause)with
deny_exceptions=[LLVM-exception]is now correctly denied
(the AND-chain inherits the exception denial).(Apache-2.0 WITH LLVM-exception) OR (Apache-2.0 WITH Classpath-exception-2.0)with
allow_exceptions=[LLVM-exception](Classpath not allowed) is
correctly permitted (the LLVM branch wins; OR doesn't poison).- Single-WITH expressions (no compound) keep the v0.9.5 wording for
back-compat. Compound violations append" (in <raw expression>)"
so users can locate the offending atom.
- Plugin process timeout now uses the
wait-timeoutcrate
(~50kb,libc-only transitive). Replaces the v0.9.6 manual
Child::try_wait()polling loop. Behavior unchanged on Unix; on
Windows the kill-on-timeout path is now first-class instead of
best-effort. Preserves the existing best-effort failure semantics
(timeout drops the offending plugin's findings, logs a warning at
BOMDRIFT_DEBUG=1, rest of the report renders).
Deps
- Added
wait-timeout = "0.2"to[dependencies]for cross-platform
process timeout insrc/plugin.rs. Single transitive (libc,
already in tree).
Tests
- 420 → 432 (+12). Eight new tests cover SPDX
WITH-chain
inheritance through every operator combination
(AND-with-denied-exception, OR-with-permitted-fallback,
back-compat single-WITH); two tests cover the--multi-major-delta
knob (override default + reflect in--debug-calibration); two
cover thewait-timeout-based plugin timeout path.
Documentation
docs/src/cli-reference.md: new--multi-major-deltaentry.docs/src/license-policy.md: new "WITH-chain exception
inheritance" subsection with three worked examples.docs/src/enrichers/version-jump.md: Calibration subsection
rewritten to cover the new knob.docs/src/architecture.md:wait-timeout = "0.2"row added to
the approved-deps table.docs/src/github-action.md: action input reference regrouped
by purpose; "What's new in v0.9.7" subsection added.docs/src/attestation.md: air-gapped subsection (above).
Roadmap
This release closes 5 of the 6 "Suggested next milestone candidates"
from v0.9.6's release notes:
| Item | Disposition |
|---|---|
Per-exception SPDX granularity through WITH chains |
Shipped |
| Multi-major version-jump calibration knob | Shipped |
| Windows plugin timeout (proper, not best-effort) | Shipped |
| Action.yml parity with newer CLI flags | Shipped |
| Cosign air-gapped Sigstore docs | Shipped |
| WASM-sandboxed plugin model | Deferred (multi-week scope, conflicts with single-binary tenet; v1.0+ candidate if external-process model proves insufficient) |
Scope notes
- WASM-sandboxed plugin model stayed deferred. The external-process
plugin model from v0.9.6 covers the use case adopters want; WASM
sandboxing would add a multi-week toolchain overhaul (wasmtime
~30MB / 80 transitive deps, orwasmi's slower runtime, plus a Rust
plugin SDK, plus dual-runtime support to not break v0.9.6 plugins).
Stays a v1.0+ candidate if demand materializes.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.9.7' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.9.7-<target>.<ext>.pem \
--signature bomdrift-v0.9.7-<target>.<ext>.sig \
bomdrift-v0.9.7-<target>.<ext>0.9.7-alpha
bomdrift is a CLI and multi-SCM action that diffs two SBOMs and surfaces supply-chain risk signals on every changed dependency — flags new CVEs (with EPSS + CISA KEV scoring), typosquats across eight ecosystems, multi-major version jumps, young-maintainer takeovers, registry-metadata signals (recently-published, deprecated, maintainer-set-changed), and license-policy violations — ready to drop into a PR comment on GitHub, GitLab, Bitbucket, or Azure DevOps.
Full Changelog: v0.9.7...0.9.7-alpha
v0.9.6
[0.9.6] - 2026-04-29
The "finish the roadmap" milestone. v0.9.6 closes out every entry in the
prior roadmap's "Future candidates (not committed)" section — by shipping
it, by closing it as a non-goal, or by documenting the explicit upstream
blocker. Headline new features: OCI attestation verification via
cosign verify-attestation, an external-process plugin system for
custom rules, full CLI calibration knobs for the remaining hardcoded
thresholds, and a comprehensive documentation refresh across every
chapter to reflect v0.7 → v0.9.6 reality.
Added
- OCI attestation verification.
bomdrift diff --before-attestation <oci-ref> --after-attestation <oci-ref>shells
out tocosign verify-attestation --type=cyclonedx, parses the
in-toto envelope, and feeds the verified SBOM payload into the
standard parser. New--cosign-identity <regex>and
--cosign-issuer <url>flags pass through to cosign's
--certificate-identity-regexp/--certificate-oidc-issuer. New
--require-attestationboolean refuses falling back to the
--before/--afterfile flags so production CI gates can enforce
attested SBOMs only. Documented indocs/src/attestation.md. New
attestationrow in--debug-calibrationso users can confirm
cosign accepted the right cert. - External-process plugin system. New
--plugin <path-to-plugin.toml>flag (repeatable). Plugin manifest in TOML
(name/description/exec/timeout_ms/invoke_on).
bomdrift invokes the plugin once per Added or VersionChanged
component with JSON on stdin ({component, event, before}) and
parses JSON from stdout ({findings: [...]}). Best-effort: timeout,
non-zero exit, or malformed JSON drops the offending plugin's
findings and logs a warning atBOMDRIFT_DEBUG=1; the rest of the
diff renders. Newbomdrift.pluginSARIF rule with stable
partialFingerprintsper(plugin_name, purl, rule_id). Worked
example shipped underexamples/plugins/banned-packages/.
Documented indocs/src/plugins.md. Protocol carries
protocol_version: 1for forward-compat. - CLI calibration knobs for the three previously-hardcoded
thresholds:--typosquat-similarity-threshold <FLOAT>(default0.92,
validated 0.0..=1.0).--young-maintainer-days <i64>(default90, validated >= 1).--cache-ttl-hours <u64>(default24, validated >= 1; applies
uniformly to OSV / EPSS / KEV / Registry caches).- Matching
[diff]config keys:typosquat_similarity_threshold,
young_maintainer_days,cache_ttl_hours. --debug-calibrationrows now emit the active threshold rather
than the hardcoded default, so calibration data collection
reflects the real run.
Changed
CACHE_TTL_SECSunified. Previously duplicated in four
modules (src/enrich/{cache,epss,kev,registry}.rs). Now a single
source of truth insrc/enrich/cache.rswitheffective_ttl_secs
helper that honors per-run overrides without globals.- Comprehensive documentation refresh across every chapter.
Notable updates:README.mdrewritten with capability-grouped feature list
(Ingest / Enrichers / Suppression / Output / Forge /
Extensibility / Packaging) and a 5-column comparison table
against Socket / Snyk / Trivy / OSV-Scanner / Grype with 11
feature-row dimensions sourced from the v0.7-v0.9 competitor
research doc.docs/src/cli-reference.mdrewritten end-to-end. Every CLI flag
now documented and grouped by purpose (Output / Suppression /
Enrichment / Calibration / License / Failure thresholds /
Forge / Attestation / Plugins / Diagnostics) with each entry
annotated with introduced-in version.docs/src/architecture.mdmodule map expanded to cover the 8
modules added across v0.7-v0.9.6 (config,clock,
attestation,plugin,vex,epss,kev,registry,
license); new "Best-effort enricher contract" and
"Byte-determinism contract" subsections; approved-deps table
includingbase64 = "0.22"(v0.9.6) andspdx = "=0.10.9"
exact pin (v0.9.5).docs/src/baseline.md6-row schema-reference table for the
unifiedBaselineEntry(id / purl / expires / reason /
vex_status / vex_justification).- Per-enricher chapters (
docs/src/enrichers/{typosquat, maintainer-age,version-jump,kev,epss,registry}.md) gained
consistent Calibration + Disabling + See-also subsections;
overview table grew from 4 to 9 rows. docs/src/SUMMARY.mdreorganized into Output / Enrichers /
Suppressions / Advanced groups for new-reader navigation.CONTRIBUTING.md"Test conventions (v0.9.5+)" subsection added
documenting theclock::test_env_lock()recipe; "Adding a new
enricher" and "Adding a new finding kind" worked recipes.- Stale content rewritten across multiple chapters
(gitlab-ci.md's v0.7-deferred section now covers what v0.9
actually shipped; release-signing pins refreshed; etc.).
Roadmap
- Closed out every "Future candidates" entry from the v0.9.5
roadmap with explicit dispositions:- Reachability → moved to Non-goals (pair with Endor / Snyk).
- GraphQL maintainer-age → decided: REST stays
(cursor-pagination-cost analysis lifted into the maintainer
enricher's module doc). - VEX vocabulary beyond OpenVEX 8 justifications →
spec-bound; documented indocs/src/vex.mdthat bomdrift
follows the OpenVEX 0.2.0 vocab verbatim. - PyPI / crates.io maintainer-set-changed → moved to a new
"Blocked on upstream" subsection with the precise API gap
documented (PyPI lacks per-version maintainers; crates.io
lacks per-versionpublished_byhistory).
- Calibration backlog section removed entirely — every threshold
(similarity, young-maintainer-days, recently-published-days,
cache-ttl-hours) is now CLI/config-configurable.
Deps
- Added
base64 = "0.22"for the cosign in-toto envelope payload
decode insrc/attestation.rs. Already a transitive dep via
ureq; promoted to direct so we own the pin.
Tests
- 389 → 420 (+31). Plugin manifest parse, plugin success/timeout/
non-zero-exit/malformed-output paths, attestation envelope parse,
fake-cosign integration test (PATH-injection with serialized env
lock), calibration knobs override default + reflect in
--debug-calibration, cache-TTL override per enricher.
Scope notes
What stayed deferred to v1.0 candidates (carried to roadmap "Blocked
on upstream" or new "Future candidates"):
- PyPI / crates.io maintainer-set-changed (upstream API blockers).
- WASM / sandboxed plugin model (current external-process model
works; revisit if demand materializes). - Bitbucket / Azure DevOps action-side
vex:/emit-vex:/
plugin:inputs (CLI surface is broader than action surface). - Multi-major version-jump
MIN_MAJOR_DELTAcalibration knob
(only remaining hardcoded threshold; revisit with calibration data).
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.9.6' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.9.6-<target>.<ext>.pem \
--signature bomdrift-v0.9.6-<target>.<ext>.sig \
bomdrift-v0.9.6-<target>.<ext>v0.9.5
[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_exceptionsarrays in.bomdrift.tomlplus matching
--allow-exception/--deny-exceptionCLI flags. License expressions
using theWITHoperator (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: ifallow_exceptionsis non-empty
and the exception is not in it, the package is denied; if
deny_exceptionsis non-empty and the exception IS in it, denied.
Empty lists preserve v0.9 behavior (exception treated as informational).
Surfaces inLicenseViolation::matched_ruleas
"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_createdonly), 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-eventService 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 withBOMDRIFT_NOTE_BODYtemplate parameter. - Public
bomdrift::vex::parse_synthetic_idhelper. Round-trips
bomdrift's synthetic finding IDs (bomdrift.typosquat:<purl>:<closest>,
etc.) back to a structuredSyntheticFindingKindenum. Lets external
VEX tooling identify which bomdrift finding a VEX statement targets
without string-splitting. Re-exported fromlib.rsfor downstream
Rust consumers.
Changed
spdxcrate exact-pinned to=0.10.9. SPDX list updates can
shiftLicenseId.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.BaselineEntryandExpiredEntryunified. They overlapped on
id,purl,expires,reason. The two are now backed by a single
internal type with a status enum; the publicExpiredEntryalias 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_ofcame in
Rust 1.94 and broke the v0.8 build until handled). Bump deliberately
viaCargo.toml'srust-versionfield in lockstep.
Refactored
- Single source of truth for the
/bomdrift suppress <ID> [reason: ...]
comment grammar. Newscripts/parse-suppress-comment.shis the
canonical regex;comment-suppress/entrypoint.shsources it,
examples/gitlab-ci/comment-bridge/worker.jsmirrors it with a
pointer comment, andscripts/check-suppress-regex-sync.shis wired
into CI to fail the build if the shell + JS copies drift. The Rust
--from-commentparser 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 thePUT /merge_requests/:id/notes/:note_idupsert preserves reviewer-reply
threading (it does — note ID stays stable, replies survive, no
re-fired note hooks for unchanged content). docs/src/license-policy.mdextended with the SPDXWITH
exceptions section + worked examples.docs/src/vex.mdextended with the synthetic-id grammar and
parse_synthetic_idreference 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-commentparser 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.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.9.5' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.9.5-<target>.<ext>.pem \
--signature bomdrift-v0.9.5-<target>.<ext>.sig \
bomdrift-v0.9.5-<target>.<ext>v0.9.0
[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/fixedsuppress matching findings (counted in the
new "Suppressed by VEX" markdown summary row);under_investigation
annotates with aVEX:badge in markdown and
properties.vexStatusin SARIF;affectedannotates 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-entryvex_status, defaulting tounder_investigation—
baseline ≠ "not affected", never auto-promoted) and un-suppressed
findings (statusaffectedwithstatus_notesdescribing the
finding kind). New baseline fieldsvex_statusand
vex_justification. New[diff] vex_authorand
[diff] vex_default_justificationconfig 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_ambiguousis deprecated with a one-time
stderr warning. - Bitbucket Pipelines + Azure DevOps Pipelines support.
Platform::BitbucketandPlatform::AzureDevOpsvariants on the
CLI; auto-detection viaBITBUCKET_BUILD_NUMBERandTF_BUILD
envs;BITBUCKET_GIT_HTTP_ORIGINandBUILD_REPOSITORY_URI
honored as--repo-urlfallbacks. Per-platform footer URL shapes
(/issues/newfor Bitbucket;/_workitems/createfor Azure
DevOps). Drop-in templates with READMEs in
examples/bitbucket-pipelines/andexamples/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, PyPIinfo.yanked+ Inactive
classifiers, crates.ioversions[].yanked), and
MaintainerSetChanged(npm only — PyPI/crates.io don't expose
per-version maintainers cleanly). New--no-registryflag and
[diff] no_registry = trueconfig key. New--fail-on recently-publishedand--fail-on deprecatedthresholds. New
SARIF rulesbomdrift.recently-published,bomdrift.deprecated,
bomdrift.maintainer-set-changedwith 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::Diffargument 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.idbecomes optional (Option<String>) to allow
--from-commentinvocations 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.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.9.0' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.9.0-<target>.<ext>.pem \
--signature bomdrift-v0.9.0-<target>.<ext>.sig \
bomdrift-v0.9.0-<target>.<ext>v0.8.0
[0.8.0] - 2026-04-29
The "supply-chain hardening" milestone. v0.8 finishes SARIF for GitHub
Code Scanning, lights up exploit-prediction (EPSS) and
known-exploited-in-the-wild (CISA KEV) signals on every advisory,
introduces an explicit license allow/deny policy with fail-closed
compound-expression handling, and adds time-boxed risk-acceptance to
the suppression baseline.
Added
-
SARIF + GitHub Code Scanning end-to-end. Every result now carries
a stablepartialFingerprints.primaryHash/v1hash so Code Scanning's
alert dedup threads correctly across runs. New action input
upload-to-code-scanning: truewires
github/codeql-action/upload-sarif@v3for one-line opt-in. New
--output-file <PATH>CLI flag avoids YAML>-redirection
quirks. Per-rule fingerprint identity tuples documented at
docs/src/sarif.md. -
EPSS scoring (FIRST.org). Every CVE-aliased advisory surfaces an
exploitation-probability badge in markdown / terminal / SARIF /
JSON.--fail-on-epss <FLOAT>trips exit 2 when any advisory exceeds
the threshold.--no-epssopt-out + 24h disk cache at
<XDG_CACHE>/bomdrift/epss/. Best-effort: network failure logs at
BOMDRIFT_DEBUG=1, diff still renders.
docs/src/enrichers/epss.md -
CISA KEV (Known Exploited Vulnerabilities). A
KEVflag flips
on every advisory whose primary id or CVE alias appears in CISA's
catalog.--fail-on kev+--no-kevflags. Once-daily catalog
cache at<XDG_CACHE>/bomdrift/kev/catalog.json.
docs/src/enrichers/kev.md -
License allow/deny policy. New
[license]block in
.bomdrift.toml(or--allow-licenses/--deny-licensesCLI flags
matching Dependency Review Action names). Atomic exact match +
*-suffix glob (AGPL-*); compound expressions like
(MIT OR GPL-3.0)fail closed by default unless
allow_ambiguous=true. Distinct from same-version license drift:
this is a policy gate. New SARIF rule
bomdrift.license-violation.--fail-on license-violationtrips
exit 2. docs/src/license-policy.md -
Suppression expiry + reason. Each
suppressed_advisoriesentry
may now be the v0.8 object form
{id, expires?: "YYYY-MM-DD", reason?: "free text"}. Expired
entries surface a stderr warning and stop suppressing; bomdrift
refuses to load malformed dates.bomdrift baseline add --expires --reasonrecords the metadata; thecomment-suppresscompanion
action picks up an optionalreason: <text>line in the trigger
comment body.
docs/src/baseline.md -
OSV CVE aliases threaded through
VulnRef. OSV/v1/vulns/{id}
responses now feed CVE aliases intoVulnRef.aliases(sorted,
byte-deterministic). EPSS / KEV / future VEX consumption all read
fromVulnRef::cves(). -
timecrate adoption +clockmodule. Newsrc/clock.rsis
the single source of truth for date/time across the codebase.
HonorsSOURCE_DATE_EPOCH(read per call so test fixtures can vary
it). All v0.8 features that emit dates / compare dates go through
this module — reproducible-build contexts stay deterministic. -
--debug-calibration-format <pipe|jsonl>. New JSONL alternative
to the v0.7 pipe-delimited calibration tap. Numeric scores stay
numeric in JSON; severity buckets stay strings. Adding a new
finding kind is one call to a dispatch helper, not a fork.
Changed
--fail-on anynow also includes KEV-flagged advisories and
license-violation findings.- SARIF rule list grew from 5 to 6 (added
bomdrift.license-violation).
Scope notes
The following items were deliberately deferred to v0.9 rather than
half-shipped:
- GitLab comment-driven suppress. The
/bomdrift suppressflow
works on GitHub via thecomment-suppresscompanion action. Porting
to GitLab needs a webhook bridge with five distinct security guards
(token verification, event-type filter, project allowlist,
commenter-permission check, MR-context guard). Shipping the bridge
without those is a vulnerability — moved to v0.9 with a security
review milestone. - Multi-SCM (Bitbucket + Azure DevOps). Templates and footer
shapes need per-platform comment-API exploration; deferred to v0.9. - VEX consume + emit. Both depend on the
timecrate
foundation that lands here. Consume in v0.9-G; emit in v0.9-H. The
baseline-expiry + reason fields added in v0.8 feed directly into
the VEXstatus_notesfield when emit lands. - SPDX expression evaluator. v0.8 fails closed on compound
expressions; v0.9 adopts thespdxcrate (~30kb) for proper
evaluation. Theallow_ambiguousflag becomes redundant at that
point.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.8.0' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.8.0-<target>.<ext>.pem \
--signature bomdrift-v0.8.0-<target>.<ext>.sig \
bomdrift-v0.8.0-<target>.<ext>v0.7.0
[0.7.0] - 2026-04-30
The "broaden the platform, polish the edges" milestone. v0.7 takes the
v0.6 policy-config foundation and adds GitLab CI as a first-class
target, closes the open issue backlog around adoption pain points,
and lays groundwork for calibration tuning.
Added
-
GitLab CI integration.
bomdrift diffnow renders a
GitLab-shaped MR-note footer when--platform gitlabis set (or
whenGITLAB_CI=trueis auto-detected). A copy-paste-ready
template ships underexamples/gitlab-ci/with a diff job
(curl + jq upsert against the GitLab notes API), a manual
suppression job, and a setup README covering the two-token model
and Self-Managed considerations. Full chapter at
docs/src/gitlab-ci.md. -
--platform <github|gitlab>flag onbomdrift diff(also
loadable from[diff] platform = "..."in.bomdrift.toml).
Default is auto-detection from the CI environment withgithub
as the fallback. Explicit flag always wins. -
CI auto-detection on GitLab. When
GITLAB_CI=trueis set,
bomdrift selects the GitLab footer shape; whenCI_PROJECT_URL
is set and--repo-url/BOMDRIFT_REPO_URLare unset, it's
used as the footer link target. -
--debug-calibrationflag. Off by default. When set, the
diff command emits one pipe-delimited record per finding to
stderr (kind|key|score|threshold). Lets adopters dump a
calibration sample across many PRs and feed back tuning data on
SIMILARITY_THRESHOLD,YOUNG_MAINTAINER_DAYS, etc. No
telemetry — the user owns the file. -
Typosquat top-package data top-up for Go (+35 entries from
CNCF / HashiCorp / gRPC ecosystem / awesome-go), Composer (+43
from Symfony / Laravel / Doctrine / testing communities), and
Gem (+44 from Rails ecosystem / dry-rb / API serializers).
Closes #6, #7, #8.
Changed
-
Markdown renderer is now platform-aware. Backward compatible:
the GitHub footer shape is preserved byte-for-byte for existing
callers. NewMarkdownOpts.platformfield controls the
switch;Defaultresolves to GitHub. -
Better "scan path not found" error in the GitHub Action.
entrypoint.shnow lists what was actually checked out and
links to the new monorepo docs section instead of the prior
one-line "no such file" error. Closes #11.
Docs
- GitLab CI chapter (
docs/src/gitlab-ci.md) — quickstart,
token model, suppression paths, Self-Managed notes, what's
scoped out for v0.7. - False-positive triage worked example in
docs/src/baseline.md(a typosquat misfire with the exact
baseline entry that suppresses it). Closes #12. - Monorepo setup section in
docs/src/github-action.md
covering matrix-per-service patterns and shared-baseline
pattern. Closes #9. - Action-broke troubleshooting checklist in
docs/src/github-action.mdcovering the top-N failure modes.
Closes #13. - CLI reference updated for
--platform,
--debug-calibration, and the newBOMDRIFT_REPO_URL/
GITLAB_CI/CI_PROJECT_URLenvironment variables.
Tests
- Regression test for
BOMDRIFT_REPO_URLenv-var → footer URL
plumbing (#10) — previously only the rendering function was
unit-tested, not the env-to-option plumbing inlib.rs. - E2E tests for
GITLAB_CIauto-detection and--platform
override. - Smoke test for
--debug-calibrationstderr output.
Scope notes
In-comment suppression on GitLab (/bomdrift suppress <ID> in an
MR note) is deferred to v0.8. GitLab note webhooks have a
different model than GitHub PR comments, and the safe wiring
(rate-limit, fork-MR safety, command parsing, double-trigger
debouncing) is materially harder. v0.7 ships the manual-job path
in examples/gitlab-ci/suppress.gitlab-ci.yml, which covers the
same user need without standing up a webhook handler.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.7.0' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.7.0-<target>.<ext>.pem \
--signature bomdrift-v0.7.0-<target>.<ext>.sig \
bomdrift-v0.7.0-<target>.<ext>v0.6.1
[0.6.1] - 2026-04-29
Fixed
- First PR after
bomdrift initno longer fails when no baseline
has been suppressed yet.bomdrift initships a.bomdrift.toml
withbaseline = ".bomdrift/baseline.json"— the path the
/bomdrift suppressflow writes to. Before this fix, the diff
hard-failed withreading baseline file: ... No such file or directoryon every PR until a reviewer used the suppress comment
at least once. Config-derived baseline paths are now tolerant of a
missing file (the diff runs with no suppressions applied), while
CLI--baseline pathkeeps its strict typo-detector behavior.
Verifying signatures
Each archive is signed with cosign keyless (Sigstore via GitHub OIDC). To verify a download:
cosign verify-blob \
--certificate-identity 'https://github.com/Metbcy/bomdrift/.github/workflows/release.yml@refs/tags/v0.6.1' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate bomdrift-v0.6.1-<target>.<ext>.pem \
--signature bomdrift-v0.6.1-<target>.<ext>.sig \
bomdrift-v0.6.1-<target>.<ext>