Skip to content

feat(sbom): CycloneDX 1.5 build-SBOM emission (rivet #107)#129

Merged
avrabe merged 2 commits into
mainfrom
feat/sbom-cyclonedx
May 22, 2026
Merged

feat(sbom): CycloneDX 1.5 build-SBOM emission (rivet #107)#129
avrabe merged 2 commits into
mainfrom
feat/sbom-cyclonedx

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 22, 2026

Summary

Adds CycloneDX 1.5 SBOM emission to synth — the synth-side slice of pulseengine/rivet#107 (Supply chain integration: Sigil attestations + SBOM/AIBOM + Rivet traceability bridge).

When synth compiles a WASM module to an ELF binary, it can now emit a build SBOM documenting exactly what went into that binary. That .cdx.json is the artifact rivet's sbom-record type ingests via rivet import --format cyclonedx.

What the SBOM contains

A build SBOM — not a transitive dependency scan (synth is a compiler, not a linker):

  • metadata.tools — the synth compiler + version ("what built it")
  • The input WASM module as a component — SHA-256 + byte size
  • The output ELF as a component — SHA-256, size, target triple, backend
  • Each WASM import as a component
  • A dependencies graph linking the ELF → WASM module → imports

All required CycloneDX 1.5 fields present: bomFormat, specVersion, serialNumber (urn:uuid, derived from the ELF digest so it's reproducible), metadata, components, dependencies.

CLI surface

A --sbom flag on synth compile (not a subcommand). Bare --sbom writes <output>.cdx.json; --sbom PATH writes verbatim. Chosen over a subcommand because compile already holds every input — the post-decode WASM bytes, decoded imports, target triple, backend, fresh ELF — that a re-deriving subcommand couldn't see. Threaded through both the single-function and --all-exports paths; demo compilations (no input file) warn and skip.

rivet #107 mapping

The emitted .cdx.json is exactly what rivet's sbom-record artifact (type: sbom-record, format: cyclonedx) points at via sbom-ref. Documented in docs/sbom.md.

Implementation

  • New crates/synth-core/src/sbom.rs (629 lines) — CycloneDxSbom struct, serde serialization, 10 unit tests asserting the required-field shape (parse the JSON back with serde_json).
  • crates/synth-cli/src/main.rs — the --sbom flag, both compile paths.
  • New dependency: sha2 = "0.10" (workspace + synth-core), for component digests. +12 Cargo.lock lines.
  • docs/sbom.md — what the SBOM contains, the rivet linkage, the rivet import consumption path.

Determinism

Two runs over the same input produce byte-identical SBOMs except metadata.timestamp (wall-clock, by design for SBOMs).

Validation

  • cargo test --workspace --exclude synth-verify — 1203 passed, 0 failed (10 new SBOM tests).
  • cargo clippy --workspace --exclude synth-verify --all-targets -- -D warnings — clean.
  • cargo fmt --check — clean.
  • End-to-end: compiled a WAT fixture with two imports → well-formed 4-component SBOM.

Follow-ups (out of scope here)

  • Release-pipeline auto-emit — wire --sbom into release.yml and upload the .cdx.json as a signed release asset (noted as "Phase 6" in docs/release-process.md; the compiler side is done).
  • SPDX export (--sbom-format spdx) if a downstream consumer needs it.
  • Import component versions are "unknown" pending WIT/Component-Model version metadata.

Test plan

  • CI green
  • synth compile in.wat out.elf --sbom produces a CycloneDX 1.5 out.elf.cdx.json
  • The JSON validates against the CycloneDX 1.5 schema shape

🤖 Generated with Claude Code

Add `synth compile --sbom`, which writes a CycloneDX 1.5 JSON SBOM next
to the output ELF (`<output>.cdx.json`, or an explicit path). The SBOM is
a *build* SBOM scoped to what synth actually knows as a compiler — not a
transitive dependency scan (synth is not a linker):

- `metadata.tools` — the synth compiler ("what built it").
- the input WASM module — component with SHA-256 + byte size.
- the output ELF — component with SHA-256, size, target triple, backend.
- the WASM module's imports — each becomes a component, linked into the
  output ELF via the CycloneDX `dependencies` graph.

The document is deterministic for a given input except `metadata.timestamp`
(wall-clock by design); the `serialNumber` urn:uuid is derived from the
output ELF digest so it too is reproducible.

This is synth's contribution to the PulseEngine supply-chain chain: the
emitted file is the artifact rivet #107's `sbom-record` ingests via
`rivet import --format cyclonedx`, sitting alongside synth's existing
`safety-manifest.json` and the release pipeline's SLSA provenance.

New `crates/synth-core/src/sbom.rs` mirrors `safety_manifest.rs`; adds the
lightweight `sha2` dependency for component digests. `--sbom` is a flag on
`compile` rather than a subcommand because synth has every input in hand
there (WASM bytes, decoded imports, target, backend, fresh ELF). Docs in
`docs/sbom.md`; release-pipeline auto-emit noted as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 80.42453% with 83 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-cli/src/main.rs 17.77% 74 Missing ⚠️
crates/synth-core/src/sbom.rs 97.30% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

The SBOM module (`synth-core/src/sbom.rs`) uses `sha2` for CycloneDX
component digests and `serde_json` for serialization. Both are declared
in `synth-core/Cargo.toml`, so the cargo build is fine — but synth's
Bazel build uses an explicit `crate.spec` list in MODULE.bazel and a
hand-maintained `crates/BUILD.bazel`, neither of which auto-derives
from Cargo.toml.

CI caught it: `Bazel Build & Proofs` failed compiling the synth-core
rlib with `unresolved import sha2` / `unresolved module serde_json`.

Fixes:
- MODULE.bazel — add `crate.spec(package = "sha2", version = "0.10")`.
- crates/BUILD.bazel — add `@crates//:serde_json` and `@crates//:sha2`
  to the `synth-core` rust_library deps. (`serde_json` was already in
  the `@crates//` set — used by other targets — just not wired to
  synth-core.)
@avrabe avrabe merged commit 24b2c63 into main May 22, 2026
9 of 12 checks passed
@avrabe avrabe deleted the feat/sbom-cyclonedx branch May 22, 2026 05:02
avrabe added a commit that referenced this pull request May 22, 2026
Promotes [Unreleased] to [0.4.0] — RV32 i64 Phase 2 (#128: mul, shifts,
rotates, 64-bit compares, clz/ctz/popcnt, sign-extends), CycloneDX SBOM
emission (#129, the synth slice of rivet #107), and the #72 closure.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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