scripts/audit-deps.js (145 lines of code) (raw):
#!/usr/bin/env node
// This nodejs script loads the .nsprc's "exceptions" list (as `nsp check` used to support) and
// and then filters the output of `npm audit --json` to check if any of the security advisories
// detected should be a blocking issue and force the CI job to fail.
//
// We can remove this script if/once npm audit will support this feature natively
// (See https://github.com/npm/npm/issues/20565).
import shell from 'shelljs';
import stripJsonComments from 'strip-json-comments';
const npmVersion = parseInt(
shell.exec('npm --version', { silent: true }).stdout.split('.')[0],
10,
);
const npmCmd = npmVersion >= 6 ? 'npm' : 'npx npm@latest';
if (npmCmd.startsWith('npx') && !shell.which('npx')) {
shell.echo('Sorry, this script requires npm >= 6 or npx installed globally');
shell.exit(1);
}
if (!shell.test('-f', 'package-lock.json')) {
console.log('audit-deps is generating the missing package-lock.json file');
shell.exec(`${npmCmd} i --package-lock-only`);
}
// Collect audit results and split them into blocking and ignored issues.
function getNpmAuditJSON() {
const res = shell.exec(`${npmCmd} audit --json`, { silent: true });
if (res.code !== 0) {
try {
return JSON.parse(res.stdout);
} catch (err) {
console.error('Error parsing npm audit output:', res.stdout);
throw err;
}
}
// npm audit didn't found any security advisories.
return null;
}
const blockingIssues = [];
const ignoredIssues = [];
let auditReport = getNpmAuditJSON();
if (auditReport) {
const cmdres = shell.cat('.nsprc');
const { exceptions } = JSON.parse(stripJsonComments(cmdres.stdout));
if (auditReport.error) {
if (auditReport.error.code === 'ENETUNREACH') {
console.log(
'npm was not able to reach the api endpoint:',
auditReport.error.summary,
);
console.log('Retrying...');
auditReport = getNpmAuditJSON();
}
// If the error code is not ENETUNREACH or it fails again after a single retry
// just log the audit error and exit with error code 2.
if (auditReport.error) {
console.error('npm audit error:', auditReport.error);
process.exit(2);
}
}
if (auditReport.auditReportVersion > 2) {
// Throw a more clear error when a new format that this script does not expect
// has been introduced.
console.error(
'ERROR: npm audit JSON is using a new format not yet supported.',
'\nPlease file a bug in the github repository and attach the following JSON data sample to it:',
`\n\n${JSON.stringify(auditReport, null, 2)}`,
);
} else if (auditReport.auditReportVersion === 2) {
// New npm audit json format introduced in npm v8.
for (const vulnerablePackage of Object.keys(auditReport.vulnerabilities)) {
const item = auditReport.vulnerabilities[vulnerablePackage];
// `item.via` can be either objects or (string) names of vulnerable
// packages in the audit json report. We need to normalize the data so
// that we always deal with a list of objects.
item.via = item.via.reduce((acc, via) => {
const addAdvisoryDetails = (entries, newEntry) => {
if (entries.some((entry) => entry.url === newEntry.url)) {
// The advisory url is already listed, no need to add a new entry.
return;
}
entries.push(newEntry);
};
if (typeof via === 'string') {
// Resolve the actual security advisory details recursively.
const recursivelyResolveVia = (currVia) => {
const resolvedVia = auditReport.vulnerabilities[currVia].via;
for (const viaEntry of resolvedVia) {
if (typeof viaEntry === 'string') {
recursivelyResolveVia(viaEntry);
} else {
addAdvisoryDetails(acc, viaEntry);
}
}
};
recursivelyResolveVia(via);
} else {
addAdvisoryDetails(acc, via);
}
return acc;
}, []);
if (item.via.every((via) => exceptions.includes(via.url))) {
ignoredIssues.push(item);
continue;
}
blockingIssues.push(item);
}
} else {
// Old npm audit json format for npm versions < npm v8
for (const advId of Object.keys(auditReport.advisories)) {
const adv = auditReport.advisories[advId];
if (exceptions.includes(adv.url)) {
ignoredIssues.push(adv);
continue;
}
blockingIssues.push(adv);
}
}
}
// Reporting.
function formatAdvisoryV1(adv) {
function formatFinding(desc) {
return `${desc.version}, paths: ${desc.paths.join(', ')}`;
}
const findings = adv.findings
.map(formatFinding)
.map((msg) => ` ${msg}`)
.join('\n');
return `${adv.module_name} (${adv.url}):\n${findings}`;
}
function formatAdvisoryV2(adv) {
function formatVia(via) {
return `${via.url}\n ${via.dependency} ${via.range}\n ${via.title}`;
}
const entryVia = adv.via
.map(formatVia)
.map((msg) => ` ${msg}`)
.join('\n');
const fixAvailable = Boolean(adv.fixAvailable);
const entryDetails = `isDirect: ${adv.isDirect}, severity: ${adv.severity}, fixAvailable: ${fixAvailable}`;
return `${adv.name} (${entryDetails}):\n${entryVia}`;
}
function formatAdvisory(adv) {
return auditReport.auditReportVersion === 2
? formatAdvisoryV2(adv)
: formatAdvisoryV1(adv);
}
if (ignoredIssues.length > 0) {
console.log(
'\n== audit-deps: ignored security issues (based on .nsprc exceptions)\n',
);
for (const adv of ignoredIssues) {
console.log(formatAdvisory(adv));
}
}
if (blockingIssues.length > 0) {
console.log('\n== audit-deps: blocking security issues\n');
for (const adv of blockingIssues) {
console.log(formatAdvisory(adv));
}
// Exit with error if blocking security issues has been found.
process.exit(1);
}