Skip to content

release: v0.8.0 — supply-chain hardening#19

Merged
Metbcy merged 10 commits intomainfrom
release/v0.8.0
Apr 29, 2026
Merged

release: v0.8.0 — supply-chain hardening#19
Metbcy merged 10 commits intomainfrom
release/v0.8.0

Conversation

@Metbcy
Copy link
Copy Markdown
Owner

@Metbcy Metbcy commented Apr 29, 2026

v0.8 finishes the SARIF integration for GitHub Code Scanning, lights up
exploit-prediction (EPSS) and known-exploited-in-the-wild (CISA KEV)
signals, introduces an explicit license allow/deny policy, and adds
time-boxed risk acceptance to the baseline.

Feature themes

  • SARIF + GitHub Code Scanning end-to-end. Stable
    partialFingerprints.primaryHash/v1 per result so Code Scanning's
    alert dedup threads correctly. New action input
    upload-to-code-scanning: true wires
    github/codeql-action/upload-sarif@v3 for one-line opt-in. New
    --output-file <PATH> CLI flag.

  • EPSS + CISA KEV scoring. Every CVE-aliased advisory carries an
    EPSS probability badge and a KEV flag in markdown / terminal /
    SARIF / JSON. --fail-on-epss <FLOAT> and --fail-on kev for
    threshold gating. --no-epss / --no-kev opt-outs. Both enrichers
    are best-effort with 24h disk caches.

  • License allow/deny policy. New [license] block in
    .bomdrift.toml (or --allow-licenses/--deny-licenses matching
    Dependency Review Action names). Atomic exact match + *-suffix
    glob; compound expressions like `(MIT OR GPL-3.0)` fail closed
    unless allow_ambiguous=true. New SARIF rule
    `bomdrift.license-violation`.

  • Baseline expires + reason. Object-form entries
    `{id, expires?, reason?}` for time-boxed risk acceptance. Expired
    entries warn to stderr and stop suppressing. The
    `comment-suppress` action picks up an optional `reason: `
    line in the trigger comment body.

Foundations

These foundation phases land first; subsequent features depend on them:

  • time crate adoption + clock module. Single source of truth
    for date/time. Honors `SOURCE_DATE_EPOCH` (read per call so test
    fixtures can vary it). Replaces all v0.7 hand-rolled date math.
  • `SOURCE_DATE_EPOCH` honored in production. Reproducible-build
    contexts get deterministic timestamps for VEX (v0.9) and audit-log
    output paths.
  • OSV CVE aliases on `VulnRef`. OSV `/v1/vulns/{id}`
    responses now feed CVE aliases into `VulnRef.aliases` (sorted,
    byte-deterministic). Prerequisite for EPSS + KEV; powers v0.9 VEX
    matching too.
  • `--debug-calibration-format jsonl`. Alternative to v0.7's
    pipe-delimited format. Numeric scores stay numeric in JSON.

Scope notes — deferred to v0.9

  • GitLab comment-driven suppress. Needs a webhook bridge with
    five distinct security guards (token verification, event-type
    filter, project allowlist, commenter-permission check, fork-MR
    safety). Shipping without those is a vulnerability.
  • Multi-SCM (Bitbucket + Azure DevOps).
  • VEX consume + emit. Both depend on the v0.8 `time` foundation.
    Baseline expiry+reason fields here feed directly into VEX's
    `status_notes` when emit lands.
  • SPDX expression evaluator. v0.8 fails closed on compound
    expressions; v0.9 adopts the `spdx` crate for proper evaluation.

CI gates

`fmt` / `clippy` / `audit` / `deny` / `diff` / `test`
(ubuntu/macOS/windows). After-commit local verification:
`cargo fmt --check && cargo clippy --all-targets --all-features --release -- -D warnings && cargo test --release` — all green.

Test count: 294 (v0.7) → 343 (v0.8). New deps: `time = "0.3"`,
`sha2 = "0.10"`.

Maintainer-side checklist (post-merge)

  • Tag `v0.8.0` on the merge commit and push.
  • Re-point the mutable `v1` tag at `v0.8.0`.
  • Cut the GitHub Release (releases/release-please workflow handles
    artifact upload).
  • Verify the cosign signature artifacts attach correctly.
  • Delete `release/v0.8.0` after the tag is pushed.

Metbcy and others added 9 commits April 29, 2026 13:09
Add time = 0.3 and sha2 = 0.10 (sha2 lands here to keep the dep
churn in one commit; used by Phase A SARIF fingerprints).

New src/clock.rs is the single source of truth for date/time:
- now()/today() honor SOURCE_DATE_EPOCH (env read per-call so
  fixtures can vary it between scenarios)
- parse_ymd is strict: rejects non-zero-padded YYYY-MM-DD
- format_rfc3339 + format_ymd byte-deterministic emitters

No public surface change yet; subsequent phases consume this.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Doctest on clock::now() locks env, calls now(), asserts the returned
timestamp matches. Combined with F1's now_is_read_per_call_not_cached
unit test this proves the env is consulted at every call site so
later phases (baseline expiry, VEX) get reproducible output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend VulnRef with aliases: Vec<String> (sorted, primary id excluded)
and a cves() iterator over CVE-prefixed identifiers (primary + aliases).

osv::fetch_detail returns (severity, aliases) from /v1/vulns/{id}.aliases
and the cache hit path keeps aliases empty (v0.7 cache schema only stored
severity; aliases populate on next live fetch).

JSON shape additive: aliases serializes via skip_serializing_if=is_empty
so existing consumers see no churn.

Tests: parse fixture (GHSA primary + CVE alias both present, primary
excluded from aliases, sort order stable), cves() iterator on both
GHSA-keyed and CVE-keyed advisories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add --debug-calibration-format <pipe|jsonl>, default pipe (back-compat).
JSONL emits {kind,key,score,threshold} per line; numeric scores stay
numeric, severity buckets ('HIGH', 'high+') stay strings. Adding new
finding kinds in subsequent phases is one call to write_calibration_row,
not a fork.

Also pre-add --output-file <PATH> flag (used by Phase A SARIF Code
Scanning workflow to avoid YAML > redirection quoting hazards). Wiring
into run_diff lands in Phase A; the flag is no-op for now.

Config: debug_calibration, debug_calibration_format, output_file all
mergeable from .bomdrift.toml.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SARIF results now carry partialFingerprints."primaryHash/v1" hashed
from a stable per-rule identity tuple (ruleId + purl + per-rule discriminator):

- bomdrift.cve:           ruleId | purl | advisoryId
- bomdrift.typosquat:     ruleId | purl | closest
- bomdrift.version-jump:  ruleId | purl | beforeVersion | afterVersion
- bomdrift.young-maint.:  ruleId | purl | topContributor
- bomdrift.license-change: ruleId | purl | beforeLicensesSorted | afterLicensesSorted

Two CVEs on the same purl now produce distinct fingerprints (the
duck-flagged collision case). The /v1 suffix on the fingerprint key
lets us evolve identity later without churning GitHub alert state.

New rule bomdrift.license-violation registered in tool.driver.rules
ahead of Phase D's policy violation emission.

CLI: --output-file <PATH> writes the chosen output format to a file
instead of stdout. Avoids YAML > redirection quoting in CI templates.

GitHub Action: new input upload-to-code-scanning (default false)
gates a github/codeql-action/upload-sarif@v3 step. Requires the
calling workflow to have permissions.security-events: write.
entrypoint.sh always passes --output-file when output=sarif so the
file path the upload step expects is populated.

Docs: new docs/src/sarif.md chapter; SUMMARY entry under Output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two new best-effort enrichers piggyback on OSV's VulnRefs after the
core enrichment runs:

- src/enrich/epss.rs queries https://api.first.org/data/v1/epss in
  100-CVE batches, populates VulnRef.epss_score (max-of-aliases),
  caches per-CVE at <XDG_CACHE>/bomdrift/epss/<cve>.json (24h TTL).
- src/enrich/kev.rs downloads CISA's known_exploited_vulnerabilities
  feed once daily, populates VulnRef.kev when any CVE alias matches,
  caches the bulk catalog at <XDG_CACHE>/bomdrift/kev/catalog.json
  (24h TTL).

Both enrichers fail closed-without-blocking: a network failure logs
at BOMDRIFT_DEBUG=1 and the diff renders with empty fields.

VulnRef extended (additive JSON shape via skip_serializing_if):
  pub epss_score: Option<f32>,
  pub kev: bool,

CLI surface:
- --no-epss / --no-kev: skip the enricher (network + cache).
- --fail-on kev: new FailOn variant; --fail-on any includes KEV too.
- --fail-on-epss <FLOAT>: sibling flag (--fail-on is a clap ValueEnum,
  parsing 'epss>=N' inside it would break v0.7 callers; sibling flag
  is cleaner). Trips exit 2 when any advisory's score >= threshold.

Render paths:
- Markdown: "EPSS 0.87 · **KEV**" badges in CVE rows.
- Term:    "EPSS 0.87 KEV" plain badges.
- SARIF:   bomdrift.cve result properties.epssScore, properties.kev.

Calibration rows for both enrichers (pipe + JSONL formats).

Phase D ahead-of-time scaffolding: Enrichment.license_violations field,
LicenseViolation/LicenseViolationKind types, FailOn::LicenseViolation
variant. Population lands in Phase D.

Docs: docs/src/enrichers/epss.md, docs/src/enrichers/kev.md, SUMMARY
entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v0.8 baseline schema is purely additive. Each entry in
suppressed_advisories may now be either:
- a bare string (v0.5 form, unchanged), or
- an object {id, purl?, expires?, reason?} (v0.8 form).

Behavior:
- expires field parsed via clock::parse_ymd (strict YYYY-MM-DD).
  Malformed dates surface as a load error naming the offending entry.
- expires < today() (clock honors SOURCE_DATE_EPOCH): entry skipped
  for suppression and recorded on Baseline.expired_entries. lib.rs
  prints one warning per expired entry to stderr after baseline load.
- No expires: entry suppresses indefinitely (v0.5 semantics).

CLI:
  bomdrift baseline add GHSA-X --expires 2026-12-31 --reason '...'

The new flags route through baseline::add_suppression_full, which
emits the v0.8 object form when either field is set; otherwise the
v0.5 string form is preserved. Idempotency now matches by id across
both shapes.

comment-suppress companion action picks up an optional 'reason: <text>'
line in the trigger comment body and forwards it via --reason.

Tests cover: expired warns + still renders, active suppresses, no-
expiry suppresses, malformed errors, round-trip, SOURCE_DATE_EPOCH
override, idempotent re-add against object-form entry.

Docs: docs/src/baseline.md extended with the new fields, CLI usage,
warning message format, worked rotation example.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… handling

New src/enrich/license.rs evaluates each Added or VersionChanged
component against a configured Policy { allow, deny, allow_ambiguous }:

- Atomic license: exact compare against allow/deny; trailing-* glob
  for deny ('AGPL-*' matches 'AGPL-3.0-only'). Deny wins when both match.
- Compound expression (any of AND/OR/WITH/parens): treated as
  ambiguous. With allow_ambiguous=false (default) and any policy
  configured, emits an Ambiguous violation. With allow_ambiguous=true,
  permitted (with the understanding that v0.9's spdx evaluator will
  replace this).
- NOASSERTION / OTHER / empty: ambiguous (same fail-closed semantics).

Distinct from existing ChangeSet::license_changed (same-version
license drift) — that's a heuristic, this is a policy gate.

CLI: --allow-licenses, --deny-licenses, --allow-ambiguous-licenses
(matches Dependency Review Action flag names exactly). [license]
block in .bomdrift.toml; CLI flags override (not merge) when set.

Render paths:
- Markdown: new 'License violations' section + summary-table row.
- Term: [LIC] tag with matched rule.
- JSON: enrichment.license_violations array (already wired in B).
- SARIF: bomdrift.license-violation results emit with stable
  partialFingerprints.primaryHash/v1 hashed from
  ruleId | purl | license. Rule was registered in Phase A.

--fail-on license-violation trips exit 2; --fail-on any includes it.
--debug-calibration row: license|<purl>|<spdx>|<deny|ambiguous|not-allowed>.

Tests cover: allow-pass, deny-fail, glob expansion, ambiguous fail-closed,
ambiguous permitted, allow+deny precedence (deny wins), version-changed
evaluation, empty-policy no-op, NOASSERTION ambiguous, SARIF roundtrip
with stable fingerprint, fail-on threshold gating.

Docs: docs/src/license-policy.md, SUMMARY entry under Output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Cargo.toml + Cargo.lock: 0.7.0 → 0.8.0
- README.md, docs/src/quickstart.md, .github/ISSUE_TEMPLATE: pin
  examples bumped v0.7.0 → v0.8.0
- CHANGELOG.md: 0.8.0 entry covering F1-F4 + A-D (foundations:
  time crate, SOURCE_DATE_EPOCH, OSV aliases, JSONL debug; features:
  SARIF Code Scanning, EPSS+KEV, license policy, baseline expiry).
  Explicit Scope notes section listing v0.9-deferred items.
- STATUS.md: new ✓ rows for SARIF Code Scanning, EPSS, KEV, license
  policy, baseline expiry. Bitbucket / Azure DevOps + VEX moved
  to 'Planned for v0.9'.
- docs/src/roadmap.md: new 'Shipped (v0.8)' section; 'Planned (v0.9)'
  refreshed with VEX consume + emit, SPDX evaluator, multi-SCM,
  registry enrichers, GitLab comment-suppress, non-goals doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

SBOM diff

Change Count
Added 14
Removed 0
Version changed 1
License changed 0

Added (14)

Show details
Ecosystem Name Version
cargo block-buffer 0.10.4
cargo cpufeatures 0.2.17
cargo crypto-common 0.1.7
cargo deranged 0.5.8
cargo digest 0.10.7
cargo generic-array 0.14.7
cargo num-conv 0.2.1
cargo powerfmt 0.2.0
cargo sha2 0.10.9
cargo time 0.3.47
cargo time-core 0.1.8
cargo time-macros 0.2.27
cargo typenum 1.20.0
cargo version_check 0.9.5

Version changed (1)

Show details
Ecosystem Name Before After
cargo bomdrift 0.7.0 0.8.0

False positive? Report it · Suppress a finding? Comment /bomdrift suppress <ID> (requires the comment-suppress sub-action) · Docs

The CI audit + deny gates caught RUSTSEC-2026-0009 (DoS via stack
exhaustion in time's RFC 2822 parser, fixed in 0.3.47+). Bumping to
0.3.47 requires Rust 1.88, so MSRV moves 1.85 -> 1.88. bomdrift does
not parse user-supplied RFC 2822 input — the advisory is not
exploitable here — but tightening the dep is the right call rather
than carrying an audit-deny exception.

Two clippy lints surfaced under 1.88 and were also fixed:
- src/baseline.rs: collapsed nested if into &&-chain.
- benches/diff.rs: used i.is_multiple_of(2).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Metbcy Metbcy merged commit ee67c0c into main Apr 29, 2026
8 checks passed
@Metbcy Metbcy deleted the release/v0.8.0 branch April 29, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant