@pnpm/deps.compliance.commands 1101.4.0 → 1101.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/audit/fix.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { mergePackageVersionSpecs } from '@pnpm/config.version-policy';
1
2
  import { writeSettings } from '@pnpm/config.writer';
2
3
  import { normalizeGhsaId } from '@pnpm/deps.compliance.audit';
3
4
  import { sortDirectKeys } from '@pnpm/object.key-sorting';
@@ -46,16 +47,16 @@ export function caretRangeForPatched(patchedRange) {
46
47
  return min ? `^${min.version}` : patchedRange;
47
48
  }
48
49
  export function createMinimumReleaseAgeExcludes(advisories) {
49
- const excludes = new Set();
50
+ const specs = [];
50
51
  for (const advisory of advisories) {
51
52
  const patchedVersions = advisory.patched_versions;
52
53
  if (!patchedVersions)
53
54
  continue;
54
55
  const minVersion = semver.minVersion(patchedVersions);
55
- if (minVersion) {
56
- excludes.add(`${advisory.module_name}@${minVersion.version}`);
57
- }
56
+ if (!minVersion)
57
+ continue;
58
+ specs.push(`${advisory.module_name}@${minVersion.version}`);
58
59
  }
59
- return Array.from(excludes);
60
+ return mergePackageVersionSpecs(specs);
60
61
  }
61
62
  //# sourceMappingURL=fix.js.map
@@ -8,6 +8,7 @@ export type SbomCommandOptions = {
8
8
  sbomSupplier?: string;
9
9
  out?: string;
10
10
  split?: boolean;
11
+ excludePeers?: boolean;
11
12
  } & Pick<Config, 'dev' | 'dir' | 'lockfileDir' | 'registries' | 'optional' | 'production' | 'storeDir' | 'virtualStoreDir' | 'modulesDir' | 'pnpmHomeDir' | 'virtualStoreDirMaxLength'> & Pick<ConfigContext, 'allProjectsGraph' | 'selectedProjectsGraph' | 'rootProjectManifest' | 'rootProjectManifestDir'> & Partial<Pick<Config, 'userConfig'>>;
12
13
  export declare function rcOptionsTypes(): Record<string, unknown>;
13
14
  export declare const cliOptionsTypes: () => Record<string, unknown>;
package/lib/sbom/sbom.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { realpath } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { FILTERING } from '@pnpm/cli.common-cli-options-help';
4
5
  import { packageManager } from '@pnpm/cli.meta';
@@ -6,10 +7,11 @@ import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils';
6
7
  import { types as allTypes } from '@pnpm/config.reader';
7
8
  import { WANTED_LOCKFILE } from '@pnpm/constants';
8
9
  import { isSpdxLicenseExpression, resolveLicenseFromDir } from '@pnpm/deps.compliance.license-resolver';
9
- import { collectSbomComponents, resolveWorkspaceDeps, serializeCycloneDx, serializeSpdx, } from '@pnpm/deps.compliance.sbom';
10
+ import { bugsUrlFromField, collectSbomComponents, resolveWorkspaceDeps, serializeCycloneDx, serializeSpdx, } from '@pnpm/deps.compliance.sbom';
10
11
  import { PnpmError } from '@pnpm/error';
11
12
  import { getLockfileImporterId, readWantedLockfile } from '@pnpm/lockfile.fs';
12
13
  import { getStorePath } from '@pnpm/store.path';
14
+ import { safeReadProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader';
13
15
  import pLimit from 'p-limit';
14
16
  import { pick } from 'ramda';
15
17
  import { renderHelp } from 'render-help';
@@ -27,6 +29,7 @@ export const cliOptionsTypes = () => ({
27
29
  'lockfile-only': Boolean,
28
30
  out: String,
29
31
  split: Boolean,
32
+ 'exclude-peers': Boolean,
30
33
  });
31
34
  export const shorthands = {
32
35
  D: '--dev',
@@ -87,6 +90,10 @@ export function help() {
87
90
  description: 'Generate a separate SBOM for each matched workspace package. Outputs NDJSON to stdout, or files when combined with --out.',
88
91
  name: '--split',
89
92
  },
93
+ {
94
+ description: 'Exclude peer dependencies (and their exclusive transitive subtrees)',
95
+ name: '--exclude-peers',
96
+ },
90
97
  ],
91
98
  },
92
99
  FILTERING,
@@ -195,6 +202,74 @@ async function buildSharedContext(opts) {
195
202
  }
196
203
  const rootManifestDir = opts.rootProjectManifestDir ?? opts.dir;
197
204
  const rootManifest = opts.rootProjectManifest ?? await readProjectManifestOnly(rootManifestDir);
205
+ const lockfileDir = opts.lockfileDir ?? opts.dir;
206
+ // peerDependencies are identified from the manifest, not the lockfile: with
207
+ // auto-install-peers they resolve into the importer's `dependencies` with no
208
+ // distinguishing marker. Map every walked importer to its peer names so the
209
+ // collector can drop them.
210
+ // Keyed by a Map, not a plain object: importer ids come from the lockfile
211
+ // (attacker-controlled in an untrusted clone), and a key like `__proto__`
212
+ // would corrupt a plain object's prototype.
213
+ let excludePeerNamesByImporter;
214
+ if (opts.excludePeers) {
215
+ const byImporter = new Map();
216
+ // Prefer the in-memory project graph(s): no extra filesystem reads, and
217
+ // reading from both graphs (not only the selected subset) covers the extra
218
+ // workspace packages collectSbomComponents reaches through `link:` deps, so
219
+ // their peers are filtered too in a filtered run.
220
+ const graphs = [opts.allProjectsGraph, opts.selectedProjectsGraph].filter(Boolean);
221
+ if (graphs.length > 0) {
222
+ for (const graph of graphs) {
223
+ for (const [projectDir, { package: project }] of Object.entries(graph)) {
224
+ byImporter.set(getLockfileImporterId(lockfileDir, projectDir), peerNamesFromManifest(project.manifest));
225
+ }
226
+ }
227
+ }
228
+ else {
229
+ // No project graph (e.g. a single-package repo or `--lockfile-only`
230
+ // outside a workspace), so collectSbomComponents walks every importer in
231
+ // the lockfile. Resolve each importer's own manifest from disk so peers in
232
+ // workspace packages are dropped too, not only those in the directory pnpm
233
+ // ran in. safeReadProjectManifestOnly returns null (rather than throwing)
234
+ // for an importer whose manifest is gone (e.g. a stale lockfile), and skips
235
+ // the installability check that would otherwise abort the SBOM.
236
+ const lockfileRoot = await realpath(lockfileDir);
237
+ // Bound the fan-out: a large workspace can have many importers, and
238
+ // reading every manifest at once would spike open file descriptors.
239
+ const limitManifestReads = pLimit(16);
240
+ await Promise.all(Object.keys(lockfile.importers).map((importerId) => limitManifestReads(async () => {
241
+ // A crafted lockfile could carry an importer key that escapes the
242
+ // project, via `..` segments or a symlinked directory. Canonicalize
243
+ // with realpath and skip anything resolving outside the project root,
244
+ // so a manifest is never read from outside the tree.
245
+ let importerDir;
246
+ try {
247
+ importerDir = await realpath(path.resolve(lockfileDir, importerId));
248
+ }
249
+ catch {
250
+ return;
251
+ }
252
+ const rel = path.relative(lockfileRoot, importerDir);
253
+ if (rel !== '' && (rel.startsWith('..') || path.isAbsolute(rel)))
254
+ return;
255
+ // safeReadProjectManifestOnly tolerates a missing manifest but still
256
+ // throws on a malformed one (parse error). In an untrusted clone a
257
+ // single junk package.json must not abort the whole SBOM, so skip it
258
+ // (fail-open: an unparseable importer's peers just aren't filtered).
259
+ let importerManifest;
260
+ try {
261
+ importerManifest = await safeReadProjectManifestOnly(importerDir);
262
+ }
263
+ catch {
264
+ return;
265
+ }
266
+ if (importerManifest) {
267
+ byImporter.set(importerId, peerNamesFromManifest(importerManifest));
268
+ }
269
+ })));
270
+ }
271
+ excludePeerNamesByImporter = byImporter;
272
+ }
198
273
  let storeDir;
199
274
  if (!opts.lockfileOnly) {
200
275
  storeDir = await getStorePath({
@@ -203,7 +278,6 @@ async function buildSharedContext(opts) {
203
278
  pnpmHomeDir: opts.pnpmHomeDir,
204
279
  });
205
280
  }
206
- const lockfileDir = opts.lockfileDir ?? opts.dir;
207
281
  const workspaceManifestsByImporterId = new Map();
208
282
  for (const graph of [opts.allProjectsGraph, opts.selectedProjectsGraph]) {
209
283
  if (!graph)
@@ -213,7 +287,7 @@ async function buildSharedContext(opts) {
213
287
  }
214
288
  }
215
289
  const rootLicense = await resolveRootLicense(rootManifest, rootManifestDir);
216
- return { lockfile, rootManifest, rootManifestDir, rootLicense, storeDir, workspaceManifestsByImporterId };
290
+ return { lockfile, rootManifest, rootManifestDir, rootLicense, storeDir, workspaceManifestsByImporterId, excludePeerNamesByImporter };
217
291
  }
218
292
  async function generateSbomForProject(opts, serialOpts, ctx, compact) {
219
293
  const { lockfile, rootManifest, rootManifestDir, rootLicense: cachedRootLicense } = ctx;
@@ -245,6 +319,8 @@ async function generateSbomForProject(opts, serialOpts, ctx, compact) {
245
319
  ?? (singleProject ? extractRepository(rootManifest) : undefined);
246
320
  const rootDescription = manifest.description
247
321
  ?? (singleProject ? rootManifest.description : undefined);
322
+ const rootBugsUrl = bugsUrlFromField(manifest.bugs)
323
+ ?? (singleProject ? bugsUrlFromField(rootManifest.bugs) : undefined);
248
324
  const lockfileDir = opts.lockfileDir ?? opts.dir;
249
325
  const includedImporterIds = opts.selectedProjectsGraph
250
326
  ? Object.keys(opts.selectedProjectsGraph)
@@ -264,6 +340,7 @@ async function generateSbomForProject(opts, serialOpts, ctx, compact) {
264
340
  rootDescription,
265
341
  rootAuthor,
266
342
  rootRepository,
343
+ rootBugsUrl,
267
344
  sbomType: serialOpts.sbomType,
268
345
  include,
269
346
  registries: opts.registries,
@@ -274,6 +351,7 @@ async function generateSbomForProject(opts, serialOpts, ctx, compact) {
274
351
  virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
275
352
  workspacePackages,
276
353
  resolvedWorkspaceDeps,
354
+ excludePeerNamesByImporter: ctx.excludePeerNamesByImporter,
277
355
  });
278
356
  const output = serialOpts.format === 'cyclonedx'
279
357
  ? serializeCycloneDx(result, {
@@ -287,6 +365,16 @@ async function generateSbomForProject(opts, serialOpts, ctx, compact) {
287
365
  : serializeSpdx(result, { compact });
288
366
  return { output, exitCode: 0, rootName, rootVersion };
289
367
  }
368
+ function peerNamesFromManifest(manifest) {
369
+ // A name declared as both a peer and a regular dependency is a real dependency
370
+ // the package pulls in itself, so keep it.
371
+ const regular = new Set([
372
+ ...Object.keys(manifest.dependencies ?? {}),
373
+ ...Object.keys(manifest.devDependencies ?? {}),
374
+ ...Object.keys(manifest.optionalDependencies ?? {}),
375
+ ]);
376
+ return new Set(Object.keys(manifest.peerDependencies ?? {}).filter((name) => !regular.has(name)));
377
+ }
290
378
  function validateSbomType(value) {
291
379
  if (!value || value === 'library')
292
380
  return 'library';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/deps.compliance.commands",
3
- "version": "1101.4.0",
3
+ "version": "1101.5.1",
4
4
  "description": "pnpm commands for audit, licenses, and sbom",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -13,9 +13,9 @@
13
13
  "funding": "https://opencollective.com/pnpm",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/pnpm/pnpm/tree/main/deps/compliance/commands"
16
+ "url": "https://github.com/pnpm/pnpm/tree/main/pnpm11/deps/compliance/commands"
17
17
  },
18
- "homepage": "https://github.com/pnpm/pnpm/tree/main/deps/compliance/commands#readme",
18
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/pnpm11/deps/compliance/commands#readme",
19
19
  "bugs": {
20
20
  "url": "https://github.com/pnpm/pnpm/issues"
21
21
  },
@@ -41,27 +41,28 @@
41
41
  "@pnpm/cli.common-cli-options-help": "1100.0.2",
42
42
  "@pnpm/cli.command": "1100.0.1",
43
43
  "@pnpm/cli.meta": "1100.0.8",
44
- "@pnpm/config.reader": "1101.10.0",
45
- "@pnpm/config.writer": "1100.0.13",
46
- "@pnpm/deps.compliance.license-resolver": "1100.0.0",
47
- "@pnpm/constants": "1100.0.0",
48
- "@pnpm/deps.compliance.audit": "1101.0.17",
49
- "@pnpm/cli.utils": "1101.0.12",
50
- "@pnpm/deps.compliance.license-scanner": "1100.0.20",
51
- "@pnpm/deps.security.signatures": "1101.2.2",
52
- "@pnpm/error": "1100.0.0",
53
- "@pnpm/deps.compliance.sbom": "1100.2.0",
54
- "@pnpm/installing.commands": "1100.10.0",
55
- "@pnpm/lockfile.fs": "1100.1.6",
44
+ "@pnpm/cli.utils": "1101.0.13",
56
45
  "@pnpm/config.pick-registry-for-package": "1100.0.9",
57
- "@pnpm/lockfile.types": "1100.0.11",
58
- "@pnpm/lockfile.walker": "1100.0.11",
59
- "@pnpm/network.auth-header": "1101.1.2",
46
+ "@pnpm/config.reader": "1101.11.0",
47
+ "@pnpm/config.version-policy": "1100.1.6",
48
+ "@pnpm/config.writer": "1100.0.15",
49
+ "@pnpm/constants": "1100.0.0",
50
+ "@pnpm/deps.compliance.license-resolver": "1100.0.0",
51
+ "@pnpm/deps.compliance.audit": "1101.0.19",
52
+ "@pnpm/deps.compliance.license-scanner": "1100.0.22",
53
+ "@pnpm/deps.compliance.sbom": "1100.3.1",
54
+ "@pnpm/error": "1100.0.1",
55
+ "@pnpm/deps.security.signatures": "1101.2.3",
56
+ "@pnpm/installing.commands": "1100.10.2",
57
+ "@pnpm/lockfile.types": "1100.0.13",
58
+ "@pnpm/lockfile.fs": "1100.1.8",
59
+ "@pnpm/lockfile.utils": "1100.1.1",
60
+ "@pnpm/network.auth-header": "1101.1.3",
61
+ "@pnpm/lockfile.walker": "1100.0.13",
60
62
  "@pnpm/object.key-sorting": "1100.0.1",
61
63
  "@pnpm/types": "1101.3.2",
62
- "@pnpm/lockfile.utils": "1100.0.13",
63
- "@pnpm/workspace.project-manifest-reader": "1100.0.13",
64
- "@pnpm/store.path": "1100.0.1"
64
+ "@pnpm/workspace.project-manifest-reader": "1100.0.14",
65
+ "@pnpm/store.path": "1100.0.2"
65
66
  },
66
67
  "peerDependencies": {
67
68
  "@pnpm/logger": "^1100.0.0"
@@ -74,15 +75,15 @@
74
75
  "load-json-file": "^7.0.1",
75
76
  "read-yaml-file": "^3.0.0",
76
77
  "tempy": "3.0.0",
78
+ "@pnpm/deps.compliance.commands": "1101.5.1",
77
79
  "@pnpm/logger": "1100.0.0",
78
- "@pnpm/pkg-manifest.reader": "1100.0.8",
79
- "@pnpm/deps.compliance.commands": "1101.4.0",
80
- "@pnpm/prepare": "1100.0.16",
81
- "@pnpm/testing.command-defaults": "1100.0.6",
82
- "@pnpm/testing.registry-mock": "1100.0.6",
80
+ "@pnpm/pkg-manifest.reader": "1100.0.9",
81
+ "@pnpm/prepare": "1100.0.18",
82
+ "@pnpm/testing.command-defaults": "1100.0.8",
83
83
  "@pnpm/test-fixtures": "1100.0.0",
84
- "@pnpm/workspace.projects-filter": "1100.0.22",
85
- "@pnpm/testing.mock-agent": "1101.0.3"
84
+ "@pnpm/testing.mock-agent": "1101.0.4",
85
+ "@pnpm/workspace.projects-filter": "1100.0.24",
86
+ "@pnpm/testing.registry-mock": "1100.0.8"
86
87
  },
87
88
  "engines": {
88
89
  "node": ">=22.13"