Skip to content

bom-tools 0.3.0: license report and SBOM from build evidence - #9

Merged
jadamcrain merged 12 commits into
mainfrom
sbom-from-build-log
Oct 9, 2026
Merged

jadamcrain merged 12 commits into
mainfrom
sbom-from-build-log

Conversation

@jadamcrain

Copy link
Copy Markdown
Member

Implements #7. Supersedes #6.

What

bom-tools 0.3.0 (renamed from allow-list) produces the third-party license report and a CycloneDX 1.5 SBOM from the evidence of the build that produced the product, and enforces allowed.json as an approval list.

  • Scope comes from the build. Per target: the cargo build --message-format json log (what compiled, with features) and two cargo tree outputs (Cargo's own resolver decides runtime vs build-time), cross-checked so that disagreeing evidence fails. Recipe in the README.
  • allowed.json approves, it doesn't filter. Every package outside the workspace must be in exactly one list; a build_only crate that becomes linked fails; reviewed licenses of shipped third_party crates are checked against the declared SPDX expression in both directions. All violations are reported together.
  • Two commands: bom-tools licenses and bom-tools sbom, each reading a directory with one subdirectory per target. sbom requires the build's Cargo.lock for SHA-256 hashes.
  • Removed: gen-licenses / gen-licenses-dir (cargo-cyclonedx input). gen-licenses-log* replaced by licenses.

Motivating regression: dnp3 1.7.0 builds most targets with tls-aws-lc, but cargo cyclonedx (no feature flags) reported ring and omitted aws-lc-rs / aws-lc-sys, which were also never approved. With this tool, dnp3's current allowed.json fails on exactly those gaps.

Design

input readers (format-specific)        core (format-independent)
build log + cargo tree ─► TargetGraph ─► inventory (merge targets, resolve via cargo metadata)
                                         ─► approval ─► license report / SBOM

Only input/ knows the evidence formats, so Cargo's SBOM precursor files can become another reader later (#8). On a nightly build of dnp3 the precursor gives the identical runtime (90) and full (140) package sets.

Verification

  • 52 tests: unit tests per module, a fixture workspace built with the installed Cargo (proc-macros, build deps, host-only features, renamed optional deps, target-specific deps), golden tests on real dnp3 evidence (roles match Cargo's precursor; license report pinned byte for byte), mutation-checked where tests were repaired.
  • dnp3 end to end (local): merged aws-lc + ring targets, the JNI library, and a cross musl build.
  • SBOMs validated by the CycloneDX CLI (also in CI), the official 1.5 JSON schema with a generic validator, Trivy and OSV-Scanner. Both scanners parse all components and independently flag GHSA-6g2r-675j-hx59 in dnp3's xxhash-rust 0.8.15.
  • Designed and reviewed in adversarial rounds with Codex until consensus, for the plan, each phase, the tests and the architecture.

Not yet verified

  • Windows and macOS: CI here is Linux-only. The plan is to migrate dnp3's CI against this branch before tagging 0.3.0.

Consumers

Breaking. Repos that cargo install --git this tool without a tag (dnp3, rodbus) will break on merge; pin --tag 0.2.1 until migrated. See the README's migration section.

Classify every compiled crate as runtime or build-time using Cargo's own
resolver (`cargo tree`), cross-checked against the build log, and approve
the result against the allow-list instead of filtering with it.

- input/: readers that turn one target's evidence (build log + full and
  runtime `cargo tree`) into a format-neutral TargetGraph (graph.rs)
- inventory.rs: merge targets (runtime wins), resolve against the cargo
  metadata dictionary, normalize declared licenses once, `embedded`
- approval.rs: every crate in exactly one list, build-only crates may not
  ship, reviewed licenses checked against the declared SPDX expression
- sbom.rs + gen-sbom-log[-dir]: CycloneDX 1.5 output
- gen-licenses-log[-dir] now take --root-package and per-target
  directories with build.json, tree.txt and runtime-tree.txt
- Apache-2.0 license, `embedded` and `commercial_license` config fields
- tests: unit tests, a fixture workspace built with the installed Cargo,
  golden tests on real dnp3 evidence, and a regression test that the
  deprecated cargo-cyclonedx commands still match 0.2.1 output
- README: what the tool does, the producer recipe for the build evidence,
  commands, allowed.json rules, and the guarantees and limits of the
  cross-checks
- CHANGELOG entry for 0.3.0 (breaking changes to gen-licenses-log*)
- GitHub Actions: fmt, clippy, tests (including the real-Cargo fixture),
  and CycloneDX 1.5 schema validation of an SBOM with a pinned,
  checksum-verified cyclonedx-cli
- the root must be a workspace member (it is never approved)
- the root's product artifact is matched by full package identity, so a
  dependency with the same name cannot satisfy it
- build log lines are parsed whole; nothing may follow build-finished
- lockfile checksums must be 64 hex digits
- third_party entries are sanity-checked whatever the package's role
- single-target flags are --log/-g, --tree, --runtime-tree
- tests: reader negatives (dropped/extra packages, feature mismatch,
  runtime tree from other arguments, roots, product artifact), direct
  TargetGraph SBOM tests (per-version hashes, bom-ref collision, license
  encoding), exact per-version hash and report/SBOM invariant checks, a
  host-only feature dependency in the Cargo fixture, Windows expectations,
  and a fresh fixture directory per run
- deserialize each cargo message from the original line rather than an
  intermediate serde_json::Value, which kept the last of duplicate keys
- fixture build evidence lives in a fixed, cleared directory inside the
  target directory instead of accumulating in the system temp dir
- README: state exactly what the cross-checks prove, scope the approval
  rules to the build-evidence commands and non-workspace packages, and
  document the workspace-root and commercial_license requirements
Concurrent test runs must not share the fixture build or the golden test's
temporary config; keep them per process inside the target directory.
Agreed with Codex review:
- cut tests duplicated elsewhere (Role ordering, path-key resolution, the
  executable-documentation limitation test, standalone 1.5 validation now
  folded into the SBOM content test as a parse-back check)
- the missing commercial license is a renderer unit test; the command
  test proves --lockfile reaches the renderer without temp files
- repaired tests that passed with a wrong implementation: build-script
  skip (distinct features), blank lines after build-finished, runtime-wins
  in both target orders, and an independent expected set for the license
  report (precursor-derived)
- one graph test helper and one Variant constructor instead of copies
Architecture review (Claude and Codex):
- remove gen-licenses / gen-licenses-dir: BOM scope always comes from
  the build, so reading cargo-cyclonedx output has no place in the tool
- licenses.rs is now only the report renderer (`licenses::write`, the
  counterpart of `sbom::write`) and renders straight from the validated
  inventory, without config lookups or unreachable error paths
- one definition per license (SPDX id and text; the URL is derived)
  instead of four parallel matches
- ValidatedInventory fields are private: only approval constructs it
- config entries named for what they are (ThirdPartyEntry, VendorEntry)
- the target-directory convention moved into `input` with the other
  file-name knowledge; doc fixes (Runtime, TargetInput, third_party)
- golden test pinning the license report format byte for byte

Report and SBOM output verified identical before and after on the dnp3
fixture and on the merged, JNI and cross-musl evidence.
- README: notice that 0.3.0 removed the cargo-cyclonedx commands, with the
  0.2.1 pin for consumers that have not migrated
- README: say explicitly that Cargo's SBOM precursor files are a future
  input reader for this tool; approval and outputs stay the same
- licenses::write only performs writes, so it returns std::io::Result
The tool produces the license report and the SBOM from build evidence and
enforces the allow list, so name it after the repository it lives in.

- crate, binary and directory: allow-list -> bom-tools
- `licenses` and `sbom` replace gen-licenses-log[-dir] and
  gen-sbom-log[-dir]; both read a directory of targets
  (-e/--evidence-dir), so a single target is one subdirectory
- README, CHANGELOG (with a migration section) and CI updated
@jadamcrain
jadamcrain merged commit a5c5797 into main Oct 9, 2026
1 check passed
@jadamcrain
jadamcrain deleted the sbom-from-build-log branch October 9, 2026 21:31
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