feat(sbom): consistent primary component, document identity, and CRA metadata - #364
feat(sbom): consistent primary component, document identity, and CRA metadata#364bomly-guy wants to merge 3 commits into
Conversation
|
Warning Review limit reached
Next review available in: 53 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR enriches SBOM generation with provenance, lifecycle, completeness, version, digest, license, vulnerability, and package-purpose metadata. It adds synthesized roots for multi-root graphs and updates CycloneDX/SPDX conversion, validation, round-trip handling, and documentation. ChangesSBOM enrichment
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant ScanCommand
participant FromDepGraph
participant SBOMEncoder
participant ToGraph
ScanCommand->>FromDepGraph: pass scan metadata and dependency graph
FromDepGraph->>SBOMEncoder: create enriched CycloneDX or SPDX document
SBOMEncoder->>ToGraph: decode components and dependency relationships
ToGraph-->>ScanCommand: reconstruct graph without synthetic root edges
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bomly Diff SummaryCompared Overview
Dependency Changes✅ No dependency changes. Vulnerabilities✅ No vulnerability changes. License Changes✅ No license changes. Project Posture✅ No project posture changes ( Policy Findings✅ No policy differences were identified. |
|
Measured this branch's SBOM output against the just-released sbom-tools v0.2.0 (their "correctness release"), same protocol as the issue-#361 baseline, same scan target:
Primary component, serialNumber, and tool version all verified fixed. Two findings for follow-up (here or #361):
🤖 Generated with Claude Code |
…metadata Improve SBOM export quality based on third-party validator findings (sbom-tools v0.1.22 reported grade F / 43.4 overall on our own scan): - Primary component: synthesize a project root (application type, pkg:generic PURL) when the graph has multiple roots, instead of letting CycloneDX metadata.component fall to an arbitrary manifest node (previously a .github workflow file). The root depends on every graph root, so the exported dependency graph is one connected component, and both formats now name the same scanned project (cross-format component identity: 0 added / 0 removed in sbom-tools diff). Single-root graphs keep their natural root. Re-ingestion skips the pseudo root. - Document identity: generate a UUIDv4 per export; CycloneDX gets a urn:uuid serialNumber and the SPDX namespace reuses the same nonce. - Tool version: emit bomly's version in CycloneDX metadata.tools and as the SPDX "bomly-cli-<version>" tool creator. - Component hashes: project detection-time dependency digests (npm SRI integrity, nuget, cocoapods) into SBOM components, normalized to lowercase hex; parse go.sum h1 tree hashes in the gomod detector and expose them as sha256 (cyclonedx-gomod convention). 89% of components in a self-scan now carry hashes (was 0%). - CRA readiness: optional sbom config section (manufacturer, security_contact, vulnerability_disclosure_url, support_end) emitted as CycloneDX metadata.manufacturer, security-contact/advisories external references, and a support-end property; SPDX gets an Organization creator, root package supplier, and creation-info comment. Self-scan score moves 43.4 (F) to 59.4 with no config and to fully recognized manufacturer/contact/support fields with the sbom section set. NTIA minimum elements remain compliant for both formats; both outputs validate clean against the official CycloneDX 1.7 and SPDX 2.3 schemas. The 3 cycles the validator reported are real Go module-graph cycles (otel, grpc/xds, cloud auth), not export artifacts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21d200d to
1384a9f
Compare
…r digests
Second round of export-quality improvements, applied to both formats where
the format can express them:
- Project version: stamp the primary component and first-party (main
module) components from --ref or `git describe --tags --always --dirty`;
omit rather than invent when Git has nothing to say. Third-party
versions are never touched.
- CycloneDX lifecycles (`pre-build` for source scans, `post-build` for
container images) and a composition completeness declaration that only
claims `complete` for unfiltered, warning-free scans (`incomplete`
under --scope, `unknown` on degraded resolution).
- CycloneDX metadata.authors (manufacturer + contact email) alongside the
existing manufacturer entity.
- Vulnerability `recommendation` derived from enrichment-known fixed
versions ("Upgrade <pkg> to <version>"); nothing is emitted when no fix
is known.
- SPDX license normalization shared by both formats: deprecated SPDX ids
are rewritten token-wise inside expressions (GPL-2.0 -> GPL-2.0-only);
free-text values pass through.
- SPDX PrimaryPackagePurpose on every package (LIBRARY default,
APPLICATION for the primary, OTHER for domain types SPDX cannot name);
decode still prefers the bomly:type comment so round-trips keep richer
types.
- GitHub Actions integrity: SHA-256 digests of workflow/action manifest
files and the pinned commit ID (sha1) of SHA-pinned actions.
Self-scan with --enrich and the sbom config section: CycloneDX 80.9 (B),
SPDX 78.2 — from 43.4/44 (F) at baseline. Vulnerability metrics 100,
integrity 95.9, zero deprecated license ids, both formats schema-clean
and NTIA-compliant. The one remaining CRA error (per-component supplier)
is deliberate: supplier data is not fabricated without a real source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
internal/sbom/transform.go (1)
428-456: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd native fuzz coverage for
normalizeSPDXLicenseExpression.The existing SBOM fuzz target does not reach
componentLicensesor this helper. Add bounded valid, malformed, and truncated seeds, then assert panic safety and deterministic output.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/sbom/transform.go` around lines 428 - 456, Add native fuzz coverage targeting normalizeSPDXLicenseExpression, including bounded seeds for valid, malformed, and truncated SPDX expressions. The fuzz test must assert that the helper never panics and returns deterministic output for identical input, while avoiding unbounded input growth.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/SBOM.md`:
- Around line 151-154: Update the supplier fallback statement in the SBOM
documentation to accurately state that SPDX PackageSupplier remains unset unless
manufacturer metadata is configured or another data source supplies it; do not
claim it defaults to the producing tool unless that fallback is implemented.
In `@internal/cli/scan_cmd.go`:
- Around line 274-290: Add a DEBUG log in gitDescribeVersion immediately before
cmd.Output(), including the git executable path, arguments, and working
directory path so the subprocess can be reproduced. Keep the existing command
execution and error handling unchanged.
In `@internal/config/config.go`:
- Around line 85-88: Add the required configuration metadata for all new SBOM
fields in Resolved and the nested SBOMFile leaf fields: retain doc and env tags,
add appropriate default tags to Resolved, and add yaml, resolved, legacy, and
pointer-backed field definitions to SBOMFile. Ensure each field follows the
existing configuration tag conventions.
In `@internal/config/load.go`:
- Around line 369-370: Update the validation error returned by the time.Parse
call in the SBOM support-end handling to wrap the original parsing error with %w
while retaining the existing contextual message and configured date value.
In `@internal/detectors/gomod/detector.go`:
- Around line 402-434: Add a native Go fuzz test named FuzzParseGoSumDigests for
parseGoSumDigests, seeding valid, malformed, and truncated go.sum inputs. Bound
fuzz data using testutil.MaxFuzzInputSize, invoke the parser through the
repository-file test setup, and assert repeated parsing produces deterministic
results without panics.
In `@internal/sbom/spdx23.go`:
- Around line 48-52: Update the SPDX package conversion around
IsProjectRootComponent to also recognize document roots from doc.Roots, using a
root-component set so natural primary packages receive PackageSupplier when
doc.Provenance.Manufacturer is set. Preserve existing synthetic-root handling,
and add coverage for a single-root document with a manufacturer.
---
Nitpick comments:
In `@internal/sbom/transform.go`:
- Around line 428-456: Add native fuzz coverage targeting
normalizeSPDXLicenseExpression, including bounded seeds for valid, malformed,
and truncated SPDX expressions. The fuzz test must assert that the helper never
panics and returns deterministic output for identical input, while avoiding
unbounded input growth.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 954e1075-4ea4-4485-a6b9-0827ea700d53
⛔ Files ignored due to path filters (2)
docs/CONFIG_REFERENCE.mdis excluded by!docs/CONFIG_REFERENCE.mdinternal/detectors/gomod/testdata/demo/go.sumis excluded by!**/*.sum,!**/testdata/**
📒 Files selected for processing (18)
dev-docs/ARCHITECTURE.mddocs/SBOM.mdinternal/cli/scan_cmd.gointernal/cli/scan_cmd_test.gointernal/config/config.gointernal/config/load.gointernal/config/validate_test.gointernal/detectors/githubactions/detector.gointernal/detectors/githubactions/detector_test.gointernal/detectors/gomod/detector.gointernal/detectors/gomod/detector_test.gointernal/detectors/gomod/parser_fuzz_test.gointernal/sbom/cyclonedx.gointernal/sbom/export_quality_test.gointernal/sbom/graph.gointernal/sbom/model.gointernal/sbom/spdx23.gointernal/sbom/transform.go
| Without these fields Bomly's exports satisfy the NTIA minimum elements | ||
| (supplier defaults to the producing tool); third-party CRA profile checks | ||
| will flag the missing manufacturer/contact metadata until the `sbom` section | ||
| is configured. Per-component supplier and description data is not invented: |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the supplier fallback statement.
The exporter does not default a package supplier to the producing tool. SPDX sets PackageSupplier only when manufacturer is configured. State that supplier metadata remains absent until a data source provides it, or implement the documented fallback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/SBOM.md` around lines 151 - 154, Update the supplier fallback statement
in the SBOM documentation to accurately state that SPDX PackageSupplier remains
unset unless manufacturer metadata is configured or another data source supplies
it; do not claim it defaults to the producing tool unless that fallback is
implemented.
| // gitDescribeVersion derives a project version from Git history when the scan | ||
| // target is a checkout with no explicit ref (local path scans). Returns "" | ||
| // when Git or history is unavailable — the version is then simply omitted. | ||
| func gitDescribeVersion(path string) string { | ||
| if strings.TrimSpace(path) == "" { | ||
| return "" | ||
| } | ||
| gitPath, err := system.LookPath("git") | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| cmd := system.Command(gitPath, "-C", path, "describe", "--tags", "--always", "--dirty") | ||
| out, err := cmd.Output() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| return strings.TrimSpace(string(out)) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Log the Git command before execution.
Line 285 starts a subprocess. It does not log the executable path, arguments, and working directory at DEBUG level. Emit the required DEBUG log before cmd.Output().
As per coding guidelines, “When invoking subprocesses, DEBUG logs must include the binary path, arguments, and working directory so the command can be reproduced.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/cli/scan_cmd.go` around lines 274 - 290, Add a DEBUG log in
gitDescribeVersion immediately before cmd.Output(), including the git executable
path, arguments, and working directory path so the subprocess can be reproduced.
Keep the existing command execution and error handling unchanged.
Source: Coding guidelines
| SBOMManufacturer string `doc:"Organization name emitted as the SBOM manufacturer/supplier (EU CRA Art. 13(15))" env:"BOMLY_SBOM_MANUFACTURER"` | ||
| SBOMSecurityContact string `doc:"Security contact URL or email emitted in exported SBOMs (EU CRA Art. 13(6))" env:"BOMLY_SBOM_SECURITY_CONTACT"` | ||
| SBOMVulnerabilityDisclosureURL string `doc:"Coordinated vulnerability disclosure policy URL emitted in exported SBOMs (EU CRA Art. 13(7))" env:"BOMLY_SBOM_VULNERABILITY_DISCLOSURE_URL"` | ||
| SBOMSupportEnd string `doc:"Support end date (YYYY-MM-DD) for security updates emitted in exported SBOMs (EU CRA Art. 13(8))" env:"BOMLY_SBOM_SUPPORT_END"` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add the required configuration tags.
The new Resolved fields omit default: tags. The new SBOMFile leaf fields omit legacy: tags. Add the required tags for each field.
As per coding guidelines, “Add new configuration fields to Resolved with doc:, env:, and default: tags and to the nested File leaf with yaml:, resolved:, legacy legacy: tags, and pointer-backed shape.”
Also applies to: 109-114
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/config/config.go` around lines 85 - 88, Add the required
configuration metadata for all new SBOM fields in Resolved and the nested
SBOMFile leaf fields: retain doc and env tags, add appropriate default tags to
Resolved, and add yaml, resolved, legacy, and pointer-backed field definitions
to SBOMFile. Ensure each field follows the existing configuration tag
conventions.
Source: Coding guidelines
| if _, err := time.Parse("2006-01-02", supportEnd); err != nil { | ||
| return fmt.Errorf("invalid sbom support_end %q: expected an ISO date such as 2030-12-31", cfg.SBOMSupportEnd) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Wrap the date parsing error.
Preserve the time.Parse error with %w. This retains the operation context and the parse failure details.
As per coding guidelines: “Always wrap errors with contextual information using %w.”
Proposed fix
- return fmt.Errorf("invalid sbom support_end %q: expected an ISO date such as 2030-12-31", cfg.SBOMSupportEnd)
+ return fmt.Errorf("validate sbom support_end %q: expected an ISO date such as 2030-12-31: %w", supportEnd, err)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if _, err := time.Parse("2006-01-02", supportEnd); err != nil { | |
| return fmt.Errorf("invalid sbom support_end %q: expected an ISO date such as 2030-12-31", cfg.SBOMSupportEnd) | |
| if _, err := time.Parse("2006-01-02", supportEnd); err != nil { | |
| return fmt.Errorf("invalid sbom support_end %q: expected an ISO date such as 2030-12-31: %w", supportEnd, err) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/config/load.go` around lines 369 - 370, Update the validation error
returned by the time.Parse call in the SBOM support-end handling to wrap the
original parsing error with %w while retaining the existing contextual message
and configured date value.
Source: Coding guidelines
| // parseGoSumDigests reads go.sum and returns a "path@version" → digest map for | ||
| // module tree hashes ("h1:" lines, excluding the "/go.mod" entries). The h1 | ||
| // hash is SHA-256 over the Go module dirhash manifest; it is exposed as a | ||
| // sha256 digest in hex, matching the convention CycloneDX's cyclonedx-gomod | ||
| // uses for Go module component hashes. | ||
| func parseGoSumDigests(path string) (map[string]sdk.Digest, error) { | ||
| data, err := system.ReadRepositoryFile(path) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("read %q: %w", path, err) | ||
| } | ||
|
|
||
| digests := make(map[string]sdk.Digest) | ||
| scanner := bufio.NewScanner(strings.NewReader(strings.ReplaceAll(string(data), "\r\n", "\n"))) | ||
| for scanner.Scan() { | ||
| fields := strings.Fields(scanner.Text()) | ||
| if len(fields) != 3 { | ||
| continue | ||
| } | ||
| modulePath, version, hash := fields[0], fields[1], fields[2] | ||
| if strings.HasSuffix(version, "/go.mod") || !strings.HasPrefix(hash, "h1:") { | ||
| continue | ||
| } | ||
| raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(hash, "h1:")) | ||
| if err != nil || len(raw) != sha256.Size { | ||
| continue | ||
| } | ||
| digests[modulePath+"@"+version] = sdk.Digest{Algorithm: sdk.DigestAlgorithmSHA256, Value: hex.EncodeToString(raw)} | ||
| } | ||
| if err := scanner.Err(); err != nil { | ||
| return nil, fmt.Errorf("scan %q: %w", path, err) | ||
| } | ||
| return digests, nil | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a fuzz target for parseGoSumDigests.
parseGoSumDigests parses untrusted repository data. The existing fuzz target only calls depGraphFromGoListWithScope. Add FuzzParseGoSumDigests with valid, malformed, and truncated seeds. Bound input with testutil.MaxFuzzInputSize and assert deterministic results.
As per coding guidelines, “New or materially changed pure in-process parsers for untrusted repository ... data must have native Go fuzz targets.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/detectors/gomod/detector.go` around lines 402 - 434, Add a native Go
fuzz test named FuzzParseGoSumDigests for parseGoSumDigests, seeding valid,
malformed, and truncated go.sum inputs. Bound fuzz data using
testutil.MaxFuzzInputSize, invoke the parser through the repository-file test
setup, and assert repeated parsing produces deterministic results without
panics.
Source: Coding guidelines
| if IsProjectRootComponent(c) { | ||
| if doc.Provenance.Manufacturer != "" { | ||
| pkg.PackageSupplier = &common.Supplier{SupplierType: "Organization", Supplier: doc.Provenance.Manufacturer} | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Assign the supplier to natural primary packages too.
A single-root graph does not create a synthetic component. Its DESCRIBES package therefore does not receive PackageSupplier, even when doc.Provenance.Manufacturer is set. CycloneDX attaches provenance to either primary-component form, so the SPDX export now diverges.
Build a set from doc.Roots and set the supplier when the current component is a document root. Add coverage for a single-root document with a manufacturer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/sbom/spdx23.go` around lines 48 - 52, Update the SPDX package
conversion around IsProjectRootComponent to also recognize document roots from
doc.Roots, using a root-component set so natural primary packages receive
PackageSupplier when doc.Provenance.Manufacturer is set. Preserve existing
synthetic-root handling, and add coverage for a single-root document with a
manufacturer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Improves SBOM export quality based on findings from running
sbom-toolsv0.1.22 againstbomly scan -o cyclonedx -o spdxoutput of this repo (baseline: grade F, 43.4 overall). Self-scan now scores 59.4 with zero config, with the remaining CRA errors being data we refuse to invent (see "Deliberate non-fixes").1. Primary component identity (finding 1)
metadata.componentpreviously fell toRoots[0]— an arbitrary manifest node (.github/workflows/auto-version.yml). When a graph has multiple roots,sbom.FromDepGraphnow synthesizes a project root: named after the scanned project,applicationtype,pkg:genericPURL, depending on every graph root. Both formats now agree on the document's subject (sbom-tools cross-format diff: 0 added / 0 removed components, was 44.7% similarity with full mismatch), and the exported graph is one connected component (islands 3 → 1, orphans 1 → 0, roots 14 → 1). Single-root graphs keep their natural root. The pseudo root is excluded from the CycloneDX component inventory and skipped on re-ingestion (round-trip verified).2. Serial number (finding 2)
Each export generates a UUIDv4: CycloneDX
serialNumber(urn:uuid:…), and the SPDX document namespace reuses the same nonce (https://bomly.dev/spdx/<uuid>), so the two exports of one scan are correlatable.3. Tool version (finding 3)
CycloneDX
metadata.tools[]now carries bomly's version; SPDX emitsCreator: Tool: bomly-cli-<version>(SPDX 2.3toolidentifier-versionconvention, same as syft/trivy). Note: sbom-tools' SPDX parser doesn't recognize the hyphen convention (onlyname vX/name (X)/name@X), sohas_tool_versionstays false on the SPDX side — checker quirk, not missing data.4. Component hashes (finding 4)
go.sumand attachesh1:tree hashes as sha256 (hex), matching cyclonedx-gomod's convention. Self-scan: 89% of components carry hashes (was 0%).supplieranddescriptionare not cheaply fillable without enrichment data sources that provide them; we don't fabricate them to satisfy profile checkers. Licenses without--enrichremain empty as expected.5. Dependency graph verification (finding 5)
The CDX
dependenciessection was already complete: all 343 components have an entry; "188/344 with edges" is simply the non-leaf count. The 3 reported cycles are real Go module-graph cycles (go.opentelemetry.io/otel*,grpc/envoyproxy/cncf-xds,cloud.google.com/go/auth*) — module-level cycles are legitimate in Go, not export artifacts. The islands/orphan were the disconnected manifest roots, fixed by the synthesized project root. The residual cross-format semantic-similarity gap (~44%) is an sbom-tools comparator artifact: it derives CDX edge scope from component scope and has no SPDX equivalent (verified: stripping scope from our CDX yields 99.97% similarity; scoped SPDX relationship types don't map either). We keep scope data rather than degrade the export.6. CRA-readiness metadata (finding 6)
New optional
sbom:config section (config file + env vars, no new CLI flags):CycloneDX:
metadata.manufacturer,security-contact/advisoriesexternal refs on the primary component,bomly:support_end_dateproperty. SPDX:Organizationcreator, primary-package supplier, creation-info comment. All four verified recognized by sbom-tools' CRA profile.support_endis validated as an ISO date.Validation
make test,make lint,make generateall clean.--sbomscans of our own exports) produces identical dependency counts, with the pseudo root correctly excluded.Notes for reviewers
SBOM interoperability assuranceworkflow (linux-only validators) should be dispatched on this branch or after merge for the external-validator pass.🤖 Generated with Claude Code
Follow-up commit: lifecycle, completeness, versions, remediation, richer digests
Per review direction, the roadmap quick wins are folded into this PR — implemented as general exporter quality for both formats, never score-chasing:
--reforgit describeoutput; omitted when Git has nothing (no invented versions). Clears the last two CRA component-version errors.pre-build/post-buildphase by target type; completeness declaredcompleteonly for unfiltered, warning-free scans —incompleteunder--scope,unknownon degraded resolution.metadata.authorswith the configured contact email.recommendationrendered from enrichment-known fixed versions only.GPL-2.0→GPL-2.0-only); free text untouched.primaryPackagePurposeon every package; decode still prefers thebomly:typecomment so round-trips keep domain types SPDX can't name.Self-scan with
--enrich+sbom:config: CycloneDX 80.9 (B), SPDX 78.2 (baseline was 43.4/44 F; sbom-tools' own released SBOM scores 68.4). Vulnerability metrics 100, integrity 95.9, zero deprecated license ids, both formats schema-clean against official CycloneDX 1.7/SPDX 2.3 schemas, NTIA-compliant, round-trip re-ingestion verified. Remaining known gaps are deliberate or roadmap: per-component supplier/description need a real data source (deps.dev enrichment), and BOM signing is a separate feature.Summary by CodeRabbit