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); }