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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Format
run: cargo fmt --all --check
- name: Clippy
run: cargo clippy --all-targets
- name: Test
# includes tests that build tests/fixtures/workspace with the installed Cargo
run: cargo test
- name: Validate an SBOM against the CycloneDX 1.5 schema
env:
CYCLONEDX_CLI_VERSION: v0.33.1
CYCLONEDX_CLI_SHA256: bfc8b2538da86fe239bc53658bbb63c1c8c510a293c1e6891aa5bea5d3c58746
run: |
curl -sSLo cyclonedx https://github.com/CycloneDX/cyclonedx-cli/releases/download/$CYCLONEDX_CLI_VERSION/cyclonedx-linux-x64
echo "$CYCLONEDX_CLI_SHA256 cyclonedx" | sha256sum -c
chmod +x cyclonedx
F=bom-tools/tests/fixtures/dnp3
cargo run -- sbom -e $F/targets --root-package dnp3-ffi -m $F/metadata.json -c $F/allowed.json --lockfile $F/Cargo.lock > sbom.json
./cyclonedx validate --input-file sbom.json --input-version v1_5 --fail-on-errors
47 changes: 45 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
# Changelog

Notable changes to the `allow-list` tool, newest first. Version numbers mark
CLI releases (so a consumer can pin a specific build, e.g. `cargo install --tag`);
Notable changes to `bom-tools` (named `allow-list` before 0.3.0), newest first. Version
numbers mark CLI releases (so a consumer can pin a specific build, e.g. `cargo install --tag`);
they are not a library API compatibility promise.

## [0.3.0] - 2026-10-09

Breaking: the executable is renamed, the commands are replaced, and the license report's
scope changes (see below). See the README for how to produce the inputs. Consumers that
install from `main` without a tag should pin `--tag 0.2.1` until they migrate.

### Added

- `sbom`: a CycloneDX 1.5 JSON SBOM from the same validated inventory as the license report.
Runtime packages have `scope: required`, build-time packages `scope: excluded`; licenses are
the declared SPDX expressions (commercial license for first-party and vendor packages);
the required `--lockfile` (the build's `Cargo.lock`) supplies every crates.io package's
SHA-256; dependencies are the observed package edges;
`--omit-serial-number` with `SOURCE_DATE_EPOCH` gives reproducible output.
- Every compiled package is classified as runtime or build-time by `cargo tree` (Cargo's own
resolver), cross-checked against the build log: packages and (package, features) variants
must match exactly. Build logs are validated (successful `build-finished`, no test, example
or bench artifacts, a product artifact for the root).
- `allowed.json` is enforced as an approval list instead of a filter: every package outside
the workspace must be in exactly one of `third_party`, `vendor`, `build_only`; a
`build_only` package that becomes linked fails; reviewed licenses of shipped `third_party`
packages are checked against the declared SPDX expression in both directions.
- `embedded` flag on `third_party` / `vendor` entries, `commercial_license`, and the
`Apache2` license.

### Changed

- The executable and crate are renamed from `allow-list` to `bom-tools`.
- `gen-licenses-log` / `gen-licenses-log-dir` are replaced by `licenses`, which reads a
directory with one subdirectory per target (`-e/--evidence-dir`), each holding
`build.json`, `tree.txt` and `runtime-tree.txt`, and requires `--root-package`.
`cargo metadata` should be generated with `--all-features`.
- The license report now lists only packages linked into the product: build-time packages
(proc-macros and their dependencies, build-script dependencies) are no longer listed, and
first-party crates are matched by name and version instead of being skipped.

### Removed

- `gen-licenses` / `gen-licenses-dir`. They read `cargo cyclonedx` SBOMs, which describe the
dependency graph for cargo-cyclonedx's own feature selection rather than the features the
product was built with (e.g. dnp3 1.7.0's report listed `ring` and omitted `aws-lc-*` for
targets built with `tls-aws-lc`).

## [0.2.1] - 2026-07-22

### Fixed
Expand Down
88 changes: 75 additions & 13 deletions 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
Expand Up @@ -2,7 +2,7 @@
resolver = "2"

members = [
"allow-list",
"bom-tools",
]

[workspace.lints.rust]
Expand Down
145 changes: 145 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# bom-tools

`bom-tools` produces the third-party license report (`third-party-licenses.txt`) and a
CycloneDX 1.5 SBOM for a Rust product, from the evidence of the build that produced it, and
checks every dependency against a reviewed allow list (`allowed.json`).

```sh
cargo install --git https://github.com/stepfunc/bom-tools.git --tag 0.3.0
```

## How it works

The scope of the report and the SBOM is what the build actually compiled, never what the
dependency graph could contain. For each target the product is built for:

1. the `cargo build --message-format json` log proves which packages compiled, with which
features;
2. `cargo tree` (Cargo's own resolver) tells which of them are linked into the product
(**runtime**) and which only run while building it (**build time**: build scripts,
proc-macros and their dependencies);
3. the two are cross-checked: if they disagree on the compiled packages or their enabled
features (for example a tree made with different flags), the run fails.

Targets are merged (a package that is runtime on any target is runtime), resolved against
`cargo metadata` for licenses and sources, and approved against `allowed.json`. The license
report and the SBOM are two views of that one validated inventory: the report lists exactly
the runtime `third_party` packages, which are exactly the SBOM's `required` third-party
components.

## Producing the build evidence

Run these in the **same job and environment as the build** (same host, toolchain, checkout,
lockfile and Cargo configuration), with **one shared argument string**, and keep the files
only if all three succeed:

```sh
ARGS="-p dnp3-ffi --target $TARGET --no-default-features --features serial,tls --locked"
TREE_FMT="--prefix depth --color never --format {p}|{f}"
mkdir -p evidence/$TARGET
cargo build --release $ARGS --message-format json > evidence/$TARGET/build.json
cargo tree $ARGS -e normal,build $TREE_FMT > evidence/$TARGET/tree.txt
cargo tree $ARGS -e normal,no-proc-macro $TREE_FMT > evidence/$TARGET/runtime-tree.txt
```

Use `cross build` instead of `cargo build` for cross builds; the trees can run on the runner
when its host triple equals the container's. The trees must come from the build's host:
build dependencies are resolved for the host, so a tree produced on a Linux packaging runner
does not describe a build that ran on Windows.

Once, anywhere, from the same checkout:

```sh
cargo metadata --format-version 1 --all-features --locked > metadata.json
```

This is only a dictionary of package details (license, source, workspace membership); it
never decides scope or roles.

## Commands

Both commands read a directory with one subdirectory per target (holding `build.json`,
`tree.txt` and `runtime-tree.txt`); a single target is a directory with one subdirectory.

```sh
bom-tools licenses -e evidence --root-package dnp3-ffi -m metadata.json -c allowed.json \
> third-party-licenses.txt
bom-tools sbom -e evidence --root-package dnp3-ffi -m metadata.json -c allowed.json \
--lockfile Cargo.lock > dnp3-ffi-1.7.0.cdx.json
```

`sbom` also takes:

- `--lockfile <Cargo.lock>` (required): the build's lockfile, for each crates.io package's
SHA-256 (the `.crate` archive hash Cargo verified when downloading it);
- `--omit-serial-number`: with `SOURCE_DATE_EPOCH` (used as the timestamp), output is
byte-for-byte reproducible.

### Migrating from 0.2

0.3.0 renamed the executable from `allow-list` to `bom-tools`, replaced `gen-licenses-log*`
with `licenses`, added `sbom`, and removed `gen-licenses` / `gen-licenses-dir`, which read
`cargo cyclonedx` SBOMs: those describe cargo-cyclonedx's own feature selection, not the
product's build. Until you migrate, pin
`cargo install --git https://github.com/stepfunc/bom-tools.git --tag 0.2.1`.

## `allowed.json`

```jsonc
{
// approved to run at build time only; license not reviewed
"build_only": ["cc", "syn"],
// licensed to the customer by the vendor under its commercial license
"vendor": {
"sfio-tokio-ffi": { "url": "https://github.com/stepfunc/tokio-ffi", "embedded": true }
},
// open-source, license reviewed
"third_party": {
"ring": { "id": "ring", "source": "crates.io", "licenses": ["Apache2", { "ISC": { "copyright": "NotPresent" } }] }
},
// names the license of first-party and vendor packages in the SBOM
"commercial_license": { "name": "Step Function I/O License Agreement", "url": "https://..." }
}
```

Rules, checked on every run (all violations are reported together). Workspace members are
our own code and need no entry; the root package must be one.

- every compiled package outside the workspace is in **exactly one** list;
- a runtime package outside the workspace must be in `third_party` or `vendor`; a `build_only`
package that becomes linked into the product is an error;
- a build-time package may be in any list;
- only crates.io and workspace packages are supported;
- for runtime `third_party` packages, every reviewed license must be part of the package's
declared SPDX `license`, and the reviewed licenses must satisfy it (reviewed `MIT` satisfies
`MIT OR Apache-2.0` but not `MIT AND Apache-2.0`).

`commercial_license` is required by the `sbom` command. `embedded: true` marks a build-time dependency whose own code ships anyway (for example a build
script that copies source it provides into the product); it is treated as runtime, its
dependencies are not. License names: `MIT`, `ISC`, `BSD3` (with `copyright`), `Apache2`,
`OpenSSL`, `BSLv1`, `MPLv2`, `UnicodeDFS2016`.

## Guarantees and limits

The cross-checks prove that the full `cargo tree` has exactly the packages, and exactly the
(package, enabled features) variants, of the build log, and that every variant in the runtime
tree is also in the full tree. They do not prove that the files came from one invocation, nor
anything about edges. That catches different feature flags, wider builds and dropped artifacts whose
features differ from the package's other variants.

Not detected, so guaranteed only by producing the files as above:

- a truncated tree file;
- a dropped artifact whose features equal another variant of the same package (e.g. the host
and target builds of a package with no features);
- differences that leave the variant sets unchanged (Cargo does not guarantee `cargo tree`
matches a build exactly).

Supported builds: `cargo build` of a library or binary (`cargo check`, tests, examples and
benches are rejected), crates.io and workspace packages. Not supported: build-script overrides,
`-Zbuild-std`, and non-Rust components inside `-sys` crates beyond the crate's declared license.

When Cargo's SBOM precursor files (`-Z sbom`) are stable, `bom-tools` will read them as its
build evidence instead of the build log and `cargo tree` outputs
([#8](https://github.com/stepfunc/bom-tools/issues/8)). The approval rules, the license report
and the SBOM stay the same; only the input reader changes.
Loading
Loading