-
Notifications
You must be signed in to change notification settings - Fork 41
/
yarn-auditer.js
139 lines (127 loc) · 4.48 KB
/
yarn-auditer.js
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
/*
* Copyright IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
const childProcess = require('child_process');
const spawn = require('cross-spawn');
const semver = require('semver');
const MINIMUM_YARN_VERSION = '1.12.3';
function getYarnVersion() {
const version = childProcess
.execSync('yarn -v')
.toString()
.replace('\n', '');
return version;
}
function yarnSupportsAudit(yarnVersion) {
return semver.gte(yarnVersion, MINIMUM_YARN_VERSION);
}
/**
* Audit your NPM project!
*
* @param {{report: boolean, whitelist: string[], advisories: string[], levels: { low: boolean, moderate: boolean, high: boolean, critical: boolean }}} config
* `report`: whether to show the NPM audit report in the console.
* `whitelist`: a list of packages that should not break the build if their vulnerability is found.
* `advisories`: a list of advisory ids that should not break the build if found.
* `levels`: the vulnerability levels to fail on, if `moderate` is set `true`, `high` and `critical` should be as well.
* @returns {Promise<none>} Returns nothing on resolve, `Error` on rejection.
*/
function audit(config) {
return new Promise((resolve, reject) => {
const yarnVersion = getYarnVersion();
const isYarnVersionSupported = yarnSupportsAudit(yarnVersion);
if (!isYarnVersionSupported) {
reject(
Error(
`Yarn ${yarnVersion} not supported, must be >=${MINIMUM_YARN_VERSION}`
)
);
}
const proc = spawn('yarn', ['audit', '--json']);
const { advisories, levels, report, whitelist } = config;
if (whitelist.length) {
console.log(`Modules to whitelist: ${whitelist.join(', ')}.`);
}
if (report) {
console.log('\x1b[36m%s\x1b[0m', 'Yarn audit report JSON-lines:');
}
const failedLevels = {
low: false,
moderate: false,
high: false,
critical: false,
};
const whitelistedModulesFound = [];
const whitelistedAdvisoriesFound = [];
let missingLockFile = false;
let bufferedOutput = '';
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', data => {
/** @type {{ type: string, data: any }} */
bufferedOutput += data;
});
proc.stderr.setEncoding('utf8');
proc.stderr.on('data', jsonl => {
/** @type {{ type: string, data: any }} */
const errorLine = JSON.parse(jsonl);
if (errorLine.type === 'error') {
reject(Error(errorLine.data));
}
});
proc.on('close', () => {
bufferedOutput
.split('\n')
.filter(line => line.trim().length > 0)
.forEach(jsonBlob => {
const auditLine = JSON.parse(jsonBlob);
const { type, data } = auditLine;
if (report) {
console.log(JSON.stringify(auditLine, null, 2));
}
if (type === 'auditAdvisory') {
const { id, module_name: moduleName, severity } = data.advisory;
if (levels[severity]) {
if (whitelist.some(m => m === moduleName)) {
whitelistedModulesFound.push(moduleName);
} else if (advisories.some(a => +a === id)) {
whitelistedAdvisoriesFound.push(id);
} else {
failedLevels[severity] = true;
}
}
} else if (type === 'info' && data === 'No lockfile found.') {
missingLockFile = true;
}
});
if (missingLockFile) {
console.warn(
'\x1b[33m%s\x1b[0m',
'No yarn.lock file. This does not affect auditing, but it may be a mistake.'
);
}
if (whitelistedModulesFound.length) {
const found = whitelistedModulesFound.join(', ');
const msg = `Vulnerable whitelisted modules found: ${found}.`;
console.warn('\x1b[33m%s\x1b[0m', msg);
}
if (whitelistedAdvisoriesFound.length) {
const found = whitelistedAdvisoriesFound.join(', ');
const msg = `Vulnerable whitelisted advisories found: ${found}.`;
console.warn('\x1b[33m%s\x1b[0m', msg);
}
// Get the levels that have failed by filtering the keys with true values
const failedLevelsFound = Object.keys(failedLevels)
.filter(l => failedLevels[l])
.join(', ');
// If any of the levels have been failed
if (failedLevelsFound) {
const err = `Failed security audit due to ${failedLevelsFound} vulnerabilities.`;
reject(Error(err));
} else {
resolve();
}
});
});
}
module.exports = { audit };