@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 +6 -5
- package/lib/sbom/sbom.d.ts +5 -1
- package/lib/sbom/sbom.js +316 -35
- package/package.json +33 -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
|
@@ -6,11 +6,15 @@ export type SbomCommandOptions = {
|
|
|
6
6
|
lockfileOnly?: boolean;
|
|
7
7
|
sbomAuthors?: string;
|
|
8
8
|
sbomSupplier?: string;
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
?
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
124
|
-
const
|
|
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(
|
|
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
|
|
340
|
+
rootDescription,
|
|
145
341
|
rootAuthor,
|
|
146
342
|
rootRepository,
|
|
147
|
-
|
|
343
|
+
rootBugsUrl,
|
|
344
|
+
sbomType: serialOpts.sbomType,
|
|
148
345
|
include,
|
|
149
346
|
registries: opts.registries,
|
|
150
|
-
lockfileDir
|
|
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
|
+
"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.
|
|
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.
|
|
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/
|
|
47
|
-
"@pnpm/
|
|
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.
|
|
50
|
-
"@pnpm/
|
|
51
|
-
"@pnpm/
|
|
52
|
-
"@pnpm/
|
|
53
|
-
"@pnpm/
|
|
54
|
-
"@pnpm/lockfile.utils": "1100.0
|
|
55
|
-
"@pnpm/lockfile.walker": "1100.0.
|
|
56
|
-
"@pnpm/
|
|
57
|
-
"@pnpm/
|
|
58
|
-
"@pnpm/
|
|
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/
|
|
63
|
-
"@pnpm/
|
|
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/
|
|
77
|
-
"@pnpm/
|
|
78
|
-
"@pnpm/
|
|
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/
|
|
81
|
-
"@pnpm/testing.registry-mock": "1100.0.
|
|
82
|
-
"@pnpm/
|
|
83
|
-
"@pnpm/
|
|
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"
|