diff --git a/.README/all_good.png b/.README/all_good.png new file mode 100644 index 0000000..d6d1d8e Binary files /dev/null and b/.README/all_good.png differ diff --git a/.README/exceptions_table.png b/.README/exceptions_table.png new file mode 100644 index 0000000..73818c9 Binary files /dev/null and b/.README/exceptions_table.png differ diff --git a/.README/highlighted_exceptions.png b/.README/highlighted_exceptions.png new file mode 100644 index 0000000..886a40b Binary files /dev/null and b/.README/highlighted_exceptions.png differ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b9e995b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: jeemok +custom: https://www.buymeacoffee.com/jeemok \ No newline at end of file diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index e62dec3..e36440f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -27,4 +27,5 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint - - run: npm run test \ No newline at end of file + - run: npm run test + - run: npm run audit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d458e..0707497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,54 @@ +## Next: 2.0.0-rc + +### Notable changes + +* Simplified the workflow and improved overall performance by running less. + * Reduce code size and package size in half (! +* Added own table display for security report +* Added table overview of exceptions from `.nsprc` file + +### Breaking changes + +* Renamed `ignore` field to `active` in `.nsprc` file for better clarity. +* Renamed `reason` field to `notes` in `.nsprc` file for better clarity. +* Removed `--display-full` flag that was used to ignore the maximum display limit. Now with the summary table it would be unlikely to display large size of information. +* Removed `--display-notes` flag that was used for displaying exception notes. Now it is included in the exceptions table. + +### Others + +* Removed logging of flags used +* Added npm audit into CI pipeline +* Added FUNDING.md +* Updated README.md + +## Closed issues + +* # []() + +--- + ## 1.12.0 (June 18, 2021) -* [Display warning when exceptionIds are unused](https://github.com/jeemok/better-npm-audit/pull/38) +* [#38](https://github.com/jeemok/better-npm-audit/pull/38) Display warning when `exceptionIds` are unused ## 1.11.2 (June 11, 2021) -* [Fixed security CVE-2020-28469: Bump glob-parent from 5.1.1 to 5.1.2](https://github.com/jeemok/better-npm-audit/pull/37) +* [#37](https://github.com/jeemok/better-npm-audit/pull/37) Fixed security CVE-2020-28469: Bump glob-parent from 5.1.1 to 5.1.2 ## 1.11.1 (June 11, 2021) -* Updated README +* Updated `README.md` ## 1.11.0 (June 11, 2021) -* [Added environment variable support `process.env.NPM_CONFIG_AUDIT_LEVEL` to set the audit level](https://github.com/jeemok/better-npm-audit/pull/36) +* [#36](https://github.com/jeemok/better-npm-audit/pull/36) Added environment variable support `process.env.NPM_CONFIG_AUDIT_LEVEL` to set the audit level ## 1.10.1 (June 7, 2021) * Updated `--full` flag logging from `[full log mode enabled]` to `[report display limit disabled]` -* [Added new flag `--display-notes` to display reasons for the exceptions](https://github.com/jeemok/better-npm-audit/issues/32) +* [#32](https://github.com/jeemok/better-npm-audit/issues/32) Added new flag `--display-notes` to display reasons for the exceptions ## 1.9.3 (June 6, 2021) -### Features - -* [Added CHANGELOG.md](https://github.com/jeemok/better-npm-audit/issues/31) +* [#31](https://github.com/jeemok/better-npm-audit/issues/31) Added `CHANGELOG.md` * Updated `README.md` \ No newline at end of file diff --git a/README.md b/README.md index d5df53c..cdf7e62 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,21 @@ or ## Usage -### `package.json` +### Run global + +```bash +better-npm-audit audit +``` + +### Run with exceptions + +Demo of table displaying the security report + +Unhandled or new exceptions will be highlighted: + +Demo of table displaying the security report + +### Add into package scripts ```JSON { @@ -43,10 +57,10 @@ or } ``` -### Run global +Now you can run locally or in your CI pipeline: ```bash -better-npm-audit audit +npm run audit ```
@@ -58,8 +72,6 @@ better-npm-audit audit | `--level` | `-l` | Same as the original `--audit-level` flag | | `--production` | `-p` | Skip checking `devDependencies` | | `--ignore` | `-i` | For skipping certain advisories | -| `--full` | `-f` | Display full audit report. There is a character limit set to the audit report to prevent overwhelming details to the console. | -| `--display-notes` | `-d` | Display the reasons of matched exceptions from `.nsprc` file. |
@@ -67,7 +79,7 @@ better-npm-audit audit | Variable | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| `process.env.NPM_CONFIG_AUDIT_LEVEL` | Used in setting the audit level.
*Note: this will be disregard if the audit level flag is passed onto the command.* | +| `NPM_CONFIG_AUDIT_LEVEL` | Used in setting the audit level.
*Note: this will be disregard if the audit level flag is passed onto the command.* |
@@ -78,153 +90,23 @@ You may add a file `.nsprc` to your project root directory to manage the excepti ```json { "1337": { - "ignore": true, - "reason": "Ignored since we don't use xxx method", + "active": true, + "notes": "Ignored since we don't use xxx method", "expiry": 1615462134681 }, "4501": { - "ignore": false, - "reason": "Ignored since we don't use xxx method" + "active": false, + "notes": "Ignored since we don't use xxx method" }, "980": "Ignored since we don't use xxx method", - "Note": "Any non number key will be ignored" + "Note": "Any non number key will not be excepted" } ``` -
- -## Examples - -**NPM v6** - -Running `node node_modules/better-npm-audit audit` with vulnerabilities, will receive the error: - -```bash -2 vulnerabilities found. Node security advisories: 118,577 -``` - -Added the ignore flags `node node_modules/better-npm-audit audit -i 118,577` and rerun: - -```bash -Executing script: audit - -to be executed: "node node_modules/better-npm-audit audit -i 118,577" -Exception Vulnerabilities IDs: [ '118', '577' ] -=== npm audit security report === - - - Manual Review - Some vulnerabilities require your attention to resolve - - Visit https://go.npm.me/audit-guide for additional guidance +When using `.nsprc` file, you will see this report display when it starts running: +Demo of table displaying a list of exceptions - High Regular Expression Denial of Service - - Package minimatch - - Patched in >=3.0.2 - - Dependency of semantic-ui - - Path semantic-ui > gulp > vinyl-fs > glob-stream > glob > - minimatch - - More info https://nodesecurity.io/advisories/118 - - - High Regular Expression Denial of Service - - Package minimatch - - Patched in >=3.0.2 - - Dependency of semantic-ui - - Path semantic-ui > gulp > vinyl-fs > glob-watcher > gaze > - globule > minimatch - - More info https://nodesecurity.io/advisories/118 - - - Low Prototype Pollution - - Package lodash - - Patched in >=4.17.5 - - Dependency of semantic-ui - - Path semantic-ui > gulp > vinyl-fs > glob-watcher > gaze > - globule > lodash - - More info https://nodesecurity.io/advisories/577 - -found 5 vulnerabilities (1 low, 4 high) in 30441 scanned packages - 5 vulnerabilities require manual review. See the full report for details. - -🤝 All good -``` - -**NPM v7** - -```bash -# npm audit report - -bl <=1.2.2 || 2.0.1 - 2.2.0 || 3.0.0 || 4.0.0 - 4.0.2 -Severity: high -Remote Memory Exposure - https://npmjs.com/advisories/1555 -fix available via `npm audit fix` -node_modules/bl - -dot-prop <4.2.1 || >=5.0.0 <5.1.1 -Severity: high -Prototype Pollution - https://npmjs.com/advisories/1213 -fix available via `npm audit fix` -node_modules/dot-prop - -mem <4.0.0 -Denial of Service - https://npmjs.com/advisories/1084 -fix available via `npm audit fix` -node_modules/loopback-connector-rest/node_modules/mem - os-locale 2.0.0 - 3.0.0 - Depends on vulnerable versions of mem - node_modules/loopback-connector-rest/node_modules/os-locale - strong-globalize 2.8.4 || 2.10.0 - 4.1.1 - Depends on vulnerable versions of os-locale - node_modules/loopback-connector-rest/node_modules/strong-globalize - -swagger-ui <=3.20.8 -Severity: moderate -Reverse Tabnapping - https://npmjs.com/advisories/975 -Cross-Site Scripting - https://npmjs.com/advisories/976 -Cross-Site Scripting - https://npmjs.com/advisories/985 -fix available via `npm audit fix --force` -Will install loopback-component-explorer@2.7.0, which is a breaking change -node_modules/swagger-ui - loopback-component-explorer >=3.0.0 - Depends on vulnerable versions of swagger-ui - node_modules/loopback-component-explorer - -yargs-parser <=13.1.1 || 14.0.0 - 15.0.0 || 16.0.0 - 18.1.1 -Prototype Pollution - https://npmjs.com/advisories/1500 -fix available via `npm audit fix` -node_modules/mocha/node_modules/yargs-parser -node_modules/yargs-unparser/node_modules/yargs-parser - mocha 1.21.5 - 6.2.2 || 7.0.0-esm1 - 7.1.0 - Depends on vulnerable versions of mkdirp - Depends on vulnerable versions of yargs-parser - Depends on vulnerable versions of yargs-unparser - node_modules/mocha - yargs 4.0.0-alpha1 - 12.0.5 || 14.1.0 || 15.0.0 - 15.2.0 - Depends on vulnerable versions of yargs-parser - node_modules/yargs-unparser/node_modules/yargs - yargs-unparser 1.1.0 - 1.5.0 - Depends on vulnerable versions of yargs - node_modules/yargs-unparser - -18 vulnerabilities (14 low, 2 moderate, 2 high) -```
diff --git a/index.js b/index.js index 4598fbc..78cf46a 100755 --- a/index.js +++ b/index.js @@ -8,125 +8,75 @@ const get = require('lodash.get'); const program = require('commander'); const { exec } = require('child_process'); const packageJson = require('./package'); -const { isWholeNumber, mapLevelToNumber, getRawVulnerabilities, filterValidException, filterExceptions } = require('./utils/common'); + +const { getExceptionsIds, processAuditJson } = require('./utils/vulnerability'); +const { printSecurityReport } = require('./utils/print'); +const { isWholeNumber } = require('./utils/common'); const { readFile } = require('./utils/file'); -const consoleUtil = require('./utils/console'); -const EXCEPTION_FILE_PATH = '.nsprc'; -const BASE_COMMAND = 'npm audit'; -const SEPARATOR = ','; -const DEFAULT_MESSSAGE_LIMIT = 100000; // characters const MAX_BUFFER_SIZE = 1024 * 1000 * 50; // 50 MB -const RESPONSE_MESSAGE = { - SUCCESS: '🤝 All good!', - LOGS_EXCEEDED: '[MAXIMUM EXCEEDED] Logs exceeded the maximum length limit. Add the flag `-f` to see the full audit logs.', -}; /** - * Handle the analyzed result and log display - * @param {Array} vulnerabilities List of found vulnerabilities - * @param {String} logData Logs - * @param {Object} configs Configurations - * @param {Array} unusedExceptionIds List of unused exceptionsIds. + * Process and analyze the NPM audit JSON + * @param {String} jsonBuffer NPM audit stringified JSON payload + * @param {Number} auditLevel The level of vulnerabilities we care about + * @param {Array} exceptionIds List of vulnerability IDs to ignore + * @return {undefined} */ -function handleFinish(vulnerabilities, logData = '', configs = {}, unusedExceptionIds = []) { - const { - displayFullLog = false, - maxLength = DEFAULT_MESSSAGE_LIMIT, - } = configs; - - let toDisplay = logData.substring(0, maxLength); - - // Display an additional information if we not displaying the full logs - if (toDisplay.length < logData.length) { - toDisplay += '\n\n'; - toDisplay += '...'; - toDisplay += '\n\n'; - toDisplay += RESPONSE_MESSAGE.LOGS_EXCEEDED; - toDisplay += '\n\n'; +function handleFinish(jsonBuffer, auditLevel, exceptionIds) { + const { unhandledIds, vulnerabilityIds, report } = processAuditJson(jsonBuffer, auditLevel, exceptionIds); + + // If unable to process the audit JSON + if (!Array.isArray(unhandledIds) || !Array.isArray(vulnerabilityIds) || !Array.isArray(report)) { + console.error('Unable to process the JSON buffer string.'); + // Exit failed + process.exit(1); + return; } - if (displayFullLog) { - console.info(logData); - } else { - console.info(toDisplay); + // Print the security report + if (report.length) { + printSecurityReport(report); } - if (unusedExceptionIds.length > 0) { + // Grab any un-filtered vulnerabilities at the appropriate level + const unusedExceptionIds = exceptionIds.filter(id => !vulnerabilityIds.includes(id)); + + // Display the unused exceptionId's + if (unusedExceptionIds.length) { // eslint-disable-next-line max-len - const message = `${unusedExceptionIds.length} vulnerabilities where ignored but did not result in a vulnerabilities: ${unusedExceptionIds}. They can be removed from the .nsprc file or -ignore -i flags.`; - consoleUtil.info(message); + const message = `${unusedExceptionIds.length} vulnerabilities where ignored but did not result in a vulnerabilities: ${unusedExceptionIds.join(', ')}. They can be removed from the .nsprc file or -ignore -i flags.`; + console.warn(message); } - // Display the error if found vulnerabilities - if (vulnerabilities.length > 0) { - consoleUtil.error(`${vulnerabilities.length} vulnerabilities found. Node security advisories: ${vulnerabilities}`); - + // Display the found unhandled vulnerabilities + if (unhandledIds.length) { + console.error(`${unhandledIds.length} vulnerabilities found. Node security advisories: ${unhandledIds.join(', ')}`); // Exit failed process.exit(1); } else { // Happy happy, joy joy - consoleUtil.info(RESPONSE_MESSAGE.SUCCESS); + console.info('🤝 All good!'); } } /** - * Re-runs the audit in human readable form - * @param {String} auditCommand The NPM audit command to use (with flags) - * @param {Boolean} displayFullLog True if full log should be displayed in the case of no vulnerabilities - * @param {Array} vulnerabilities List of vulnerabilities - * @param {Array} unusedExceptionIds List of unused exceptionsIds. - */ -function auditLog(auditCommand, displayFullLog, vulnerabilities, unusedExceptionIds) { - // Execute `npm audit` command again, but this time we don't use the JSON flag - const audit = exec(auditCommand); - - // Set a temporary string - // Note: collect all buffers' data before displaying it later to avoid unintentional line breaking in the report display - let bufferData = ''; - - audit.stdout.on('data', data => bufferData += data); - - // Once the stdout has completed - audit.stderr.on('close', () => handleFinish(vulnerabilities, bufferData, { displayFullLog }, unusedExceptionIds)); - - // stderr - audit.stderr.on('data', console.error); -} - -/** - * Run the main Audit + * Run audit * @param {String} auditCommand The NPM audit command to use (with flags) * @param {Number} auditLevel The level of vulnerabilities we care about - * @param {Boolean} fullLog True if the full log should be displayed in the case of no vulnerabilities * @param {Array} exceptionIds List of vulnerability IDs to ignore */ -function audit(auditCommand, auditLevel, fullLog, exceptionIds) { - // Execute `npm audit` command to get the security report, taking into account - // any additional flags that have been passed through. Using the JSON flag - // to make this easier to process - // NOTE: Increase max buffer size from default 1MB +function audit(auditCommand, auditLevel, exceptionIds) { + // Increase the default max buffer size (1 MB) const audit = exec(`${auditCommand} --json`, { maxBuffer: MAX_BUFFER_SIZE }); // Grab the data in chunks and buffer it as we're unable to parse JSON straight from stdout let jsonBuffer = ''; - audit.stdout.on('data', data => (jsonBuffer += data)); - // Once the stdout has completed process the output - audit.stderr.on('close', () => { - // Grab any un-filtered vulnerabilities at the appropriate level - const rawVulnerabilities = getRawVulnerabilities(jsonBuffer, auditLevel); - - // filter out exceptions - const vulnerabilities = filterExceptions(rawVulnerabilities, exceptionIds); - - // Display the unused exceptionId's - const exceptionsIdsAsArray = Array.isArray(exceptionIds) ? exceptionIds : [exceptionIds]; - const unusedExceptionIds = exceptionsIdsAsArray.filter(id => !rawVulnerabilities.includes(id)); + audit.stdout.on('data', data => (jsonBuffer += data)); - // Display the original audit logs - auditLog(auditCommand, fullLog, vulnerabilities, unusedExceptionIds); - }); + // Once the stdout has completed, process the output + audit.stderr.on('close', () => handleFinish(jsonBuffer, auditLevel, exceptionIds)); // stderr audit.stderr.on('data', console.error); @@ -137,49 +87,23 @@ function audit(auditCommand, auditLevel, fullLog, exceptionIds) { * @param {Object} options User's options or flags * @param {Function} fn The function to handle the inputs */ -function handleUserInput(options, fn) { - let auditCommand = BASE_COMMAND; - let auditLevel = 0; - let exceptionIds = []; - let displayFullLog = false; - - // Check `.nsprc` file for exceptions - const fileException = readFile(EXCEPTION_FILE_PATH); - const filteredExceptions = filterValidException(fileException); - if (fileException) { - exceptionIds = filteredExceptions.map(details => details.id); - } - // Check also if any exception IDs passed via command flags - if (options && options.ignore) { - const cmdExceptions = options.ignore.split(SEPARATOR).filter(isWholeNumber).map(Number); - exceptionIds = exceptionIds.concat(cmdExceptions); - } - if (Array.isArray(exceptionIds) && exceptionIds.length) { - consoleUtil.info(`Exception vulnerabilities ID(s): ${exceptionIds}`); - } - if (options && options.displayNotes && filteredExceptions.length) { - console.info(''); // Add some spacings - console.info('Exceptions notes:'); - console.info(''); - filteredExceptions.forEach(({ id, reason }) => console.info(`${id}: ${reason || 'n/a'}`)); - console.info(''); - } +function handleAction(options, fn) { + // Generate NPM Audit command + const auditCommand = [ + 'npm audit', + // flags + get(options, 'production') ? '--production' : '', + ].join(' '); + // Taking the audit level from the command or environment variable - const level = get(options, 'level', process.env.NPM_CONFIG_AUDIT_LEVEL); - if (level) { - console.info(`[level: ${level}]`); - auditLevel = mapLevelToNumber(level); - } - if (options && options.production) { - console.info('[production mode enabled]'); - auditCommand += ' --production'; - } - if (options && options.full) { - console.info('[report display limit disabled]'); - displayFullLog = true; - } + const auditLevel = get(options, 'level', process.env.NPM_CONFIG_AUDIT_LEVEL) || 'info'; + + // Get the exceptions + const nsprc = readFile('.nsprc'); + const cmdExceptions = get(options, 'ignore', '').split(',').filter(isWholeNumber).map(Number); + const exceptionIds = getExceptionsIds(nsprc, cmdExceptions); - fn(auditCommand, auditLevel, displayFullLog, exceptionIds); + fn(auditCommand, auditLevel, exceptionIds); } program.version(packageJson.version); @@ -188,18 +112,13 @@ program .command('audit') .description('execute npm audit') .option('-i, --ignore ', 'Vulnerabilities ID(s) to ignore.') - .option('-f, --full', `Display complete audit report. Limit to ${DEFAULT_MESSSAGE_LIMIT} characters by default.`) .option('-l, --level ', 'The minimum audit level to validate.') .option('-p, --production', 'Skip checking devDependencies.') - .option('-d, --display-notes', 'Display exception notes.') - .action(userOptions => handleUserInput(userOptions, audit)); + .action(options => handleAction(options, audit)); program.parse(process.argv); module.exports = { handleFinish, - handleUserInput, - BASE_COMMAND, - SUCCESS_MESSAGE: RESPONSE_MESSAGE.SUCCESS, - LOGS_EXCEEDED_MESSAGE: RESPONSE_MESSAGE.LOGS_EXCEEDED, + handleAction, }; diff --git a/package-lock.json b/package-lock.json index 45efc10..b46cd40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -216,7 +216,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -246,8 +245,7 @@ "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, "balanced-match": { "version": "1.0.0", @@ -404,7 +402,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -412,8 +409,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "commander": { "version": "2.19.0", @@ -493,8 +489,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "enquirer": { "version": "2.3.6", @@ -711,8 +706,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", @@ -1018,14 +1012,7 @@ "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.get": { "version": "4.4.2", @@ -1035,8 +1022,7 @@ "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=" }, "log-symbols": { "version": "4.0.0", @@ -1239,8 +1225,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "randombytes": { "version": "2.1.0", @@ -1275,8 +1260,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "resolve-from": { "version": "4.0.0", @@ -1367,7 +1351,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -1377,8 +1360,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" } } }, @@ -1423,14 +1405,12 @@ } }, "table": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.6.0.tgz", - "integrity": "sha512-iZMtp5tUvcnAdtHpZTWLPF0M7AgiQsURR2DwmxnJwSy8I3+cY+ozzVvYha3BOLG2TB+L0CqjIz+91htuj6yCXg==", - "dev": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", "requires": { "ajv": "^8.0.1", "lodash.clonedeep": "^4.5.0", - "lodash.flatten": "^4.4.0", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.0", @@ -1438,10 +1418,9 @@ }, "dependencies": { "ajv": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz", - "integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==", - "dev": true, + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", + "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -1452,26 +1431,22 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1482,7 +1457,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "requires": { "ansi-regex": "^5.0.0" } @@ -1529,7 +1503,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index bcee1f7..19d95e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "better-npm-audit", - "version": "1.12.0", + "version": "2.0.0-rc", "author": "Jee Mok ", "description": "Made to allow skipping certain vulnerabilities, and any extra handling that are not supported by the default npm audit in the future.", "license": "MIT", @@ -31,13 +31,15 @@ }, "dependencies": { "commander": "^2.19.0", - "lodash.get": "^4.4.2" + "lodash.get": "^4.4.2", + "table": "^6.7.1" }, "engines": { "node": ">= 8.12" }, "scripts": { - "test": "mocha test/index.js", + "audit": "node . audit", + "test": "mocha test --recursive", "lint": "eslint ." }, "devDependencies": { diff --git a/test/__mocks__/nsprc.json b/test/__mocks__/nsprc.json new file mode 100644 index 0000000..8153510 --- /dev/null +++ b/test/__mocks__/nsprc.json @@ -0,0 +1,33 @@ +{ + "1213": "Ignored since we don't use xxx method", + "1084": { + "expiry": 1615462134681, + "active": false, + "notes": "Inactive package; consider replacing it." + }, + "1179": { + "expiry": 1615462134681, + "active": true + }, + "1556": { + "expiry": 1615462134681, + "active": true, + "notes": "Issue: https://github.com/jeemok/better-npm-audit/issues/28" + }, + "975": { + "expiry": 1615462134681 + }, + "976": { + "active": false + }, + "985": "", + "1651": { + "expiry": 1615462134681, + "notes": "This will be fixed by the maintainers by June 14" + }, + "1654": { + "expiry": 1640966400000 + }, + "2100": "Unused", + "Note": "personal note" +} \ No newline at end of file diff --git a/test/__mocks__/v7-json-buffer.json b/test/__mocks__/v7-json-buffer.json index 48d07f7..842513e 100644 --- a/test/__mocks__/v7-json-buffer.json +++ b/test/__mocks__/v7-json-buffer.json @@ -328,7 +328,7 @@ "dependency": "yargs-parser", "title": "Prototype Pollution", "url": "https://npmjs.com/advisories/1500", - "severity": "info", + "severity": "low", "range": "<13.1.2 || >=14.0.0 <15.0.1 || >=16.0.0 <18.1.2" } ], diff --git a/test/flags.js b/test/flags.js new file mode 100644 index 0000000..8838cc8 --- /dev/null +++ b/test/flags.js @@ -0,0 +1,186 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const { expect } = chai; + +const { handleAction } = require('../index'); + +describe('Flags', () => { + describe('default', () => { + it('should be able to handle default correctly', () => { + const callbackStub = sinon.stub(); + const options = {}; + + expect(callbackStub.called).to.equal(false); + handleAction(options, callbackStub); + expect(callbackStub.called).to.equal(true); + + const auditCommand = 'npm audit '; + const auditLevel = 'info'; + const exceptionIds = []; + expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds)).to.equal(true); + }); + }); + + describe('--ignore', () => { + it('should be able to pass exception IDs using the command flag smoothly', () => { + const callbackStub = sinon.stub(); + const consoleStub = sinon.stub(console, 'info'); + const options = { ignore: '1567,919' }; + const auditCommand = 'npm audit '; + const auditLevel = 'info'; + const exceptionIds = [1567, 919]; + + expect(callbackStub.called).to.equal(false); + handleAction(options, callbackStub); + expect(callbackStub.called).to.equal(true); + expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds)).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1567, 919')).to.equal(true); + + // with space + options.ignore = '1567, 1902'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, auditLevel, [1567, 1902])).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1567, 1902')).to.equal(true); + + // invalid exceptions + options.ignore = '1134,undefined,888'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, auditLevel, [1134, 888])).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1134, 888')).to.equal(true); + + // invalid NaN + options.ignore = '1134,NaN,3e,828'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, auditLevel, [1134, 828])).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1134, 828')).to.equal(true); + + // invalid decimals + options.ignore = '1199,29.41,628'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, auditLevel, [1199, 628])).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1199, 628')).to.equal(true); + + consoleStub.restore(); + }); + + it('should info log the vulnerabilities if it is only passed in command line', () => { + const callbackStub = sinon.stub(); + const consoleStub = sinon.stub(console, 'info'); + const options = { ignore: '1567,919' }; + const auditCommand = 'npm audit '; + const auditLevel = 'info'; + const exceptionIds = [1567, 919]; + + expect(callbackStub.called).to.equal(false); + handleAction(options, callbackStub); + + expect(callbackStub.called).to.equal(true); + expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds)).to.equal(true); + expect(consoleStub.called).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1567, 919')).to.equal(true); + + consoleStub.restore(); + }); + + it('should not info log the vulnerabilities if there are no exceptions given', () => { + const callbackStub = sinon.stub(); + const consoleStub = sinon.stub(console, 'info'); + const options = {}; + const auditCommand = 'npm audit '; + const auditLevel = 'info'; + const exceptionIds = []; + + expect(callbackStub.called).to.equal(false); + handleAction(options, callbackStub); + + expect(callbackStub.called).to.equal(true); + expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds)).to.equal(true); + expect(consoleStub.called).to.equal(false); + + consoleStub.restore(); + }); + }); + + describe('--production', () => { + it('should be able to set production mode from the command flag correctly', () => { + const callbackStub = sinon.stub(); + const options = { production: true }; + const auditCommand = 'npm audit --production'; + const auditLevel = 'info'; + const exceptionIds = []; + + expect(callbackStub.called).to.equal(false); + handleAction(options, callbackStub); + expect(callbackStub.called).to.equal(true); + expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds)).to.equal(true); + }); + + // TODO check this, or maybe not? use picha eat to check json output + it('should not exit on dev dependencies vulnerabilites when using production flag', () => {}); + }); + + describe('--level', () => { + it('should be able to pass audit level from the command flag correctly', () => { + const callbackStub = sinon.stub(); + const options = { level: 'info' }; + + const auditCommand = 'npm audit '; + const exceptionIds = []; + + expect(callbackStub.called).to.equal(false); + handleAction(options, callbackStub); + expect(callbackStub.called).to.equal(true); + expect(callbackStub.calledWith(auditCommand, 'info', exceptionIds)).to.equal(true); + + options.level = 'low'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'low', exceptionIds)).to.equal(true); + + options.level = 'moderate'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'moderate', exceptionIds)).to.equal(true); + + options.level = 'high'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'high', exceptionIds)).to.equal(true); + + options.level = 'critical'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'critical', exceptionIds)).to.equal(true); + }); + + it('should be able to pass audit level from the environment variables correctly', () => { + const callbackStub = sinon.stub(); + const options = {}; + const auditCommand = 'npm audit '; + + // info + process.env.NPM_CONFIG_AUDIT_LEVEL = 'info'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'info')).to.equal(true); + + // low + process.env.NPM_CONFIG_AUDIT_LEVEL = 'low'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'low')).to.equal(true); + + // moderate + process.env.NPM_CONFIG_AUDIT_LEVEL = 'moderate'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'moderate')).to.equal(true); + + // high + process.env.NPM_CONFIG_AUDIT_LEVEL = 'high'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'high')).to.equal(true); + + // critical + process.env.NPM_CONFIG_AUDIT_LEVEL = 'critical'; + handleAction(options, callbackStub); + expect(callbackStub.calledWith(auditCommand, 'critical')).to.equal(true); + + // Clean up + process.env.NPM_CONFIG_AUDIT_LEVEL = undefined; + }); + }); +}); diff --git a/test/index.js b/test/index.js index 06e3b01..24e30c8 100644 --- a/test/index.js +++ b/test/index.js @@ -1,558 +1,119 @@ const sinon = require('sinon'); const chai = require('chai'); const { expect } = chai; -const V6_LOG_REPORT = require('./__mocks__/v6-log-data'); + const V6_JSON_BUFFER = require('./__mocks__/v6-json-buffer.json'); const V6_JSON_BUFFER_EMPTY = require('./__mocks__/v6-json-buffer-empty.json'); -const V7_JSON_BUFFER = require('./__mocks__/v7-json-buffer.json'); -const V7_JSON_BUFFER_EMPTY = require('./__mocks__/v7-json-buffer-empty.json'); -const consoleUtil = require('../utils/console'); -const { isWholeNumber, mapLevelToNumber, getRawVulnerabilities, isJsonString, filterValidException, filterExceptions } = require('../utils/common'); -const { handleFinish, handleUserInput, BASE_COMMAND, SUCCESS_MESSAGE, LOGS_EXCEEDED_MESSAGE } = require('../index'); - -const { FG_WHITE, RESET_COLOR } = consoleUtil; - -describe('console utils', () => { - it('should wrap error console message with styling format correctly', () => { - const stub = sinon.stub(console, 'error'); - const message = 'console message'; - - expect(stub.called).to.equal(false); - consoleUtil.error(message); - - expect(stub.called).to.equal(true); - expect(stub.calledWith(`${FG_WHITE}${message}${RESET_COLOR}`)).to.equal(true); - stub.restore(); - }); - - it('should wrap error info message with styling format correctly', () => { - const stub = sinon.stub(console, 'info'); - const message = 'console message'; - - expect(stub.called).to.equal(false); - consoleUtil.info(message); - - expect(stub.called).to.equal(true); - expect(stub.calledWith(`${FG_WHITE}${message}${RESET_COLOR}`)).to.equal(true); - stub.restore(); - }); -}); - -describe('common utils', () => { - it('should return true for valid JSON object', () => { - expect(isJsonString(JSON.stringify({ a: 1, b: { c: 2 } }))).to.equal(true); - }); - - it('should return false if it is not a valid JSON object', () => { - expect(isJsonString('abc')).to.equal(false); - }); - it('should be able to determine a whole number', () => { - expect(isWholeNumber()).to.equal(false); - expect(isWholeNumber(0.14)).to.equal(false); - expect(isWholeNumber(20.45)).to.equal(false); - expect(isWholeNumber('')).to.equal(false); - expect(isWholeNumber('2.50')).to.equal(false); - expect(isWholeNumber(null)).to.equal(false); - expect(isWholeNumber('true')).to.equal(false); +const { handleFinish } = require('../index'); - expect(isWholeNumber(1)).to.equal(true); - expect(isWholeNumber(2920)).to.equal(true); - expect(isWholeNumber(934)).to.equal(true); - expect(isWholeNumber('0920')).to.equal(true); - - expect(isWholeNumber(true)).to.equal(true); // Should handle this? - }); - - it('should be able to map audit level to correct numbers', () => { - expect(mapLevelToNumber('info')).to.equal(0); - expect(mapLevelToNumber('low')).to.equal(1); - expect(mapLevelToNumber('moderate')).to.equal(2); - expect(mapLevelToNumber('high')).to.equal(3); - expect(mapLevelToNumber('critical')).to.equal(4); - // default and exceptions - expect(mapLevelToNumber('unknown')).to.equal(0); - expect(mapLevelToNumber()).to.equal(0); - expect(mapLevelToNumber(true)).to.equal(0); - expect(mapLevelToNumber(false)).to.equal(0); - expect(mapLevelToNumber({})).to.equal(0); - }); - - it('should be able to filter valid file exceptions correctly', () => { - const exceptions = { - '137': { - ignore: true, - reason: 'Ignored since we dont use xxx method', - }, - '581': { - reason: 'Ignored since we dont use xxx method', - }, - '980': 'Ignored since we dont use xxx method', - '5': '', - '3': null, - '2': undefined, - '1': false, - 'invalid': 'Ignored since we dont use xxx method', - }; - const expected = [ - { id: 1, reason: undefined }, - { id: 2, reason: undefined }, - { id: 3, reason: undefined }, - { id: 5, reason: undefined }, - { id: 137, ignore: true, reason: 'Ignored since we dont use xxx method' }, - { id: 980, reason: 'Ignored since we dont use xxx method' }, - ]; - - expect(filterValidException(exceptions)).to.deep.equal(expected); - }); - - it('should be able to filter valid file exceptions with expiry dates correctly', () => { - const exceptions = { - '137': { - ignore: true, - expiry: 1615462130000, - }, - '581': { - ignore: true, - expiry: 1615462140000, - }, - '980': { - ignore: true, - expiry: 1615462150000, - }, - }; - - expect(filterValidException(exceptions)).to.deep.equal([]); - let clock = sinon.stub(Date, 'now').returns(1615462140000); - - expect(filterValidException(exceptions)).to.deep.equal([ - { id: 980, ignore: true, expiry: 1615462150000 }, - ]); - - clock.restore(); - clock = sinon.stub(Date, 'now').returns(1615462130000); - - expect(filterValidException(exceptions)).to.deep.equal([ - { id: 581, ignore: true, expiry: 1615462140000 }, - { id: 980, ignore: true, expiry: 1615462150000 }, - ]); - - clock.restore(); - }); - - it('should know how to filter raw vulnerabilities based on the exceptionIds', () => { - const exceptions = [1213, 1500, 1555, 9999]; - const result = filterExceptions([975, 1213, 976, 985, 1500, 1084, 1179, 1523, 1555, 1556, 1589, 9999], exceptions); - - expect(result).to.have.length(8).and.to.deep.equal([975, 976, 985, 1084, 1179, 1523, 1556, 1589]); - }); -}); - -describe('event handlers', () => { - it('should be able to pass exceptions from the command correctly', () => { - const stub = sinon.stub(); - const options = { - ignore: '1567,919', - }; - - expect(stub.called).to.equal(false); - handleUserInput(options, stub); - expect(stub.called).to.equal(true); - - const auditCommand = BASE_COMMAND; - const auditLevel = 0; - const fullLog = false; - const exceptionIds = [1567, 919]; - expect(stub.calledWith(auditCommand, auditLevel, fullLog, exceptionIds)).to.equal(true); - - // with space - options.ignore = '1567, 1902'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, auditLevel, fullLog, [1567, 1902])).to.equal(true); - - // invalid exceptions - options.ignore = '1134,undefined,888'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, auditLevel, fullLog, [1134, 888])).to.equal(true); - - // invalid NaN - options.ignore = '1134,NaN,3e,828'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, auditLevel, fullLog, [1134, 828])).to.equal(true); - - // invalid decimals - options.ignore = '1199,29.41,628'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, auditLevel, fullLog, [1199, 628])).to.equal(true); - }); - - it('should be able to handle audit level from the command correctly', () => { - const stub = sinon.stub(); - const consoleStub = sinon.stub(console, 'info'); - const options = { - level: 'info', - }; - - expect(stub.called).to.equal(false); - handleUserInput(options, stub); - expect(stub.called).to.equal(true); - - const auditCommand = BASE_COMMAND; - const fullLog = false; +describe('Events handling', () => { + it('should exit if unable to process the JSON buffer', () => { + const processStub = sinon.stub(process, 'exit'); + const consoleStub = sinon.stub(console, 'error'); + const jsonBuffer = ''; + const auditLevel = 'info'; const exceptionIds = []; - expect(stub.calledWith(auditCommand, 0, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: info]')).to.equal(true); - // low - options.level = 'low'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 1, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: low]')).to.equal(true); + expect(processStub.called).to.equal(false); + expect(consoleStub.called).to.equal(false); - // moderate - options.level = 'moderate'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 2, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: moderate]')).to.equal(true); + handleFinish(jsonBuffer, auditLevel, exceptionIds); - // high - options.level = 'high'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 3, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: high]')).to.equal(true); + expect(processStub.called).to.equal(true); + expect(processStub.calledWith(1)).to.equal(true); - // critical - options.level = 'critical'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 4, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: critical]')).to.equal(true); + expect(consoleStub.called).to.equal(true); + expect(consoleStub.calledWith('Unable to process the JSON buffer string.')).to.equal(true); + processStub.restore(); consoleStub.restore(); }); - it('should be able to use audit level from the environment variables correctly', () => { - const stub = sinon.stub(); + it('should be able to handle success case properly', () => { const consoleStub = sinon.stub(console, 'info'); - const options = {}; - const auditCommand = BASE_COMMAND; - const fullLog = false; + const jsonBuffer = JSON.stringify(V6_JSON_BUFFER_EMPTY); + const auditLevel = 'info'; const exceptionIds = []; - // info - process.env.NPM_CONFIG_AUDIT_LEVEL = 'info'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 0, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: info]')).to.equal(true); - - // low - process.env.NPM_CONFIG_AUDIT_LEVEL = 'low'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 1, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: low]')).to.equal(true); - - // moderate - process.env.NPM_CONFIG_AUDIT_LEVEL = 'moderate'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 2, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: moderate]')).to.equal(true); + expect(consoleStub.called).to.equal(false); + handleFinish(jsonBuffer, auditLevel, exceptionIds); + expect(consoleStub.called).to.equal(true); + expect(consoleStub.calledWith('🤝 All good!')).to.equal(true); - // high - process.env.NPM_CONFIG_AUDIT_LEVEL = 'high'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 3, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: high]')).to.equal(true); - - // critical - process.env.NPM_CONFIG_AUDIT_LEVEL = 'critical'; - handleUserInput(options, stub); - expect(stub.calledWith(auditCommand, 4, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[level: critical]')).to.equal(true); - - // Clean up consoleStub.restore(); - process.env.NPM_CONFIG_AUDIT_LEVEL = undefined; }); - it('should be able to handle production flag from the command correctly', () => { - const stub = sinon.stub(); + it('should be able to except vulnerabilities properly', () => { const consoleStub = sinon.stub(console, 'info'); - const options = { - production: true, - }; - - expect(stub.called).to.equal(false); - handleUserInput(options, stub); - expect(stub.called).to.equal(true); + const jsonBuffer = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const exceptionIds = [975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]; - const auditCommand = `${BASE_COMMAND} --production`; - const auditLevel = 0; - const fullLog = false; - const exceptionIds = []; - expect(stub.calledWith(auditCommand, auditLevel, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[production mode enabled]')).to.equal(true); + expect(consoleStub.called).to.equal(false); + handleFinish(jsonBuffer, auditLevel, exceptionIds); + expect(consoleStub.called).to.equal(true); + expect(consoleStub.calledWith('🤝 All good!')).to.equal(true); consoleStub.restore(); }); - it('should be able to handle full logs flag from the command correctly', () => { - const stub = sinon.stub(); - const consoleStub = sinon.stub(console, 'info'); - const options = { - full: true, - }; + it('should be able to handle found vulnerabilities properly', () => { + const processStub = sinon.stub(process, 'exit'); + const consoleErrorStub = sinon.stub(console, 'error'); + const consoleInfoStub = sinon.stub(console, 'info'); + const jsonBuffer = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const exceptionIds = [975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555]; - expect(stub.called).to.equal(false); - handleUserInput(options, stub); - expect(stub.called).to.equal(true); + expect(processStub.called).to.equal(false); + expect(consoleErrorStub.called).to.equal(false); + expect(consoleInfoStub.called).to.equal(false); - const auditCommand = BASE_COMMAND; - const auditLevel = 0; - const fullLog = true; - const exceptionIds = []; - expect(stub.calledWith(auditCommand, auditLevel, fullLog, exceptionIds)).to.equal(true); - expect(consoleStub.calledWith('[report display limit disabled]')).to.equal(true); + handleFinish(jsonBuffer, auditLevel, exceptionIds); - consoleStub.restore(); - }); - - it('should be able to handle default command correctly', () => { - const stub = sinon.stub(); - const options = {}; + expect(processStub.called).to.equal(true); + expect(consoleErrorStub.called).to.equal(true); + expect(consoleInfoStub.called).to.equal(true); // Print security report - expect(stub.called).to.equal(false); - handleUserInput(options, stub); - expect(stub.called).to.equal(true); + expect(processStub.calledWith(1)).to.equal(true); + expect(consoleErrorStub.calledWith('2 vulnerabilities found. Node security advisories: 1556, 1589')).to.equal(true); - const auditCommand = BASE_COMMAND; - const auditLevel = 0; - const fullLog = false; - const exceptionIds = []; - expect(stub.calledWith(auditCommand, auditLevel, fullLog, exceptionIds)).to.equal(true); - }); - - it('should be able to handle the success result properly', () => { - const stub = sinon.stub(consoleUtil, 'info'); - const vulnerabilities = []; - - expect(stub.called).to.equal(false); - handleFinish(vulnerabilities); - expect(stub.called).to.equal(true); - expect(stub.calledWith(SUCCESS_MESSAGE)).to.equal(true); - stub.restore(); - }); - - it('should be able to handle the found vulnerabilities properly', () => { - const stubProcess = sinon.stub(process, 'exit'); - const stubConsole = sinon.stub(consoleUtil, 'error'); - const vulnerabilities = [1165, 1890]; - - expect(stubProcess.called).to.equal(false); - expect(stubConsole.called).to.equal(false); - - handleFinish(vulnerabilities); - - expect(stubProcess.called).to.equal(true); - expect(stubConsole.called).to.equal(true); - - expect(stubProcess.calledWith(1)).to.equal(true); - expect(stubConsole.calledWith('2 vulnerabilities found. Node security advisories: 1165,1890')).to.equal(true); - - stubProcess.restore(); - stubConsole.restore(); + processStub.restore(); + consoleErrorStub.restore(); + consoleInfoStub.restore(); }); it('should inform the developer when exceptionsIds are unused', () => { - const stubProcess = sinon.stub(process, 'exit'); - const stubErrorConsole = sinon.stub(consoleUtil, 'error'); - const stubInfoConsole = sinon.stub(consoleUtil, 'info'); - const vulnerabilities = [1165, 1890, 1337]; - const exceptionIds = [2000, 4242]; - - expect(stubProcess.called).to.equal(false); - expect(stubErrorConsole.called).to.equal(false); - expect(stubInfoConsole.called).to.equal(false); - - handleFinish(vulnerabilities, '', {}, exceptionIds); - - expect(stubProcess.called).to.equal(true); - expect(stubProcess.calledWith(1)).to.equal(true); - - expect(stubErrorConsole.called).to.equal(true); - expect(stubErrorConsole.calledWith('3 vulnerabilities found. Node security advisories: 1165,1890,1337')).to.equal(true); - - expect(stubInfoConsole.called).to.equal(true); + const processStub = sinon.stub(process, 'exit'); + const consoleErrorStub = sinon.stub(console, 'error'); + const consoleWarnStub = sinon.stub(console, 'warn'); + const consoleInfoStub = sinon.stub(console, 'info'); + const jsonBuffer = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const exceptionIds = [975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 2001, 2002]; + + expect(processStub.called).to.equal(false); + expect(consoleErrorStub.called).to.equal(false); + expect(consoleWarnStub.called).to.equal(false); + expect(consoleInfoStub.called).to.equal(false); + + handleFinish(jsonBuffer, auditLevel, exceptionIds); + + expect(processStub.called).to.equal(true); + expect(processStub.calledWith(1)).to.equal(true); + expect(consoleErrorStub.called).to.equal(true); + expect(consoleErrorStub.calledWith('2 vulnerabilities found. Node security advisories: 1556, 1589')).to.equal(true); + + expect(consoleInfoStub.called).to.equal(true); // Print security report + expect(consoleWarnStub.called).to.equal(true); // eslint-disable-next-line max-len - const message = `2 vulnerabilities where ignored but did not result in a vulnerabilities: 2000,4242. They can be removed from the .nsprc file or -ignore -i flags.`; - expect(stubInfoConsole.calledWith(message)).to.equal(true); - - stubProcess.restore(); - stubErrorConsole.restore(); - stubInfoConsole.restore(); - }); - - it('should be able to handle normal log display correctly', () => { - const stub = sinon.stub(console, 'info'); - const smallLog = '123456789'; - const displayFullLog = true; - const maxLength = 50; - const vulnerabilities = []; - - expect(stub.called).to.equal(false); - handleFinish(vulnerabilities, smallLog, { displayFullLog, maxLength }); - expect(stub.called).to.equal(true); - expect(stub.calledWith(smallLog)).to.equal(true); - stub.restore(); - }); - - it('should display overlength log properly', () => { - const stub = sinon.stub(console, 'info'); - const displayFullLog = true; - const maxLength = 500; - const vulnerabilities = []; - - expect(stub.called).to.equal(false); - handleFinish(vulnerabilities, V6_LOG_REPORT, { displayFullLog, maxLength }); - expect(stub.called).to.equal(true); - // Full log - expect(stub.calledWith(V6_LOG_REPORT)).to.equal(true); - stub.restore(); - }); - - it('should display an additional message on overlength log', () => { - const stub = sinon.stub(console, 'info'); - const displayFullLog = false; - const maxLength = 500; - const vulnerabilities = []; - - let expectedDisplay = V6_LOG_REPORT.substring(0, maxLength); - expectedDisplay += '\n\n'; - expectedDisplay += '...'; - expectedDisplay += '\n\n'; - expectedDisplay += LOGS_EXCEEDED_MESSAGE; - expectedDisplay += '\n\n'; - - expect(stub.called).to.equal(false); - handleFinish(vulnerabilities, V6_LOG_REPORT, { displayFullLog, maxLength }); - expect(stub.called).to.equal(true); - expect(stub.calledWith(expectedDisplay)).to.equal(true); - stub.restore(); - }); - - it('should be able to handle log display within maximum length properly', () => { - const stub = sinon.stub(console, 'info'); - const data = '123456789'; - const fullLog = false; - const maxLength = 9; - const vulnerabilities = []; - - expect(stub.called).to.equal(false); - handleFinish(vulnerabilities, data, fullLog, maxLength); - expect(stub.called).to.equal(true); - expect(stub.calledWith('123456789')).to.equal(true); - stub.restore(); - }); -}); - -describe('npm v6', () => { - describe('retrieve vulnerabilities', () => { - it('should be able to handle correctly for empty vulnerability scan', () => { - const jsonString = JSON.stringify(V6_JSON_BUFFER_EMPTY); - const auditLevel = 0; // info - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(0).and.to.deep.equal([]); - }); - - it('should be able to get info level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V6_JSON_BUFFER); - const auditLevel = 0; // info - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(11).and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); - }); - - it('should be able to get low level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V6_JSON_BUFFER); - const auditLevel = 1; // low - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(11).and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); - }); - - it('should be able to get moderate level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V6_JSON_BUFFER); - const auditLevel = 2; // moderate - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(5).and.to.deep.equal([975, 976, 985, 1213, 1555]); - }); - - it('should be able to get high level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V6_JSON_BUFFER); - const auditLevel = 3; // high - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(2).and.to.deep.equal([1213, 1555]); - }); - - it('should be able to get critical level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V6_JSON_BUFFER); - const auditLevel = 4; // critical - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(1).and.to.deep.equal([1555]); - }); - }); -}); - -describe('npm v7', () => { - describe('retrieve vulnerabilities', () => { - it('should be able to handle correctly for empty vulnerability scan', () => { - const jsonString = JSON.stringify(V7_JSON_BUFFER_EMPTY); - const auditLevel = 0; // info - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(0).and.to.deep.equal([]); - }); - - it('should be able to get info level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V7_JSON_BUFFER); - const auditLevel = 0; // info - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(11).and.to.deep.equal([1555, 1213, 1589, 1523, 1084, 1179, 1556, 975, 976, 985, 1500]); - }); - - it('should be able to get low level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V7_JSON_BUFFER); - const auditLevel = 1; // low - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(10).and.to.deep.equal([1555, 1213, 1589, 1523, 1084, 1179, 1556, 975, 976, 985]); - }); - - it('should be able to get moderate level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V7_JSON_BUFFER); - const auditLevel = 2; // moderate - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(5).and.to.deep.equal([1555, 1213, 975, 976, 985]); - }); - - it('should be able to get high level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V7_JSON_BUFFER); - const auditLevel = 3; // high - const result = getRawVulnerabilities(jsonString, auditLevel); - - expect(result).to.have.length(2).and.to.deep.equal([1555, 1213]); - }); - - it('should be able to get critical level vulnerabilities from JSON buffer', () => { - const jsonString = JSON.stringify(V7_JSON_BUFFER); - const auditLevel = 4; // critical - const result = getRawVulnerabilities(jsonString, auditLevel); + const message = `2 vulnerabilities where ignored but did not result in a vulnerabilities: 2001, 2002. They can be removed from the .nsprc file or -ignore -i flags.`; + expect(consoleWarnStub.calledWith(message)).to.equal(true); - expect(result).to.have.length(1).and.to.deep.equal([1555]); - }); + processStub.restore(); + consoleErrorStub.restore(); + consoleWarnStub.restore(); + consoleInfoStub.restore(); }); }); diff --git a/test/utils/color.js b/test/utils/color.js new file mode 100644 index 0000000..43f6e7a --- /dev/null +++ b/test/utils/color.js @@ -0,0 +1,49 @@ +const chai = require('chai'); +const { expect } = chai; +const { color, getSeverityBgColor } = require('../../utils/color'); + +describe('Color utils', () => { + describe('#color', () => { + it('should handle correctly without given colors specificed', () => { + expect(color('message')).to.equal('message\x1b[0m'); + }); + + it('should be able to color message foreground correctly', () => { + expect(color('message', 'black')).to.equal('\033[30mmessage\x1b[0m'); + expect(color('message', 'red')).to.equal('\033[31mmessage\x1b[0m'); + expect(color('message', 'green')).to.equal('\033[32mmessage\x1b[0m'); + expect(color('message', 'yellow')).to.equal('\033[33mmessage\x1b[0m'); + expect(color('message', 'blue')).to.equal('\033[34mmessage\x1b[0m'); + expect(color('message', 'magenta')).to.equal('\033[35mmessage\x1b[0m'); + expect(color('message', 'cyan')).to.equal('\033[36mmessage\x1b[0m'); + expect(color('message', 'white')).to.equal('\033[37mmessage\x1b[0m'); + }); + + it('should be able to color message background correctly', () => { + expect(color('message', null, 'black')).to.equal('\033[40mmessage\x1b[0m'); + expect(color('message', null, 'red')).to.equal('\033[41mmessage\x1b[0m'); + expect(color('message', null, 'green')).to.equal('\033[42mmessage\x1b[0m'); + expect(color('message', null, 'yellow')).to.equal('\033[43mmessage\x1b[0m'); + expect(color('message', null, 'blue')).to.equal('\033[44mmessage\x1b[0m'); + expect(color('message', null, 'magenta')).to.equal('\033[45mmessage\x1b[0m'); + expect(color('message', null, 'cyan')).to.equal('\033[46mmessage\x1b[0m'); + expect(color('message', null, 'white')).to.equal('\033[47mmessage\x1b[0m'); + }); + + it('should be able to color message foreground and background correctly', () => { + expect(color('message', 'black', 'green')).to.equal('\033[30m\033[42mmessage\x1b[0m'); + expect(color('message', 'white', 'cyan')).to.equal('\033[37m\033[46mmessage\x1b[0m'); + }); + }); + + describe('#getSeverityBgColor', () => { + it('should return correctly', () => { + expect(getSeverityBgColor()).to.equal(undefined); + expect(getSeverityBgColor('info')).to.equal(undefined); + expect(getSeverityBgColor('low')).to.equal(undefined); + expect(getSeverityBgColor('moderate')).to.equal(undefined); + expect(getSeverityBgColor('high')).to.equal('red'); + expect(getSeverityBgColor('critical')).to.equal('red'); + }); + }); +}); diff --git a/test/utils/common.js b/test/utils/common.js new file mode 100644 index 0000000..ad01806 --- /dev/null +++ b/test/utils/common.js @@ -0,0 +1,35 @@ +const chai = require('chai'); +const { expect } = chai; + +const { isWholeNumber, isJsonString } = require('../../utils/common'); + +describe('Common utils', () => { + describe('#isJsonString', () => { + it('should return true for valid JSON object', () => { + expect(isJsonString(JSON.stringify({ a: 1, b: { c: 2 } }))).to.equal(true); + }); + + it('should return false if it is not a valid JSON object', () => { + expect(isJsonString('abc')).to.equal(false); + }); + }); + + describe('#isWholeNumber', () => { + it('should be able to determine a whole number', () => { + expect(isWholeNumber()).to.equal(false); + expect(isWholeNumber(0.14)).to.equal(false); + expect(isWholeNumber(20.45)).to.equal(false); + expect(isWholeNumber('')).to.equal(false); + expect(isWholeNumber('2.50')).to.equal(false); + expect(isWholeNumber(null)).to.equal(false); + expect(isWholeNumber('true')).to.equal(false); + + expect(isWholeNumber(1)).to.equal(true); + expect(isWholeNumber(2920)).to.equal(true); + expect(isWholeNumber(934)).to.equal(true); + expect(isWholeNumber('0920')).to.equal(true); + + expect(isWholeNumber(true)).to.equal(true); // Should handle this? + }); + }); +}); diff --git a/test/utils/vulnerability.js b/test/utils/vulnerability.js new file mode 100644 index 0000000..6b598e2 --- /dev/null +++ b/test/utils/vulnerability.js @@ -0,0 +1,594 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const { expect } = chai; + +const NSPRC = require('../__mocks__/nsprc'); +const V6_JSON_BUFFER = require('../__mocks__/v6-json-buffer.json'); +const V6_JSON_BUFFER_EMPTY = require('../__mocks__/v6-json-buffer-empty.json'); +const V7_JSON_BUFFER = require('../__mocks__/v7-json-buffer.json'); +const V7_JSON_BUFFER_EMPTY = require('../__mocks__/v7-json-buffer-empty.json'); + +const { mapLevelToNumber, processAuditJson, processExceptions, getExceptionsIds } = require('../../utils/vulnerability'); + +describe('Vulnerability utils', () => { + describe('#mapLevelToNumber', () => { + it('should be able to map audit level to correct numbers', () => { + expect(mapLevelToNumber('info')).to.equal(0); + expect(mapLevelToNumber('low')).to.equal(1); + expect(mapLevelToNumber('moderate')).to.equal(2); + expect(mapLevelToNumber('high')).to.equal(3); + expect(mapLevelToNumber('critical')).to.equal(4); + }); + + it('should be able to handle exceptions properly', () => { + expect(mapLevelToNumber('unknown')).to.equal(0); + expect(mapLevelToNumber()).to.equal(0); + expect(mapLevelToNumber(true)).to.equal(0); + expect(mapLevelToNumber(false)).to.equal(0); + expect(mapLevelToNumber({})).to.equal(0); + }); + }); + + describe('#getExceptionsIds', () => { + it('should display the vulnerabilities from command line if .nsprc file not given', () => { + const consoleStub = sinon.stub(console, 'info'); + const cmdExceptions = [1165, 1890]; + expect(consoleStub.called).to.equal(false); + const result = getExceptionsIds(null, cmdExceptions); + expect(result).to.have.length(2).and.deep.equal([1165, 1890]); + expect(consoleStub.called).to.equal(true); + expect(consoleStub.calledWith('Exception IDs: 1165, 1890')).to.equal(true); + consoleStub.restore(); + }); + + it('should combine the exceptions from command line and .nsprc file', () => { + const consoleStub = sinon.stub(console, 'info'); + const cmdExceptions = [1165, 1890]; + expect(consoleStub.called).to.equal(false); + const result = getExceptionsIds(NSPRC, cmdExceptions); + expect(result).to.have.length(6).and.deep.equal([1165, 1890, 985, 1213, 1654, 2100]); + expect(consoleStub.called).to.equal(true); // Print security report + consoleStub.restore(); + }); + }); + + describe('#processExceptions', () => { + it('should be able to process exceptions correctly', () => { + const cmdExceptions = [1165, 1890]; + const result = processExceptions(NSPRC, cmdExceptions); + + expect(result).to.have.property('exceptionIds'); + expect(result.exceptionIds).to.have.length(6).and.to.deep.equal([1165, 1890, 985, 1213, 1654, 2100]); + expect(result).to.have.property('report'); + /* eslint-disable indent */ + expect(result.report) + .to.have.length(13) + .and.to.deep.equal([ + [ + '1165', + '\u001b[32mactive\u001b[0m', + '', + '', + ], + [ + '1890', + '\u001b[32mactive\u001b[0m', + '', + '', + ], + [ + '975', + '\u001b[31mexpired\u001b[0m', + 'Thu, 11 Mar 2021 11:28:54 GMT', + '', + ], + [ + '976', + '\u001b[33minactive\u001b[0m', + '', + '', + ], + [ + '985', + '\u001b[32mactive\u001b[0m', + '', + '', + ], + [ + '1084', + '\u001b[31mexpired\u001b[0m', + 'Thu, 11 Mar 2021 11:28:54 GMT', + 'Inactive package; consider replacing it.', + ], + [ + '1179', + '\u001b[31mexpired\u001b[0m', + 'Thu, 11 Mar 2021 11:28:54 GMT', + '', + ], + [ + '1213', + '\u001b[32mactive\u001b[0m', + '', + 'Ignored since we don\'t use xxx method', + ], + [ + '1556', + '\u001b[31mexpired\u001b[0m', + 'Thu, 11 Mar 2021 11:28:54 GMT', + 'Issue: https://github.com/jeemok/better-npm-audit/issues/28', + ], + [ + '1651', + '\u001b[31mexpired\u001b[0m', + 'Thu, 11 Mar 2021 11:28:54 GMT', + 'This will be fixed by the maintainers by June 14', + ], + [ + '1654', + '\u001b[32mactive\u001b[0m', + 'Fri, 31 Dec 2021 16:00:00 GMT', + '', + ], + [ + '2100', + '\u001b[32mactive\u001b[0m', + '', + 'Unused', + ], + [ + 'Note', + '\u001b[31minvalid\u001b[0m', + '', + 'personal note', + ], + ]); + /* eslint-enable indent */ + }); + + it('should be able to filter active exceptions and label correctly', () => { + const result = processExceptions(NSPRC); + expect(result).to.have.property('exceptionIds').and.to.have.length(4); + expect(result).to.have.property('report'); + + const activeExceptionIds = result.report.filter(exception => exception[1] === '\u001b[32mactive\u001b[0m').map(each => Number(each[0])); + expect(activeExceptionIds).to.have.length(4).to.deep.equal([985, 1213, 1654, 2100]); + }); + + it('should be able to filter inactive exceptions and label correctly', () => { + const result = processExceptions(NSPRC); + expect(result).to.have.property('exceptionIds').and.to.have.length(4); + expect(result).to.have.property('report'); + + const activeExceptionIds = result.report.filter(exception => exception[1] === '\u001b[33minactive\u001b[0m').map(each => Number(each[0])); + expect(activeExceptionIds).to.have.length(1).to.deep.equal([976]); + }); + + it('should be able to filter expired exceptions and label correctly', () => { + const dateStub = sinon.stub(Date, 'now').returns(new Date(Date.UTC(2021, 6, 1)).valueOf()); + const result = processExceptions(NSPRC); + expect(result).to.have.property('exceptionIds').and.to.have.length(4); + expect(result).to.have.property('report'); + + const activeExceptionIds = result.report.filter(exception => exception[1] === '\u001b[31mexpired\u001b[0m').map(each => Number(each[0])); + expect(activeExceptionIds).to.have.length(5).to.deep.equal([975, 1084, 1179, 1556, 1651]); + + // Clean up + dateStub.restore(); + }); + + it('should be able to filter invalid exceptions and label correctly', () => { + const result = processExceptions(NSPRC); + expect(result).to.have.property('exceptionIds').and.to.have.length(4); + expect(result).to.have.property('report'); + + const activeExceptionIds = result.report.filter(exception => exception[1] === '\u001b[31minvalid\u001b[0m').map(each => each[0]); + expect(activeExceptionIds).to.have.length(1).to.deep.equal(['Note']); + }); + }); + + describe('#processAuditJson', () => { + describe('npm v6', () => { + it('should be able to handle correctly for empty vulnerability scan', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER_EMPTY); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds'); + expect(result.vulnerabilityIds).to.have.length(0).and.to.deep.equal([]); + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(0).and.to.deep.equal([]); + expect(result).to.have.property('report'); + expect(result.report).to.have.length(0).and.to.deep.equal([]); + }); + + it('should be able to except some of the reported vulnerabilities', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + + expect(processAuditJson(jsonString, auditLevel)) + .to.have.property('unhandledIds') + .and.to.have.length(11) + .and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); + + expect(processAuditJson(jsonString, auditLevel, [975, 1179, 1589])) + .to.have.property('unhandledIds') + .and.to.have.length(8) + .and.to.deep.equal([976, 985, 1084, 1213, 1500, 1523, 1555, 1556]); + }); + + it('should be able to list all the reported vulnerabilities', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds'); + expect(result.vulnerabilityIds).to.have.length(11).and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); + }); + + it('should be able to generate a report of all the reported vulnerabilities', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('report'); + /* eslint-disable indent */ + expect(result.report) + .to.have.length(11) + .and.to.deep.equal([ + [ + '\u001b[33m975\u001b[0m', + '\u001b[33mswagger-ui\u001b[0m', + '\u001b[33mReverse Tabnapping\u001b[0m', + '\u001b[33mmoderate\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/975\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m976\u001b[0m', + '\u001b[33mswagger-ui\u001b[0m', + '\u001b[33mCross-Site Scripting\u001b[0m', + '\u001b[33mmoderate\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/976\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m985\u001b[0m', + '\u001b[33mswagger-ui\u001b[0m', + '\u001b[33mCross-Site Scripting\u001b[0m', + '\u001b[33mmoderate\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/985\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1084\u001b[0m', + '\u001b[33mmem\u001b[0m', + '\u001b[33mDenial of Service\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1084\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1179\u001b[0m', + '\u001b[33mminimist\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1179\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1213\u001b[0m', + '\u001b[33mdot-prop\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33m\u001b[41mhigh\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1213\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1500\u001b[0m', + '\u001b[33myargs-parser\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1500\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1523\u001b[0m', + '\u001b[33mlodash\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1523\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1555\u001b[0m', + '\u001b[33mbl\u001b[0m', + '\u001b[33mRemote Memory Exposure\u001b[0m', + '\u001b[33m\u001b[41mcritical\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1555\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1556\u001b[0m', + '\u001b[33mnode-fetch\u001b[0m', + '\u001b[33mDenial of Service\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1556\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + [ + '\u001b[33m1589\u001b[0m', + '\u001b[33mini\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1589\u001b[0m', + '\u001b[33mn\u001b[0m', + ], + ]); + /* eslint-enable indent */ + }); + + it('should be able to get info level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(11).and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); + }); + + it('should be able to get low level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'low'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(11).and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); + }); + + it('should be able to get moderate level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'moderate'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(5).and.to.deep.equal([975, 976, 985, 1213, 1555]); + }); + + it('should be able to get high level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'high'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(2).and.to.deep.equal([1213, 1555]); + }); + + it('should be able to get critical level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'critical'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(1).and.to.deep.equal([1555]); + }); + }); + + describe('npm v7', () => { + it('should be able to handle correctly for empty vulnerability scan', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER_EMPTY); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds'); + expect(result.vulnerabilityIds).to.have.length(0).and.to.deep.equal([]); + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(0).and.to.deep.equal([]); + expect(result).to.have.property('report'); + expect(result.report).to.have.length(0).and.to.deep.equal([]); + }); + + it('should be able to except some of the reported vulnerabilities', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'info'; + + expect(processAuditJson(jsonString, auditLevel)) + .to.have.property('unhandledIds') + .and.to.have.length(11) + .and.to.deep.equal([1555, 1213, 1589, 1523, 1084, 1179, 1556, 975, 976, 985, 1500]); + + expect(processAuditJson(jsonString, auditLevel, [975, 1179, 1589])) + .to.have.property('unhandledIds') + .and.to.have.length(8) + .and.to.deep.equal([1555, 1213, 1523, 1084, 1556, 976, 985, 1500]); + }); + + it('should be able to list all the reported vulnerabilities', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds'); + expect(result.vulnerabilityIds).to.have.length(11).and.to.deep.equal([1555, 1213, 1589, 1523, 1084, 1179, 1556, 975, 976, 985, 1500]); + }); + + it('should be able to generate a report of all the reported vulnerabilities', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('report'); + + /* eslint-disable indent */ + expect(result.report) + .to.have.length(11) + .and.to.deep.equal([ + [ + '\u001b[33m1555\u001b[0m', + '\u001b[33mbl\u001b[0m', + '\u001b[33mRemote Memory Exposure\u001b[0m', + '\u001b[33m\u001b[41mcritical\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1555\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1213\u001b[0m', + '\u001b[33mdot-prop\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33m\u001b[41mhigh\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1213\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1589\u001b[0m', + '\u001b[33mini\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1589\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1523\u001b[0m', + '\u001b[33mlodash\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1523\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1084\u001b[0m', + '\u001b[33mmem\u001b[0m', + '\u001b[33mDenial of Service\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1084\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1179\u001b[0m', + '\u001b[33mminimist\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1179\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1556\u001b[0m', + '\u001b[33mnode-fetch\u001b[0m', + '\u001b[33mDenial of Service\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1556\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m975\u001b[0m', + '\u001b[33mswagger-ui\u001b[0m', + '\u001b[33mReverse Tabnapping\u001b[0m', + '\u001b[33mmoderate\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/975\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m976\u001b[0m', + '\u001b[33mswagger-ui\u001b[0m', + '\u001b[33mCross-Site Scripting\u001b[0m', + '\u001b[33mmoderate\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/976\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m985\u001b[0m', + '\u001b[33mswagger-ui\u001b[0m', + '\u001b[33mCross-Site Scripting\u001b[0m', + '\u001b[33mmoderate\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/985\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + [ + '\u001b[33m1500\u001b[0m', + '\u001b[33myargs-parser\u001b[0m', + '\u001b[33mPrototype Pollution\u001b[0m', + '\u001b[33mlow\u001b[0m', + '\u001b[33mhttps://npmjs.com/advisories/1500\u001b[0m', + '\u001b[31mn\u001b[0m', + ], + ]); + /* eslint-enable indent */ + }); + + it('should be able to get info level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'info'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(11).and.to.deep.equal([1555, 1213, 1589, 1523, 1084, 1179, 1556, 975, 976, 985, 1500]); + }); + + it('should be able to get low level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'low'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(11).and.to.deep.equal([1555, 1213, 1589, 1523, 1084, 1179, 1556, 975, 976, 985, 1500]); + }); + + it('should be able to get moderate level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'moderate'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(5).and.to.deep.equal([1555, 1213, 975, 976, 985]); + }); + + it('should be able to get high level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'high'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(2).and.to.deep.equal([1555, 1213]); + }); + + it('should be able to get critical level vulnerabilities from JSON buffer', () => { + const jsonString = JSON.stringify(V7_JSON_BUFFER); + const auditLevel = 'critical'; + const result = processAuditJson(jsonString, auditLevel); + + expect(result).to.have.property('vulnerabilityIds').and.to.have.length(11); + expect(result).to.have.property('report').and.to.have.length(11); + + expect(result).to.have.property('unhandledIds'); + expect(result.unhandledIds).to.have.length(1).and.to.deep.equal([1555]); + }); + }); + }); +}); diff --git a/utils/color.js b/utils/color.js new file mode 100644 index 0000000..a1bf9ea --- /dev/null +++ b/utils/color.js @@ -0,0 +1,80 @@ +const get = require('lodash.get'); + +const COLORS = { + RESET: '\x1b[0m', + black: { + fg: '\033[30m', + bg: '\033[40m', + }, + red: { + fg: '\033[31m', + bg: '\033[41m', + }, + green: { + fg: '\033[32m', + bg: '\033[42m', + }, + yellow: { + fg: '\033[33m', + bg: '\033[43m', + }, + blue: { + fg: '\033[34m', + bg: '\033[44m', + }, + magenta: { + fg: '\033[35m', + bg: '\033[45m', + }, + cyan: { + fg: '\033[36m', + bg: '\033[46m', + }, + white: { + fg: '\033[37m', + bg: '\033[47m', + }, +}; + +/** + * Color a console message's foreground and background + * @param {String} message Message + * @param {String} fgColor Foreground color + * @param {String} bgColor Background color + * @return {String} Message + */ +function color(message, fgColor, bgColor) { + return [ + get(COLORS, `${fgColor}.fg`, ''), + get(COLORS, `${bgColor}.bg`, ''), + message, + COLORS.RESET, // Reset the color at the end + ].join(''); +} + +/** + * Get background color based on severity + * @param {String} severity Vulnerability's severity + * @return {(String | undefined)} Background color or undefined + */ +function getSeverityBgColor(severity) { + switch (severity) { + case 'info': + return undefined; + case 'low': + return undefined; + case 'moderate': + return undefined; + case 'high': + return 'red'; + case 'critical': + return 'red'; + default: + return undefined; + } +} + +module.exports = { + color, + getSeverityBgColor, +}; diff --git a/utils/common.js b/utils/common.js index 05bd374..de28847 100644 --- a/utils/common.js +++ b/utils/common.js @@ -1,110 +1,3 @@ -const get = require('lodash.get'); - -/** - * Converts an audit level to a numeric value for filtering purposes - * @param {String} auditLevel The npm audit level - * @return {Number} Returns the numeric value, higher is more severe - */ -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; - } -} - -/** - * Analyze the JSON string buffer for vulnerabilities - * @param {String} jsonBuffer NPM audit's JSON string buffer - * @param {Integer} auditLevel Audit level in integer - * @param {Array} exceptionIds List of exception vulnerabilities - * @return {Array} Returns the list of found vulnerabilities - */ -function getRawVulnerabilities(jsonBuffer = '', auditLevel = 0) { - // NPM v6 uses `advisories` - // NPM v7 uses `vulnerabilities` - // Refer to the test folder for some sample mockups - const { advisories, vulnerabilities } = JSON.parse(jsonBuffer); - - // NPM v6 handling - if (advisories) { - return Object.values(advisories) - .filter(advisory => mapLevelToNumber(advisory.severity) >= auditLevel) // Filter out if there is requested audit level - .map(advisory => advisory.id); // Map out the vulnerabilities IDs - } - - // NPM v7 handling - if (vulnerabilities) { - return Object.values(vulnerabilities) - .filter(vulnerability => mapLevelToNumber(vulnerability.severity) >= auditLevel) // Filter out if there is requested audit level - // Map out the vulnerabilities IDs - .reduce((acc, vulnerability) => { - // Its stored inside `via` array, but sometimes it might be a String - const cleanedArray = get(vulnerability, 'via', []).map(each => get(each, 'source')).filter(Boolean); - // Compile into a single array - return acc.concat(cleanedArray); - }, []); - } - - return []; -} - -/** - * Takes the rawVulnerabilities and filters out the exceptionIds - * @param {Array} rawVulnerabilities List of raw vulnerabilities to filter - * @param {Array} exceptionIds List of exception vulnerabilities - * @return {Array} Returns the list of found vulnerabilities - */ -function filterExceptions(rawVulnerabilities, exceptionIds = []) { - return rawVulnerabilities.filter(id => !exceptionIds.includes(id)); -} - -/** - * Filter the given list in the `.nsprc` file for valid exceptions - * @param {Object} fileException The exception object - * @return {Array} Returns the list of found vulnerabilities - */ -function filterValidException(fileException) { - if (typeof fileException !== 'object') { - return []; - } - return Object.entries(fileException).reduce((acc, [id, details]) => { - const numberId = Number(id); - // has to be valid number - if (isNaN(numberId)) { - return acc; - } - // if the details is not an config object, we will accept this ID - if (!details || typeof details !== 'object') { - return acc.concat(Object.assign({}, { id: numberId, reason: details || undefined })); - } - // `ignore` flag has to be true - if (!details.ignore) { - return acc; - } - // if it given an expiry date, validate the date - if (details.expiry) { - // if the expiry time is in the future, accept it - if (details.expiry > new Date(Date.now()).getTime()) { - return acc.concat(Object.assign({}, { id: numberId }, details)); - } - // else it is expired, so don't accept it - return acc; - } - // Accept the ID - return acc.concat(Object.assign({}, { id: numberId }, details)); - }, []); -} - /** * @param {Any} value The input number * @return {Boolean} Returns true if the input is a whole number @@ -130,10 +23,6 @@ function isJsonString(string) { } module.exports = { - filterValidException, isWholeNumber, isJsonString, - mapLevelToNumber, - getRawVulnerabilities, - filterExceptions, }; diff --git a/utils/console.js b/utils/console.js deleted file mode 100644 index b8e921c..0000000 --- a/utils/console.js +++ /dev/null @@ -1,27 +0,0 @@ -const RESET_COLOR = '\x1b[0m'; -const FG_WHITE = '\x1b[37m'; - -/** - * @param {String} string The error message - * @return {Boolean} Returns `true` - */ -function error(string) { - console.error(`${FG_WHITE}${string}${RESET_COLOR}`); - return true; -} - -/** - * @param {String} string The info message - * @return {Boolean} Returns `true` - */ -function info(string) { - console.info(`${FG_WHITE}${string}${RESET_COLOR}`); - return true; -} - -module.exports = { - error, - info, - RESET_COLOR, - FG_WHITE, -}; diff --git a/utils/file.js b/utils/file.js index 7953429..4e7a55b 100644 --- a/utils/file.js +++ b/utils/file.js @@ -2,8 +2,9 @@ const fs = require('fs'); const { isJsonString } = require('./common'); /** - * @param {String} path The file path - * @return {(Object|Boolean)} Returns the parsed data if found, or else returns `false` + * Read file from path + * @param {String} path File path + * @return {(Object | Boolean)} Returns the parsed data if found, or else returns `false` */ function readFile(path) { try { diff --git a/utils/print.js b/utils/print.js new file mode 100644 index 0000000..4625142 --- /dev/null +++ b/utils/print.js @@ -0,0 +1,43 @@ +const table = require('table').table; + +const SECURITY_REPORT_HEADER = ['ID', 'Module', 'Title', 'Sev.', 'URL', 'Ex.']; +const EXCEPTION_REPORT_HEADER = ['ID', 'Status', 'Expiry', 'Notes']; + +/** + * Print the security report in a table format + * @param {Array} data Array of arrays + * @return {undefined} Returns void + */ +function printSecurityReport(data) { + const configs = { + singleLine: true, + header: { + alignment: 'center', + content: '=== npm audit security report ===\n', + }, + }; + + console.info(table([SECURITY_REPORT_HEADER, ...data], configs)); +} + +/** + * Print the exception report in a table format + * @param {Array} data Array of arrays + * @return {undefined} Returns void + */ +function printExceptionReport(data) { + const configs = { + singleLine: true, + header: { + alignment: 'center', + content: '=== list of exceptions ===\n', + }, + }; + + console.info(table([EXCEPTION_REPORT_HEADER, ...data], configs)); +} + +module.exports = { + printSecurityReport, + printExceptionReport, +}; diff --git a/utils/vulnerability.js b/utils/vulnerability.js new file mode 100644 index 0000000..acc5c79 --- /dev/null +++ b/utils/vulnerability.js @@ -0,0 +1,192 @@ +const get = require('lodash.get'); + +const { isJsonString } = require('./common'); +const { color, getSeverityBgColor } = require('./color'); +const { printExceptionReport } = require('./print'); + +/** + * Converts an audit level to a numeric value + * @param {String} auditLevel Audit level + * @return {Number} Numberic 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; + } +} + +/** + * Analyze the JSON string buffer + * @param {String} jsonBuffer NPM Audit JSON string buffer + * @param {String} auditLevel User's target audit level + * @param {Array} exceptionIds User's exception IDs + * @return {Object} Processed vulnerabilities details + */ +function processAuditJson(jsonBuffer = '', auditLevel = 'info', exceptionIds = []) { + if (!isJsonString(jsonBuffer)) { + return {}; + } + // NPM v6 uses `advisories` + // NPM v7 uses `vulnerabilities` + // Refer to the `test/__mocks__` folder for some sample mockups + const { advisories, vulnerabilities } = JSON.parse(jsonBuffer); + + // NPM v6 handling + if (advisories) { + return Object.values(advisories).reduce((acc, cur) => { + const shouldAudit = mapLevelToNumber(cur.severity) >= mapLevelToNumber(auditLevel); + const isExcepted = exceptionIds.includes(cur.id); + + // Record this vulnerability into the report, and highlight it using yellow color if it's new + acc.report.push([ + color(cur.id, isExcepted ? '' : 'yellow'), + color(cur.module_name, isExcepted ? '' : 'yellow'), + color(cur.title, isExcepted ? '' : 'yellow'), + color(cur.severity, isExcepted ? '' : 'yellow', getSeverityBgColor(cur.severity)), + color(cur.url, isExcepted ? '' : 'yellow'), + isExcepted ? 'y' : color('n', 'yellow'), + ]); + + acc.vulnerabilityIds.push(cur.id); + + // Found unhandled vulnerabilites + if (shouldAudit && !isExcepted) { + acc.unhandledIds.push(cur.id); + } + + return acc; + }, + { + unhandledIds: [], + vulnerabilityIds: [], + report: [], + }); + } + + // NPM v7 handling + if (vulnerabilities) { + return Object.values(vulnerabilities).reduce((acc, cur) => { + // Inside `via` array, its either the related module name or the vulnerability source object. + get(cur, 'via', []).forEach(vul => { + // The vulnerability ID is labeled as `source` + const id = get(vul, 'source'); + + // Let's skip if ID is a string (module name), and only focus on the root vulnerabilities + if (!id || typeof id === 'string') { + return; + } + + const shouldAudit = mapLevelToNumber(vul.severity) >= mapLevelToNumber(auditLevel); + const isExcepted = exceptionIds.includes(id); + + // Record this vulnerability into the report, and highlight it using yellow color if it's new + acc.report.push([ + color(id, isExcepted ? '' : 'yellow'), + color(vul.name, isExcepted ? '' : 'yellow'), + color(vul.title, isExcepted ? '' : 'yellow'), + color(vul.severity, isExcepted ? '' : 'yellow', getSeverityBgColor(vul.severity)), + color(vul.url, isExcepted ? '' : 'yellow'), + isExcepted ? 'y' : color('n', 'red'), + ]); + + acc.vulnerabilityIds.push(id); + + // Found unhandled vulnerabilites + if (shouldAudit && !isExcepted) { + acc.unhandledIds.push(id); + } + }); + + return acc; + }, + { + unhandledIds: [], + vulnerabilityIds: [], + report: [], + }); + } + return {}; +} + +/** + * Process all exceptions and return a list of exception IDs + * @param {Object} 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 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 + const { exceptionIds, report } = processExceptions(nsprc, cmdExceptions); + + printExceptionReport(report); + + return exceptionIds; +} + +/** + * 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 = []) { + return Object.entries(nsprc).reduce((acc, [id, details]) => { + const numberId = Number(id); + const isValidId = !isNaN(numberId); + const isActive = Boolean(get(details, 'active', true)); // default to true + const expiryDate = get(details, 'expiry') ? new Date(details.expiry).toUTCString() : ''; + const hasExpired = get(details, 'expiry') ? details.expiry < new Date(Date.now()).getTime() : false; + const notes = typeof details === 'string' ? details : get(details, 'notes', ''); + + let status = color('active', 'green'); + if (hasExpired) { + status = color('expired', 'red'); + } else if (!isValidId) { + status = color('invalid', 'red'); + } else if (!isActive) { + status = color('inactive', 'yellow'); + } + + acc.report.push([id, status, expiryDate, notes]); + + if (isValidId && isActive && !hasExpired) { + acc.exceptionIds.push(numberId); + } + + return acc; + }, + { + exceptionIds: cmdExceptions, + report: cmdExceptions.map(id => [String(id), color('active', 'green'), '', '']), + }); +} + +module.exports = { + mapLevelToNumber, + getExceptionsIds, + processAuditJson, + processExceptions, +};