@pnpm/deps.compliance.commands 1000.0.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/audit.d.ts +24 -0
- package/lib/audit/audit.js +350 -0
- package/lib/audit/fix.d.ts +3 -0
- package/lib/audit/fix.js +26 -0
- package/lib/audit/fixWithUpdate.d.ts +13 -0
- package/lib/audit/fixWithUpdate.js +110 -0
- package/lib/audit/ignore.d.ts +13 -0
- package/lib/audit/ignore.js +31 -0
- package/lib/audit/index.d.ts +2 -0
- package/lib/audit/index.js +3 -0
- package/lib/audit/lockfileToPackages.d.ts +7 -0
- package/lib/audit/lockfileToPackages.js +23 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +4 -0
- package/lib/licenses/LicensesCommandResult.d.ts +4 -0
- package/lib/licenses/LicensesCommandResult.js +2 -0
- package/lib/licenses/index.d.ts +2 -0
- package/lib/licenses/index.js +3 -0
- package/lib/licenses/licenses.d.ts +10 -0
- package/lib/licenses/licenses.js +85 -0
- package/lib/licenses/licensesList.d.ts +9 -0
- package/lib/licenses/licensesList.js +48 -0
- package/lib/licenses/outputRenderer.d.ts +14 -0
- package/lib/licenses/outputRenderer.js +134 -0
- package/lib/sbom/index.d.ts +2 -0
- package/lib/sbom/index.js +3 -0
- package/lib/sbom/sbom.d.ts +17 -0
- package/lib/sbom/sbom.js +159 -0
- package/package.json +85 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Config, type UniversalOptions } from '@pnpm/config.reader';
|
|
2
|
+
import { type AuditReport } from '@pnpm/deps.compliance.audit';
|
|
3
|
+
import { type InstallCommandOptions } from '@pnpm/installing.commands';
|
|
4
|
+
import type { Registries } from '@pnpm/types';
|
|
5
|
+
import { type FixWithUpdateResult } from './fixWithUpdate.js';
|
|
6
|
+
export declare function rcOptionsTypes(): Record<string, unknown>;
|
|
7
|
+
export declare function cliOptionsTypes(): Record<string, unknown>;
|
|
8
|
+
export declare const shorthands: Record<string, string>;
|
|
9
|
+
export declare const commandNames: string[];
|
|
10
|
+
export declare function help(): string;
|
|
11
|
+
export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
|
|
12
|
+
fix?: boolean | 'override' | 'update';
|
|
13
|
+
ignoreRegistryErrors?: boolean;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
lockfileDir?: string;
|
|
16
|
+
registries: Registries;
|
|
17
|
+
ignore?: string[];
|
|
18
|
+
ignoreUnfixable?: boolean;
|
|
19
|
+
} & Pick<Config, 'auditConfig' | 'auditLevel' | 'ca' | 'cert' | 'httpProxy' | 'httpsProxy' | 'key' | 'localAddress' | 'maxSockets' | 'noProxy' | 'strictSsl' | 'fetchRetries' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchRetryFactor' | 'fetchTimeout' | 'production' | 'dev' | 'overrides' | 'optional' | 'userConfig' | 'rawConfig' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'virtualStoreDirMaxLength' | 'workspaceDir'> & InstallCommandOptions;
|
|
20
|
+
export declare function handler(opts: AuditOptions): Promise<{
|
|
21
|
+
exitCode: number;
|
|
22
|
+
output: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function formatFixWithUpdateOutput(result: FixWithUpdateResult, auditReport: AuditReport): string;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { docsUrl, TABLE_OPTIONS } from '@pnpm/cli.utils';
|
|
2
|
+
import { types as allTypes } from '@pnpm/config.reader';
|
|
3
|
+
import { WANTED_LOCKFILE } from '@pnpm/constants';
|
|
4
|
+
import { audit } from '@pnpm/deps.compliance.audit';
|
|
5
|
+
import { PnpmError } from '@pnpm/error';
|
|
6
|
+
import { update } from '@pnpm/installing.commands';
|
|
7
|
+
import { readEnvLockfile, readWantedLockfile } from '@pnpm/lockfile.fs';
|
|
8
|
+
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header';
|
|
9
|
+
import { table } from '@zkochan/table';
|
|
10
|
+
import chalk, {} from 'chalk';
|
|
11
|
+
import { difference, pick, pickBy } from 'ramda';
|
|
12
|
+
import { renderHelp } from 'render-help';
|
|
13
|
+
import { fix } from './fix.js';
|
|
14
|
+
import { fixWithUpdate } from './fixWithUpdate.js';
|
|
15
|
+
import { ignore } from './ignore.js';
|
|
16
|
+
const AUDIT_LEVEL_NUMBER = {
|
|
17
|
+
low: 0,
|
|
18
|
+
moderate: 1,
|
|
19
|
+
high: 2,
|
|
20
|
+
critical: 3,
|
|
21
|
+
};
|
|
22
|
+
const AUDIT_COLOR = {
|
|
23
|
+
low: chalk.bold,
|
|
24
|
+
moderate: chalk.bold.yellow,
|
|
25
|
+
high: chalk.bold.red,
|
|
26
|
+
critical: chalk.bold.red,
|
|
27
|
+
};
|
|
28
|
+
const AUDIT_TABLE_OPTIONS = {
|
|
29
|
+
...TABLE_OPTIONS,
|
|
30
|
+
columns: {
|
|
31
|
+
1: {
|
|
32
|
+
width: 54, // = table width of 80
|
|
33
|
+
wrapWord: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const MAX_PATHS_COUNT = 3;
|
|
38
|
+
export function rcOptionsTypes() {
|
|
39
|
+
return {
|
|
40
|
+
...update.rcOptionsTypes(),
|
|
41
|
+
...pick([
|
|
42
|
+
'dev',
|
|
43
|
+
'json',
|
|
44
|
+
'only',
|
|
45
|
+
'optional',
|
|
46
|
+
'production',
|
|
47
|
+
'registry',
|
|
48
|
+
], allTypes),
|
|
49
|
+
'audit-level': ['low', 'moderate', 'high', 'critical'],
|
|
50
|
+
// For fix, use String instead of a list of allowed string values.
|
|
51
|
+
// Otherwise, an unexpected value will get coerced to true because of the Boolean type.
|
|
52
|
+
fix: [String, Boolean],
|
|
53
|
+
'ignore-registry-errors': Boolean,
|
|
54
|
+
ignore: [String, Array],
|
|
55
|
+
'ignore-unfixable': Boolean,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function cliOptionsTypes() {
|
|
59
|
+
return {
|
|
60
|
+
...pick([
|
|
61
|
+
'recursive',
|
|
62
|
+
'workspace',
|
|
63
|
+
], update.cliOptionsTypes()),
|
|
64
|
+
...rcOptionsTypes(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export const shorthands = {
|
|
68
|
+
D: '--dev',
|
|
69
|
+
P: '--production',
|
|
70
|
+
};
|
|
71
|
+
export const commandNames = ['audit'];
|
|
72
|
+
export function help() {
|
|
73
|
+
return renderHelp({
|
|
74
|
+
description: 'Checks for known security issues with the installed packages.',
|
|
75
|
+
descriptionLists: [
|
|
76
|
+
{
|
|
77
|
+
title: 'Options',
|
|
78
|
+
list: [
|
|
79
|
+
{
|
|
80
|
+
description: 'Fix the audited vulnerabilities using the specified method: "override" or "update". "override" adds overrides to the package.json file in order to force non-vulnerable versions of the dependencies. "update" attempts to update the vulnerable packages in the lockfile to non-vulnerable versions. If no method is specified, "override" is used by default.',
|
|
81
|
+
name: '--fix [method]',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
description: 'Output audit report in JSON format',
|
|
85
|
+
name: '--json',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
description: 'Only print advisories with severity greater than or equal to one of the following: low|moderate|high|critical. Default: low',
|
|
89
|
+
name: '--audit-level <severity>',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
description: 'Only audit "devDependencies"',
|
|
93
|
+
name: '--dev',
|
|
94
|
+
shortAlias: '-D',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
description: 'Only audit "dependencies" and "optionalDependencies"',
|
|
98
|
+
name: '--prod',
|
|
99
|
+
shortAlias: '-P',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
description: 'Don\'t audit "optionalDependencies"',
|
|
103
|
+
name: '--no-optional',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
description: 'Use exit code 0 if the registry responds with an error. Useful when audit checks are used in CI. A build should not fail because the registry has issues.',
|
|
107
|
+
name: '--ignore-registry-errors',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
description: 'Ignore a vulnerability by CVE',
|
|
111
|
+
name: '--ignore <vulnerability>',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
description: 'Ignore all CVEs with no resolution',
|
|
115
|
+
name: '--ignore-unfixable',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
url: docsUrl('audit'),
|
|
121
|
+
usages: ['pnpm audit [options]'],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const DEFAULT_FIX_METHOD = 'override';
|
|
125
|
+
export async function handler(opts) {
|
|
126
|
+
const lockfileDir = opts.lockfileDir ?? opts.dir;
|
|
127
|
+
const lockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: true });
|
|
128
|
+
if (lockfile == null) {
|
|
129
|
+
throw new PnpmError('AUDIT_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found: Cannot audit a project without a lockfile`);
|
|
130
|
+
}
|
|
131
|
+
const envLockfile = await readEnvLockfile(opts.workspaceDir ?? lockfileDir);
|
|
132
|
+
const include = {
|
|
133
|
+
dependencies: opts.production !== false,
|
|
134
|
+
devDependencies: opts.dev !== false,
|
|
135
|
+
optionalDependencies: opts.optional !== false,
|
|
136
|
+
};
|
|
137
|
+
let auditReport;
|
|
138
|
+
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.rawConfig, userSettings: opts.userConfig });
|
|
139
|
+
try {
|
|
140
|
+
auditReport = await audit(lockfile, getAuthHeader, {
|
|
141
|
+
agentOptions: {
|
|
142
|
+
ca: opts.ca,
|
|
143
|
+
cert: opts.cert,
|
|
144
|
+
httpProxy: opts.httpProxy,
|
|
145
|
+
httpsProxy: opts.httpsProxy,
|
|
146
|
+
key: opts.key,
|
|
147
|
+
localAddress: opts.localAddress,
|
|
148
|
+
maxSockets: opts.maxSockets,
|
|
149
|
+
noProxy: opts.noProxy,
|
|
150
|
+
strictSsl: opts.strictSsl,
|
|
151
|
+
timeout: opts.fetchTimeout,
|
|
152
|
+
},
|
|
153
|
+
envLockfile,
|
|
154
|
+
include,
|
|
155
|
+
lockfileDir,
|
|
156
|
+
registry: opts.registries.default,
|
|
157
|
+
retry: {
|
|
158
|
+
factor: opts.fetchRetryFactor,
|
|
159
|
+
maxTimeout: opts.fetchRetryMaxtimeout,
|
|
160
|
+
minTimeout: opts.fetchRetryMintimeout,
|
|
161
|
+
retries: opts.fetchRetries,
|
|
162
|
+
},
|
|
163
|
+
timeout: opts.fetchTimeout,
|
|
164
|
+
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) { // eslint-disable-line
|
|
168
|
+
if (opts.ignoreRegistryErrors) {
|
|
169
|
+
return {
|
|
170
|
+
exitCode: 0,
|
|
171
|
+
output: err.message,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
let fixMethod;
|
|
177
|
+
if (opts.fix === 'update' || opts.fix === 'override') {
|
|
178
|
+
fixMethod = opts.fix;
|
|
179
|
+
}
|
|
180
|
+
else if (opts.fix === true) {
|
|
181
|
+
fixMethod = DEFAULT_FIX_METHOD;
|
|
182
|
+
}
|
|
183
|
+
else if (!opts.fix) {
|
|
184
|
+
fixMethod = undefined;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw new PnpmError('INVALID_FIX_OPTION', `Invalid value for --fix: ${opts.fix}. Should be one of "override" or "update"`);
|
|
188
|
+
}
|
|
189
|
+
if (fixMethod === 'update') {
|
|
190
|
+
const result = await fixWithUpdate(auditReport, { ...opts, include });
|
|
191
|
+
return {
|
|
192
|
+
exitCode: result.remaining.length > 0 ? 1 : 0,
|
|
193
|
+
output: formatFixWithUpdateOutput(result, auditReport),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (fixMethod === 'override') {
|
|
197
|
+
const newOverrides = await fix(auditReport, opts);
|
|
198
|
+
if (Object.values(newOverrides).length === 0) {
|
|
199
|
+
return {
|
|
200
|
+
exitCode: 0,
|
|
201
|
+
output: 'No fixes were made',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
exitCode: 0,
|
|
206
|
+
output: `${Object.values(newOverrides).length} overrides were added to package.json to fix vulnerabilities.
|
|
207
|
+
Run "pnpm install" to apply the fixes.
|
|
208
|
+
|
|
209
|
+
The added overrides:
|
|
210
|
+
${JSON.stringify(newOverrides, null, 2)}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (opts.ignore !== undefined || opts.ignoreUnfixable) {
|
|
214
|
+
const newIgnores = await ignore({
|
|
215
|
+
auditConfig: opts.auditConfig,
|
|
216
|
+
auditReport,
|
|
217
|
+
ignore: opts.ignore,
|
|
218
|
+
ignoreUnfixable: opts.ignoreUnfixable === true,
|
|
219
|
+
dir: opts.dir,
|
|
220
|
+
rootProjectManifest: opts.rootProjectManifest,
|
|
221
|
+
rootProjectManifestDir: opts.rootProjectManifestDir,
|
|
222
|
+
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
|
|
223
|
+
});
|
|
224
|
+
if (newIgnores.length === 0) {
|
|
225
|
+
return {
|
|
226
|
+
exitCode: 0,
|
|
227
|
+
output: 'No new vulnerabilities were ignored',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
exitCode: 0,
|
|
232
|
+
output: `${newIgnores.length} new vulnerabilities were ignored:
|
|
233
|
+
${newIgnores.join('\n')}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const vulnerabilities = auditReport.metadata.vulnerabilities;
|
|
237
|
+
const ignoredVulnerabilities = {
|
|
238
|
+
low: 0,
|
|
239
|
+
moderate: 0,
|
|
240
|
+
high: 0,
|
|
241
|
+
critical: 0,
|
|
242
|
+
};
|
|
243
|
+
const totalVulnerabilityCount = Object.values(vulnerabilities)
|
|
244
|
+
.reduce((sum, vulnerabilitiesCount) => sum + vulnerabilitiesCount, 0);
|
|
245
|
+
const ignoreGhsas = opts.auditConfig?.ignoreGhsas;
|
|
246
|
+
if (ignoreGhsas) {
|
|
247
|
+
auditReport.advisories = pickBy(({ github_advisory_id: githubAdvisoryId, severity }) => {
|
|
248
|
+
if (!ignoreGhsas.includes(githubAdvisoryId)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
ignoredVulnerabilities[severity] += 1;
|
|
252
|
+
return false;
|
|
253
|
+
}, auditReport.advisories);
|
|
254
|
+
}
|
|
255
|
+
const ignoreCves = opts.auditConfig?.ignoreCves;
|
|
256
|
+
if (ignoreCves) {
|
|
257
|
+
auditReport.advisories = pickBy(({ cves, severity }) => {
|
|
258
|
+
if (cves.length === 0 || difference(cves, ignoreCves).length > 0) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
ignoredVulnerabilities[severity] += 1;
|
|
262
|
+
return false;
|
|
263
|
+
}, auditReport.advisories);
|
|
264
|
+
}
|
|
265
|
+
const auditLevel = AUDIT_LEVEL_NUMBER[opts.auditLevel ?? 'low'];
|
|
266
|
+
const advisoryEntries = Object.entries(auditReport.advisories)
|
|
267
|
+
.filter(([, { severity }]) => AUDIT_LEVEL_NUMBER[severity] >= auditLevel);
|
|
268
|
+
if (opts.json) {
|
|
269
|
+
const advisories = Object.fromEntries(advisoryEntries);
|
|
270
|
+
return {
|
|
271
|
+
exitCode: Object.keys(advisories).length > 0 ? 1 : 0,
|
|
272
|
+
output: JSON.stringify({ ...auditReport, advisories }, null, 2),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
let output = '';
|
|
276
|
+
advisoryEntries.sort(([, a1], [, a2]) => AUDIT_LEVEL_NUMBER[a2.severity] - AUDIT_LEVEL_NUMBER[a1.severity]);
|
|
277
|
+
for (const [, advisory] of advisoryEntries) {
|
|
278
|
+
const paths = advisory.findings.map(({ paths }) => paths).flat();
|
|
279
|
+
output += table([
|
|
280
|
+
[AUDIT_COLOR[advisory.severity](advisory.severity), chalk.bold(advisory.title)],
|
|
281
|
+
['Package', advisory.module_name],
|
|
282
|
+
['Vulnerable versions', advisory.vulnerable_versions],
|
|
283
|
+
['Patched versions', advisory.patched_versions],
|
|
284
|
+
[
|
|
285
|
+
'Paths',
|
|
286
|
+
(paths.length > MAX_PATHS_COUNT
|
|
287
|
+
? paths
|
|
288
|
+
.slice(0, MAX_PATHS_COUNT)
|
|
289
|
+
.concat([
|
|
290
|
+
`... Found ${paths.length} paths, run \`pnpm why ${advisory.module_name}\` for more information`,
|
|
291
|
+
])
|
|
292
|
+
: paths).join('\n\n'),
|
|
293
|
+
],
|
|
294
|
+
['More info', advisory.url],
|
|
295
|
+
], AUDIT_TABLE_OPTIONS);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
exitCode: output ? 1 : 0,
|
|
299
|
+
output: `${output}${reportSummary(auditReport.metadata.vulnerabilities, totalVulnerabilityCount, ignoredVulnerabilities)}`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function reportSummary(vulnerabilities, totalVulnerabilityCount, ignoredVulnerabilities) {
|
|
303
|
+
if (totalVulnerabilityCount === 0)
|
|
304
|
+
return 'No known vulnerabilities found\n';
|
|
305
|
+
return `${chalk.red(totalVulnerabilityCount)} vulnerabilities found\nSeverity: ${Object.entries(vulnerabilities)
|
|
306
|
+
.filter(([_auditLevel, vulnerabilitiesCount]) => vulnerabilitiesCount > 0)
|
|
307
|
+
.map(([auditLevel, vulnerabilitiesCount]) => AUDIT_COLOR[auditLevel](`${vulnerabilitiesCount} ${auditLevel}${ignoredVulnerabilities[auditLevel] > 0 ? ` (${ignoredVulnerabilities[auditLevel]} ignored)` : ''}`))
|
|
308
|
+
.join(' | ')}`;
|
|
309
|
+
}
|
|
310
|
+
export function formatFixWithUpdateOutput(result, auditReport) {
|
|
311
|
+
const output = [];
|
|
312
|
+
/**
|
|
313
|
+
* Sort the given array of advisory IDs by severity descending
|
|
314
|
+
*/
|
|
315
|
+
function sortBySeverity(ids) {
|
|
316
|
+
return ids.map(id => ({ id, advisory: auditReport.advisories[id] })).sort((a, b) => {
|
|
317
|
+
const aValue = a.advisory ? AUDIT_LEVEL_NUMBER[a.advisory.severity] : -1;
|
|
318
|
+
const bValue = b.advisory ? AUDIT_LEVEL_NUMBER[b.advisory.severity] : -1;
|
|
319
|
+
return bValue - aValue;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const fixed = sortBySeverity(result.fixed);
|
|
323
|
+
const remaining = sortBySeverity(result.remaining);
|
|
324
|
+
const fixedString = fixed.length === 1 ? 'vulnerability was fixed' : 'vulnerabilities were fixed';
|
|
325
|
+
const remainingString = remaining.length === 1 ? 'vulnerability remains' : 'vulnerabilities remain';
|
|
326
|
+
output.push(`${chalk.green(fixed.length)} ${fixedString}, ${chalk.red(remaining.length)} ${remainingString}.`);
|
|
327
|
+
function summarizeAdvisory(fixed, { id, advisory }) {
|
|
328
|
+
if (advisory) {
|
|
329
|
+
const color = fixed ? chalk.green : AUDIT_COLOR[advisory.severity];
|
|
330
|
+
return `- (${color(advisory.severity)}) "${color(advisory.title)}" ${chalk.blue(advisory.module_name)}`;
|
|
331
|
+
}
|
|
332
|
+
return `- Advisory with ID ${id} (details not found in the audit report)`;
|
|
333
|
+
}
|
|
334
|
+
if (fixed.length > 0) {
|
|
335
|
+
output.push('\nThe fixed vulnerabilities are:');
|
|
336
|
+
for (const f of fixed) {
|
|
337
|
+
output.push(summarizeAdvisory(true, f));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (remaining.length > 0) {
|
|
341
|
+
output.push('\nThe remaining vulnerabilities are:');
|
|
342
|
+
for (const r of remaining) {
|
|
343
|
+
output.push(summarizeAdvisory(false, r));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Add trailing newline
|
|
347
|
+
output.push('');
|
|
348
|
+
return output.join('\n');
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=audit.js.map
|
package/lib/audit/fix.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { writeSettings } from '@pnpm/config.writer';
|
|
2
|
+
import { difference } from 'ramda';
|
|
3
|
+
export async function fix(auditReport, opts) {
|
|
4
|
+
const vulnOverrides = createOverrides(Object.values(auditReport.advisories), opts.auditConfig?.ignoreCves);
|
|
5
|
+
if (Object.values(vulnOverrides).length === 0)
|
|
6
|
+
return vulnOverrides;
|
|
7
|
+
await writeSettings({
|
|
8
|
+
updatedOverrides: vulnOverrides,
|
|
9
|
+
rootProjectManifest: opts.rootProjectManifest,
|
|
10
|
+
rootProjectManifestDir: opts.rootProjectManifestDir,
|
|
11
|
+
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
|
|
12
|
+
});
|
|
13
|
+
return vulnOverrides;
|
|
14
|
+
}
|
|
15
|
+
function createOverrides(advisories, ignoreCves) {
|
|
16
|
+
if (ignoreCves) {
|
|
17
|
+
advisories = advisories.filter(({ cves }) => difference(cves, ignoreCves).length > 0);
|
|
18
|
+
}
|
|
19
|
+
return Object.fromEntries(advisories
|
|
20
|
+
.filter(({ vulnerable_versions: vulnerableVersions, patched_versions: patchedVersions }) => vulnerableVersions !== '>=0.0.0' && patchedVersions !== '<0.0.0')
|
|
21
|
+
.map((advisory) => [
|
|
22
|
+
`${advisory.module_name}@${advisory.vulnerable_versions}`,
|
|
23
|
+
advisory.patched_versions,
|
|
24
|
+
]));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=fix.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuditReport } from '@pnpm/deps.compliance.audit';
|
|
2
|
+
import type { DependenciesField } from '@pnpm/types';
|
|
3
|
+
import type { AuditOptions } from './audit.js';
|
|
4
|
+
export interface FixWithUpdateResult {
|
|
5
|
+
fixed: number[];
|
|
6
|
+
remaining: number[];
|
|
7
|
+
}
|
|
8
|
+
export type FixWithUpdateOptions = AuditOptions & {
|
|
9
|
+
include?: {
|
|
10
|
+
[dependenciesField in DependenciesField]: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function fixWithUpdate(auditReport: AuditReport, opts: FixWithUpdateOptions): Promise<FixWithUpdateResult>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { WANTED_LOCKFILE } from '@pnpm/constants';
|
|
2
|
+
import { PnpmError } from '@pnpm/error';
|
|
3
|
+
import { update } from '@pnpm/installing.commands';
|
|
4
|
+
import { readWantedLockfile } from '@pnpm/lockfile.fs';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
import { lockfileToPackages } from './lockfileToPackages.js';
|
|
7
|
+
export async function fixWithUpdate(auditReport, opts) {
|
|
8
|
+
const vulnerabilitiesByPackage = new Map();
|
|
9
|
+
const unfixableVulnerabilities = new Map();
|
|
10
|
+
for (const advisory of Object.values(auditReport.advisories)) {
|
|
11
|
+
let packageVulnerabilities = vulnerabilitiesByPackage.get(advisory.module_name);
|
|
12
|
+
if (!packageVulnerabilities) {
|
|
13
|
+
packageVulnerabilities = [];
|
|
14
|
+
vulnerabilitiesByPackage.set(advisory.module_name, packageVulnerabilities);
|
|
15
|
+
}
|
|
16
|
+
const severity = advisory.severity;
|
|
17
|
+
const versionRange = advisory.vulnerable_versions;
|
|
18
|
+
if (versionRange === '>=0.0.0' || versionRange === '*') {
|
|
19
|
+
// skip unfixable vulnerabilities
|
|
20
|
+
let unfixableForPackage = unfixableVulnerabilities.get(advisory.module_name);
|
|
21
|
+
if (!unfixableForPackage) {
|
|
22
|
+
unfixableForPackage = new Set();
|
|
23
|
+
unfixableVulnerabilities.set(advisory.module_name, unfixableForPackage);
|
|
24
|
+
}
|
|
25
|
+
unfixableForPackage.add(advisory.id);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
packageVulnerabilities.push({
|
|
29
|
+
vulnerability: {
|
|
30
|
+
versionRange,
|
|
31
|
+
severity,
|
|
32
|
+
},
|
|
33
|
+
id: advisory.id,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const packageVulnerabilityAudit = {
|
|
37
|
+
isVulnerable(packageName, version) {
|
|
38
|
+
const vulnerabilities = vulnerabilitiesByPackage.get(packageName);
|
|
39
|
+
if (!vulnerabilities)
|
|
40
|
+
return false;
|
|
41
|
+
for (const vulnerabilityWithRange of vulnerabilities) {
|
|
42
|
+
let { semverRange } = vulnerabilityWithRange;
|
|
43
|
+
if (!semverRange) {
|
|
44
|
+
semverRange = new semver.Range(vulnerabilityWithRange.vulnerability.versionRange);
|
|
45
|
+
vulnerabilityWithRange.semverRange = semverRange;
|
|
46
|
+
}
|
|
47
|
+
if (semver.satisfies(version, semverRange)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
getVulnerabilities() {
|
|
54
|
+
const allVulnerabilities = new Map();
|
|
55
|
+
for (const [pkgName, vulnerabilities] of vulnerabilitiesByPackage) {
|
|
56
|
+
allVulnerabilities.set(pkgName, vulnerabilities.map(v => v.vulnerability));
|
|
57
|
+
}
|
|
58
|
+
return allVulnerabilities;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
await update.handler({
|
|
62
|
+
...opts,
|
|
63
|
+
packageVulnerabilityAudit,
|
|
64
|
+
}, []);
|
|
65
|
+
const lockfileDir = opts.lockfileDir ?? opts.dir;
|
|
66
|
+
const lockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: true });
|
|
67
|
+
if (lockfile == null) {
|
|
68
|
+
throw new PnpmError('AUDIT_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found after update: Cannot report fixed vulnerabilities`);
|
|
69
|
+
}
|
|
70
|
+
const updatedPackages = lockfileToPackages(lockfile, { include: opts.include });
|
|
71
|
+
const fixed = [];
|
|
72
|
+
const remaining = [];
|
|
73
|
+
for (const [pkgName, vulnerabilities] of vulnerabilitiesByPackage) {
|
|
74
|
+
const updatedVersions = updatedPackages.get(pkgName);
|
|
75
|
+
if (!updatedVersions) {
|
|
76
|
+
fixed.push(...vulnerabilities.map(v => v.id));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const vulnerability of vulnerabilities) {
|
|
80
|
+
let wasFixed = true;
|
|
81
|
+
for (const updatedVersion of updatedVersions) {
|
|
82
|
+
let { semverRange } = vulnerability;
|
|
83
|
+
if (!semverRange) {
|
|
84
|
+
semverRange = new semver.Range(vulnerability.vulnerability.versionRange);
|
|
85
|
+
vulnerability.semverRange = semverRange;
|
|
86
|
+
}
|
|
87
|
+
if (semver.satisfies(updatedVersion, semverRange)) {
|
|
88
|
+
wasFixed = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (wasFixed) {
|
|
93
|
+
fixed.push(vulnerability.id);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
remaining.push(vulnerability.id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const [pkgName, unfixableIds] of unfixableVulnerabilities) {
|
|
101
|
+
if (updatedPackages.has(pkgName)) {
|
|
102
|
+
remaining.push(...unfixableIds);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
fixed.push(...unfixableIds);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { fixed, remaining };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=fixWithUpdate.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuditReport } from '@pnpm/deps.compliance.audit';
|
|
2
|
+
import type { AuditConfig, ProjectManifest } from '@pnpm/types';
|
|
3
|
+
export interface IgnoreVulnerabilitiesOptions {
|
|
4
|
+
dir: string;
|
|
5
|
+
ignore?: string[];
|
|
6
|
+
ignoreUnfixable: boolean;
|
|
7
|
+
auditReport: AuditReport;
|
|
8
|
+
rootProjectManifest?: ProjectManifest;
|
|
9
|
+
rootProjectManifestDir: string;
|
|
10
|
+
workspaceDir: string;
|
|
11
|
+
auditConfig?: AuditConfig;
|
|
12
|
+
}
|
|
13
|
+
export declare function ignore(opts: IgnoreVulnerabilitiesOptions): Promise<string[]>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { writeSettings } from '@pnpm/config.writer';
|
|
2
|
+
import { difference } from 'ramda';
|
|
3
|
+
export async function ignore(opts) {
|
|
4
|
+
const currentCves = opts?.auditConfig?.ignoreCves ?? [];
|
|
5
|
+
const currentUniqueCves = new Set(currentCves);
|
|
6
|
+
const advisoryWthNoResolutions = filterAdvisoriesWithNoResolutions(Object.values(opts.auditReport.advisories));
|
|
7
|
+
if (opts.ignoreUnfixable) {
|
|
8
|
+
Object.values(advisoryWthNoResolutions).forEach((advisory) => {
|
|
9
|
+
advisory.cves.forEach((cve) => currentUniqueCves.add(cve));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
opts.ignore?.forEach((cve) => currentUniqueCves.add(cve));
|
|
14
|
+
}
|
|
15
|
+
const newIgnoreCves = currentUniqueCves.size > 0 ? Array.from(currentUniqueCves) : undefined;
|
|
16
|
+
const diffCve = difference(newIgnoreCves ?? [], currentCves);
|
|
17
|
+
await writeSettings({
|
|
18
|
+
...opts,
|
|
19
|
+
updatedSettings: {
|
|
20
|
+
auditConfig: {
|
|
21
|
+
...opts.auditConfig,
|
|
22
|
+
ignoreCves: newIgnoreCves,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return [...diffCve];
|
|
27
|
+
}
|
|
28
|
+
function filterAdvisoriesWithNoResolutions(advisories) {
|
|
29
|
+
return advisories.filter(({ patched_versions: patchedVersions }) => patchedVersions === '<0.0.0');
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=ignore.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LockfileObject } from '@pnpm/lockfile.types';
|
|
2
|
+
import type { DependenciesField } from '@pnpm/types';
|
|
3
|
+
export declare function lockfileToPackages(lockfile: LockfileObject, opts: {
|
|
4
|
+
include?: {
|
|
5
|
+
[dependenciesField in DependenciesField]: boolean;
|
|
6
|
+
};
|
|
7
|
+
}): Map<string, Set<string>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
|
|
2
|
+
import { lockfileWalkerGroupImporterSteps } from '@pnpm/lockfile.walker';
|
|
3
|
+
export function lockfileToPackages(lockfile, opts) {
|
|
4
|
+
const importerWalkers = lockfileWalkerGroupImporterSteps(lockfile, Object.keys(lockfile.importers), { include: opts?.include });
|
|
5
|
+
const packages = new Map();
|
|
6
|
+
for (const importerWalker of importerWalkers) {
|
|
7
|
+
addPackages(packages, importerWalker.step);
|
|
8
|
+
}
|
|
9
|
+
return packages;
|
|
10
|
+
}
|
|
11
|
+
function addPackages(packages, step) {
|
|
12
|
+
for (const { depPath, pkgSnapshot, next } of step.dependencies) {
|
|
13
|
+
const { name, version } = nameVerFromPkgSnapshot(depPath, pkgSnapshot);
|
|
14
|
+
if (version != null) {
|
|
15
|
+
if (!packages.has(name)) {
|
|
16
|
+
packages.set(name, new Set());
|
|
17
|
+
}
|
|
18
|
+
packages.get(name).add(version);
|
|
19
|
+
}
|
|
20
|
+
addPackages(packages, next());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=lockfileToPackages.js.map
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CompletionFunc } from '@pnpm/cli.command';
|
|
2
|
+
import type { LicensesCommandResult } from './LicensesCommandResult.js';
|
|
3
|
+
import { type LicensesCommandOptions } from './licensesList.js';
|
|
4
|
+
export declare function rcOptionsTypes(): Record<string, unknown>;
|
|
5
|
+
export declare const cliOptionsTypes: () => Record<string, unknown>;
|
|
6
|
+
export declare const shorthands: Record<string, string>;
|
|
7
|
+
export declare const commandNames: string[];
|
|
8
|
+
export declare function help(): string;
|
|
9
|
+
export declare const completion: CompletionFunc;
|
|
10
|
+
export declare function handler(opts: LicensesCommandOptions, params?: string[]): Promise<LicensesCommandResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { FILTERING } from '@pnpm/cli.common-cli-options-help';
|
|
2
|
+
import { docsUrl, readDepNameCompletions, } from '@pnpm/cli.utils';
|
|
3
|
+
import { types as allTypes } from '@pnpm/config.reader';
|
|
4
|
+
import { PnpmError } from '@pnpm/error';
|
|
5
|
+
import { pick } from 'ramda';
|
|
6
|
+
import { renderHelp } from 'render-help';
|
|
7
|
+
import { licensesList } from './licensesList.js';
|
|
8
|
+
export function rcOptionsTypes() {
|
|
9
|
+
return {
|
|
10
|
+
...pick(['dev', 'global-dir', 'global', 'json', 'long', 'optional', 'production'], allTypes),
|
|
11
|
+
compatible: Boolean,
|
|
12
|
+
table: Boolean,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export const cliOptionsTypes = () => ({
|
|
16
|
+
...rcOptionsTypes(),
|
|
17
|
+
recursive: Boolean,
|
|
18
|
+
});
|
|
19
|
+
export const shorthands = {
|
|
20
|
+
D: '--dev',
|
|
21
|
+
P: '--production',
|
|
22
|
+
};
|
|
23
|
+
export const commandNames = ['licenses'];
|
|
24
|
+
export function help() {
|
|
25
|
+
return renderHelp({
|
|
26
|
+
description: 'Check the licenses of the installed packages.',
|
|
27
|
+
descriptionLists: [
|
|
28
|
+
{
|
|
29
|
+
title: 'Options',
|
|
30
|
+
list: [
|
|
31
|
+
{
|
|
32
|
+
description: 'Show more details (such as a link to the repo) are not displayed. \
|
|
33
|
+
To display the details, pass this option.',
|
|
34
|
+
name: '--long',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
description: 'Show information in JSON format',
|
|
38
|
+
name: '--json',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description: 'Check only "dependencies" and "optionalDependencies"',
|
|
42
|
+
name: '--prod',
|
|
43
|
+
shortAlias: '-P',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
description: 'Check only "devDependencies"',
|
|
47
|
+
name: '--dev',
|
|
48
|
+
shortAlias: '-D',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description: 'Don\'t check "optionalDependencies"',
|
|
52
|
+
name: '--no-optional',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
FILTERING,
|
|
57
|
+
],
|
|
58
|
+
url: docsUrl('licenses'),
|
|
59
|
+
usages: [
|
|
60
|
+
'pnpm licenses ls',
|
|
61
|
+
'pnpm licenses ls --long',
|
|
62
|
+
'pnpm licenses list',
|
|
63
|
+
'pnpm licenses list --long',
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export const completion = async (cliOpts) => {
|
|
68
|
+
return readDepNameCompletions(cliOpts.dir);
|
|
69
|
+
};
|
|
70
|
+
export async function handler(opts, params = []) {
|
|
71
|
+
if (params.length === 0) {
|
|
72
|
+
throw new PnpmError('LICENCES_NO_SUBCOMMAND', 'Please specify the subcommand', {
|
|
73
|
+
hint: help(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
switch (params[0]) {
|
|
77
|
+
case 'list':
|
|
78
|
+
case 'ls':
|
|
79
|
+
return licensesList(opts);
|
|
80
|
+
default: {
|
|
81
|
+
throw new PnpmError('LICENSES_UNKNOWN_SUBCOMMAND', 'This subcommand is not known');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=licenses.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Config } from '@pnpm/config.reader';
|
|
2
|
+
import type { LicensesCommandResult } from './LicensesCommandResult.js';
|
|
3
|
+
export type LicensesCommandOptions = {
|
|
4
|
+
compatible?: boolean;
|
|
5
|
+
long?: boolean;
|
|
6
|
+
recursive?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
} & Pick<Config, 'dev' | 'dir' | 'lockfileDir' | 'registries' | 'optional' | 'production' | 'storeDir' | 'virtualStoreDir' | 'modulesDir' | 'pnpmHomeDir' | 'selectedProjectsGraph' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'supportedArchitectures' | 'virtualStoreDirMaxLength'> & Partial<Pick<Config, 'userConfig'>>;
|
|
9
|
+
export declare function licensesList(opts: LicensesCommandOptions): Promise<LicensesCommandResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readProjectManifestOnly } from '@pnpm/cli.utils';
|
|
3
|
+
import { WANTED_LOCKFILE } from '@pnpm/constants';
|
|
4
|
+
import { findDependencyLicenses } from '@pnpm/deps.compliance.license-scanner';
|
|
5
|
+
import { PnpmError } from '@pnpm/error';
|
|
6
|
+
import { getLockfileImporterId, readWantedLockfile } from '@pnpm/lockfile.fs';
|
|
7
|
+
import { getStorePath } from '@pnpm/store.path';
|
|
8
|
+
import { renderLicences } from './outputRenderer.js';
|
|
9
|
+
export async function licensesList(opts) {
|
|
10
|
+
const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, {
|
|
11
|
+
ignoreIncompatible: true,
|
|
12
|
+
});
|
|
13
|
+
if (lockfile == null) {
|
|
14
|
+
throw new PnpmError('LICENSES_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found: Cannot check a project without a lockfile`);
|
|
15
|
+
}
|
|
16
|
+
const include = {
|
|
17
|
+
dependencies: opts.production !== false,
|
|
18
|
+
devDependencies: opts.dev !== false,
|
|
19
|
+
optionalDependencies: opts.optional !== false,
|
|
20
|
+
};
|
|
21
|
+
const manifest = await readProjectManifestOnly(opts.dir);
|
|
22
|
+
const includedImporterIds = opts.selectedProjectsGraph
|
|
23
|
+
? Object.keys(opts.selectedProjectsGraph)
|
|
24
|
+
.map((path) => getLockfileImporterId(opts.lockfileDir ?? opts.dir, path))
|
|
25
|
+
: undefined;
|
|
26
|
+
const storeDir = await getStorePath({
|
|
27
|
+
pkgRoot: opts.dir,
|
|
28
|
+
storePath: opts.storeDir,
|
|
29
|
+
pnpmHomeDir: opts.pnpmHomeDir,
|
|
30
|
+
});
|
|
31
|
+
const licensePackages = await findDependencyLicenses({
|
|
32
|
+
include,
|
|
33
|
+
lockfileDir: opts.lockfileDir ?? opts.dir,
|
|
34
|
+
storeDir,
|
|
35
|
+
virtualStoreDir: opts.virtualStoreDir ?? path.join(opts.modulesDir ?? 'node_modules', '.pnpm'),
|
|
36
|
+
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
|
37
|
+
modulesDir: opts.modulesDir,
|
|
38
|
+
registries: opts.registries,
|
|
39
|
+
wantedLockfile: lockfile,
|
|
40
|
+
manifest,
|
|
41
|
+
includedImporterIds,
|
|
42
|
+
supportedArchitectures: opts.supportedArchitectures,
|
|
43
|
+
});
|
|
44
|
+
if (licensePackages.length === 0)
|
|
45
|
+
return { output: 'No licenses in packages found', exitCode: 0 };
|
|
46
|
+
return renderLicences(licensePackages, opts);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=licensesList.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LicensePackage } from '@pnpm/deps.compliance.license-scanner';
|
|
2
|
+
import type { LicensesCommandResult } from './LicensesCommandResult.js';
|
|
3
|
+
export declare function renderLicences(licensesMap: LicensePackage[], opts: {
|
|
4
|
+
long?: boolean;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}): LicensesCommandResult;
|
|
7
|
+
export interface LicensePackageJson {
|
|
8
|
+
name: string;
|
|
9
|
+
versions: string[];
|
|
10
|
+
license: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
homepage?: string;
|
|
13
|
+
paths: Array<string | null>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { TABLE_OPTIONS } from '@pnpm/cli.utils';
|
|
2
|
+
import { table } from '@zkochan/table';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { groupBy, omit, pick, sortWith } from 'ramda';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
function sortLicensesPackages(licensePackages) {
|
|
7
|
+
return sortWith([
|
|
8
|
+
(o1, o2) => o1.license.localeCompare(o2.license),
|
|
9
|
+
], licensePackages);
|
|
10
|
+
}
|
|
11
|
+
function renderPackageName({ belongsTo, name: packageName }) {
|
|
12
|
+
switch (belongsTo) {
|
|
13
|
+
case 'devDependencies':
|
|
14
|
+
return `${packageName} ${chalk.dim('(dev)')}`;
|
|
15
|
+
case 'optionalDependencies':
|
|
16
|
+
return `${packageName} ${chalk.dim('(optional)')}`;
|
|
17
|
+
default:
|
|
18
|
+
return packageName;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function renderPackageLicense({ license }) {
|
|
22
|
+
const output = license ?? 'Unknown';
|
|
23
|
+
return output;
|
|
24
|
+
}
|
|
25
|
+
function renderDetails(licensePackage) {
|
|
26
|
+
const outputs = [];
|
|
27
|
+
if (licensePackage.author) {
|
|
28
|
+
outputs.push(licensePackage.author);
|
|
29
|
+
}
|
|
30
|
+
if (licensePackage.description) {
|
|
31
|
+
outputs.push(licensePackage.description);
|
|
32
|
+
}
|
|
33
|
+
if (licensePackage.homepage) {
|
|
34
|
+
outputs.push(licensePackage.homepage);
|
|
35
|
+
}
|
|
36
|
+
return outputs.join('\n');
|
|
37
|
+
}
|
|
38
|
+
export function renderLicences(licensesMap, opts) {
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
return { output: renderLicensesJson(licensesMap), exitCode: 0 };
|
|
41
|
+
}
|
|
42
|
+
return { output: renderLicensesTable(licensesMap, opts), exitCode: 0 };
|
|
43
|
+
}
|
|
44
|
+
function renderLicensesJson(licensePackages) {
|
|
45
|
+
const data = licensePackages
|
|
46
|
+
.map((item) => pick(['name', 'version', 'path', 'license', 'author', 'homepage', 'description'], item));
|
|
47
|
+
const output = {};
|
|
48
|
+
const groupedByLicense = groupBy((item) => item.license, data);
|
|
49
|
+
for (const license in groupedByLicense) {
|
|
50
|
+
const outputList = [];
|
|
51
|
+
const groupedByName = groupBy((item) => item.name, groupedByLicense[license] ?? []);
|
|
52
|
+
for (const inputList of Object.values(groupedByName)) {
|
|
53
|
+
if (inputList == null)
|
|
54
|
+
continue;
|
|
55
|
+
inputList.sort((a, b) => semver.compare(a.version, b.version));
|
|
56
|
+
const versions = inputList.map((item) => item.version);
|
|
57
|
+
const paths = inputList.map((item) => item.path ?? null);
|
|
58
|
+
const lastInputItem = inputList.at(-1); // last item is chosen for its latest information
|
|
59
|
+
const outputItem = {
|
|
60
|
+
name: lastInputItem.name,
|
|
61
|
+
versions,
|
|
62
|
+
paths,
|
|
63
|
+
...omit(['name', 'version', 'path'], lastInputItem),
|
|
64
|
+
};
|
|
65
|
+
outputList.push(outputItem);
|
|
66
|
+
}
|
|
67
|
+
output[license] = outputList;
|
|
68
|
+
}
|
|
69
|
+
return JSON.stringify(output, null, 2);
|
|
70
|
+
}
|
|
71
|
+
function renderLicensesTable(licensePackages, opts) {
|
|
72
|
+
const columnNames = ['Package', 'License'];
|
|
73
|
+
const columnFns = [renderPackageName, renderPackageLicense];
|
|
74
|
+
if (opts.long) {
|
|
75
|
+
columnNames.push('Details');
|
|
76
|
+
columnFns.push(renderDetails);
|
|
77
|
+
}
|
|
78
|
+
// Avoid the overhead of allocating a new array caused by calling `array.map()`
|
|
79
|
+
for (let i = 0; i < columnNames.length; i++)
|
|
80
|
+
columnNames[i] = chalk.blueBright(columnNames[i]);
|
|
81
|
+
const data = [
|
|
82
|
+
columnNames,
|
|
83
|
+
...deduplicateLicensesPackages(sortLicensesPackages(licensePackages))
|
|
84
|
+
.map((licensePkg) => columnFns.map((fn) => fn(licensePkg))),
|
|
85
|
+
];
|
|
86
|
+
let detailsColumnMaxWidth = 40;
|
|
87
|
+
let packageColumnMaxWidth = 0;
|
|
88
|
+
let licenseColumnMaxWidth = 0;
|
|
89
|
+
if (opts.long) {
|
|
90
|
+
// Use the package link to determine the width of the details column
|
|
91
|
+
detailsColumnMaxWidth = licensePackages.reduce((max, pkg) => Math.max(max, pkg.homepage?.length ?? 0), 0);
|
|
92
|
+
for (let i = 1; i < data.length; i++) {
|
|
93
|
+
const row = data[i];
|
|
94
|
+
const detailsLineCount = row[2].split('\n').length;
|
|
95
|
+
const linesNumber = Math.max(0, detailsLineCount - 1);
|
|
96
|
+
row[0] += '\n '.repeat(linesNumber); // Add extra spaces to the package column
|
|
97
|
+
row[1] += '\n '.repeat(linesNumber); // Add extra spaces to the license column
|
|
98
|
+
packageColumnMaxWidth = Math.max(packageColumnMaxWidth, row[0].length);
|
|
99
|
+
licenseColumnMaxWidth = Math.max(licenseColumnMaxWidth, row[1].length);
|
|
100
|
+
}
|
|
101
|
+
const remainColumnWidth = process.stdout.columns - packageColumnMaxWidth - licenseColumnMaxWidth - 20;
|
|
102
|
+
if (detailsColumnMaxWidth > remainColumnWidth) {
|
|
103
|
+
detailsColumnMaxWidth = remainColumnWidth;
|
|
104
|
+
}
|
|
105
|
+
detailsColumnMaxWidth = Math.max(detailsColumnMaxWidth, 40);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return table(data, {
|
|
109
|
+
...TABLE_OPTIONS,
|
|
110
|
+
columns: {
|
|
111
|
+
...TABLE_OPTIONS.columns,
|
|
112
|
+
2: {
|
|
113
|
+
width: detailsColumnMaxWidth,
|
|
114
|
+
wrapWord: true,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Fallback to the default table if the details column width is too large, avoiding the error
|
|
121
|
+
return table(data, TABLE_OPTIONS);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function deduplicateLicensesPackages(licensePackages) {
|
|
125
|
+
const result = [];
|
|
126
|
+
const rowEqual = (a, b) => a.name === b.name && a.license === b.license;
|
|
127
|
+
const hasRow = (row) => result.some((x) => rowEqual(row, x));
|
|
128
|
+
for (const row of licensePackages.reverse()) { // reverse + unshift to prioritize latest package description
|
|
129
|
+
if (!hasRow(row))
|
|
130
|
+
result.unshift(row);
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=outputRenderer.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Config } from '@pnpm/config.reader';
|
|
2
|
+
export type SbomCommandOptions = {
|
|
3
|
+
sbomFormat?: string;
|
|
4
|
+
sbomType?: string;
|
|
5
|
+
lockfileOnly?: boolean;
|
|
6
|
+
sbomAuthors?: string;
|
|
7
|
+
sbomSupplier?: string;
|
|
8
|
+
} & Pick<Config, 'dev' | 'dir' | 'lockfileDir' | 'registries' | 'optional' | 'production' | 'storeDir' | 'virtualStoreDir' | 'modulesDir' | 'pnpmHomeDir' | 'selectedProjectsGraph' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'virtualStoreDirMaxLength'> & Partial<Pick<Config, 'userConfig'>>;
|
|
9
|
+
export declare function rcOptionsTypes(): Record<string, unknown>;
|
|
10
|
+
export declare const cliOptionsTypes: () => Record<string, unknown>;
|
|
11
|
+
export declare const shorthands: Record<string, string>;
|
|
12
|
+
export declare const commandNames: string[];
|
|
13
|
+
export declare function help(): string;
|
|
14
|
+
export declare function handler(opts: SbomCommandOptions, _params?: string[]): Promise<{
|
|
15
|
+
output: string;
|
|
16
|
+
exitCode: number;
|
|
17
|
+
}>;
|
package/lib/sbom/sbom.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { FILTERING } from '@pnpm/cli.common-cli-options-help';
|
|
2
|
+
import { packageManager } from '@pnpm/cli.meta';
|
|
3
|
+
import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils';
|
|
4
|
+
import { types as allTypes } from '@pnpm/config.reader';
|
|
5
|
+
import { WANTED_LOCKFILE } from '@pnpm/constants';
|
|
6
|
+
import { collectSbomComponents, serializeCycloneDx, serializeSpdx, } from '@pnpm/deps.compliance.sbom';
|
|
7
|
+
import { PnpmError } from '@pnpm/error';
|
|
8
|
+
import { getLockfileImporterId, readWantedLockfile } from '@pnpm/lockfile.fs';
|
|
9
|
+
import { getStorePath } from '@pnpm/store.path';
|
|
10
|
+
import { pick } from 'ramda';
|
|
11
|
+
import { renderHelp } from 'render-help';
|
|
12
|
+
export function rcOptionsTypes() {
|
|
13
|
+
return pick(['dev', 'global-dir', 'global', 'optional', 'production', 'store-dir'], allTypes);
|
|
14
|
+
}
|
|
15
|
+
export const cliOptionsTypes = () => ({
|
|
16
|
+
...rcOptionsTypes(),
|
|
17
|
+
recursive: Boolean,
|
|
18
|
+
'sbom-format': String,
|
|
19
|
+
'sbom-type': String,
|
|
20
|
+
'sbom-authors': String,
|
|
21
|
+
'sbom-supplier': String,
|
|
22
|
+
'lockfile-only': Boolean,
|
|
23
|
+
});
|
|
24
|
+
export const shorthands = {
|
|
25
|
+
D: '--dev',
|
|
26
|
+
P: '--production',
|
|
27
|
+
};
|
|
28
|
+
export const commandNames = ['sbom'];
|
|
29
|
+
export function help() {
|
|
30
|
+
return renderHelp({
|
|
31
|
+
description: 'Generate a Software Bill of Materials (SBOM) for the project.',
|
|
32
|
+
descriptionLists: [
|
|
33
|
+
{
|
|
34
|
+
title: 'Options',
|
|
35
|
+
list: [
|
|
36
|
+
{
|
|
37
|
+
description: 'The SBOM output format (required)',
|
|
38
|
+
name: '--sbom-format <cyclonedx|spdx>',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description: 'The component type for the root package (default: library)',
|
|
42
|
+
name: '--sbom-type <library|application>',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
description: 'Only use lockfile data (skip reading from the store)',
|
|
46
|
+
name: '--lockfile-only',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
description: 'Comma-separated list of SBOM authors (CycloneDX metadata.authors)',
|
|
50
|
+
name: '--sbom-authors <names>',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
description: 'SBOM supplier name (CycloneDX metadata.supplier)',
|
|
54
|
+
name: '--sbom-supplier <name>',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
description: 'Only include "dependencies" and "optionalDependencies"',
|
|
58
|
+
name: '--prod',
|
|
59
|
+
shortAlias: '-P',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: 'Only include "devDependencies"',
|
|
63
|
+
name: '--dev',
|
|
64
|
+
shortAlias: '-D',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
description: 'Don\'t include "optionalDependencies"',
|
|
68
|
+
name: '--no-optional',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
FILTERING,
|
|
73
|
+
],
|
|
74
|
+
url: docsUrl('sbom'),
|
|
75
|
+
usages: [
|
|
76
|
+
'pnpm sbom --sbom-format cyclonedx',
|
|
77
|
+
'pnpm sbom --sbom-format spdx',
|
|
78
|
+
'pnpm sbom --sbom-format cyclonedx --lockfile-only',
|
|
79
|
+
'pnpm sbom --sbom-format spdx --prod',
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function handler(opts, _params = []) {
|
|
84
|
+
if (!opts.sbomFormat) {
|
|
85
|
+
throw new PnpmError('SBOM_NO_FORMAT', 'The --sbom-format option is required. Use --sbom-format cyclonedx or --sbom-format spdx.', { hint: help() });
|
|
86
|
+
}
|
|
87
|
+
const format = opts.sbomFormat.toLowerCase();
|
|
88
|
+
if (format !== 'cyclonedx' && format !== 'spdx') {
|
|
89
|
+
throw new PnpmError('SBOM_INVALID_FORMAT', `Invalid SBOM format "${opts.sbomFormat}". Use "cyclonedx" or "spdx".`);
|
|
90
|
+
}
|
|
91
|
+
const sbomType = validateSbomType(opts.sbomType);
|
|
92
|
+
const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, {
|
|
93
|
+
ignoreIncompatible: true,
|
|
94
|
+
});
|
|
95
|
+
if (lockfile == null) {
|
|
96
|
+
throw new PnpmError('SBOM_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found: Cannot generate SBOM without a lockfile`);
|
|
97
|
+
}
|
|
98
|
+
const include = {
|
|
99
|
+
dependencies: opts.production !== false,
|
|
100
|
+
devDependencies: opts.dev !== false,
|
|
101
|
+
optionalDependencies: opts.optional !== false,
|
|
102
|
+
};
|
|
103
|
+
const manifest = await readProjectManifestOnly(opts.dir);
|
|
104
|
+
const rootName = manifest.name ?? 'unknown';
|
|
105
|
+
const rootVersion = manifest.version ?? '0.0.0';
|
|
106
|
+
const rootLicense = typeof manifest.license === 'string' ? manifest.license : undefined;
|
|
107
|
+
const rootAuthor = typeof manifest.author === 'string'
|
|
108
|
+
? manifest.author
|
|
109
|
+
: manifest.author?.name;
|
|
110
|
+
const rootRepository = typeof manifest.repository === 'string'
|
|
111
|
+
? manifest.repository
|
|
112
|
+
: manifest.repository?.url;
|
|
113
|
+
const includedImporterIds = opts.selectedProjectsGraph
|
|
114
|
+
? Object.keys(opts.selectedProjectsGraph)
|
|
115
|
+
.map((p) => getLockfileImporterId(opts.lockfileDir ?? opts.dir, p))
|
|
116
|
+
: undefined;
|
|
117
|
+
let storeDir;
|
|
118
|
+
if (!opts.lockfileOnly) {
|
|
119
|
+
storeDir = await getStorePath({
|
|
120
|
+
pkgRoot: opts.dir,
|
|
121
|
+
storePath: opts.storeDir,
|
|
122
|
+
pnpmHomeDir: opts.pnpmHomeDir,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const result = await collectSbomComponents({
|
|
126
|
+
lockfile,
|
|
127
|
+
rootName,
|
|
128
|
+
rootVersion,
|
|
129
|
+
rootLicense,
|
|
130
|
+
rootDescription: manifest.description,
|
|
131
|
+
rootAuthor,
|
|
132
|
+
rootRepository,
|
|
133
|
+
sbomType,
|
|
134
|
+
include,
|
|
135
|
+
registries: opts.registries,
|
|
136
|
+
lockfileDir: opts.lockfileDir ?? opts.dir,
|
|
137
|
+
includedImporterIds,
|
|
138
|
+
lockfileOnly: opts.lockfileOnly,
|
|
139
|
+
storeDir,
|
|
140
|
+
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
|
141
|
+
});
|
|
142
|
+
const output = format === 'cyclonedx'
|
|
143
|
+
? serializeCycloneDx(result, {
|
|
144
|
+
pnpmVersion: packageManager.version,
|
|
145
|
+
lockfileOnly: opts.lockfileOnly,
|
|
146
|
+
sbomAuthors: opts.sbomAuthors?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
147
|
+
sbomSupplier: opts.sbomSupplier,
|
|
148
|
+
})
|
|
149
|
+
: serializeSpdx(result);
|
|
150
|
+
return { output, exitCode: 0 };
|
|
151
|
+
}
|
|
152
|
+
function validateSbomType(value) {
|
|
153
|
+
if (!value || value === 'library')
|
|
154
|
+
return 'library';
|
|
155
|
+
if (value === 'application')
|
|
156
|
+
return 'application';
|
|
157
|
+
throw new PnpmError('SBOM_INVALID_TYPE', `Invalid SBOM type "${value}". Use "library" or "application".`);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=sbom.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pnpm/deps.compliance.commands",
|
|
3
|
+
"version": "1000.0.0",
|
|
4
|
+
"description": "pnpm commands for audit, licenses, and sbom",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pnpm",
|
|
7
|
+
"pnpm11",
|
|
8
|
+
"audit",
|
|
9
|
+
"licenses",
|
|
10
|
+
"sbom"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"funding": "https://opencollective.com/pnpm",
|
|
14
|
+
"repository": "https://github.com/pnpm/pnpm/tree/main/deps/compliance/commands",
|
|
15
|
+
"homepage": "https://github.com/pnpm/pnpm/tree/main/deps/compliance/commands#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/pnpm/pnpm/issues"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "lib/index.js",
|
|
21
|
+
"types": "lib/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./lib/index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"!*.map"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@zkochan/table": "^2.0.1",
|
|
31
|
+
"chalk": "^5.6.0",
|
|
32
|
+
"memoize": "^10.2.0",
|
|
33
|
+
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
34
|
+
"render-help": "^2.0.0",
|
|
35
|
+
"semver": "^7.7.2",
|
|
36
|
+
"@pnpm/cli.command": "1000.0.0",
|
|
37
|
+
"@pnpm/cli.common-cli-options-help": "1000.0.1",
|
|
38
|
+
"@pnpm/cli.utils": "1001.2.8",
|
|
39
|
+
"@pnpm/config.reader": "1004.4.2",
|
|
40
|
+
"@pnpm/cli.meta": "1000.0.11",
|
|
41
|
+
"@pnpm/constants": "1001.3.1",
|
|
42
|
+
"@pnpm/deps.compliance.audit": "1002.0.14",
|
|
43
|
+
"@pnpm/deps.compliance.license-scanner": "1001.0.27",
|
|
44
|
+
"@pnpm/deps.compliance.sbom": "1000.0.0-0",
|
|
45
|
+
"@pnpm/error": "1000.0.5",
|
|
46
|
+
"@pnpm/installing.commands": "1004.6.10",
|
|
47
|
+
"@pnpm/lockfile.fs": "1001.1.21",
|
|
48
|
+
"@pnpm/config.writer": "1000.0.14",
|
|
49
|
+
"@pnpm/lockfile.utils": "1003.0.3",
|
|
50
|
+
"@pnpm/lockfile.types": "1002.0.2",
|
|
51
|
+
"@pnpm/store.path": "1000.0.5",
|
|
52
|
+
"@pnpm/network.auth-header": "1000.0.6",
|
|
53
|
+
"@pnpm/types": "1000.9.0",
|
|
54
|
+
"@pnpm/lockfile.walker": "1001.0.16",
|
|
55
|
+
"@pnpm/workspace.project-manifest-reader": "1001.1.4"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@pnpm/registry-mock": "5.2.4",
|
|
59
|
+
"@types/ramda": "0.29.12",
|
|
60
|
+
"@types/semver": "7.7.1",
|
|
61
|
+
"@types/zkochan__table": "npm:@types/table@6.0.0",
|
|
62
|
+
"load-json-file": "^7.0.1",
|
|
63
|
+
"nock": "13.3.4",
|
|
64
|
+
"read-yaml-file": "^3.0.0",
|
|
65
|
+
"tempy": "3.0.0",
|
|
66
|
+
"@pnpm/deps.compliance.commands": "1000.0.0",
|
|
67
|
+
"@pnpm/pkg-manifest.reader": "1000.1.2",
|
|
68
|
+
"@pnpm/workspace.projects-filter": "1000.0.43",
|
|
69
|
+
"@pnpm/test-fixtures": "1000.0.0",
|
|
70
|
+
"@pnpm/prepare": "1000.0.4"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=22.13"
|
|
74
|
+
},
|
|
75
|
+
"jest": {
|
|
76
|
+
"preset": "@pnpm/jest-config/with-registry"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
80
|
+
"_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
|
|
81
|
+
"test": "pnpm run compile && pnpm run _test",
|
|
82
|
+
"compile": "tsgo --build && pnpm run lint --fix",
|
|
83
|
+
"update-responses": "node test/audit/utils/responses/update.ts"
|
|
84
|
+
}
|
|
85
|
+
}
|