Skip to content

Add bun pm sbom command - #29512

Open
robobun wants to merge 1 commit into
mainfrom
farm/de84d354/pm-sbom
Open

Add bun pm sbom command#29512
robobun wants to merge 1 commit into
mainfrom
farm/de84d354/pm-sbom

Conversation

@robobun

@robobun robobun commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Adds bun pm sbom, which generates a Software Bill of Materials from the lockfile.

Usage

# CycloneDX 1.7 (default) to stdout
bun pm sbom

# SPDX 2.3 to a file
bun pm sbom --format spdx -o sbom.spdx.json

What's in the SBOM

For every package in the lockfile:

Field CycloneDX SPDX
Name / version name, version name, versionInfo
Identifier bom-ref (e.g. lodash@4.17.21) SPDXID (SPDXRef-Package-lodash-4.17.21)
purl purl (pkg:npm/lodash@4.17.21, scopes %-encoded) externalRefs[].referenceLocator
Download location externalReferences[].url downloadLocation
Integrity hashes[] (SHA-512 hex) checksums[] (SHA512 hex)
Dependency edges dependencies[].dependsOn relationships[]

Registry packages use the tarball URL as the download location; git and github packages use an SPDX 2.3 VCS locator (git+<url>@<revision>). Dev dependencies are marked with scope: "excluded" (CycloneDX) / DEV_DEPENDENCY_OF (SPDX); optional dependencies use scope: "optional" / OPTIONAL_DEPENDENCY_OF. Workspaces, folders, git/github, and tarball resolutions are all represented.

The root component is emitted under metadata.component (CycloneDX) and as the documentDescribes target (SPDX), with Bun listed as the generating tool.

Example (CycloneDX, abridged)

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:...",
  "metadata": {
    "timestamp": "2026-04-20T12:34:56Z",
    "tools": { "components": [{ "type": "application", "name": "bun", "version": "1.x.x" }] },
    "component": { "type": "application", "bom-ref": "my-app@1.0.0", "name": "my-app", "version": "1.0.0" }
  },
  "components": [
    {
      "type": "library",
      "bom-ref": "lodash@4.17.21",
      "name": "lodash",
      "version": "4.17.21",
      "scope": "required",
      "purl": "pkg:npm/lodash@4.17.21",
      "externalReferences": [{ "type": "distribution", "url": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" }],
      "hashes": [{ "alg": "SHA-512", "content": "bf690311..." }]
    }
  ],
  "dependencies": [
    { "ref": "my-app@1.0.0", "dependsOn": ["lodash@4.17.21"] },
    { "ref": "lodash@4.17.21", "dependsOn": [] }
  ]
}

Tests

test/cli/install/bun-pm-sbom.test.ts uses the Verdaccio test registry to install real packages, then validates both output formats against the required fields from the CycloneDX 1.7 and SPDX 2.3 JSON schemas (serial number pattern, timestamp format, purl encoding, hash hex length, SPDXID charset, dependency graph integrity, relationship types).

Fixes #8483


Rebase notes

  • Ported from Zig to Rust after src/ was restructured and Zig left the build (src/runtime/cli/pm_sbom_command.rs, following audit_command.rs / pm_why_command.rs).
  • After Narrow crate-internal Rust visibility across all targets and delete the code it proves dead #36184 narrowed bun_install visibility, this PR re-widens to pub exactly the items it reads cross-crate: the remaining Resolution tags, Resolution::repository(), Integrity::slice and its SHA1/384/512 tags, and Meta.integrity.
  • The test/harness.ts Verdaccio 127.0.0.1 bind fix landed on main independently and is no longer part of this diff.
  • Latest rebase: bun pm licenses landed alongside; the conflicts were keep-both in the pm argument struct, dispatch chain and docs page. The missing-lockfile test now matches the reworded shared error (missing lockfile). The first push of that rebase dropped the Global::exit(0) at the end of the licenses dispatch arm (CI caught it via bun-pm-licenses.test.ts); it is restored, and the dispatch chain now differs from main only by the added sbom arm.

no test proof · iteration 13 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-pm-sbom.test.ts

@robobun

robobun commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:47 PM PT - Aug 14th, 2026

@robobun, your commit 36ae2ab has 3 failures in Build #96923 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29512

That installs a local version of the PR into your bun-29512 executable, so you can run:

bun-29512 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. SBOM support for supply chain security integrations #8483 - Requests SBOM support for supply chain security integrations, which is exactly what bun pm sbom implements with CycloneDX 1.7 and SPDX 2.3 output

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #8483

🤖 Generated with Claude Code

@robobun
robobun force-pushed the farm/de84d354/pm-sbom branch from ef8b96f to 75a7827 Compare April 20, 2026 17:08
@robobun
robobun marked this pull request as ready for review April 20, 2026 18:09
@robobun

robobun commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Ready for maintainer review at 36ae2ab, a single commit rebased onto current main (most recent rebase resolved keep-both conflicts with the new bun pm licenses command; 36ae2ab restores a Global::exit(0) in the licenses dispatch arm that the first push of that rebase dropped, which CI caught through bun-pm-licenses.test.ts, now 79/79 locally). 0 unresolved review threads; 14/14 tests pass locally; cargo clippy clean. The concurrent suite isolates per-test registry state, cache and temp dir (verified 12/12 clean runs on a Windows box after it flaked there in CI).

Verified with test/cli/install/bun-pm-sbom.test.ts (14 tests: both formats' structure, purl encoding, scope propagation incl. optional, transitive-dev and mixed prod+dev paths, workspaces, running from a workspace subdir, git and github download locations, -o absolute/relative/over-long, --format validation, missing lockfile).

CI on 36ae2ab: bun-pm-licenses.test.ts is green again. The remaining red in the build is unrelated to this diff: test/js/node/test/parallel/test-http-chunk-problem.js (failing on main as well, reported separately), test/js/web/timers/setInterval.test.js (a leak test timing out on the x64-asan lane only, also reported separately), and one x64-asan shard that never started because the build artifact download timed out.

Rebase details are in the PR description. Closes #8483.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new bun pm sbom CLI command that generates SBOMs (CycloneDX 1.7 or SPDX 2.3 JSON) from the project lockfile and introduces integration tests validating outputs and error cases.

Changes

Cohort / File(s) Summary
SBOM Command Implementation
src/cli/pm_sbom_command.zig
New PmSbomCommand with exec, printHelp, and Format.fromString. Builds SBOM Generator from lockfile: timestamps/UUID, components (PURL, download_url, hashes), deduplicated refs/SPDX IDs, scope determination, dependencies collection, and serializers writeCycloneDX / writeSPDX. Handles --format, --outfile, stdout, error exits, and resource deinit. Attention: ref/SPDX ID collision logic, purl/download_url generation, dependency scope mapping, and file-write error handling.
SBOM Integration Tests
test/cli/install/bun-pm-sbom.test.ts
Adds tests that run a local Verdaccio registry, create packages/workspaces, run bun pm sbom (various flags), and assert CycloneDX/SPDX JSON shape and content, file output behavior, workspace handling, invalid-format and missing-lockfile error cases, and help text. Attention: fragile network/registry setup and strict JSON schema assertions.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses issue #8483 requirements: implements standardized SBOM output (CycloneDX 1.7 and SPDX 2.3), makes output compatible with existing security tooling, exposes generation via CLI (bun pm sbom), and reduces workarounds.
Out of Scope Changes check ✅ Passed All changes directly support the bun pm sbom feature: implementation file introduces the command, test file validates output formats, both aligned with the feature's objectives.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding the bun pm sbom command.
Description check ✅ Passed The description explains the command, supported formats, output fields, usage, tests, issue linkage, and verification status.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 365-388: The function collectDeps declares unused parameters
deps_buf and pkg_dependencies (they are immediately discarded with `_ =`) —
either remove these parameters from collectDeps' signature and update every call
site to stop passing those arguments, or, if intended, implement the missing
logic to use pkg_dependencies and deps_buf (e.g., filter resolved_id by
consulting pkg_dependencies and map via deps_buf) inside collectDeps; update the
symbol names mentioned (collectDeps, deps_buf, pkg_dependencies,
comp.deps.append) accordingly so callers and internal logic stay consistent.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: d795fc1d-65eb-4fda-887b-426caa6f4a0d

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2d207 and 75a7827.

📒 Files selected for processing (5)
  • src/cli/package_manager_command.zig
  • src/cli/pm_sbom_command.zig
  • src/install/PackageManager/CommandLineArguments.zig
  • src/install/PackageManager/PackageManagerOptions.zig
  • test/cli/install/bun-pm-sbom.test.ts

Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 76-78: The writeAll call currently swallows all errors via
`Output.writer().writeAll(output) catch {}`, so change it to explicitly handle
errors: capture the result, if the error is the broken-pipe EPIPE (e.g., compare
against `error.EPIPE` or the platform-specific EPIPE constant) then ignore it,
otherwise log the error or propagate/return it (for example use `|err| { if (err
== error.EPIPE) {} else std.debug.print("stdout write failed: {}\n", .{err});
}`). Update the block around the `Output.writer().writeAll(output)` invocation
to implement this explicit branching so non-EPIPE I/O failures are not silently
discarded while preserving the common broken-pipe behavior.
- Around line 332-340: The scope selection for packages (using pkg_flags[idx],
flags, and Scope) misclassifies packages with optional=true and dev=true as
.required; update the conditional order so that after checking for required and
the dev-only excluded case you check if flags.optional and !flags.required and
return .optional (i.e., add a branch evaluating flags.optional and
!flags.required before the final else), leaving the existing .dev-only
(.excluded) and default .required branches intact so packages with only optional
non-dev edges are marked .optional.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: 8131f36e-636e-4aae-8b29-bfb08554f5ed

📥 Commits

Reviewing files that changed from the base of the PR and between 75a7827 and aa6b852c0115f0935430f92c0fefa163e4ef9efa.

📒 Files selected for processing (1)
  • src/cli/pm_sbom_command.zig

Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/cli/pm_sbom_command.zig (2)

76-78: ⚠️ Potential issue | 🟡 Minor

Do not silently swallow stdout write failures.

catch {} masks all output errors, which can hide real write failures in redirected/CI environments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/pm_sbom_command.zig` around lines 76 - 78, The write to stdout is
silently ignored by the `Output.writer().writeAll(output) catch {}` which can
hide real failures; replace the empty catch with proper error handling—either
propagate the error with `try Output.writer().writeAll(output);` or handle and
report it (e.g., log to stderr or return the error) inside the `pm_sbom_command`
flow so write failures are not swallowed. Ensure you update any surrounding
function signature to propagate the error if you choose `try`.

337-344: ⚠️ Potential issue | 🟠 Major

Fix scope classification when a package has both dev and optional incoming edges.

The current branch order marks optional=true + dev=true + required=false as .required, but your own rule says it should be .optional when every non-dev edge is optional.

♻️ Proposed fix
-            const scope: Scope = if (flags.required or (res.tag == .root or res.tag == .workspace))
+            const scope: Scope = if (flags.required or (res.tag == .root or res.tag == .workspace))
                 .required
-            else if (flags.optional and !flags.dev)
-                .optional
             else if (flags.dev and !flags.optional and !flags.required)
                 .excluded
+            else if (flags.optional and !flags.required)
+                .optional
             else
                 .required;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/pm_sbom_command.zig` around lines 337 - 344, The scope classification
logic in the const declaration for scope incorrectly falls through to .required
when flags.optional and flags.dev are true but flags.required is false; to fix,
update the branch order/conditions in the expression that sets scope (the const
scope: Scope = ... using flags and res.tag) so that after the initial required
check (flags.required or res.tag == .root or .workspace) you check
flags.optional (e.g., else if flags.optional => .optional) before the dev-only
check, then keep the dev-exclusive branch (flags.dev and !flags.optional and
!flags.required => .excluded) and the fallback; adjust only the condition
ordering in that const expression to ensure optional wins when every non-dev
edge is optional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 76-78: The write to stdout is silently ignored by the
`Output.writer().writeAll(output) catch {}` which can hide real failures;
replace the empty catch with proper error handling—either propagate the error
with `try Output.writer().writeAll(output);` or handle and report it (e.g., log
to stderr or return the error) inside the `pm_sbom_command` flow so write
failures are not swallowed. Ensure you update any surrounding function signature
to propagate the error if you choose `try`.
- Around line 337-344: The scope classification logic in the const declaration
for scope incorrectly falls through to .required when flags.optional and
flags.dev are true but flags.required is false; to fix, update the branch
order/conditions in the expression that sets scope (the const scope: Scope = ...
using flags and res.tag) so that after the initial required check
(flags.required or res.tag == .root or .workspace) you check flags.optional
(e.g., else if flags.optional => .optional) before the dev-only check, then keep
the dev-exclusive branch (flags.dev and !flags.optional and !flags.required =>
.excluded) and the fallback; adjust only the condition ordering in that const
expression to ensure optional wins when every non-dev edge is optional.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f4efe230-88fd-4e71-a809-095c3179c381

📥 Commits

Reviewing files that changed from the base of the PR and between aa6b852c0115f0935430f92c0fefa163e4ef9efa and 832ef7f443f5da2979d83a726c1a236863c601e3.

📒 Files selected for processing (4)
  • src/cli/package_manager_command.zig
  • src/cli/pm_sbom_command.zig
  • src/install/PackageManager/CommandLineArguments.zig
  • src/install/PackageManager/PackageManagerOptions.zig

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/cli/pm_sbom_command.zig (1)

76-78: ⚠️ Potential issue | 🟠 Major

Do not treat every stdout write failure as success.

catch {} hides real I/O failures like ENOSPC or EBADF, so callers can get exit code 0 even though no SBOM was written. Ignoring broken pipes is fine, but other write errors should still fail the command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/pm_sbom_command.zig` around lines 76 - 78, The current
Output.writer().writeAll(output) call swallows all write errors via catch {};
change it to explicitly handle broken-pipe errors only and propagate any other
I/O failures so the command can fail. Replace the silent catch with a catcher
that matches the write error (capture the error from
Output.writer().writeAll(output) catch |err| { ... }), ignore only the broken
pipe/EPIPE case (compare against std.os.EPIPE or the framework's broken-pipe
error) and otherwise return or propagate the error (so the process exits
non-zero); keep the write call and the output variable as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 238-255: The cloned name returned by
json.getStringCloned(allocator, "name") is being duplicated again with
allocator.dupe for this.root.name, leaking the original clone; modify the branch
that sets root_name so that when json.getStringCloned(...) returns a value you
either (A) assign that owned buffer directly to this.root.name and avoid the
later allocator.dupe, or (B) if you must keep the pattern of using root_name,
free the original clone after creating the duplicate (use defer or explicit
free) so ownership is balanced; update construction of root_ref/this.root to use
the chosen owned buffer and ensure deinit() can free it unconditionally.
- Around line 329-334: The current uniqueness check uses the raw ref but
SpdxIdFormatter later sanitizes refs, so generate a sanitized SPDX id (using the
same sanitation logic as SpdxIdFormatter) for each ref (including the root
package) before inserting into seen_refs and before serialization; if the
sanitized id already exists, append a disambiguator like "~{idx}" to the
sanitized id (allocating with allocator as done for unique), free any replaced
allocations, then insert the sanitized id into seen_refs and use that sanitized
identifier for output. Apply the same fix in both places referenced (the shown
block around the ref handling and the similar block at 665-677) so collisions
are prevented after sanitization.

---

Duplicate comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 76-78: The current Output.writer().writeAll(output) call swallows
all write errors via catch {}; change it to explicitly handle broken-pipe errors
only and propagate any other I/O failures so the command can fail. Replace the
silent catch with a catcher that matches the write error (capture the error from
Output.writer().writeAll(output) catch |err| { ... }), ignore only the broken
pipe/EPIPE case (compare against std.os.EPIPE or the framework's broken-pipe
error) and otherwise return or propagate the error (so the process exits
non-zero); keep the write call and the output variable as-is.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: cdf93f2d-f318-4f8b-b549-c3406a602a38

📥 Commits

Reviewing files that changed from the base of the PR and between 832ef7f443f5da2979d83a726c1a236863c601e3 and 8cf31778d29d083666c891929fab30eae6fd2979.

📒 Files selected for processing (1)
  • src/cli/pm_sbom_command.zig

Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
@robobun
robobun force-pushed the farm/de84d354/pm-sbom branch from 8cf3177 to 26a5d1c Compare April 20, 2026 18:45
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

No merge conflicts detected when merging into main.

Your branch is good to go!

Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 666-682: The current writeSPDXRelationships function (inside
writeSPDXRelationships and the relationshipType block) stops at the first
matching raw dependency entry and returns that edge's relationship, making
output input-order dependent; instead, scan all deps/resolved pairs for the
given dep_id (using pkg_dep_resolutions, pkg_deps and the for (deps, resolved)
loop) and accumulate flags for whether any matching entry is required, optional,
or dev, then pick the relationship by precedence (required -> DEPENDS_ON,
optional -> OPTIONAL_DEPENDENCY_OF, dev -> DEV_DEPENDENCY_OF); replace the early
breaks with logic that checks all matches and sets rel_type accordingly so
duplicate resolved_ids yield the correct aggregated relationship (consistent
with collectDeps/component scope).
- Around line 345-362: The current single contains() check for seen_refs and
seen_spdx_ids can still produce collisions because the first disambiguated
candidate may already exist; change both branches (the ref disambiguation and
the spdx_id disambiguation around sanitizeSpdxId) to loop-generating new
candidates until a unique value is produced (e.g., try "{s}~{d}" or "{s}.{d}"
with an incrementing suffix) and only call try seen_refs.put(ref) / try
seen_spdx_ids.put(spdx_id) once the final unique string is chosen; ensure each
intermediate allocated string (ref or spdx_id and any generated unique
candidate) is freed via allocator.free() when it is replaced to avoid leaks, and
keep using allocator, idx, seen_refs, seen_spdx_ids, sanitizeSpdxId, ref, and
spdx_id to locate the changes.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: e48ece5c-015c-493a-a511-945a869e9037

📥 Commits

Reviewing files that changed from the base of the PR and between 8cf31778d29d083666c891929fab30eae6fd2979 and 26a5d1c.

📒 Files selected for processing (1)
  • src/cli/pm_sbom_command.zig

Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/de84d354/pm-sbom branch from c7cfa9f to 2a72ba0 Compare May 4, 2026 10:36
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread test/cli/install/bun-pm-sbom.test.ts Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated
Comment thread src/cli/pm_sbom_command.zig Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior review feedback is addressed and I didn't find new issues, but this is a ~800-line new user-facing feature implementing two external SBOM specs (CycloneDX 1.7 / SPDX 2.3) with several design choices made along the way (whole-lockfile vs workspace-scoped output, BFS scope semantics, global --format/-o flags on bun pm), plus a shared test/harness.ts change — worth a human pass before merge.

Extended reasoning...

Overview

This PR adds bun pm sbom, a new CLI subcommand that generates a Software Bill of Materials from the lockfile in CycloneDX 1.7 or SPDX 2.3 JSON. The bulk of the change is the new src/cli/pm_sbom_command.zig (~790 lines) containing a Generator that walks the lockfile, runs a BFS from the root to classify each package's scope (required/optional/excluded), deduplicates bom-refs and SPDXIDs, and serializes to either format. Wiring changes in package_manager_command.zig, CommandLineArguments.zig, and PackageManagerOptions.zig are small and mechanical. A ~400-line integration test exercises both formats against a Verdaccio registry. test/harness.ts is modified to bind Verdaccio on 127.0.0.1 explicitly (affects all tests using VerdaccioRegistry, not just this one).

Security risks

No injection/auth/permission surface — the command is read-only over the lockfile and writes to a user-specified path. However, SBOMs are a supply-chain security artifact: downstream scanners (Grype, Trivy, Dependency-Track) will rely on the scope/relationship fields to decide which vulnerabilities to surface, so semantic correctness here has security implications even though the code itself isn't security-sensitive.

Level of scrutiny

This warrants human review. It is a brand-new public CLI command implementing two external specifications, and it went through ~15 rounds of bot-driven fixes during review — including two rewrites of the scope-classification logic (per-edge → BFS → BFS-from-lockfile-root), an arena-allocator refactor, and a late fix for -o cwd resolution. Several design decisions were made without human input: always describing the whole lockfile regardless of cwd (vs. a workspace-scoped SBOM), adding --format/-o to the shared bun pm clap param set, and the precise scope/relationship precedence rules. Someone familiar with how Bun wants to position this against npm sbom/cyclonedx-npm should sanity-check those choices and ideally spot-check the output against a real scanner.

Other factors

Test coverage is solid (12 tests covering both formats, scope propagation, workspaces, subdir invocation, -o resolution, error cases). All inline review threads are resolved. The test/harness.ts change is reasonable but touches shared test infrastructure used across the install test suite, which is another reason for a human glance.

@robobun
robobun force-pushed the farm/de84d354/pm-sbom branch from 023fc1c to 5826fa1 Compare May 17, 2026 12:01
Comment thread src/runtime/cli/package_manager_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs Outdated
Comment thread src/runtime/cli/pm_sbom_command.rs Outdated
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread test/cli/install/bun-pm-sbom.test.ts
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs Outdated
Comment thread src/runtime/cli/pm_sbom_command.rs Outdated
Comment thread src/runtime/cli/pm_sbom_command.rs Outdated
Comment thread src/runtime/cli/pm_sbom_command.rs
Comment thread src/runtime/cli/pm_sbom_command.rs Outdated
Comment thread test/cli/install/bun-pm-sbom.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the automated pass found no bugs — all earlier nits (in-tree hex/date helpers, optional-dependency coverage, over-long -o handling, SPDX §7.7 VCS locators, the orphaned doc comment, and describe.concurrent) are addressed through 8bfa0d8. Because this adds a new user-facing subcommand with two spec-defined output formats and re-widens several bun_install items to pub, a maintainer look at the API surface would still be worthwhile.

What was reviewed: scope-propagation BFS and the Required > Optional > Excluded lattice; bom-ref/SPDXID dedup and the sanitizer charset; purl percent-encoding and vcs_locator against SPDX 2.3 §7.7; -o path resolution via join_abs_string_buf_checked; and test hermeticity (Verdaccio + local GITHUB_API_URL server, per-test temp dirs, concurrent describe).

Extended reasoning...

Overview

This PR adds bun pm sbom, a new subcommand that emits a Software Bill of Materials from the lockfile in CycloneDX 1.7 or SPDX 2.3 JSON. The core is a new ~1000-line src/runtime/cli/pm_sbom_command.rs that walks the lockfile, computes per-package scope via a strongest-path BFS, builds Component records (name/version/purl/download URL/integrity/deps), and hand-serializes both formats. It is wired into the pm dispatcher, CLI argument parser, options struct, help text, docs, and shell completions. To read lockfile internals cross-crate it widens Resolution::repository(), the Resolution::Tag constants, Integrity::slice() + its SHA tags, and Meta.integrity from pub(crate) to pub. shell/builtin/ls.rs::civil_from_days becomes pub(crate) so the timestamp helper can be reused rather than duplicated. A 486-line test file exercises both formats via Verdaccio, workspaces, git/github deps served locally, error paths, and CLI edge cases.

Security risks

Low. The command reads a trusted local lockfile and emits JSON; there is no network I/O in the command itself. The one user-controlled write path (-o <path>) now uses join_abs_string_buf_checked and exits cleanly on overflow rather than panicking. JSON string values are escaped via bun_core::fmt::format_json_string_utf8, so lockfile-derived names/URLs cannot break out of string context. purl encoding percent-escapes outside the RFC 3986 unreserved set. No auth, crypto, or permission logic is touched.

Level of scrutiny

High — this is a new user-facing API surface. Per the repo's landing-PR guidance, API additions call for maintainer agreement on shape: whether CycloneDX 1.7 / SPDX 2.3 are the right targets, whether the flag spellings (--format, -o/--outfile) match sibling commands, whether the pub(crate)pub widenings in bun_install are acceptable long-term, and whether hand-rolled JSON serialization is preferred over a schema-checked emitter. The implementation itself has been through several review iterations and looks sound, but these are design calls a bot should not sign off on unilaterally.

Other factors

Test coverage is solid after the review-driven additions: required/optional/excluded scopes, transitive-dev vs mixed prod+dev paths, scoped-package purl encoding, workspaces (including running from a subdir with relative -o), git+file:// and github: deps via a local tarball server, unknown --format, missing lockfile, over-long -o, and the help-text entry. Tests are hermetic (no public network) and now run under describe.concurrent. The finder-flagged "ported from" trailer comment referencing a deleted Zig file is cosmetic and was ruled out. All prior inline threads on this PR are resolved.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the bug-hunting pass found no issues; all earlier review threads have been addressed. Since this adds a new user-facing CLI command (bun pm sbom) with ~1000 lines of new Rust plus visibility widening in bun_install, a maintainer sign-off on the API surface and SBOM format choices would still be worthwhile.

Checked: scope-propagation BFS and SPDX *_OF edge direction against the test assertions; -o path handling now uses join_abs_string_buf_checked (no panic on long argv); git/github downloadLocation now emits SPDX §7.7 VCS locators; hex/timestamp/percent-encoding reuse in-tree bun_core::fmt and civil_from_days; visibility changes in integrity.rs/resolution.rs/Meta.rs are read-only accessors matching what the command consumes.

Extended reasoning...

Overview

This PR adds bun pm sbom, a new CLI subcommand that reads the lockfile and emits a Software Bill of Materials in CycloneDX 1.7 or SPDX 2.3 JSON. The core is a ~1000-line new file src/runtime/cli/pm_sbom_command.rs implementing a Generator that walks the lockfile's package list, computes per-package scope via BFS from the root, builds Component records (bom-ref, SPDXID, purl, download URL, integrity hash, dep edges), and hand-serializes both output formats. Supporting changes wire the subcommand into package_manager_command.rs, add --format/--outfile options to CommandLineArguments/PackageManagerOptions, widen pub(crate)pub on the Resolution tags, Resolution::repository(), Integrity::slice()/tag constants, and Meta.integrity so the runtime crate can read them, expose civil_from_days from shell/builtin/ls.rs as pub(crate), and add docs/completions/help text. A 486-line Verdaccio-backed test file covers both formats, scope propagation (required/optional/excluded, mixed-path), workspaces, git/github locators, -o handling, and error cases.

Security risks

Low. The command is read-only against the lockfile and writes only to the user-specified -o path (or stdout). Input surface is CLI argv and lockfile contents. The -o path now goes through join_abs_string_buf_checked and exits cleanly on overflow rather than panicking. JSON output escapes all lockfile-derived strings via bun_core::fmt::format_json_string_utf8, and SPDXID generation sanitizes to [A-Za-z0-9.-] with collision-dedup. No network, no auth, no crypto beyond reading the stored integrity digest. The vcs_locator helper reads repo.resolved/repo.committish from the lockfile string buffer with bounds derived from stored String handles. The visibility widening exposes only read-only data (union tag constants, a slice getter, a Copy field) — no new mutation surface.

Level of scrutiny

This warrants human review. It is a net-new user-facing feature with API-design decisions a maintainer should ratify: the CLI flag names (--format, -o/--outfile), the default format (CycloneDX 1.7), the SBOM field mappings (e.g. dev deps → scope: "excluded" / DEV_DEPENDENCY_OF, whole-lockfile scope even from a workspace subdir), and the choice to hand-serialize JSON rather than use a schema library. REVIEW.md's referenced landing-prs.md specifically calls out API design as requiring a targeted read before adding user-facing surface. The visibility widening in bun_install is a small architectural change that a maintainer should acknowledge. None of this is risky per se — it's additive, well-tested, and follows the sibling-command patterns (pm_why_command.rs, audit_command.rs) — but ~1500 net-new lines of feature code is beyond the "simple/mechanical/obvious" bar for auto-approval.

Other factors

The PR has been through many rounds of automated review over its lifetime (the timeline shows dozens of resolved threads from CodeRabbit, comment-cop, and prior bug-hunter passes), and every finding from earlier runs of this reviewer — optional-dep coverage, in-tree helper reuse, -o overflow, SPDX VCS-locator syntax, orphaned doc comment, describe.concurrent — was addressed in follow-up commits and resolved. Test coverage is solid: 14 concurrent Verdaccio-backed tests exercising both formats, all three scope states, transitive-dev vs mixed-path, workspaces from root and subdir, git+file:// and github: (via local GITHUB_API_URL), -o relative/absolute/overlong, unknown --format, and missing lockfile. No human reviewer has commented on the PR.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the bug-hunting pass found no issues; all earlier inline findings have been addressed. Because this adds a new user-facing bun pm subcommand (~1000 lines of new Rust plus ~500 lines of tests) and makes API-surface choices — CycloneDX 1.7 vs SPDX 2.3, purl/VCS-locator formatting, scope semantics, and re-widening several bun_install items to pub — a maintainer sign-off on the design would still be worthwhile.

What was reviewed:

  • Scope-propagation BFS and SPDX relationship direction (*_OF subject/object ordering) — covered by tests for required/optional/excluded and mixed prod+dev paths.
  • JSON serialization: all user-derived strings go through json_str/format_json_string_utf8; SPDXIDs are sanitized and independently deduplicated from bom-refs.
  • -o path handling uses join_abs_string_buf_checked (no panic on over-long argv); write errors exit non-zero.
  • Visibility widenings in integrity.rs/resolution.rs/Meta.rs are read-only accessors and tag constants; no new mutation surface.
Extended reasoning...

Overview

This PR adds bun pm sbom, a new CLI subcommand that reads the lockfile and emits a Software Bill of Materials in CycloneDX 1.7 or SPDX 2.3 JSON. The bulk of the change is a new file src/runtime/cli/pm_sbom_command.rs (~1000 lines) plus wiring in package_manager_command.rs, CLI argument plumbing in CommandLineArguments.rs/PackageManagerOptions.rs, help text, docs, shell completions, and a 500-line Verdaccio-backed test file. It also widens a handful of bun_install items from pub(crate) to pub (the Resolution::Tag constants, Resolution::repository(), Integrity::slice() and its tag constants, Meta.integrity) so the new command can read them cross-crate, and makes civil_from_days in shell/builtin/ls.rs pub(crate) for reuse.

Security risks

Low. The command is read-only against the lockfile and writes either to stdout or to a user-specified file. All strings interpolated into the JSON output (package names, versions, URLs, refs) are routed through bun_core::fmt::format_json_string_utf8, so lockfile content cannot break out of a JSON string. SPDXIDs are sanitized to [A-Za-z0-9.-] and independently deduplicated. The -o path is joined against the original cwd via join_abs_string_buf_checked and errors cleanly on overflow rather than panicking. No network, no auth, no untrusted-input parsing beyond what PackageManager::init already does.

Level of scrutiny

High — this is a new user-facing CLI subcommand with externally-specified output formats. Per the repo guidance, adding user-facing API surface calls for the API-design section of .claude/docs/landing-prs.md and maintainer agreement on the shape (flag names, default format, spec versions, whether --format/-o on bun pm collide with future subcommands). The implementation itself is self-contained and well-tested, but the design choices — CycloneDX 1.7 as default, SPDX 2.3 field selection, NOASSERTION for licenses, scope: excluded for dev deps, VCS-locator formatting — are the kind of thing a maintainer should ratify before they become a compatibility surface for downstream SBOM consumers.

Other factors

The PR has been through many review iterations (all inline threads resolved): optional-dependency coverage, in-tree helper reuse (hex_lower, hex_byte_upper, civil_from_days), -o path-length hardening, SPDX §7.7-conformant VCS locators, describe.concurrent with per-test cache/tmp isolation, and dead-comment cleanup. Test coverage is thorough (14 tests: both formats, purl encoding, scope propagation including transitive-dev and mixed paths, workspaces from root and subdir, git+github locators via a local server, error paths). This run's bug-hunting pass found nothing. The remaining reason to defer is scope, not correctness concerns.

Comment thread src/runtime/cli/package_manager_command.rs
Generates a Software Bill of Materials from the lockfile in either
CycloneDX 1.7 (default) or SPDX 2.3 JSON format.

  bun pm sbom                               # CycloneDX to stdout
  bun pm sbom --format spdx -o sbom.json    # SPDX to file

For each package in the lockfile the SBOM includes the package name,
resolved version, purl identifier, download location (registry tarball
URL, or a git+<url>@<revision> VCS locator for git and github packages),
and integrity hash. Dependency relationships are emitted as the CycloneDX
`dependencies` graph or SPDX `relationships`.

Scope classification is computed via BFS from the lockfile root: a
package's scope is the strongest (required > optional > excluded) over
all paths that reach it, where a path's scope is the weakest edge along
it, so transitive deps of a root devDependency are `excluded`, but a
package reachable via both a dev path and a prod path is `required`.
This matches what `bun install --production` would actually install.

The SBOM always describes the whole lockfile, regardless of which
workspace subdirectory the command is run from. Relative `-o` paths
resolve against the invocation directory.

Re-widens to `pub` the bun_install items this command reads cross-crate
(the remaining Resolution tags, Resolution::repository, Integrity::slice
and its SHA1/384/512 tags, Meta.integrity), which #36184 had narrowed
because nothing outside the crate used them at the time, and exposes the
shell's civil_from_days within the runtime crate for the timestamp.

Closes #8483.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rebase regression I flagged on the licenses dispatch arm is fixed — both licenses and sbom now terminate with their own Global::exit(0). This pass found no further issues. Given the size (~1000 lines of new Rust implementing a user-facing bun pm sbom subcommand against two external specs, plus visibility widening across bun_install), a human sign-off on the API surface and SBOM spec conformance is still worthwhile.

What was reviewed: the package_manager_command.rs dispatch chain (verified licenses/sbom/pkg each exit correctly); scope-propagation BFS and ref/SPDXID dedup in Generator::init; vcs_locator against SPDX §7.7; the -o path handling (checked variant, pooled buffer). Ruled out: test temp-dir leakage via newProject() (verifiers confirmed harmless).

Extended reasoning...

Overview

This PR adds bun pm sbom, a new subcommand that generates CycloneDX 1.7 or SPDX 2.3 SBOM documents from the lockfile. The bulk of the change is a new ~1000-line file src/runtime/cli/pm_sbom_command.rs plus ~500 lines of Verdaccio-backed integration tests. Supporting changes wire the subcommand into CLI parsing, help text, docs, and completions, and re-widen a handful of bun_install items (Resolution::repository(), Integrity::slice(), several Tag consts, Meta.integrity) from pub(crate) to pub so the runtime crate can read them. One incidental change makes civil_from_days in shell/builtin/ls.rs pub(crate) for reuse.

Prior finding resolved

My previous review (2026-08-14) flagged a 🔴 rebase mistake where the new sbom arm was inserted between PmLicensesCommand::exec(...)? and its Global::exit(0), causing bun pm licenses to fall through to the unknown-command handler. The current diff shows this is fixed: the licenses arm at package_manager_command.rs:754-756 now ends with its own Global::exit(0) before the sbom arm begins, and sbom has its own terminator. All earlier nits (SPDX VCS locator format, join_abs_string_buf_checked, orphaned doc comment, describe.concurrent, in-tree helper reuse) were also addressed in prior commits and remain in place.

Security risks

Low. The command reads the lockfile and package.json from the workspace root and writes JSON to stdout or a user-specified file. Output-path handling uses the checked path-join variant with a clean error on overflow. The SBOM output includes package names, versions, tarball URLs, and integrity hashes — all already present in bun.lock. No network I/O, no untrusted input parsing beyond the lockfile Bun already trusts.

Level of scrutiny

Moderate-to-high. This is a new user-facing CLI subcommand implementing two external specifications (CycloneDX 1.7 JSON schema, SPDX 2.3). The JSON is hand-serialized line-by-line rather than via a structured serializer, so field ordering, escaping, and comma placement are all manual. The test suite validates the required schema fields for both formats, but spec conformance for edge cases (unusual package names, git URL shapes, purl encoding subtleties) is the kind of thing a maintainer familiar with SBOM tooling should eyeball. The visibility widening on bun_install types is minimal and read-only, but reverses part of #36184's narrowing and should be acknowledged.

Other factors

The PR has been through ~10 rounds of automated review with every finding addressed. Test coverage is solid: both formats, scoped packages, workspaces, git+github deps via a local server, dev/optional/transitive scope propagation, error paths, and the -o path-too-long guard. The bug-hunting pass this run raised only two candidate findings about test temp-dir cleanup, both refuted by verifiers. I'm deferring rather than approving because a ~1500-line new feature that defines public CLI surface and claims conformance to two external standards is exactly the case where a human should confirm the API shape and output format before it ships.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SBOM support for supply chain security integrations

2 participants