UNPKG

better-npm-audit

Version:

Reshape into a better npm audit for the community and encourage more people to include security audit into their process.

320 lines (319 loc) 15 kB
"use strict"; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.handleUnusedExceptions = exports.processExceptions = exports.getExceptionsIds = exports.processAuditJson = exports.validateV7Vulnerability = exports.validateV6Vulnerability = exports.mapLevelToNumber = void 0; var lodash_get_1 = __importDefault(require("lodash.get")); var common_1 = require("./common"); var color_1 = require("./color"); var print_1 = require("./print"); var date_1 = require("./date"); var MAX_PATHS_SIZE = 5; /** * Converts an audit level to a numeric value * @param {String} auditLevel Audit level * @return {Number} Numeric level: the higher the number, the more severe it is */ function mapLevelToNumber(auditLevel) { switch (auditLevel) { case 'info': return 0; case 'low': return 1; case 'moderate': return 2; case 'high': return 3; case 'critical': return 4; default: return 0; } } exports.mapLevelToNumber = mapLevelToNumber; /** * Validate if the vulnerability should be excepted * @param {Object} vulnerability NPM v6 audit report's vulnerability * @param {Array} exceptionIds Exception IDs * @return {Object} Validation result */ function validateV6Vulnerability(vulnerability, exceptionIds) { return exceptionIds.reduce(function (acc, id) { // check if ID matches if (id === String(vulnerability.id)) { return { isExcepted: true, usedExceptionKey: id }; } // check if any of the CVEs matches if (Array.isArray(vulnerability.cves) && vulnerability.cves.includes(id)) { return { isExcepted: true, usedExceptionKey: id }; } // check if the CWE matches if (vulnerability.cwe === id) { return { isExcepted: true, usedExceptionKey: id }; } // check if the URL matches if (vulnerability.url && vulnerability.url.includes(id)) { return { isExcepted: true, usedExceptionKey: id }; } return acc; }, { isExcepted: false, usedExceptionKey: '', }); } exports.validateV6Vulnerability = validateV6Vulnerability; /** * Validate if the vulnerability should be excepted * @param {Object} vulnerability NPM v7 audit report's vulnerability * @param {Array} exceptionIds Exception IDs * @return {Object} Validation result */ function validateV7Vulnerability(vulnerability, exceptionIds) { return exceptionIds.reduce(function (acc, id) { // check if ID matches if (id === String(vulnerability.source)) { return { isExcepted: true, usedExceptionKey: id }; } // check if the URL matches if (vulnerability.url && vulnerability.url.includes(id)) { return { isExcepted: true, usedExceptionKey: id }; } return acc; }, { isExcepted: false, usedExceptionKey: '', }); } exports.validateV7Vulnerability = validateV7Vulnerability; /** * Analyze the JSON string buffer * @param {String} jsonBuffer NPM Audit JSON string buffer * @param {String} auditLevel User's target audit level * @param {Array} exceptionIds Exception IDs (ID to be ignored) * @param {Array} exceptionModules Exception modules (modules to be ignored) * @return {Object} Processed vulnerabilities details */ function processAuditJson(jsonBuffer, auditLevel, exceptionIds, exceptionModules) { if (jsonBuffer === void 0) { jsonBuffer = ''; } if (auditLevel === void 0) { auditLevel = 'info'; } if (exceptionIds === void 0) { exceptionIds = []; } if (exceptionModules === void 0) { exceptionModules = []; } if (!common_1.isJsonString(jsonBuffer)) { return { unhandledIds: [], vulnerabilityIds: [], vulnerabilityModules: [], unusedExceptionIds: exceptionIds, unusedExceptionModules: exceptionModules, report: [], failed: true, }; } // NPM v6 uses `advisories` // NPM v7 uses `vulnerabilities` // Refer to the `test/__mocks__` folder for some sample mockups var _a = JSON.parse(jsonBuffer), advisories = _a.advisories, vulnerabilities = _a.vulnerabilities; // NPM v6 handling if (advisories) { return Object.values(advisories).reduce(function (acc, cur) { var shouldAudit = mapLevelToNumber(cur.severity) >= mapLevelToNumber(auditLevel); var _a = validateV6Vulnerability(cur, exceptionIds), isIdExcepted = _a.isExcepted, usedExceptionKey = _a.usedExceptionKey; var isModuleExcepted = exceptionModules.includes(cur.module_name); var isExcepted = isIdExcepted || isModuleExcepted; // Record used exception ID/module if (isIdExcepted) { acc.unusedExceptionIds = acc.unusedExceptionIds.filter(function (id) { return id !== usedExceptionKey; }); } if (isModuleExcepted) { acc.unusedExceptionModules = acc.unusedExceptionModules.filter(function (module) { return module !== cur.module_name; }); } // Record this vulnerability into the report, and highlight it using yellow color if it's new acc.report.push([ color_1.color(cur.id.toString(), isExcepted ? '' : 'yellow'), color_1.color(cur.module_name, isExcepted ? '' : 'yellow'), color_1.color(cur.title, isExcepted ? '' : 'yellow'), color_1.color(common_1.trimArray(cur.findings.reduce(function (a, c) { return __spreadArray(__spreadArray([], a), c.paths); }, []), MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'), color_1.color(cur.severity, isExcepted ? '' : 'yellow', color_1.getSeverityBgColor(cur.severity)), color_1.color(cur.url, isExcepted ? '' : 'yellow'), isExcepted ? 'y' : color_1.color('n', 'yellow'), ]); acc.vulnerabilityIds.push(cur.id.toString()); if (!acc.vulnerabilityModules.includes(cur.module_name)) { acc.vulnerabilityModules.push(cur.module_name); } // Found unhandled vulnerabilities if (shouldAudit && !isExcepted) { acc.unhandledIds.push(cur.id.toString()); } return acc; }, { unhandledIds: [], vulnerabilityIds: [], vulnerabilityModules: [], unusedExceptionIds: exceptionIds, unusedExceptionModules: exceptionModules, report: [], }); } // NPM v7 handling if (vulnerabilities) { return Object.values(vulnerabilities).reduce(function (acc, cur) { // Inside `via` array, its either the related module name or the vulnerability source object. lodash_get_1.default(cur, 'via', []).forEach(function (vul) { // The vulnerability ID is labeled as `source` var id = lodash_get_1.default(vul, 'source'); var moduleName = lodash_get_1.default(vul, 'name', ''); // Let's skip if ID is a string (module name), and only focus on the root vulnerabilities if (!id || typeof id === 'string' || typeof vul === 'string') { return; } var shouldAudit = mapLevelToNumber(vul.severity) >= mapLevelToNumber(auditLevel); var _a = validateV7Vulnerability(vul, exceptionIds), isIdExcepted = _a.isExcepted, usedExceptionKey = _a.usedExceptionKey; var isModuleExcepted = exceptionModules.includes(moduleName); var isExcepted = isIdExcepted || isModuleExcepted; // Record used exception ID/module if (isIdExcepted) { acc.unusedExceptionIds = acc.unusedExceptionIds.filter(function (id) { return id !== usedExceptionKey; }); } if (isModuleExcepted) { acc.unusedExceptionModules = acc.unusedExceptionModules.filter(function (module) { return module !== moduleName; }); } // Record this vulnerability into the report, and highlight it using yellow color if it's new acc.report.push([ color_1.color(String(id), isExcepted ? '' : 'yellow'), color_1.color(vul.name, isExcepted ? '' : 'yellow'), color_1.color(vul.title, isExcepted ? '' : 'yellow'), color_1.color(common_1.trimArray(lodash_get_1.default(cur, 'nodes', []).map(common_1.shortenNodePath), MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'), color_1.color(vul.severity, isExcepted ? '' : 'yellow', color_1.getSeverityBgColor(vul.severity)), color_1.color(vul.url, isExcepted ? '' : 'yellow'), isExcepted ? 'y' : color_1.color('n', 'yellow'), ]); acc.vulnerabilityIds.push(String(id)); if (!acc.vulnerabilityModules.includes(moduleName)) { acc.vulnerabilityModules.push(moduleName); } // Found unhandled vulnerabilities if (shouldAudit && !isExcepted) { acc.unhandledIds.push(String(id)); } }); return acc; }, { unhandledIds: [], vulnerabilityIds: [], vulnerabilityModules: [], unusedExceptionIds: exceptionIds, unusedExceptionModules: exceptionModules, report: [], }); } return { unhandledIds: [], vulnerabilityIds: [], vulnerabilityModules: [], unusedExceptionIds: exceptionIds, unusedExceptionModules: exceptionModules, report: [], failed: true, }; } exports.processAuditJson = processAuditJson; /** * Process all exceptions and return a list of exception IDs * @param {Object | Boolean} nsprc File content from `.nsprc` * @param {Array} cmdExceptions Exceptions passed in via command line * @return {Array} List of found vulnerabilities */ function getExceptionsIds(nsprc, cmdExceptions) { if (cmdExceptions === void 0) { cmdExceptions = []; } // If file does not exists if (!nsprc || typeof nsprc !== 'object') { // If there are exceptions passed in from command line if (cmdExceptions.length) { // Display simple info console.info("Exception IDs: " + cmdExceptions.join(', ')); return cmdExceptions; } return []; } // Process the content of the file along with the command line exceptions var _a = processExceptions(nsprc, cmdExceptions), exceptionIds = _a.exceptionIds, report = _a.report; print_1.printExceptionReport(report); return exceptionIds; } exports.getExceptionsIds = getExceptionsIds; /** * Filter the given list in the `.nsprc` file for valid exceptions * @param {Object} nsprc The nsprc file content, contains exception info * @param {Array} cmdExceptions Exceptions passed in via command line * @return {Object} Processed vulnerabilities details */ function processExceptions(nsprc, cmdExceptions) { if (cmdExceptions === void 0) { cmdExceptions = []; } return Object.entries(nsprc).reduce(function (acc, _a) { var id = _a[0], details = _a[1]; var isActive = Boolean(lodash_get_1.default(details, 'active', true)); // default to true var notes = typeof details === 'string' ? details : lodash_get_1.default(details, 'notes', ''); var _b = date_1.analyzeExpiry(lodash_get_1.default(details, 'expiry')), valid = _b.valid, expired = _b.expired, years = _b.years; // Color the status accordingly var status = color_1.color('active', 'green'); if (expired) { status = color_1.color('expired', 'red'); } else if (!valid) { status = color_1.color('invalid', 'red'); } else if (!isActive) { status = color_1.color('inactive', 'yellow'); } // Color the date accordingly var expiryDate = lodash_get_1.default(details, 'expiry') ? new Date(lodash_get_1.default(details, 'expiry')).toUTCString() : ''; // If it was expired for more than 5 years ago, warn by coloring the date in red if (years && years <= -5) { expiryDate = color_1.color(expiryDate, 'red'); } else if (years && years <= -1) { expiryDate = color_1.color(expiryDate, 'yellow'); } acc.report.push([id, status, expiryDate, notes]); if (isActive && !expired) { acc.exceptionIds.push(id); } return acc; }, { exceptionIds: cmdExceptions, report: cmdExceptions.map(function (id) { return [String(id), color_1.color('active', 'green'), '', '']; }), }); } exports.processExceptions = processExceptions; /** * Handle unused exceptions from user: console log them * @param {Array} unusedExceptionIds List of unused exception IDs * @param {Array} unusedExceptionModules List of unused exception module names */ function handleUnusedExceptions(unusedExceptionIds, unusedExceptionModules) { var cleanedUnusedExceptionIds = unusedExceptionIds.filter(Boolean); var cleanedUnusedExceptionModules = unusedExceptionModules.filter(Boolean); var message = [ cleanedUnusedExceptionIds.length && cleanedUnusedExceptionIds.length + " of the excluded vulnerabilities did not match any of the found vulnerabilities: " + cleanedUnusedExceptionIds.join(', ') + ".", cleanedUnusedExceptionIds.length && (cleanedUnusedExceptionIds.length > 1 ? 'They' : 'It') + " can be removed from the .nsprc file or --exclude -x flags.", cleanedUnusedExceptionModules.length && cleanedUnusedExceptionModules.length + " of the ignored modules did not match any of the found vulnerabilities: " + cleanedUnusedExceptionModules.join(', ') + ".", cleanedUnusedExceptionModules.length && (cleanedUnusedExceptionModules.length > 1 ? 'They' : 'It') + " can be removed from the --module-ignore -m flags.", ] .filter(Boolean) .join(' '); if (message) { console.warn(message); } } exports.handleUnusedExceptions = handleUnusedExceptions;