@pnpm/deps.compliance.commands 1101.4.0 → 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 +6 -5
- package/lib/sbom/sbom.d.ts +1 -0
- package/lib/sbom/sbom.js +91 -3
- package/package.json +32 -31
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
|
|
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
|
-
|
|
57
|
-
}
|
|
56
|
+
if (!minVersion)
|
|
57
|
+
continue;
|
|
58
|
+
specs.push(`${advisory.module_name}@${minVersion.version}`);
|
|
58
59
|
}
|
|
59
|
-
return
|
|
60
|
+
return mergePackageVersionSpecs(specs);
|
|
60
61
|
}
|
|
61
62
|
//# sourceMappingURL=fix.js.map
|
package/lib/sbom/sbom.d.ts
CHANGED
|
@@ -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.
|
|
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
|
},
|
|
@@ -38,30 +38,31 @@
|
|
|
38
38
|
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
39
39
|
"render-help": "^2.0.0",
|
|
40
40
|
"semver": "^7.8.4",
|
|
41
|
-
"@pnpm/cli.
|
|
42
|
-
"@pnpm/cli.command": "1100.0.1",
|
|
41
|
+
"@pnpm/cli.utils": "1101.0.13",
|
|
43
42
|
"@pnpm/cli.meta": "1100.0.8",
|
|
44
|
-
"@pnpm/config.reader": "1101.10.
|
|
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",
|
|
43
|
+
"@pnpm/config.reader": "1101.10.1",
|
|
56
44
|
"@pnpm/config.pick-registry-for-package": "1100.0.9",
|
|
57
|
-
"@pnpm/
|
|
58
|
-
"@pnpm/
|
|
59
|
-
"@pnpm/
|
|
45
|
+
"@pnpm/config.writer": "1100.0.14",
|
|
46
|
+
"@pnpm/constants": "1100.0.0",
|
|
47
|
+
"@pnpm/deps.compliance.audit": "1101.0.18",
|
|
48
|
+
"@pnpm/deps.compliance.license-resolver": "1100.0.0",
|
|
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",
|
|
60
59
|
"@pnpm/object.key-sorting": "1100.0.1",
|
|
60
|
+
"@pnpm/store.path": "1100.0.2",
|
|
61
|
+
"@pnpm/network.auth-header": "1101.1.3",
|
|
61
62
|
"@pnpm/types": "1101.3.2",
|
|
62
|
-
"@pnpm/
|
|
63
|
-
"@pnpm/
|
|
64
|
-
"@pnpm/
|
|
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"
|
|
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",
|
|
77
|
-
"@pnpm/
|
|
78
|
-
"@pnpm/pkg-manifest.reader": "1100.0.
|
|
79
|
-
"@pnpm/
|
|
80
|
-
"@pnpm/
|
|
81
|
-
"@pnpm/testing.command-defaults": "1100.0.6",
|
|
82
|
-
"@pnpm/testing.registry-mock": "1100.0.6",
|
|
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",
|
|
83
82
|
"@pnpm/test-fixtures": "1100.0.0",
|
|
84
|
-
"@pnpm/workspace.projects-filter": "1100.0.
|
|
85
|
-
"@pnpm/testing.mock
|
|
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"
|
|
86
87
|
},
|
|
87
88
|
"engines": {
|
|
88
89
|
"node": ">=22.13"
|