better-npm-audit
Version:
Reshape into a better npm audit for the community and encourage more people to include security audit into their process.
343 lines (342 loc) • 15.9 kB
JavaScript
;
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)
* @param {Array} columnsToInclude List of columns to include in audit results
* @return {Object} Processed vulnerabilities details
*/
function processAuditJson(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude) {
if (jsonBuffer === void 0) { jsonBuffer = ''; }
if (auditLevel === void 0) { auditLevel = 'info'; }
if (exceptionIds === void 0) { exceptionIds = []; }
if (exceptionModules === void 0) { exceptionModules = []; }
if (columnsToInclude === void 0) { columnsToInclude = []; }
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; });
}
var rowData = [
{ key: 'ID', value: cur.id.toString() },
{ key: 'Module', value: cur.module_name },
{ key: 'Title', value: cur.title },
{
key: 'Paths',
value: common_1.trimArray(cur.findings.reduce(function (a, c) { return __spreadArray(__spreadArray([], a), c.paths); }, []), MAX_PATHS_SIZE).join('\n'),
},
{ key: 'Severity', value: cur.severity },
{ key: 'URL', value: cur.url },
{ key: 'Ex.', value: isExcepted ? 'y' : 'n' },
]
.filter(function (_a) {
var key = _a.key;
return (columnsToInclude.length ? columnsToInclude.includes(key) : true);
})
.map(function (_a) {
var key = _a.key, value = _a.value;
return color_1.color(value, isExcepted ? '' : 'yellow', key === 'Severity' ? color_1.getSeverityBgColor(cur.severity) : undefined);
});
// Record this vulnerability into the report, and highlight it using yellow color if it's new
acc.report.push(rowData);
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; });
}
var rowData = [
{ key: 'ID', value: String(id) },
{ key: 'Module', value: vul.name },
{ key: 'Title', value: vul.title },
{ key: 'Paths', value: common_1.trimArray(lodash_get_1.default(cur, 'nodes', []).map(common_1.shortenNodePath), MAX_PATHS_SIZE).join('\n') },
{ key: 'Severity', value: vul.severity, bgColor: color_1.getSeverityBgColor(vul.severity) },
{ key: 'URL', value: vul.url },
{ key: 'Ex.', value: isExcepted ? 'y' : 'n' },
]
.filter(function (_a) {
var key = _a.key;
return (columnsToInclude.length ? columnsToInclude.includes(key) : true);
})
.map(function (_a) {
var key = _a.key, value = _a.value, bgColor = _a.bgColor;
return color_1.color(value, isExcepted ? '' : 'yellow', key === 'Severity' ? bgColor : undefined);
});
// Record this vulnerability into the report, and highlight it using yellow color if it's new
acc.report.push(rowData);
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;