mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 09:55:39 -04:00
* feat(sbom): add monorepo workspace support for SBOM generation When --filter selects a single workspace, the SBOM root component now uses that workspace's name, version, description, license, and author instead of the workspace root's metadata. Author, repository, and license fall back to the root manifest when the workspace package doesn't define them. Workspace inter-dependencies (workspace: protocol) and their transitive dependencies are now included in the SBOM. Dev-only workspace deps are correctly excluded when --prod is used, and lockfile-only mode skips workspace resolution entirely to avoid unexpected disk reads. * fix(sbom): add recursiveByDefault so --filter works in workspaces Without recursiveByDefault, the sbom command in a workspace didn't enter the recursive code path that populates selectedProjectsGraph from --filter. The handler received no filter info and always used the workspace root manifest for the root component. * feat(sbom): add --out and --split for per-package SBOM generation Add --out <path> to write SBOM to a file instead of stdout. Supports %s (package name) and %v (version) placeholders. When %s is present, generates one SBOM per workspace package automatically. Add --split to output NDJSON (one compact JSON per line) to stdout, for piping into tooling that processes multiple SBOMs. Add compact option to CycloneDX and SPDX serializers so NDJSON lines are single-line JSON without re-parsing. Sanitize %s and %v values to prevent path traversal in output paths. * fix: use sbom-out instead of sboms to pass spellcheck * fix(sbom): expand %v in single-output path and fix workspace dep parent relationships Two bugs found by CodeRabbit: 1. --out with %v but no %s wrote a literal %v filename. Now both %s and %v are expanded in the single-output path using the SBOM root component's name and version. 2. The lockfile walker used rootPurl as parent for every importer, including additional workspace dep importers. This caused shared-lib's external deps (like is-odd) to appear as direct deps of the root package. Now each importer's walk uses the correct parent PURL based on whether it's an original or workspace dep importer. * fix(sbom): fall back to workspace root manifest for filtered package metadata Read the root manifest from rootProjectManifest/rootProjectManifestDir instead of opts.dir so license/author/repository/description fall back to the workspace root when a filtered package omits them. Add the missing rootDescription fallback. Fix the per-package CycloneDX test assertions to match the standard group/name split for scoped names. * fix(sbom): harden path handling and fix lockfile-only/versionless workspace deps - Neutralize '.', '..' and blank segments in --out path templates so a crafted package name/version cannot escape the output directory. - Skip lockfile link: targets that resolve outside the workspace root, and guard the workspace manifest reads with a containment check, preventing arbitrary package.json reads from a malicious lockfile. - Honor --lockfile-only inside collectSbomComponents so workspace links and their transitive deps are no longer traversed. - Include workspace packages that omit a version (default to 0.0.0, matching the root component). - Bound workspace manifest reads with p-limit to avoid EMFILE on large monorepos. * fix(sbom): error on per-package output path collisions and use O(n) workspace BFS - Throw SBOM_OUT_PATH_COLLISION when two workspace packages sanitize to the same --out path instead of silently overwriting one SBOM with another. - Replace the resolveWorkspaceDeps BFS queue.shift() (O(n) per dequeue) with a moving head index for O(n) traversal on large workspaces. * fix(sbom): strip control chars from output paths and skip unreadable workspace importers - Strip ASCII control characters (incl. newlines) in sanitizePathSegment so a crafted package name/version cannot inject lines into the --split --out summary or produce confusing filenames. - Skip walking a reachable workspace importer when its package info is missing (e.g. unreadable manifest) instead of misattributing its deps to the root. - Bound importer-walker fan-out with p-limit to avoid resource pressure on large workspaces. * perf(sbom): reuse workspace manifests across split outputs and use a Set for path collisions - Read workspace package manifests once into SharedContext (keyed by importer id) from the project graph, so split mode no longer re-reads them from disk for every emitted SBOM. - Track written --out paths in a Set instead of Array#includes to make collision detection O(1) per package rather than O(n2) overall. * fix(sbom): support %s in single-project --out, harden importer lookup, cache root license - Only treat %s in --out as per-package (split) mode when a workspace project graph is present; in a single-project repo %s/%v interpolate from the root component on the single-output path. Adds a regression test. - Use an own-property check for lockfile importer existence so a crafted link: target cannot follow inherited keys (e.g. toString) via the prototype chain. - Fast-path resolveRootLicense when the manifest already declares an SPDX license and cache the workspace-root license in SharedContext, avoiding redundant on-disk LICENSE probing per emitted SBOM. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>