Skip to content
Draft
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
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Bomly is a **customer-facing, security-sensitive CLI** for dependency intelligen
```sh
make build # build both `bin/bomly` (builtin Syft/Grype) and `bin/bomly-lite`
make build-lite # go build -tags "bomly_external_syft,bomly_external_grype" -o bin/bomly-lite ./cmd/bomly
make fmt # format Go code with the repo formatter
make lint # run golangci-lint
make test # go test ./...
make smoke # end-to-end smoke tests against real repos/containers (slow, needs network)
make smoke ARGS="-update" # regenerate golden files for smoke tests
Expand All @@ -16,7 +18,7 @@ make run ARGS="scan" # go run ./cmd/bomly <ARGS>
make generate # regenerate config reference, JSON schemas, schema docs, and support matrix
```

Always run `make test` after changes. All tests must pass before marking work is done.
Always run `make fmt`, `make lint`, and `make test` after changes and before pushing. All checks must pass before marking work is done.
If you change `internal/cli/config.go`, `internal/output/*`, `sdk/catalog.go`, `sdk/support_matrix.go`, or `internal/registry/support.go`, also run `make generate`.

### Git Worktrees
Expand All @@ -42,6 +44,7 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full detail. Component ma
| `internal/matchers/*` | External enrichment matchers and shared matcher cache (osv, grype, deps.dev, ClearlyDefined, eol, scorecard) |
| `internal/auditors/*` | Policy evaluators and audit-only logic (policy, noop) |
| `internal/sbom` | SBOM codec (SPDX 2.3, CycloneDX) |
| `internal/attestation` | Experimental SBOM attestation subject resolution, in-toto statement construction, and bundle verification |
| `internal/benchmark` | Hidden local dependency-graph benchmark, baseline comparison, scoring, and embedded presets |
| `internal/output` | Output rendering plus structured command payloads and schema generation for `scan`, `diff`, `explain`, JSON, and SARIF 2.1.0 |
| `internal/plugin` | Plugin discovery, protocol, handshake, and execution |
Expand Down Expand Up @@ -142,6 +145,7 @@ Core passes these env vars. Plugin discovery: `~/.bomly/plugins/bomly-*` overrid
## Quality Bar

- Every exported type/function has a doc comment.
- Use TDD for security-sensitive user-visible features: write failing unit or command tests first, implement the smallest clean change that passes, then refactor for readability and maintainability.
- Unit tests for new logic; integration tests for new commands.
- Test helpers: `t.TempDir()`, `testutil.BuildGoBinary()`, `httptest.NewServer()`.
- Generated docs are part of the contract: update `docs/CONFIG_REFERENCE.md`, `docs/schemas/*`, and `docs/SUPPORT_MATRIX.md` via `make generate` when their source packages change.
Expand Down
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ make benchmark # Hidden local dependency-graph benchmark
make benchmark-report # Analyze benchmark artifacts with Copilot CLI
make run ARGS="scan" # Run the CLI directly
make fmt # Format code
make lint # golangci-lint v1.64.8
make lint # golangci-lint v2.12.0
make generate # Regenerate config reference, JSON schemas, support matrix
```

**Always run `make test` after changes.** If you change `internal/config/config.go`, `internal/output/*`, `sdk/catalog.go`, `sdk/support_matrix.go`, or `internal/registry/support.go`, also run `make generate`.
**Always run `make fmt`, `make lint`, and `make test` after changes and before pushing.** If you change `internal/config/config.go`, `internal/output/*`, `sdk/catalog.go`, `sdk/support_matrix.go`, or `internal/registry/support.go`, also run `make generate`.

**Go version**: 1.25.8 (pinned — use exactly this to match CI formatting and build behavior).

Expand Down Expand Up @@ -77,6 +77,8 @@ internal/analyzers/* Reachability analyzers (govulncheck — Go;
pipeline on failure
internal/auditors/* Policy evaluators (policy, noop)
internal/sbom/ SPDX 2.3 / CycloneDX codec
internal/attestation/ Experimental SBOM attestation subject resolution,
in-toto statement construction, and bundle verification
internal/benchmark/ Hidden local dependency-graph benchmark, baseline scoring,
and embedded smoke/benchmark repository presets
internal/output/ Text, JSON, SARIF 2.1.0, SBOM rendering + schema generation
Expand Down Expand Up @@ -140,6 +142,8 @@ Cache failures are non-fatal — log a warning and continue.

**Testing helpers**: `t.TempDir()`, `testutil.BuildGoBinary()`, `httptest.NewServer()`. Shared fake-binary setup lives in `internal/cli/root_test_main_test.go`. No tests may be conditionally skipped without a recorded reason.

**TDD for security-sensitive features**: for user-visible security functionality, write failing unit or command tests first, implement the smallest clean change that passes, then refactor for readability and maintainability before broadening coverage.

## Feature Checklist

When adding a new user-visible feature (new CLI flag, new component class, new pipeline stage, new analyzer, etc.), walk this checklist before requesting review. Reviewers will ask for everything that applies, and the surface that gets forgotten most often is **MCP** + **plugin command** + **smoke test**.
Expand Down
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ Cache failures are non-fatal. The command should warn and continue rather than f
| `internal/engine/scan` | Scan command pipeline API |
| `internal/output` | Text, JSON, SARIF rendering, plus structured response payloads and schema generation |
| `internal/sbom` | SPDX and CycloneDX codecs |
| `internal/attestation` | Experimental SBOM attestation subject resolution, in-toto statement construction, and bundle verification |
| `internal/benchmark` | Hidden local dependency-graph benchmark, baseline comparison, scoring, and embedded presets |
| `sdk` | Shared domain types |
| `internal/plugin` | Managed plugin manifests, installation, verification, store state, adapters, and runtime glue |
Expand Down
2 changes: 2 additions & 0 deletions docs/CI_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ Tune `--fail-on` to taste. `pre-push` keeps commits fast and only runs on push.

## See also

- [SBOM attestations](SBOM_ATTESTATIONS.md) — experimental SBOM signing and verification

- [Exit codes](EXIT_CODES.md) — what each CI exit means
- [Output formats](OUTPUT_FORMATS.md) — SARIF, JSON, SBOM details
- [Auditors](AUDITORS.md) — `--fail-on` and `--fail-on-scope`
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Task-oriented walkthroughs.
- [Scan Targets](SCAN_TARGETS.md) — directories, Git repos, containers, SBOMs
- [Output Formats](OUTPUT_FORMATS.md) — text, JSON, SARIF, SBOM
- [SBOM Formats](SBOM.md) — SPDX vs. CycloneDX, write and ingest
- [SBOM Attestations](SBOM_ATTESTATIONS.md) — experimental signing and verification for SBOM artifacts
- [CI Integration](CI_INTEGRATION.md) — GitHub Actions, GitLab, Jenkins, Azure, CircleCI
- [Interactive TUI](TUI.md) — keybindings and tabs for `--interactive`
- [Troubleshooting](TROUBLESHOOTING.md) — common errors and fixes
Expand Down
4 changes: 4 additions & 0 deletions docs/SBOM.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ A Software Bill of Materials is a structured list of every package in a piece of

You produce an SBOM once and consume it many times: in PR checks, in release artifacts, in supplier audits, in attestation pipelines.

To sign and verify an SBOM claim about a file, folder, Git snapshot, or immutable container digest, see [SBOM attestations](SBOM_ATTESTATIONS.md).

## Format comparison

| | SPDX 2.3 | CycloneDX 1.6 |
Expand Down Expand Up @@ -96,6 +98,8 @@ Bomly does not advertise a one-shot `convert` command — the scan pipeline is t

## See also

- [SBOM attestations](SBOM_ATTESTATIONS.md) — experimental signing and verification

- [Scan targets](SCAN_TARGETS.md) — every input Bomly accepts
- [Output formats](OUTPUT_FORMATS.md) — text, JSON, SARIF, SBOM details
- [SBOM detector](detectors/ecosystems/sbom/sbom.md) — ingest specifics
134 changes: 134 additions & 0 deletions docs/SBOM_ATTESTATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# SBOM Attestations

> **Experimental.** SBOM attestations are opt-in and under active development. The MVP creates and verifies local Sigstore-style DSSE bundles around in-toto statements, but certificate identity verification and registry attachment are not part of the first release.

An SBOM attestation binds an SBOM to the thing it describes. Verification checks that:

- The attestation signature is valid.
- The requested subject digest matches the attested subject.
- The embedded SBOM is SPDX 2.3 or CycloneDX JSON and parses successfully.

An attestation does not prove the SBOM is complete or correct. It proves that a signer made a claim about a subject. Trust still depends on who signed it and how the SBOM was produced.

## Create an attestation

Generate an SBOM first, then attest it:

```bash
bomly scan -o spdx=sbom.spdx.json
bomly sbom attest \
--sbom sbom.spdx.json \
--subject dir:. \
--output sbom.att.json \
--keyless
```

The experimental `--keyless` mode creates a self-contained local bundle for convenient testing and CI artifact integrity checks. It is not an identity-backed Fulcio/OIDC signature yet.

For a durable local signing key, pass an ECDSA P-256 PEM private key:

```bash
bomly sbom attest \
--sbom sbom.spdx.json \
--subject git \
--output sbom.att.json \
--key signing-key.pem
```

Key-signed attestations do not embed the public verification key. Keep the matching public key and pass it during verification.

## Verify an attestation

Verify the bundle against the same subject:

```bash
bomly sbom verify \
--attestation sbom.att.json \
--subject dir:.
```

For an attestation created with `--key`, pass the matching ECDSA P-256 PEM public key:

```bash
bomly sbom verify \
--attestation sbom.att.json \
--subject git \
--key signing-key.pub.pem
```

To extract the verified embedded SBOM:

```bash
bomly sbom verify \
--attestation sbom.att.json \
--subject git \
--extract-sbom verified.spdx.json
```

`--extract-sbom` writes only after signature, subject, predicate, and SBOM checks pass.
Bundles preserve the original SBOM bytes for extraction, including JSON key ordering and whitespace, without duplicating the SBOM as a second parsed JSON object in the predicate.

## How verification works

An attestation is not an encrypted file. Anyone who has the attestation bundle can decode and read the signed payload, including the embedded SBOM. The key is used to prove authenticity, not secrecy.

Think of the bundle as three parts:

| Part | What it means |
| --- | --- |
| Payload | The readable claim: subject digest, SBOM digest, predicate type, and embedded SBOM bytes |
| Signature | A tamper-proof seal over that exact payload |
| Public key or identity material | Information used to check who made the seal |

The private key creates the signature. The public key checks the signature. The public key does not hide or unlock the payload.

Verification answers this question:

> Was this exact SBOM attestation signed by the holder of the expected private key, and does it describe the exact subject I asked Bomly to verify?

Bomly verifies an SBOM attestation in this order:

1. Decode the readable DSSE payload from the bundle.
2. Verify the signature over that exact payload.
3. Resolve the requested subject, such as `file:<path>`, `dir:<path>`, `git`, or `container:<image@sha256:...>`.
4. Hash the requested subject.
5. Compare that digest to the subject digest inside the signed payload.
6. Confirm the predicate type is Bomly's experimental SBOM predicate.
7. Decode the embedded SBOM bytes.
8. Parse the SBOM as supported SPDX or CycloneDX JSON.
9. Hash the embedded SBOM and compare it to the signed SBOM digest.
10. Write `--extract-sbom` only after all checks pass.

If someone changes the subject digest, SBOM bytes, predicate type, or any other signed payload field, the signature check fails. If someone signs a new fake payload with a different key, verification fails unless you trust and pass that signer's public key.

This means:

- Decoding the payload proves nothing by itself.
- A valid signature proves the payload was not changed after signing.
- A trusted public key tells Bomly which signer you expected.
- An attestation proves a signed claim about an SBOM and subject; it does not prove the SBOM is complete, correct, or secret.

## Subjects

| Subject | Use for | Behavior |
| --- | --- | --- |
| `file:<path>` | Release archives, binaries, package files | Hashes the file bytes with SHA-256 |
| `dir:<path>` | Local source folders and monorepos | Computes one deterministic tree digest over regular files |
| `git` | CI source snapshots | Requires a clean worktree and hashes a deterministic `git archive HEAD` view |
| `container:<image@sha256:...>` | Container scan results | Accepts digest references only; tags are rejected |

For folders with multiple subprojects, Bomly creates one attestation for the whole folder snapshot. The SBOM is treated as one document describing the full scan result. If you need per-subproject attestations, scan each subpath separately.

## Limitations

- The feature is experimental and the bundle shape may evolve.
- Container subjects must use `image@sha256:<digest>`; Bomly does not resolve tags for attestation subjects.
- Certificate identity verification flags are reserved for future identity-backed Sigstore bundles.
- Registry attachment is deferred. Store the SBOM and attestation as CI/release artifacts for now.
- The tree digest excludes VCS metadata and any SBOM or attestation output path passed to the command when those outputs live inside the subject folder.

## See also

- [SBOM formats](SBOM.md)
- [CI integration](CI_INTEGRATION.md)
- [Output formats](OUTPUT_FORMATS.md)
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ require (
github.com/glebarez/sqlite v1.11.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.8.0
github.com/in-toto/attestation v1.2.0
github.com/mark3labs/mcp-go v0.54.1
github.com/pandatix/go-cvss v0.6.2
github.com/sigstore/protobuf-specs v0.5.0
github.com/spdx/tools-golang v0.5.7
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down Expand Up @@ -185,7 +187,6 @@ require (
github.com/henvic/httpretty v0.1.4 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/in-toto/attestation v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,8 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY=
github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
Expand Down
Loading
Loading