Skip to content

Add bun pm sbom command - #29512

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

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 diff (Add bun pm diff #39229) landed alongside; the two conflicts were keep-both in CommandLineArguments.rs (the pm option fields and their parsing). Its new flags do not overlap with --format / -o, --outfile; bun-pm-sbom, bun-pm-licenses and bun-pm-diff test files all pass on the rebased build.
  • Earlier rebase (bun pm licenses): 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 18 · 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 8:53 PM PT - Aug 17th, 2026

✅ @robobun, your commit c623d42767dc32d92308b7ec4a00cb5bf7fb4bf5 passed in Build #100326! 🎉


🧪   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 c623d42, a single commit rebased onto current main (latest rebase: two keep-both conflicts with bun pm diff in CommandLineArguments.rs). 0 unresolved review threads; 14/14 sbom tests pass locally, and the neighbouring bun-pm-licenses (79/79) and bun-pm-diff (45 pass, 1 skipped) files still pass; cargo clippy clean on bun_install and bun_runtime. 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).

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, SBOM contents, usage, tests, and verification details.

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 Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved

@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 Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved

@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 Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
@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 Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved

@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 Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
@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 Resolved
Comment thread test/cli/install/bun-pm-sbom.test.ts Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig Resolved
Comment thread src/cli/pm_sbom_command.zig 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.

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/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread test/cli/install/bun-pm-sbom.test.ts Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread src/runtime/cli/pm_sbom_command.rs Resolved
Comment thread test/cli/install/bun-pm-sbom.test.ts 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 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.

Jarred-Sumner added a commit that referenced this pull request Aug 14, 2026
…ilter/--catalog, nested overrides, transitive update, and workspace fixes (#38333)

Brings Bun's package manager to parity with pnpm for monorepo workflows,
and fixes the bugs found while checking every command against pnpm's
implementation, pnpm's test suites, pnpm's open issue tracker, pnpm's
docs, npm's arborist fixtures, and — for `bun update` — running real
pnpm and Bun side by side on the same projects.

### What does this PR do?

#### New commands

- **`bun dedupe [--check]`** — collapses duplicate versions in
`bun.lock` onto the smallest set that still satisfies every dependent's
range, using only versions already in the lockfile, then installs. Never
downgrades a direct dependency unless that is the only way to drop a
version; keeps patched versions (and anything needed to reach them, and
says so); refuses to run on a lockfile that is behind `package.json`.
`--check` exits 1 without writing.
- **`bun prune [--production | --omit=…] [--dry-run] [--filter <ws>]`**
— removes everything in `node_modules` that the lockfile does not put
there; `--production` leaves exactly what `bun install --production`
would. Hoisted and isolated layouts, Windows junctions and shims,
workspace links, bundled deps; refuses when `package.json` and
`bun.lock` disagree or when `node_modules` was laid out by a different
linker; understands turbo-pruned checkouts.
- **`bun pm licenses [--json] [--prod|--dev] [--long] [--filter <ws>]`**
— installed packages grouped by license, with a `(dev)` marker and
`paths`/`license`/`description` in `--json`.
- **`bun audit fix [--latest] [--dry-run] [--json]`** — moves each
vulnerable package to the lowest safe version its dependents accept, per
installed instance; rewrites exact pins when that is the only way;
`--latest` also rewrites your own declared ranges (root, workspace,
catalog) so a semver-major fix can be taken, and every blocked or
unfixable item is followed by the command that resolves it (`bun audit
fix --latest`, `bun audit --ignore GHSA-…`); re-audits the tree it
actually installed and reports/exits from that second response (npm's
`_submitQuickAudit`), so an advisory that starts at the version it moved
to is not missed; works across registries; security fixes bypass
`minimumReleaseAge` with an annotation. `bun audit --json` honors
`--audit-level`/`--ignore` for its exit code; `--omit` is honored by
`audit` and `licenses`.

#### `bun update` semantics (pnpm's model)

- A bare `bun update` re-resolves **transitive** packages too — every
edge moves to the newest version its own range (or dist-tag) allows, per
dependent, so `bun.lock` no longer stays stale after an update;
overrides/catalogs changed since the last install are honored. From a
workspace member or `--filter`, only what the selected workspaces reach
is re-resolved; from the root, everything.
- `bun update <name>` reaches any depth, matches `npm:` aliases by real
name, updates in place, never adds to `package.json`, and errors on a
name nothing selected depends on. `-r`/`--filter` fan a named update out
across workspaces. `--latest` never downgrades a locked version that is
ahead of the tag, and `update <name> --latest` also refreshes that
package's own dependencies.
- Plain updates keep dist-tag literals and non-caret ranges (`*`, `1.x`,
`^1 || ^2`) exactly as written and only move the lockfile; `--latest`
rewrites them to the resolved version as before. `bun update -i` applies
only what you selected. New: positional patterns (`bun update
'@types/*'`), `--dev`/`--prod`/`--no-optional`, `-L`, `bun up`.
- `package.json` is written after resolution and `bun.lock`'s declared
ranges, overrides and catalogs are re-derived from the final
`package.json`, replacing the per-command literal rewriting; a no-op
update leaves the file byte-identical.

#### Overrides

- **Nested overrides** (#6608): npm's nested objects, yarn's `a/b` paths
and pnpm's `a>b` selectors, applied to the direct parent→child edge;
**version-scoped targets** (`"lodash@<4.17.21": "4.17.21"`, the shape
`pnpm audit --fix` writes), matched against the dependent's declared
range as pnpm does. Rules persist inside the `overrides` section and the
file is stamped `lockfileVersion: 3` **only when such rules exist** —
existing lockfiles are byte-identical. Flat overrides additionally fix
`$ref` to workspace-member deps, catalog-valued rules going stale, and
warn on pnpm's `-` / `pkg@` forms.

#### Workspaces and filters

- `bun add|remove|update … --filter <ws>` (also `-F`, also `bun install
<pkg> --filter`) edits the selected workspaces' `package.json` files and
**links only those workspaces**, like `bun install --filter`. Filters
gain pnpm's relation selectors (`foo...`, `...foo`, `foo^...`,
`...^foo`) and `{dir}` subtrees, for the install family **and** `bun run
--filter`; `--filter` may precede the subcommand; every command warns
about patterns that match nothing; `add`/`remove` no longer select the
root implicitly.
- `bun add <pkg> --catalog[=name]` reuses an existing catalog entry,
keeps a range an explicit version fits, catalogs the range a package
already declares, decides per target, and refuses workspace names and
local paths; a plain `bun add` uses a default-catalog entry when one
exists. A package defined in both `catalog` and `catalogs.default` is an
error. `catalog:` peers of registry packages bind to the importer's copy
instead of the root catalog.
- `--frozen-lockfile` / `bun ci` on turbo-pruned monorepos: pruned-away
workspaces are tolerated, a survivor depending on a pruned workspace is
an error, catalog subsets are accepted, and an overrides/catalogs change
is a frozen failure.

#### One output vocabulary

Every command here prints the install family's shapes: header, glyph
rows (`+`/`-`/`↑`, dedupe's `↳ name old → new`), exactly one noun-first
summary line with counts and a duration (`2 duplicate versions removed,
3 packages installed (checked 5 packages) [12ms]`, `N packages removed
(checked C) [t]`), no-ops that say what was checked, remedies printed as
copy-pasteable command lines, warnings as `warn:`, `--silent` printing
nothing, and errors with their remedy together on stderr. Transitive and
named updates render as the summary's `↑` rows (once per package;
`--dry-run` prints the same rows plus `N packages would be updated`);
dedupe reports after the install it triggers, so lifecycle-script output
never splits it. A lockfile whose bytes did not change is no longer
rewritten (`Saved lockfile` only prints on a real write;
`--lockfile-only` no-ops print `Done! Checked N packages (no changes)`).
This came out of running every command against fixtures and comparing
with `install`/`add`/`remove` (95 findings, all fixed).

#### Config precedence

A project's `bunfig.toml` now beats any `.npmrc` (project or user-level)
for the same key (npmrc files → bunfig's set fields → CLI); npmrc-only
settings such as `//host/:_authToken` still attach to bunfig-declared
registries, matched by host and path regardless of how either file
spells the trailing slash.

#### Lockfile migration

- `package-lock.json`: rebuilt around a reachability walk that derives
each resolution from the entry itself. Fixes `git+https://github.com/…`
resolutions being written unparseably (the next install threw the
lockfile away), root `bundleDependencies` migrating to an **empty**
lockfile, lockfileVersion 1 (and npm's upcoming 4) making `bun install`
exit 1 instead of resolving fresh, dependency-level bundles,
`dependencies`+`optionalDependencies` double edges, unreferenced entries
aborting the migration, duplicate packages for identical `name@version`
at nested paths, lost `optionalPeers`, lost integrity when a bundled
copy was seen first, and `overrides` not being carried over. All 57 of
arborist's v2/v3 fixture projects are vendored and migrated under
snapshot.
- `pnpm-lock.yaml` v9: bare-hash `patchedDependencies`, snapshot
aliases, `catalog:default`, recorded tarball URLs, git `path:`,
multi-document files, `runtime:` entries, named registries,
peer-suffixed keys chosen per importer, injected workspaces,
manifest-only importer deps.

#### Isolated linker

- An existing store entry whose dependencies re-resolved (override,
dedupe, update) now has its links refreshed on the next install
(measured cost below). `bun prune` builds the same store the installer
builds, so stale `name@version+<peerhash>` variants left by peer bumps
are removed and a kept package's real entry never is (whether it was
installed with full or `--production` features); on the hoisted linker,
dedupe / audit fix / update delete the nested copies whose rows they
collapsed instead of leaving the old copy loadable.
- Blocked entries resume through per-entry intrusive waiter lists
instead of a scan of every store entry after each completion (robobun's
#25983/#28425 attempted this). Measured on the reporter's repro from
#25799 (2,259 store entries) and a synthetic 6,425-entry monorepo, PR
build vs merge-base build: main-thread CPU in the link phase drops 0.85
→ 0.33 s and 5.5 → 0.85 s (the removed work grows quadratically); wall
time is unchanged with spare cores and 16% / 26% faster pinned to one
CPU, the CI/Docker shape in those reports. (The minute-long installs
originally reported were peer resolution, fixed before this PR's base.)
- Two pre-existing leaks surfaced by the new LSan-enabled tests are
fixed: the header buffer of every authenticated registry request, and
the per-entry lifecycle-script lists.

#### Other bug fixes

`bun add x@npm:pkg` writes a range; `bun add --trust a b` no longer
drops `b` when `a` was already trusted; `bun add x --dev` no longer
rewrote every group in `bun.lock`; `catalog:` peer hoisting;
`dependency::Version::eql` treated all `catalog:` specifiers as equal; a
`file:` package whose dependencies reach itself (its own name, an `npm:`
alias under its own name, two link targets depending on each other — the
shapes a `package-lock.json` migration produces, and #25202's
`workspace:.` self-reference) hung `bun install` forever in the hoisting
tree — the migration shapes now install, and #25202's literal shape now
terminates with `Workspace dependency "foo" not found` rather than
installing as npm does; a peer of a `file:` package that was only placed
nested was also written to `optionalPeers` in a migrated bun.lock, so
the next `--frozen-lockfile` failed and a plain install rewrote the
lockfile; `catalog:` literals in `bun update`; alias output in the
install summary; help/completions for everything above.

#### Behavior changes to note in the release notes

- `bun update` moves transitive packages; `bun update <name>` no longer
adds an undeclared package (exit 1); `--production`/`--prod` on update
means "only update `dependencies` and `optionalDependencies`" (a group
filter like `--dev`, not the install flag) and `-r`+names with no match
is an error; `-i` updates only the selection.
- Project `bunfig.toml` overrides any `.npmrc`.
- `bun install <pkg> --filter x` edits `x` (not the root); `bun add y
--filter x` no longer installs a package named `x`; `add`/`remove
--filter '*'` no longer includes the root.
- A plain `bun add x` in a workspace whose default catalog lists `x`
writes `catalog:`; `audit fix` may rewrite exact pins;
`--frozen-lockfile --lockfile-only` writes nothing; overrides/catalog
changes fail frozen installs.
- One-time lockfile churn after upgrading for projects with `catalog:`
peers or dead `pkg@range` override rows; lockfiles that use
nested/scoped overrides are v3 and unreadable by older Bun (only when
opted in). Turborepo, Nx and Dependabot have been checked; the needed
upstream changes are open (nrwl/nx#36666 covers v2 and v3;
vercel/turborepo#13740 accepts v3 and preserves the object rows through
prune — turborepo main today parses v2 and rejects v3; dependabot needs
nothing). Note v2 itself only exists on the 1.4 line.
- `bun audit --json` keeps npm's contract: `--audit-level`/`--ignore`
decide the exit code, the JSON document is the full registry report
(closes #31013 as won't-change). Automatic removal of stale
`node_modules` entries on plain `bun install` (#32974) is separate from
this PR: `bun prune` is the manual form, and dedupe / audit fix / update
now clean up the nested copies they collapse on the hoisted linker;
#32974 should reuse prune's planner, and #29512 (sbom) is sequenced
after this so it can build on `reachable.rs` instead of carrying its own
walk.
- Deliberately kept where we differ from pnpm: root `bun update` covers
the whole workspace; dependents whose ranges allow follow a moved
version (one copy, not two); `--latest` works on transitive names;
`--no-save` touches neither file; prune deletion failures exit 1; audit
requests stay per-registry.

#### Performance

Measured on a 1,113-package Next/Prisma/MUI app (PR build vs a PR build
of the merge base, interleaved, plus canary and 1.3.14): every hoisted
cell is within noise except no-op install, +0.8 ms (+2%, identical
syscalls); isolated no-op is +1.9 ms (+3.9%) — the deliberate cost of
re-checking existing entries' links every install rather than persisting
a stamp file. Everything added is otherwise off the plain-install path
(gated on the feature being used or on a diff), and id-indexed sets are
bitsets.

### How did you verify your code works?

~1,100 new or ported test cases across the install suites (designed
behavior, cases ported from pnpm's suites, pinning tests for pnpm bugs
this implementation is immune to, arborist's fixtures, and the CI review
findings), all `toStrictEqual`; the whole `test/cli/install` directory
passes locally and the existing suites are unchanged except where a
pre-existing expectation was deliberately changed (each listed above).
`bun update` was additionally verified with a rerunnable differential
harness that runs pnpm 11 and this branch on 26 scenario families
against one registry and diffs the resulting resolutions edge by edge —
after this PR only the deliberate differences above remain. Ecosystem:
Turborepo, Nx and Dependabot were checked against the new lockfile
output.

Co-authored work absorbed with credit: @kjanat's #38190 (alias handling,
co-author on the commit), @charpeni's #31143 and @crystalin's #34407
(both superseded), and the tests of the earlier `bun update` PRs (#31752
by @zlotnika, #33127, #36381, #36729, #38224). robobun's #34688
(folder-dependency cycles; its tests are lifted, co-author on the
commit) and #37289 (migrated optionalPeers; its test is lifted,
co-author on the commit) were fixed independently here and are closed by
this PR. #28422's quadratic scan is fixed here as well (already closed).

Related but not closed — `bun prune` gives these a manual fix while the
automatic-cleanup asks stay open: #8662, #26305, #29793, #21216, #16176.
Also related: #10930, #26970, #26751.

Fixes #1343
Fixes #3605
Fixes #14719
Fixes #24122
Fixes #18612
Fixes #20238
Fixes #25826
Fixes #23615
Fixes #26973
Fixes #20593
Closes #31013
Fixes #28959
Fixes #28402
Fixes #27897
Fixes #26675
Fixes #10949
Fixes #18504
Fixes #13388
Fixes #24523
Fixes #6608
Fixes #19059
Fixes #16569
Fixes #8262
Fixes #11901
Fixes #13469
Fixes #25202
Closes #29664
Closes #31143
Closes #34407
Closes #34688
Closes #37289
Closes #38190

---------

Co-authored-by: Kaj Kowalski <info@kajkowalski.nl>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: robobun <117481402+robobun@users.noreply.github.com>
@robobun
robobun force-pushed the farm/de84d354/pm-sbom branch from f04bc62 to 7f4d005 Compare August 14, 2026 22:49

@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.

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.
@robobun
robobun force-pushed the farm/de84d354/pm-sbom branch from 36ae2ab to c623d42 Compare August 18, 2026 03:08

@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 bug hunt found no issues this pass. Because it adds a new user-facing bun pm subcommand (~1000 lines) that emits two external spec formats (CycloneDX 1.7 / SPDX 2.3) consumed by downstream scanners, a maintainer sign-off on the API surface and spec conformance would still be worthwhile.

What was reviewed:

  • Dispatch chain in package_manager_command.rs — the licenses arm's Global::exit(0) is restored; only the sbom arm is added.
  • Visibility widenings in resolution.rs/integrity.rs/Meta.rs — read-only accessors, no behavior change.
  • -o path handling uses join_abs_string_buf_checked and exits cleanly on overflow.
  • Scope BFS, ref/SPDXID dedup, VCS locator formatting, and JSON string escaping traced against the test assertions.
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 ~1000-line file (src/runtime/cli/pm_sbom_command.rs) plus a ~500-line test file. Supporting changes are mechanical: CLI arg plumbing in CommandLineArguments.rs/PackageManagerOptions.rs, a new dispatch arm in package_manager_command.rs, help-text/docs/completions, and pub(crate)→pub widenings on a handful of bun_install items the generator reads (resolution tags, Resolution::repository(), Integrity::slice/tags, Meta.integrity). civil_from_days in shell/builtin/ls.rs is made pub(crate) for reuse.

Security risks

Low. The command is read-only over the lockfile and writes either to stdout or to a user-specified file via bun_sys::File::write_file. All string values written into the JSON go through bun_core::fmt::format_json_string_utf8, so lockfile-sourced bytes cannot break JSON structure. The -o path is resolved with the checked path-join variant against the invocation cwd. No network, no auth, no untrusted input beyond the user's own lockfile and CLI args.

Level of scrutiny

High, for two reasons: (1) it introduces new user-facing API surface (a subcommand with two flags and two output formats), which per this repo's review guidance is a design decision that wants maintainer agreement; and (2) the output is a standards document that third-party tools (Grype, Trivy, Dependency-Track) will parse — spec-conformance mistakes are silent and only surface downstream. Earlier review rounds already caught and fixed several such issues (SPDX §7.7 VCS locator syntax, purl percent-encoding, SPDXID charset), which is exactly the class of thing a human familiar with these specs should spot-check once more.

Other factors

The PR has been through many automated review rounds over four months; every prior finding is addressed and marked resolved, including a rebase regression that briefly broke bun pm licenses (now restored — verified in the current diff that both the licenses and sbom arms end in Global::exit(0)). Test coverage is thorough (14 concurrent Verdaccio-backed tests covering both formats, scoped packages, workspaces, git/github deps, error paths, and the long -o path). No human maintainer has reviewed yet; robobun noted in May it was "waiting on a maintainer to review and merge". Given the size and the new-API nature, deferring rather than approving.

This branch was successfully deployed

1 active (outdated) deployment
staging - docs — 36ae2abf Deployed Aug 15, 2026 by mintlify[bot]
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