-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
audit.ts
237 lines (190 loc) Β· 10.5 KB
/
audit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
import {Configuration, Project, MessageName, treeUtils, LightReport, StreamReport, semverUtils, LocatorHash, Locator, miscUtils} from '@yarnpkg/core';
import {structUtils} from '@yarnpkg/core';
import {npmConfigUtils, npmHttpUtils} from '@yarnpkg/plugin-npm';
import {Command, Option, Usage} from 'clipanion';
import micromatch from 'micromatch';
import * as t from 'typanion';
import * as npmAuditTypes from '../../npmAuditTypes';
import * as npmAuditUtils from '../../npmAuditUtils';
// eslint-disable-next-line arca/no-default-export
export default class NpmAuditCommand extends BaseCommand {
static paths = [
[`npm`, `audit`],
];
static usage: Usage = Command.Usage({
description: `perform a vulnerability audit against the installed packages`,
details: `
This command checks for known security reports on the packages you use. The reports are by default extracted from the npm registry, and may or may not be relevant to your actual program (not all vulnerabilities affect all code paths).
For consistency with our other commands the default is to only check the direct dependencies for the active workspace. To extend this search to all workspaces, use \`-A,--all\`. To extend this search to both direct and transitive dependencies, use \`-R,--recursive\`.
Applying the \`--severity\` flag will limit the audit table to vulnerabilities of the corresponding severity and above. Valid values are ${npmAuditUtils.allSeverities.map(value => `\`${value}\``).join(`, `)}.
If the \`--json\` flag is set, Yarn will print the output exactly as received from the registry. Regardless of this flag, the process will exit with a non-zero exit code if a report is found for the selected packages.
If certain packages produce false positives for a particular environment, the \`--exclude\` flag can be used to exclude any number of packages from the audit. This can also be set in the configuration file with the \`npmAuditExcludePackages\` option.
If particular advisories are needed to be ignored, the \`--ignore\` flag can be used with Advisory ID's to ignore any number of advisories in the audit report. This can also be set in the configuration file with the \`npmAuditIgnoreAdvisories\` option.
To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why package\` to get more information as to who depends on them.
`,
examples: [[
`Checks for known security issues with the installed packages. The output is a list of known issues.`,
`yarn npm audit`,
], [
`Audit dependencies in all workspaces`,
`yarn npm audit --all`,
], [
`Limit auditing to \`dependencies\` (excludes \`devDependencies\`)`,
`yarn npm audit --environment production`,
], [
`Show audit report as valid JSON`,
`yarn npm audit --json`,
], [
`Audit all direct and transitive dependencies`,
`yarn npm audit --recursive`,
], [
`Output moderate (or more severe) vulnerabilities`,
`yarn npm audit --severity moderate`,
], [
`Exclude certain packages`,
`yarn npm audit --exclude package1 --exclude package2`,
], [
`Ignore specific advisories`,
`yarn npm audit --ignore 1234567 --ignore 7654321`,
]],
});
all = Option.Boolean(`-A,--all`, false, {
description: `Audit dependencies from all workspaces`,
});
recursive = Option.Boolean(`-R,--recursive`, false, {
description: `Audit transitive dependencies as well`,
});
environment = Option.String(`--environment`, npmAuditTypes.Environment.All, {
description: `Which environments to cover`,
validator: t.isEnum(npmAuditTypes.Environment),
});
json = Option.Boolean(`--json`, false, {
description: `Format the output as an NDJSON stream`,
});
noDeprecations = Option.Boolean(`--no-deprecations`, false, {
description: `Don't warn about deprecated packages`,
});
severity = Option.String(`--severity`, npmAuditTypes.Severity.Info, {
description: `Minimal severity requested for packages to be displayed`,
validator: t.isEnum(npmAuditTypes.Severity),
});
excludes = Option.Array(`--exclude`, [], {
description: `Array of glob patterns of packages to exclude from audit`,
});
ignores = Option.Array(`--ignore`, [], {
description: `Array of glob patterns of advisory ID's to ignore in the audit report`,
});
async execute() {
const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
const {project, workspace} = await Project.find(configuration, this.context.cwd);
if (!workspace)
throw new WorkspaceRequiredError(project.cwd, this.context.cwd);
await project.restoreInstallState();
const topLevelDependencies = npmAuditUtils.getTopLevelDependencies(project, workspace, {all: this.all, environment: this.environment});
const packages = npmAuditUtils.getPackages(project, topLevelDependencies, {recursive: this.recursive});
const excludedPackages = Array.from(new Set([
...configuration.get(`npmAuditExcludePackages`),
...this.excludes,
]));
const payload: Record<string, Array<string>> = Object.create(null);
for (const [packageName, versions] of packages)
if (!excludedPackages.some(pattern => micromatch.isMatch(packageName, pattern)))
payload[packageName] = [...versions.keys()];
const registry = npmConfigUtils.getAuditRegistry({configuration});
let result!: npmAuditTypes.AuditResponse;
const httpReport = await LightReport.start({
configuration,
stdout: this.context.stdout,
}, async () => {
// Note: No `await` there, so that the request is sent in parallel with the deprecation checks
const auditRequest = npmHttpUtils.post(`/-/npm/v1/security/advisories/bulk`, payload, {
authType: npmHttpUtils.AuthType.BEST_EFFORT,
configuration,
jsonResponse: true,
registry,
}) as unknown as Promise<npmAuditTypes.AuditResponse>;
const deprecations = this.noDeprecations ? [] : await Promise.all(Array.from(Object.entries(payload), async ([packageName, versions]) => {
const registryData = await npmHttpUtils.getPackageMetadata(structUtils.parseIdent(packageName), {
project,
});
return miscUtils.mapAndFilter(versions, version => {
const {deprecated} = registryData.versions[version];
// Apparently some packages have a `deprecated` field set to an empty string
// (even though that shouldn't be possible since `npm deprecate ... ""` undeprecates
// the package, completely removing the `deprecated` field). Both the npm website
// and all other package managers skip showing deprecation warnings in this case.
if (!deprecated)
return miscUtils.mapAndFilter.skip;
return [packageName, version, deprecated] as const;
});
}));
const auditResult = await auditRequest;
for (const [packageName, version, message] of deprecations.flat(1)) {
// No need to report the deprecation audit message if the package is already reported as vulnerable
if (Object.hasOwn(auditResult, packageName))
if (auditResult[packageName].some(advisory => semverUtils.satisfiesWithPrereleases(version, advisory.vulnerable_versions)))
continue;
auditResult[packageName] ??= [];
auditResult[packageName].push({
id: `${packageName} (deprecation)`,
title: message.trim() || `This package has been deprecated.`,
severity: npmAuditTypes.Severity.Moderate,
vulnerable_versions: version,
});
}
result = auditResult;
});
if (httpReport.hasErrors())
return httpReport.exitCode();
const severities = npmAuditUtils.getSeverityInclusions(this.severity);
const ignoredAdvisories = Array.from(new Set([
...configuration.get(`npmAuditIgnoreAdvisories`),
...this.ignores,
]));
const expandedResult: npmAuditTypes.AuditExtendedResponse = Object.create(null);
for (const [packageName, advisories] of Object.entries(result)) {
const filteredAdvisories = advisories.filter(advisory => {
return !micromatch.isMatch(`${advisory.id}`, ignoredAdvisories) && severities.has(advisory.severity);
});
if (filteredAdvisories.length > 0) {
expandedResult[packageName] = filteredAdvisories.map(advisory => {
const packageVersions = packages.get(packageName);
if (typeof packageVersions === `undefined`)
throw new Error(`Assertion failed: Expected the registry to only return packages that were requested`);
const versions = [...packageVersions.keys()].filter(version => {
return semverUtils.satisfiesWithPrereleases(version, advisory.vulnerable_versions);
});
const dependents = new Map<LocatorHash, Locator>();
for (const version of versions)
for (const pkg of packageVersions.get(version)!)
dependents.set(pkg.locatorHash, pkg);
return {
...advisory,
versions,
dependents: [...dependents.values()],
};
});
}
}
const hasError = Object.keys(expandedResult).length > 0;
if (hasError) {
treeUtils.emitTree(npmAuditUtils.getReportTree(expandedResult), {
configuration,
json: this.json,
stdout: this.context.stdout,
separators: 2,
});
return 1;
}
await StreamReport.start({
configuration,
includeFooter: false,
json: this.json,
stdout: this.context.stdout,
}, async report => {
report.reportInfo(MessageName.EXCEPTION, `No audit suggestions`);
});
return hasError ? 1 : 0;
}
}