@pnpm/deps.compliance.commands 1101.3.5 → 1101.5.0

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
@@ -6,11 +6,15 @@ export type SbomCommandOptions = {
6
6
  lockfileOnly?: boolean;
7
7
  sbomAuthors?: string;
8
8
  sbomSupplier?: string;
9
- } & Pick<Config, 'dev' | 'dir' | 'lockfileDir' | 'registries' | 'optional' | 'production' | 'storeDir' | 'virtualStoreDir' | 'modulesDir' | 'pnpmHomeDir' | 'virtualStoreDirMaxLength'> & Pick<ConfigContext, 'selectedProjectsGraph' | 'rootProjectManifest' | 'rootProjectManifestDir'> & Partial<Pick<Config, 'userConfig'>>;
9
+ out?: string;
10
+ split?: boolean;
11
+ excludePeers?: boolean;
12
+ } & Pick<Config, 'dev' | 'dir' | 'lockfileDir' | 'registries' | 'optional' | 'production' | 'storeDir' | 'virtualStoreDir' | 'modulesDir' | 'pnpmHomeDir' | 'virtualStoreDirMaxLength'> & Pick<ConfigContext, 'allProjectsGraph' | 'selectedProjectsGraph' | 'rootProjectManifest' | 'rootProjectManifestDir'> & Partial<Pick<Config, 'userConfig'>>;
10
13
  export declare function rcOptionsTypes(): Record<string, unknown>;
11
14
  export declare const cliOptionsTypes: () => Record<string, unknown>;
12
15
  export declare const shorthands: Record<string, string>;
13
16
  export declare const commandNames: string[];
17
+ export declare const recursiveByDefault = true;
14
18
  export declare function help(): string;
15
19
  export declare function handler(opts: SbomCommandOptions, _params?: string[]): Promise<{
16
20
  output: string;
package/lib/sbom/sbom.js CHANGED
@@ -1,13 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import { realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
1
4
  import { FILTERING } from '@pnpm/cli.common-cli-options-help';
2
5
  import { packageManager } from '@pnpm/cli.meta';
3
6
  import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils';
4
7
  import { types as allTypes } from '@pnpm/config.reader';
5
8
  import { WANTED_LOCKFILE } from '@pnpm/constants';
6
9
  import { isSpdxLicenseExpression, resolveLicenseFromDir } from '@pnpm/deps.compliance.license-resolver';
7
- import { collectSbomComponents, serializeCycloneDx, serializeSpdx, } from '@pnpm/deps.compliance.sbom';
10
+ import { bugsUrlFromField, collectSbomComponents, resolveWorkspaceDeps, serializeCycloneDx, serializeSpdx, } from '@pnpm/deps.compliance.sbom';
8
11
  import { PnpmError } from '@pnpm/error';
9
12
  import { getLockfileImporterId, readWantedLockfile } from '@pnpm/lockfile.fs';
10
13
  import { getStorePath } from '@pnpm/store.path';
14
+ import { safeReadProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader';
15
+ import pLimit from 'p-limit';
11
16
  import { pick } from 'ramda';
12
17
  import { renderHelp } from 'render-help';
13
18
  export function rcOptionsTypes() {
@@ -22,12 +27,16 @@ export const cliOptionsTypes = () => ({
22
27
  'sbom-authors': String,
23
28
  'sbom-supplier': String,
24
29
  'lockfile-only': Boolean,
30
+ out: String,
31
+ split: Boolean,
32
+ 'exclude-peers': Boolean,
25
33
  });
26
34
  export const shorthands = {
27
35
  D: '--dev',
28
36
  P: '--production',
29
37
  };
30
38
  export const commandNames = ['sbom'];
39
+ export const recursiveByDefault = true;
31
40
  export function help() {
32
41
  return renderHelp({
33
42
  description: 'Generate a Software Bill of Materials (SBOM) for the project.',
@@ -73,6 +82,18 @@ export function help() {
73
82
  description: 'Don\'t include "optionalDependencies"',
74
83
  name: '--no-optional',
75
84
  },
85
+ {
86
+ description: 'Write SBOM to a file instead of stdout. Use %s for the package name and %v for the version.',
87
+ name: '--out <path>',
88
+ },
89
+ {
90
+ description: 'Generate a separate SBOM for each matched workspace package. Outputs NDJSON to stdout, or files when combined with --out.',
91
+ name: '--split',
92
+ },
93
+ {
94
+ description: 'Exclude peer dependencies (and their exclusive transitive subtrees)',
95
+ name: '--exclude-peers',
96
+ },
76
97
  ],
77
98
  },
78
99
  FILTERING,
@@ -83,6 +104,9 @@ export function help() {
83
104
  'pnpm sbom --sbom-format spdx',
84
105
  'pnpm sbom --sbom-format cyclonedx --lockfile-only',
85
106
  'pnpm sbom --sbom-format spdx --prod',
107
+ 'pnpm sbom --sbom-format cyclonedx --filter ./apps/my-app',
108
+ 'pnpm sbom --sbom-format cyclonedx --out out/%s.cdx.json',
109
+ 'pnpm sbom --sbom-format cyclonedx --split',
86
110
  ],
87
111
  });
88
112
  }
@@ -96,73 +120,260 @@ export async function handler(opts, _params = []) {
96
120
  }
97
121
  const sbomType = validateSbomType(opts.sbomType);
98
122
  const sbomSpecVersion = validateSbomSpecVersion(opts.sbomSpecVersion, format);
123
+ const ctx = await buildSharedContext(opts);
124
+ const serialOpts = { format, sbomType, sbomSpecVersion };
125
+ // `%s` in --out only implies per-package output inside a workspace; in a
126
+ // single-project repo it is interpolated from the root component on the
127
+ // single-output path below.
128
+ const hasWorkspaceGraph = opts.selectedProjectsGraph != null || opts.allProjectsGraph != null;
129
+ const shouldSplit = opts.split || (opts.out != null && opts.out.includes('%s') && hasWorkspaceGraph);
130
+ if (shouldSplit) {
131
+ return handleSplit(opts, serialOpts, ctx);
132
+ }
133
+ const { output, rootName, rootVersion } = await generateSbomForProject(opts, serialOpts, ctx);
134
+ if (opts.out) {
135
+ const filePath = opts.out
136
+ .replaceAll('%s', sanitizePathSegment(sanitizePackageName(rootName)))
137
+ .replaceAll('%v', sanitizePathSegment(rootVersion));
138
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
139
+ fs.writeFileSync(filePath, output);
140
+ return { output: filePath, exitCode: 0 };
141
+ }
142
+ return { output, exitCode: 0 };
143
+ }
144
+ async function handleSplit(opts, serialOpts, ctx) {
145
+ const projectsGraph = opts.selectedProjectsGraph ?? opts.allProjectsGraph;
146
+ if (!projectsGraph) {
147
+ throw new PnpmError('SBOM_NO_PROJECTS', 'No workspace projects found. --split requires a workspace.');
148
+ }
149
+ if (opts.out && !opts.out.includes('%s')) {
150
+ throw new PnpmError('SBOM_OUT_MISSING_PLACEHOLDER', 'When using --split with --out, the path must contain %s as a placeholder for the package name.');
151
+ }
152
+ const entries = Object.entries(projectsGraph);
153
+ const ndjsonLines = [];
154
+ const files = [];
155
+ const writtenPaths = new Set();
156
+ const compact = !opts.out;
157
+ const createdDirs = new Set();
158
+ for (const [dir, entry] of entries) {
159
+ const manifest = entry.package.manifest;
160
+ if (!manifest.name)
161
+ continue;
162
+ const singleProjectGraph = { [dir]: entry };
163
+ // eslint-disable-next-line no-await-in-loop
164
+ const { output } = await generateSbomForProject({ ...opts, selectedProjectsGraph: singleProjectGraph, allProjectsGraph: undefined, split: false, out: undefined }, serialOpts, ctx, compact);
165
+ if (opts.out) {
166
+ const filePath = opts.out
167
+ .replaceAll('%s', sanitizePathSegment(sanitizePackageName(manifest.name)))
168
+ .replaceAll('%v', sanitizePathSegment(manifest.version ?? '0.0.0'));
169
+ // Sanitizing package names is lossy (e.g. "@a/b" and "a-b" both map to "a-b"),
170
+ // so two packages can resolve to the same path. Fail loudly instead of letting
171
+ // one SBOM silently overwrite another.
172
+ if (writtenPaths.has(filePath)) {
173
+ throw new PnpmError('SBOM_OUT_PATH_COLLISION', `Multiple workspace packages resolve to the same output path "${filePath}". Include %v in the --out pattern to disambiguate.`);
174
+ }
175
+ writtenPaths.add(filePath);
176
+ const fileDir = path.dirname(filePath);
177
+ if (!createdDirs.has(fileDir)) {
178
+ fs.mkdirSync(fileDir, { recursive: true });
179
+ createdDirs.add(fileDir);
180
+ }
181
+ fs.writeFileSync(filePath, output);
182
+ files.push(filePath);
183
+ }
184
+ else {
185
+ ndjsonLines.push(output);
186
+ }
187
+ }
188
+ if (opts.out) {
189
+ return {
190
+ output: `Generated ${files.length} SBOMs:\n${files.map((f) => ` ${f}`).join('\n')}`,
191
+ exitCode: 0,
192
+ };
193
+ }
194
+ return { output: ndjsonLines.join('\n'), exitCode: 0 };
195
+ }
196
+ async function buildSharedContext(opts) {
99
197
  const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, {
100
198
  ignoreIncompatible: true,
101
199
  });
102
200
  if (lockfile == null) {
103
201
  throw new PnpmError('SBOM_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found: Cannot generate SBOM without a lockfile`);
104
202
  }
203
+ const rootManifestDir = opts.rootProjectManifestDir ?? opts.dir;
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
+ }
273
+ let storeDir;
274
+ if (!opts.lockfileOnly) {
275
+ storeDir = await getStorePath({
276
+ pkgRoot: opts.dir,
277
+ storePath: opts.storeDir,
278
+ pnpmHomeDir: opts.pnpmHomeDir,
279
+ });
280
+ }
281
+ const workspaceManifestsByImporterId = new Map();
282
+ for (const graph of [opts.allProjectsGraph, opts.selectedProjectsGraph]) {
283
+ if (!graph)
284
+ continue;
285
+ for (const [dir, entry] of Object.entries(graph)) {
286
+ workspaceManifestsByImporterId.set(getLockfileImporterId(lockfileDir, dir), entry.package.manifest);
287
+ }
288
+ }
289
+ const rootLicense = await resolveRootLicense(rootManifest, rootManifestDir);
290
+ return { lockfile, rootManifest, rootManifestDir, rootLicense, storeDir, workspaceManifestsByImporterId, excludePeerNamesByImporter };
291
+ }
292
+ async function generateSbomForProject(opts, serialOpts, ctx, compact) {
293
+ const { lockfile, rootManifest, rootManifestDir, rootLicense: cachedRootLicense } = ctx;
105
294
  const include = {
106
295
  dependencies: opts.production !== false,
107
296
  devDependencies: opts.dev !== false,
108
297
  optionalDependencies: opts.optional !== false,
109
298
  };
110
- const manifest = await readProjectManifestOnly(opts.dir);
299
+ const selectedEntries = opts.selectedProjectsGraph
300
+ ? Object.entries(opts.selectedProjectsGraph)
301
+ : undefined;
302
+ const singleProject = selectedEntries?.length === 1
303
+ ? selectedEntries[0]
304
+ : undefined;
305
+ const manifest = singleProject
306
+ ? singleProject[1].package.manifest
307
+ : rootManifest;
308
+ const projectDir = singleProject
309
+ ? singleProject[0]
310
+ : rootManifestDir;
111
311
  const rootName = manifest.name ?? 'unknown';
112
312
  const rootVersion = manifest.version ?? '0.0.0';
113
- // Keep the root in sync with transitive deps: consult manifest `license` /
114
- // legacy `licenses` first, then fall back to an on-disk LICENSE file. Drop
115
- // file-scanned values that aren't SPDX-valid to avoid non-compliant output.
116
- const rootLicenseInfo = await resolveLicenseFromDir({ manifest, dir: opts.dir });
117
- const rootLicense = rootLicenseInfo && rootLicenseInfo.name !== 'Unknown' &&
118
- (!rootLicenseInfo.licenseFile || isSpdxLicenseExpression(rootLicenseInfo.name))
119
- ? rootLicenseInfo.name
120
- : undefined;
121
- const rootAuthor = typeof manifest.author === 'string'
122
- ? manifest.author
123
- : manifest.author?.name;
124
- const rootRepository = typeof manifest.repository === 'string'
125
- ? manifest.repository
126
- : manifest.repository?.url;
313
+ const rootLicense = singleProject
314
+ ? (await resolveRootLicense(manifest, projectDir) ?? cachedRootLicense)
315
+ : cachedRootLicense;
316
+ const rootAuthor = extractAuthor(manifest)
317
+ ?? (singleProject ? extractAuthor(rootManifest) : undefined);
318
+ const rootRepository = extractRepository(manifest)
319
+ ?? (singleProject ? extractRepository(rootManifest) : undefined);
320
+ const rootDescription = manifest.description
321
+ ?? (singleProject ? rootManifest.description : undefined);
322
+ const rootBugsUrl = bugsUrlFromField(manifest.bugs)
323
+ ?? (singleProject ? bugsUrlFromField(rootManifest.bugs) : undefined);
324
+ const lockfileDir = opts.lockfileDir ?? opts.dir;
127
325
  const includedImporterIds = opts.selectedProjectsGraph
128
326
  ? Object.keys(opts.selectedProjectsGraph)
129
- .map((p) => getLockfileImporterId(opts.lockfileDir ?? opts.dir, p))
327
+ .map((p) => getLockfileImporterId(lockfileDir, p))
328
+ : undefined;
329
+ const resolvedWorkspaceDeps = opts.lockfileOnly
330
+ ? undefined
331
+ : resolveWorkspaceDeps(lockfile, includedImporterIds ?? Object.keys(lockfile.importers), include);
332
+ const workspacePackages = resolvedWorkspaceDeps
333
+ ? await buildWorkspacePackagesMap(resolvedWorkspaceDeps.additionalImporterIds, lockfileDir, ctx.workspaceManifestsByImporterId)
130
334
  : undefined;
131
- let storeDir;
132
- if (!opts.lockfileOnly) {
133
- storeDir = await getStorePath({
134
- pkgRoot: opts.dir,
135
- storePath: opts.storeDir,
136
- pnpmHomeDir: opts.pnpmHomeDir,
137
- });
138
- }
139
335
  const result = await collectSbomComponents({
140
336
  lockfile,
141
337
  rootName,
142
338
  rootVersion,
143
339
  rootLicense,
144
- rootDescription: manifest.description,
340
+ rootDescription,
145
341
  rootAuthor,
146
342
  rootRepository,
147
- sbomType,
343
+ rootBugsUrl,
344
+ sbomType: serialOpts.sbomType,
148
345
  include,
149
346
  registries: opts.registries,
150
- lockfileDir: opts.lockfileDir ?? opts.dir,
347
+ lockfileDir,
151
348
  includedImporterIds,
152
349
  lockfileOnly: opts.lockfileOnly,
153
- storeDir,
350
+ storeDir: ctx.storeDir,
154
351
  virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
352
+ workspacePackages,
353
+ resolvedWorkspaceDeps,
354
+ excludePeerNamesByImporter: ctx.excludePeerNamesByImporter,
155
355
  });
156
- const output = format === 'cyclonedx'
356
+ const output = serialOpts.format === 'cyclonedx'
157
357
  ? serializeCycloneDx(result, {
158
358
  pnpmVersion: packageManager.version,
159
359
  lockfileOnly: opts.lockfileOnly,
160
360
  sbomAuthors: opts.sbomAuthors?.split(',').map((s) => s.trim()).filter(Boolean),
161
361
  sbomSupplier: opts.sbomSupplier,
162
- specVersion: sbomSpecVersion,
362
+ specVersion: serialOpts.sbomSpecVersion,
363
+ compact,
163
364
  })
164
- : serializeSpdx(result);
165
- return { output, exitCode: 0 };
365
+ : serializeSpdx(result, { compact });
366
+ return { output, exitCode: 0, rootName, rootVersion };
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)));
166
377
  }
167
378
  function validateSbomType(value) {
168
379
  if (!value || value === 'library')
@@ -171,8 +382,6 @@ function validateSbomType(value) {
171
382
  return 'application';
172
383
  throw new PnpmError('SBOM_INVALID_TYPE', `Invalid SBOM type "${value}". Use "library" or "application".`);
173
384
  }
174
- // Versions whose schema is fully covered by what we currently emit
175
- // (e.g. metadata.lifecycles requires CycloneDX 1.5+).
176
385
  const SUPPORTED_CYCLONEDX_SPEC_VERSIONS = ['1.5', '1.6', '1.7'];
177
386
  function validateSbomSpecVersion(value, format) {
178
387
  if (value == null)
@@ -186,4 +395,76 @@ function validateSbomSpecVersion(value, format) {
186
395
  }
187
396
  return normalized;
188
397
  }
398
+ async function resolveRootLicense(manifest, dir) {
399
+ // Skip the on-disk LICENSE probing when the manifest already declares a usable
400
+ // SPDX license; resolveLicenseFromDir would return the same value after the scan.
401
+ if (typeof manifest.license === 'string' && isSpdxLicenseExpression(manifest.license)) {
402
+ return manifest.license;
403
+ }
404
+ const info = await resolveLicenseFromDir({ manifest, dir });
405
+ if (info && info.name !== 'Unknown' && (!info.licenseFile || isSpdxLicenseExpression(info.name))) {
406
+ return info.name;
407
+ }
408
+ return undefined;
409
+ }
410
+ function extractAuthor(manifest) {
411
+ if (typeof manifest.author === 'string')
412
+ return manifest.author;
413
+ return manifest.author?.name;
414
+ }
415
+ function extractRepository(manifest) {
416
+ if (typeof manifest.repository === 'string')
417
+ return manifest.repository;
418
+ return manifest.repository?.url;
419
+ }
420
+ const WORKSPACE_MANIFEST_READ_CONCURRENCY = 8;
421
+ async function buildWorkspacePackagesMap(reachableImporterIds, lockfileDir, manifestsByImporterId) {
422
+ if (reachableImporterIds.length === 0)
423
+ return {};
424
+ const readManifest = pLimit(WORKSPACE_MANIFEST_READ_CONCURRENCY);
425
+ const entries = await Promise.all(reachableImporterIds.map((importerId) => readManifest(async () => {
426
+ const known = manifestsByImporterId.get(importerId);
427
+ const manifest = known
428
+ ? known
429
+ : isInsideDir(lockfileDir, importerId)
430
+ ? await readManifestSafe(path.join(lockfileDir, importerId))
431
+ : undefined;
432
+ if (!manifest?.name)
433
+ return null;
434
+ return [importerId, {
435
+ name: manifest.name,
436
+ version: manifest.version ?? '0.0.0',
437
+ license: typeof manifest.license === 'string' ? manifest.license : undefined,
438
+ description: manifest.description,
439
+ author: extractAuthor(manifest),
440
+ repository: extractRepository(manifest),
441
+ }];
442
+ })));
443
+ return Object.fromEntries(entries.filter((e) => e !== null));
444
+ }
445
+ async function readManifestSafe(dir) {
446
+ try {
447
+ return await readProjectManifestOnly(dir);
448
+ }
449
+ catch {
450
+ return undefined;
451
+ }
452
+ }
453
+ function sanitizePackageName(name) {
454
+ return name.replace(/^@/, '').replace(/\//g, '-');
455
+ }
456
+ function sanitizePathSegment(value) {
457
+ // Control characters (e.g. newlines) would produce confusing filenames and could
458
+ // inject extra lines into the printed `--split --out` summary; filesystem
459
+ // metacharacters are replaced so the value stays a single path segment.
460
+ // eslint-disable-next-line no-control-regex
461
+ const sanitized = value.replace(/[/\\:*?"<>|\x00-\x1F\x7F]/g, '-');
462
+ // `.`, `..`, or a blank value would let a crafted name/version escape or replace
463
+ // the intended output directory once interpolated into an `--out` template.
464
+ return sanitized === '.' || sanitized === '..' || sanitized.trim() === '' ? '-' : sanitized;
465
+ }
466
+ function isInsideDir(parentDir, childRelativePath) {
467
+ const rel = path.relative(parentDir, path.resolve(parentDir, childRelativePath));
468
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
469
+ }
189
470
  //# sourceMappingURL=sbom.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/deps.compliance.commands",
3
- "version": "1101.3.5",
3
+ "version": "1101.5.0",
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
  },
@@ -34,33 +34,35 @@
34
34
  "@zkochan/table": "^2.0.1",
35
35
  "chalk": "^5.6.2",
36
36
  "memoize": "^11.0.0",
37
+ "p-limit": "^7.3.0",
37
38
  "ramda": "npm:@pnpm/ramda@0.28.1",
38
39
  "render-help": "^2.0.0",
39
40
  "semver": "^7.8.4",
40
- "@pnpm/cli.common-cli-options-help": "1100.0.2",
41
- "@pnpm/cli.utils": "1101.0.12",
41
+ "@pnpm/cli.utils": "1101.0.13",
42
42
  "@pnpm/cli.meta": "1100.0.8",
43
- "@pnpm/config.writer": "1100.0.13",
44
- "@pnpm/constants": "1100.0.0",
43
+ "@pnpm/config.reader": "1101.10.1",
45
44
  "@pnpm/config.pick-registry-for-package": "1100.0.9",
46
- "@pnpm/deps.compliance.audit": "1101.0.16",
47
- "@pnpm/deps.compliance.license-scanner": "1100.0.19",
45
+ "@pnpm/config.writer": "1100.0.14",
46
+ "@pnpm/constants": "1100.0.0",
47
+ "@pnpm/deps.compliance.audit": "1101.0.18",
48
48
  "@pnpm/deps.compliance.license-resolver": "1100.0.0",
49
- "@pnpm/deps.security.signatures": "1101.2.2",
50
- "@pnpm/deps.compliance.sbom": "1100.1.9",
51
- "@pnpm/error": "1100.0.0",
52
- "@pnpm/cli.command": "1100.0.1",
53
- "@pnpm/installing.commands": "1100.9.0",
54
- "@pnpm/lockfile.utils": "1100.0.13",
55
- "@pnpm/lockfile.walker": "1100.0.11",
56
- "@pnpm/config.reader": "1101.9.0",
57
- "@pnpm/workspace.project-manifest-reader": "1100.0.13",
58
- "@pnpm/types": "1101.3.2",
59
- "@pnpm/network.auth-header": "1101.1.2",
60
- "@pnpm/store.path": "1100.0.1",
49
+ "@pnpm/deps.compliance.license-scanner": "1100.0.21",
50
+ "@pnpm/config.version-policy": "1100.1.6",
51
+ "@pnpm/deps.compliance.sbom": "1100.3.0",
52
+ "@pnpm/error": "1100.0.1",
53
+ "@pnpm/lockfile.types": "1100.0.12",
54
+ "@pnpm/lockfile.utils": "1100.1.0",
55
+ "@pnpm/lockfile.walker": "1100.0.12",
56
+ "@pnpm/deps.security.signatures": "1101.2.3",
57
+ "@pnpm/lockfile.fs": "1100.1.7",
58
+ "@pnpm/installing.commands": "1100.10.1",
61
59
  "@pnpm/object.key-sorting": "1100.0.1",
62
- "@pnpm/lockfile.types": "1100.0.11",
63
- "@pnpm/lockfile.fs": "1100.1.5"
60
+ "@pnpm/store.path": "1100.0.2",
61
+ "@pnpm/network.auth-header": "1101.1.3",
62
+ "@pnpm/types": "1101.3.2",
63
+ "@pnpm/workspace.project-manifest-reader": "1100.0.14",
64
+ "@pnpm/cli.common-cli-options-help": "1100.0.2",
65
+ "@pnpm/cli.command": "1100.0.1"
64
66
  },
65
67
  "peerDependencies": {
66
68
  "@pnpm/logger": "^1100.0.0"
@@ -73,15 +75,15 @@
73
75
  "load-json-file": "^7.0.1",
74
76
  "read-yaml-file": "^3.0.0",
75
77
  "tempy": "3.0.0",
76
- "@pnpm/pkg-manifest.reader": "1100.0.8",
77
- "@pnpm/deps.compliance.commands": "1101.3.5",
78
- "@pnpm/testing.mock-agent": "1101.0.3",
78
+ "@pnpm/deps.compliance.commands": "1101.5.0",
79
+ "@pnpm/pkg-manifest.reader": "1100.0.9",
80
+ "@pnpm/prepare": "1100.0.17",
81
+ "@pnpm/testing.mock-agent": "1101.0.4",
79
82
  "@pnpm/test-fixtures": "1100.0.0",
80
- "@pnpm/prepare": "1100.0.16",
81
- "@pnpm/testing.registry-mock": "1100.0.6",
82
- "@pnpm/logger": "1100.0.0",
83
- "@pnpm/workspace.projects-filter": "1100.0.21",
84
- "@pnpm/testing.command-defaults": "1100.0.6"
83
+ "@pnpm/workspace.projects-filter": "1100.0.23",
84
+ "@pnpm/testing.registry-mock": "1100.0.7",
85
+ "@pnpm/testing.command-defaults": "1100.0.7",
86
+ "@pnpm/logger": "1100.0.0"
85
87
  },
86
88
  "engines": {
87
89
  "node": ">=22.13"