Skip to content

Commit 8d3430f

Browse files
jdanilquinnturner
andauthored
feat: support yarn v2 (#171)
* feat: support yarn v2 Since Yarn Berry uses NPM for audits, the Yarn auditer is not supported. Yarn Classic uses a JSON-lines format for audit results. Yarn Berry audit cmd: `yarn npm audit --all --recursive --json` The following work is still required: - Make Yarn Berry audits use npm-auditer.js logic for runProgram - Have the Yarn Berry tests detect that Yarn Berry should be used (unknown reason why it thinks it's Yarn Classic) * docs: Add support for Yarn Berry in README * fix: pass cwd to yarn version check * fix: skip yarn berry tests on incompatible node versions Co-authored-by: Quinn Turner <[email protected]>
1 parent d04b46e commit 8d3430f

16 files changed

+67167
-35
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ threshold while ignoring allowlisted advisories.
1010

1111
## Requirements
1212

13-
- Node >= 8
14-
- _(Optional)_ Yarn >= 1.12.3 && Yarn < 2
13+
- Node >=8 (except Yarn Berry, which requires Node >=12.13.0)
14+
- _(Optional)_ Yarn ^1.12.3 || Yarn >=2.4.0
1515

1616
## Set up
1717

lib/yarn-auditer.js

+71-33
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@ const { blue, red, yellow } = require("./colors");
44
const { reportAudit, runProgram } = require("./common");
55
const Model = require("./Model");
66

7-
const MINIMUM_YARN_VERSION = "1.12.3";
7+
const MINIMUM_YARN_CLASSIC_VERSION = "1.12.3";
8+
const MINIMUM_YARN_BERRY_VERSION = "2.4.0";
89
/**
910
* Change this to the appropriate version when
1011
* yarn audit --registry is supported:
1112
* @see https://github.com/yarnpkg/yarn/issues/7012
1213
*/
1314
const MINIMUM_YARN_AUDIT_REGISTRY_VERSION = "99.99.99";
1415

15-
function getYarnVersion() {
16-
const version = childProcess.execSync("yarn -v").toString().replace("\n", "");
16+
function getYarnVersion(cwd) {
17+
const version = childProcess
18+
.execSync("yarn -v", { cwd })
19+
.toString()
20+
.replace("\n", "");
1721
return version;
1822
}
1923

24+
function yarnSupportsClassicAudit(yarnVersion) {
25+
return semver.satisfies(yarnVersion, `^${MINIMUM_YARN_CLASSIC_VERSION}`);
26+
}
27+
28+
function yarnSupportsBerryAudit(yarnVersion) {
29+
return semver.gte(yarnVersion, MINIMUM_YARN_BERRY_VERSION);
30+
}
31+
2032
function yarnSupportsAudit(yarnVersion) {
21-
return semver.gte(yarnVersion, MINIMUM_YARN_VERSION);
33+
return (
34+
yarnSupportsClassicAudit(yarnVersion) || yarnSupportsBerryAudit(yarnVersion)
35+
);
2236
}
2337

2438
function yarnAuditSupportsRegistry(yarnVersion) {
@@ -44,23 +58,25 @@ async function audit(config, reporter = reportAudit) {
4458
let missingLockFile = false;
4559
const model = new Model(config);
4660

47-
const yarnVersion = getYarnVersion();
61+
const yarnVersion = getYarnVersion(config.directory);
4862
const isYarnVersionSupported = yarnSupportsAudit(yarnVersion);
4963
if (!isYarnVersionSupported) {
5064
throw new Error(
51-
`Yarn ${yarnVersion} not supported, must be >=${MINIMUM_YARN_VERSION}`
65+
`Yarn ${yarnVersion} not supported, must be ^${MINIMUM_YARN_CLASSIC_VERSION} or >=${MINIMUM_YARN_BERRY_VERSION}`
5266
);
5367
}
68+
const isYarnClassic = yarnSupportsClassicAudit(yarnVersion);
69+
const yarnName = isYarnClassic ? `Yarn` : `Yarn Berry`;
5470

5571
switch (reportType) {
5672
case "full":
57-
console.log(blue, "Yarn audit report JSON:");
73+
console.log(blue, `${yarnName} audit report JSON:`);
5874
break;
5975
case "important":
60-
console.log(blue, "Yarn audit report results:");
76+
console.log(blue, `${yarnName} audit report results:`);
6177
break;
6278
case "summary":
63-
console.log(blue, "Yarn audit report summary:");
79+
console.log(blue, `${yarnName} audit report summary:`);
6480
break;
6581
default:
6682
throw new Error(
@@ -80,21 +96,33 @@ async function audit(config, reporter = reportAudit) {
8096
};
8197
break;
8298
case "important":
83-
printAuditData = ({ type, data }) => {
84-
if (
85-
(type === "auditAdvisory" && levels[data.advisory.severity]) ||
86-
type === "auditSummary"
87-
) {
88-
printJson(data);
89-
}
90-
};
99+
if (isYarnClassic) {
100+
printAuditData = ({ type, data }) => {
101+
if (
102+
(type === "auditAdvisory" && levels[data.advisory.severity]) ||
103+
type === "auditSummary"
104+
) {
105+
printJson(data);
106+
}
107+
};
108+
} else {
109+
printAuditData = ({ metadata }) => {
110+
printJson(metadata);
111+
};
112+
}
91113
break;
92114
case "summary":
93-
printAuditData = ({ type, data }) => {
94-
if (type === "auditSummary") {
95-
printJson(data);
96-
}
97-
};
115+
if (isYarnClassic) {
116+
printAuditData = ({ type, data }) => {
117+
if (type === "auditSummary") {
118+
printJson(data);
119+
}
120+
};
121+
} else {
122+
printAuditData = ({ metadata }) => {
123+
printJson(metadata);
124+
};
125+
}
98126
break;
99127
default:
100128
throw new Error(
@@ -104,19 +132,27 @@ async function audit(config, reporter = reportAudit) {
104132

105133
function outListener(line) {
106134
try {
107-
const { type, data } = line;
108-
printAuditData(line);
135+
if (isYarnClassic) {
136+
const { type, data } = line;
137+
printAuditData(line);
109138

110-
if (type === "info" && data === "No lockfile found.") {
111-
missingLockFile = true;
112-
return;
113-
}
139+
if (type === "info" && data === "No lockfile found.") {
140+
missingLockFile = true;
141+
return;
142+
}
114143

115-
if (type !== "auditAdvisory") {
116-
return;
117-
}
144+
if (type !== "auditAdvisory") {
145+
return;
146+
}
118147

119-
model.process(data.advisory);
148+
model.process(data.advisory);
149+
} else {
150+
printAuditData(line);
151+
152+
Object.values(line.advisories).forEach((advisory) => {
153+
model.process(advisory);
154+
});
155+
}
120156
} catch (err) {
121157
console.error(red, `ERROR: Cannot JSONStream.parse response:`);
122158
console.error(line);
@@ -133,7 +169,9 @@ async function audit(config, reporter = reportAudit) {
133169
}
134170
}
135171
const options = { cwd: config.directory };
136-
const args = ["audit", "--json"];
172+
const args = isYarnClassic
173+
? ["audit", "--json"]
174+
: ["npm", "audit", "--all", "--recursive", "--json"];
137175
if (registry) {
138176
const auditRegistrySupported = yarnAuditSupportsRegistry(yarnVersion);
139177
if (auditRegistrySupported) {

test/yarn-auditer.js

+57
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const { expect } = require("chai");
2+
const childProcess = require("child_process");
23
const path = require("path");
4+
const semver = require("semver");
35
const audit = require("../lib/audit").bind(null, "yarn");
46
const Allowlist = require("../lib/allowlist");
57
const { summaryWithDefault } = require("./common");
@@ -27,6 +29,11 @@ function testDir(s) {
2729
return path.resolve(__dirname, s);
2830
}
2931

32+
const canRunYarnBerry = semver.gte(
33+
childProcess.execSync("node -v").toString().replace("\n", ""),
34+
"12.13.0"
35+
);
36+
3037
// To modify what slow times are, need to use
3138
// function() {} instead of () => {}
3239
describe("yarn-auditer", function testYarnAuditer() {
@@ -198,6 +205,56 @@ describe("yarn-auditer", function testYarnAuditer() {
198205
(_summary) => _summary
199206
);
200207
});
208+
(canRunYarnBerry ? it : it.skip)(
209+
"[Yarn Berry] reports important info with moderate severity",
210+
async () => {
211+
const summary = await audit(
212+
config({
213+
directory: testDir("yarn-berry-moderate"),
214+
levels: { moderate: true },
215+
"report-type": "important",
216+
}),
217+
(_summary) => _summary
218+
);
219+
expect(summary).to.eql(
220+
summaryWithDefault({
221+
failedLevelsFound: ["moderate"],
222+
advisoriesFound: [658],
223+
})
224+
);
225+
}
226+
);
227+
(canRunYarnBerry ? it : it.skip)(
228+
"[Yarn Berry] does not report moderate severity if it set to false",
229+
async () => {
230+
const summary = await audit(
231+
config({
232+
directory: testDir("yarn-berry-moderate"),
233+
levels: { moderate: false },
234+
}),
235+
(_summary) => _summary
236+
);
237+
expect(summary).to.eql(summaryWithDefault());
238+
}
239+
);
240+
(canRunYarnBerry ? it : it.skip)(
241+
"[Yarn Berry] ignores an advisory if it is allowlisted",
242+
async () => {
243+
const summary = await audit(
244+
config({
245+
directory: testDir("yarn-berry-moderate"),
246+
levels: { moderate: true },
247+
allowlist: new Allowlist([658]),
248+
}),
249+
(_summary) => _summary
250+
);
251+
expect(summary).to.eql(
252+
summaryWithDefault({
253+
allowlistedAdvisoriesFound: [658],
254+
})
255+
);
256+
}
257+
);
201258
// it('prints unexpected https://registry.yarnpkg.com 503 error message', () => {
202259
// const directory = testDir('yarn-503');
203260
// const errorMessagePath = path.resolve(directory, 'error-message');
15.6 KB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Rather than have a bunch of yarn-berry.cjs of different versions,
2+
// we can specify a single yarn-berry.cjs and require the file for each package.
3+
module.exports = require("../../../yarn-berry.cjs");

test/yarn-berry-low/.yarnrc.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
yarnPath: ".yarn/releases/yarn-berry.cjs"

test/yarn-berry-low/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Yarn Berry tests
2+
3+
When creating Yarn Berry tests, there are several files and folders that may generate that are not necessary for auditing using `yarn npm audit --all --recursive --json`.
4+
5+
- .pnp.js
6+
7+
- .yarn/cache
8+
9+
Consider manually deleting them before committing.
10+
11+
Also, the `.yarn/releases/yarn-berry.cjs` file in each project re-exports the `yarn-berry.cjs` file at the root of tests.
12+
Re-exporting the file reduces duplication and version mismatching for tests.
13+
Currently, this project is set up to use the latest version v2.4.0 (at the time of writing this, Dec 6th, 2020).

test/yarn-berry-low/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "audit-ci-yarn-berry-low-vulnerability",
3+
"description": "Test package.json with low vulnerability",
4+
"dependencies": {
5+
"micromatch": "2.3.0"
6+
}
7+
}

0 commit comments

Comments
 (0)