Skip to content

Commit 49a157c

Browse files
rally25rsarcanis
authored andcommitted
feat(audit) Initial addition of yarn audit command. (yarnpkg#6409)
* WIP: audit command added. sends data to registry. * code cleanup * WIP: Added Audit command. No tests. Existing test fail. * Print audit summary when no problems found * Don't send package version to audit API if it is not in the manifest. * Add audit functions to json-reporter * WIP: First successful audit command test * added more audit tests * feat(audit): Initial addition of yarn audit command and --audit flag Added "yarn audit" command which copies the behavior of "npm audit". Unline npm, yarn does not automatically run "audit" during "add/install/upgrade" commands. Since this would cause an additional network call, it broke all existing unit tests to add this feature and have it run automatically. In the interest of getting an initial release in the hands of our users the "add/install/upgrade" commands accept a "--audit" flag that will enable the audit. If you want audit to always execute, you can add "--*.audit true" to .yarnrc fix yarnpkg#5808 * gzip the JSON sent to npm audit API to reduce payload * fix audit test for gzip data * Update install.js * removed audit correction suggestions due to them being unreliable * Updates the changelog
1 parent 9028f9a commit 49a157c

22 files changed

+993
-3
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Please add one entry in this file for each change in Yarn's behavior. Use the sa
88

99
[#6447](https://github.com/yarnpkg/yarn/pull/6447) - [**John-David Dalton**](https://twitter.com/jdalton)
1010

11+
- Adds `yarn audit` (and the `--audit` flag for all installs)
12+
13+
[#6409](https://github.com/yarnpkg/yarn/pull/6409) - [**Jeff Valore**](https://github.com/rally25rs)
14+
1115
- Adds a special logic to PnP for ESLint compatibility (temporary, until [eslint/eslint#10125](https://github.com/eslint/eslint/issues/10125) is fixed)
1216

1317
[#6449](https://github.com/yarnpkg/yarn/pull/6449) - [**Maël Nison**](https://twitter.com/arcanis)

__tests__/commands/audit.js

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* @flow */
2+
3+
import {NoopReporter} from '../../src/reporters/index.js';
4+
import {run as buildRun} from './_helpers.js';
5+
import {run as audit} from '../../src/cli/commands/audit.js';
6+
import {promisify} from '../../src/util/promise.js';
7+
8+
const path = require('path');
9+
const zlib = require('zlib');
10+
const gunzip = promisify(zlib.gunzip);
11+
12+
const fixturesLoc = path.join(__dirname, '..', 'fixtures', 'audit');
13+
14+
const setupMockRequestManager = function(config) {
15+
const apiResponse = JSON.stringify(getAuditResponse(config), null, 2);
16+
// $FlowFixMe
17+
config.requestManager.request = jest.fn();
18+
config.requestManager.request.mockReturnValue(
19+
new Promise(resolve => {
20+
resolve(apiResponse);
21+
}),
22+
);
23+
};
24+
25+
const setupMockReporter = function(reporter) {
26+
// $FlowFixMe
27+
reporter.auditAdvisory = jest.fn();
28+
// $FlowFixMe
29+
reporter.auditAction = jest.fn();
30+
// $FlowFixMe
31+
reporter.auditSummary = jest.fn();
32+
};
33+
34+
const getAuditResponse = function(config): Object {
35+
// $FlowFixMe
36+
return require(path.join(config.cwd, 'audit-api-response.json'));
37+
};
38+
39+
const runAudit = buildRun.bind(
40+
null,
41+
NoopReporter,
42+
fixturesLoc,
43+
async (args, flags, config, reporter, lockfile, getStdout): Promise<string> => {
44+
setupMockRequestManager(config);
45+
setupMockReporter(reporter);
46+
await audit(config, reporter, flags, args);
47+
return getStdout();
48+
},
49+
);
50+
51+
test.concurrent('sends correct dependency map to audit api for single dependency.', () => {
52+
const expectedApiPost = {
53+
name: 'yarn-test',
54+
install: [],
55+
remove: [],
56+
metadata: {},
57+
requires: {
58+
minimatch: '^3.0.0',
59+
},
60+
dependencies: {
61+
minimatch: {
62+
version: '3.0.0',
63+
integrity: 'sha1-UjYVelHk8ATBd/s8Un/33Xjw74M=',
64+
requires: {
65+
'brace-expansion': '^1.0.0',
66+
},
67+
dependencies: {},
68+
},
69+
'brace-expansion': {
70+
version: '1.1.11',
71+
integrity: 'sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==',
72+
requires: {
73+
'balanced-match': '^1.0.0',
74+
'concat-map': '0.0.1',
75+
},
76+
dependencies: {},
77+
},
78+
'balanced-match': {
79+
version: '1.0.0',
80+
integrity: 'sha1-ibTRmasr7kneFk6gK4nORi1xt2c=',
81+
requires: {},
82+
dependencies: {},
83+
},
84+
'concat-map': {
85+
version: '0.0.1',
86+
integrity: 'sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=',
87+
requires: {},
88+
dependencies: {},
89+
},
90+
},
91+
version: '0.0.0',
92+
};
93+
94+
return runAudit([], {}, 'single-vulnerable-dep-installed', async config => {
95+
const calledWithPipe = config.requestManager.request.mock.calls[0][0].body;
96+
const calledWith = JSON.parse(await gunzip(calledWithPipe));
97+
expect(calledWith).toEqual(expectedApiPost);
98+
});
99+
});
100+
101+
test('calls reporter auditAdvisory with correct data', () => {
102+
return runAudit([], {}, 'single-vulnerable-dep-installed', (config, reporter) => {
103+
const apiResponse = getAuditResponse(config);
104+
expect(reporter.auditAdvisory).toBeCalledWith(apiResponse.actions[0].resolves[0], apiResponse.advisories['118']);
105+
});
106+
});
107+
108+
// *** Test temporarily removed due to inability to correctly puggest actions to the user.
109+
// test('calls reporter auditAction with correct data', () => {
110+
// return runAudit([], {}, 'single-vulnerable-dep-installed', (config, reporter) => {
111+
// const apiResponse = getAuditResponse(config);
112+
// expect(reporter.auditAction).toBeCalledWith({
113+
// cmd: 'yarn upgrade minimatch@3.0.4',
114+
// isBreaking: false,
115+
// action: apiResponse.actions[0],
116+
// });
117+
// });
118+
// });
119+
120+
test('calls reporter auditSummary with correct data', () => {
121+
return runAudit([], {}, 'single-vulnerable-dep-installed', (config, reporter) => {
122+
const apiResponse = getAuditResponse(config);
123+
expect(reporter.auditSummary).toBeCalledWith(apiResponse.metadata);
124+
});
125+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"actions": [
3+
{
4+
"action": "install",
5+
"module": "minimatch",
6+
"target": "3.0.4",
7+
"isMajor": false,
8+
"resolves": [
9+
{
10+
"id": 118,
11+
"path": "minimatch",
12+
"dev": false,
13+
"optional": false,
14+
"bundled": false
15+
}
16+
]
17+
}
18+
],
19+
"advisories": {
20+
"118": {
21+
"findings": [
22+
{
23+
"version": "3.0.0",
24+
"paths": [
25+
"minimatch"
26+
],
27+
"dev": false,
28+
"optional": false,
29+
"bundled": false
30+
}
31+
],
32+
"id": 118,
33+
"created": "2016-05-25T16:37:20.000Z",
34+
"updated": "2018-03-01T21:58:01.072Z",
35+
"deleted": null,
36+
"title": "Regular Expression Denial of Service",
37+
"found_by": {
38+
"name": "Nick Starke"
39+
},
40+
"reported_by": {
41+
"name": "Nick Starke"
42+
},
43+
"module_name": "minimatch",
44+
"cves": [
45+
"CVE-2016-10540"
46+
],
47+
"vulnerable_versions": "<=3.0.1",
48+
"patched_versions": ">=3.0.2",
49+
"overview": "Affected versions of `minimatch` are vulnerable to regular expression denial of service attacks when user input is passed into the `pattern` argument of `minimatch(path, pattern)`.\n\n\n## Proof of Concept\n```\nvar minimatch = require(“minimatch”);\n\n// utility function for generating long strings\nvar genstr = function (len, chr) {\n var result = “”;\n for (i=0; i<=len; i++) {\n result = result + chr;\n }\n return result;\n}\n\nvar exploit = “[!” + genstr(1000000, “\\\\”) + “A”;\n\n// minimatch exploit.\nconsole.log(“starting minimatch”);\nminimatch(“foo”, exploit);\nconsole.log(“finishing minimatch”);\n```",
50+
"recommendation": "Update to version 3.0.2 or later.",
51+
"references": "",
52+
"access": "public",
53+
"severity": "high",
54+
"cwe": "CWE-400",
55+
"metadata": {
56+
"module_type": "Multi.Library",
57+
"exploitability": 4,
58+
"affected_components": "Internal::Code::Function::minimatch({type:'args', key:0, vector:{type:'string'}})"
59+
},
60+
"url": "https://nodesecurity.io/advisories/118"
61+
}
62+
},
63+
"muted": [],
64+
"metadata": {
65+
"vulnerabilities": {
66+
"info": 0,
67+
"low": 0,
68+
"moderate": 0,
69+
"high": 1,
70+
"critical": 0
71+
},
72+
"dependencies": 5,
73+
"devDependencies": 0,
74+
"optionalDependencies": 0,
75+
"totalDependencies": 5
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "yarn-test",
3+
"version": "0.0.0",
4+
"dependencies": {
5+
"minimatch": "^3.0.0"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
balanced-match@^1.0.0:
6+
version "1.0.0"
7+
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
8+
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
9+
10+
brace-expansion@^1.0.0:
11+
version "1.1.11"
12+
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
13+
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
14+
dependencies:
15+
balanced-match "^1.0.0"
16+
concat-map "0.0.1"
17+
18+
concat-map@0.0.1:
19+
version "0.0.1"
20+
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
21+
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
22+
23+
minimatch@^3.0.0:
24+
version "3.0.0"
25+
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.0.tgz#5236157a51e4f004c177fb3c527ff7dd78f0ef83"
26+
integrity sha1-UjYVelHk8ATBd/s8Un/33Xjw74M=
27+
dependencies:
28+
brace-expansion "^1.0.0"

__tests__/reporters/__snapshots__/console-reporter.js.snap

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ Object {
77
}
88
`;
99

10+
exports[`ConsoleReporter.auditSummary 1`] = `
11+
Object {
12+
"stderr": "",
13+
"stdout": "1 vulnerabilities found - Packages audited: 5
14+
Severity: 1 High",
15+
}
16+
`;
17+
1018
exports[`ConsoleReporter.command 1`] = `
1119
Object {
1220
"stderr": "",

__tests__/reporters/__snapshots__/json-reporter.js.snap

+21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ Object {
1717
}
1818
`;
1919

20+
exports[`JSONReporter.auditAction 1`] = `
21+
Object {
22+
"stderr": "",
23+
"stdout": "{\\"type\\":\\"auditAction\\",\\"data\\":{\\"cmd\\":\\"yarn upgrade gulp@4.0.0\\",\\"isBreaking\\":true,\\"action\\":{\\"action\\":\\"install\\",\\"module\\":\\"gulp\\",\\"target\\":\\"4.0.0\\",\\"isMajor\\":true,\\"resolves\\":[]}}}",
24+
}
25+
`;
26+
27+
exports[`JSONReporter.auditAdvisory 1`] = `
28+
Object {
29+
"stderr": "",
30+
"stdout": "{\\"type\\":\\"auditAdvisory\\",\\"data\\":{\\"resolution\\":{\\"id\\":118,\\"path\\":\\"gulp>vinyl-fs>glob-stream>minimatch\\",\\"dev\\":false,\\"optional\\":false,\\"bundled\\":false},\\"advisory\\":{\\"findings\\":[{\\"bundled\\":false,\\"optional\\":false,\\"dev\\":false,\\"paths\\":[],\\"version\\":\\"\\"}],\\"id\\":118,\\"created\\":\\"2016-05-25T16:37:20.000Z\\",\\"updated\\":\\"2018-03-01T21:58:01.072Z\\",\\"deleted\\":null,\\"title\\":\\"Regular Expression Denial of Service\\",\\"found_by\\":{\\"name\\":\\"Nick Starke\\"},\\"reported_by\\":{\\"name\\":\\"Nick Starke\\"},\\"module_name\\":\\"minimatch\\",\\"cves\\":[\\"CVE-2016-10540\\"],\\"vulnerable_versions\\":\\"<=3.0.1\\",\\"patched_versions\\":\\">=3.0.2\\",\\"overview\\":\\"\\",\\"recommendation\\":\\"Update to version 3.0.2 or later.\\",\\"references\\":\\"\\",\\"access\\":\\"public\\",\\"severity\\":\\"high\\",\\"cwe\\":\\"CWE-400\\",\\"metadata\\":{\\"module_type\\":\\"Multi.Library\\",\\"exploitability\\":4,\\"affected_components\\":\\"\\"},\\"url\\":\\"https://nodesecurity.io/advisories/118\\"}}}",
31+
}
32+
`;
33+
34+
exports[`JSONReporter.auditSummary 1`] = `
35+
Object {
36+
"stderr": "",
37+
"stdout": "{\\"type\\":\\"auditSummary\\",\\"data\\":{\\"vulnerabilities\\":{\\"info\\":0,\\"low\\":1,\\"moderate\\":0,\\"high\\":4,\\"critical\\":0},\\"dependencies\\":29105,\\"devDependencies\\":0,\\"optionalDependencies\\":0,\\"totalDependencies\\":29105}}",
38+
}
39+
`;
40+
2041
exports[`JSONReporter.command 1`] = `
2142
Object {
2243
"stderr": "",

__tests__/reporters/console-reporter.js

+22
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,25 @@ test('ConsoleReporter.tree is silent when isSilent is true', async () => {
304304
}),
305305
).toMatchSnapshot();
306306
});
307+
308+
test('ConsoleReporter.auditSummary', async () => {
309+
const auditMetadata = {
310+
vulnerabilities: {
311+
info: 0,
312+
low: 0,
313+
moderate: 0,
314+
high: 1,
315+
critical: 0,
316+
},
317+
dependencies: 5,
318+
devDependencies: 0,
319+
optionalDependencies: 0,
320+
totalDependencies: 5,
321+
};
322+
323+
expect(
324+
await getConsoleBuff(r => {
325+
r.auditSummary(auditMetadata);
326+
}),
327+
).toMatchSnapshot();
328+
});

0 commit comments

Comments
 (0)