diff --git a/.README/all_good.png b/.README/all_good.png new file mode 100644 index 0000000..d357442 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..209f986 Binary files /dev/null and b/.README/highlighted_exceptions.png differ diff --git a/.README/unused_exception.png b/.README/unused_exception.png new file mode 100644 index 0000000..ac3992f Binary files /dev/null and b/.README/unused_exception.png differ 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/.nsprc.sample b/.nsprc.sample index 00ffb38..1bd78fd 100644 --- a/.nsprc.sample +++ b/.nsprc.sample @@ -1,20 +1,17 @@ { "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": "This will be fixed by the library maintainers by June 14" + "active": false, + "notes": "This will be fixed by the library maintainers by June 14" }, "4502": { - "ignore": true + "active": true }, "980": "This will be fixed by the library maintainers by June 14", "1024": "", - "5": false, - "3": null, - "2": undefined, - "Note": "Any non number key will be ignored" + "Note": "Any non number key will not be excepted" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f2d78..b94f4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +## 2.0.4 (June 22, 2021) + +### Notable changes + +* Simplified the workflow and improved overall performance by running lesser in the process. +* Added [`table`](https://www.npmjs.com/package/table) module to display summaries (Initially used [`cli-table`](https://www.npmjs.com/package/cli-table) for its small size, however the issues in the repo concerns me in its display quality in other OS. Hence, chosen `table` module despite its package size is much bigger) +* Added table display for security report +* Added table display of exceptions from `.nsprc` file +* Cleaned up test cases structure to be more straight forward and easier to maintain + +### Breaking changes + +* Renamed `--ignore -i` flag to `--exclude -x` 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. +* Renamed `ignore` field to `active` in `.nsprc` file for better clarity. +* Renamed `reason` field to `notes` in `.nsprc` file for better clarity. + +### Others + +* Removed logging of flags used in the command +* Added NPM audit into the CI pipeline +* Added `.github/FUNDING.yml` +* Updated `README.md` + +### Closed issues + +* [#20](https://github.com/jeemok/better-npm-audit/issues/20) Provide more output when parsing exceptions file +* [#27](https://github.com/jeemok/better-npm-audit/issues/27) Hide excepted vulnerabilities from output +* [#28](https://github.com/jeemok/better-npm-audit/issues/28) Missing [ in truncation message + +--- + ## 1.12.1 (June 21, 2021) * Added `FUNDING.yml` @@ -5,11 +38,11 @@ ## 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) @@ -17,16 +50,14 @@ ## 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 856158b..1eb4124 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Better NPM Audit -Made to allow skipping certain vulnerabilities, and any extra handling that are not supported by the default `npm audit` in the future. +The goal of this project is to help to reshape npm audit into the way the community would like, by the community itself. Giving another optionΒ for everyone and encourage more people to do security audits. [![NPM](https://nodei.co/npm/better-npm-audit.png)](https://npmjs.org/package/better-npm-audit) @@ -22,7 +22,7 @@ NPM has upgraded to version 7 in late 2020 and has breaking changes on the `npm ## Installation - $ npm install better-npm-audit + $ npm install --save better-npm-audit or @@ -32,42 +32,58 @@ 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 + +Unused exceptions will be notified: + +Demo of displaying the unused exception + +### Add into package scripts ```JSON { "scripts": { "prepush": "npm run test && npm run audit", - "audit": "node node_modules/better-npm-audit audit" + "audit": "better-npm-audit audit audit" } } ``` -### Run global +Now you can run locally or in your CI pipeline: ```bash -better-npm-audit audit +npm run audit ```
## Options -| Flag | Short | Description | -| ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | -| `--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. | +| Flag | Short | Description | +| -------------- | ----- | ------------------------------------------------------------------------------ | +| `--exclude` | `-x` | Exceptions or the vulnerabilities ID(s) to exclude | +| `--level` | `-l` | The minimum audit level to validate; Same as the original `--audit-level` flag | +| `--production` | `-p` | Skip checking the `devDependencies` |
## Environment Variables -| 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.* | +| Variable | Description | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| `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 +94,32 @@ 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 - +### Fields - High Regular Expression Denial of Service +| Attribute | Description | Default | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `active` | Boolean type to determine if we should use it for exception; `true` or `false` | `true` | +| `expiry` | Date time in milliseconds, the number of milliseconds since midnight 01 January, 1970 UTC.
You can use `new Date(2021, 1, 1).valueOf()` to get the milliseconds value. | | +| `notes` | Notes related to the vulnerability; will be displayed in the table summary. | - 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** +When using a `.nsprc` file, you will see this report display when it starts running: -```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) -``` +Demo of table displaying a list of exceptions
@@ -234,15 +129,6 @@ You can find the changelog [here](https://github.com/jeemok/better-npm-audit/blo
-## Next version - -You can install it by `npm install better-npm-audit@next` - -* [Readme](https://github.com/jeemok/better-npm-audit/blob/next/README.md) -* [Changelog](https://github.com/jeemok/better-npm-audit/blob/next/CHANGELOG.md) - -
- ## Special mentions - [@IanWright](https://github.com/IPWright83) for his solutions in improving the vulnerability validation for us to have the minimum-audit-level and production-mode flags. diff --git a/index.js b/index.js index 4598fbc..38e589e 100755 --- a/index.js +++ b/index.js @@ -8,125 +8,77 @@ 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 exclude + * @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 (displayFullLog) { - console.info(logData); - } else { - console.info(toDisplay); + // 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 (unusedExceptionIds.length > 0) { - // 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); + // Print the security report + if (report.length) { + printSecurityReport(report); } - // Display the error if found vulnerabilities - if (vulnerabilities.length > 0) { - consoleUtil.error(`${vulnerabilities.length} vulnerabilities found. Node security advisories: ${vulnerabilities}`); + // 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) { + const messages = [ + `${unusedExceptionIds.length} of the excluded vulnerabilities did not match any of the found vulnerabilities: ${unusedExceptionIds.join(', ')}.`, + `${unusedExceptionIds.length > 1 ? 'They' : 'It'} can be removed from the .nsprc file or --exclude -x flags.`, + ]; + console.warn(messages.join(' ')); + } + // 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 + * @param {Array} exceptionIds List of vulnerability IDs to exclude */ -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 +89,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, 'exclude', '').split(',').filter(isWholeNumber).map(Number); + const exceptionIds = getExceptionsIds(nsprc, cmdExceptions); - fn(auditCommand, auditLevel, displayFullLog, exceptionIds); + fn(auditCommand, auditLevel, exceptionIds); } program.version(packageJson.version); @@ -187,19 +113,14 @@ program.version(packageJson.version); 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('-x, --exclude ', 'Exceptions or the vulnerabilities ID(s) to exclude.') .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)); + .option('-p, --production', 'Skip checking the devDependencies.') + .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 8b8edba..a95d943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "better-npm-audit", - "version": "1.12.1", + "version": "2.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -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", @@ -355,6 +353,14 @@ "readdirp": "~3.5.0" } }, + "cli-table": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.6.tgz", + "integrity": "sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==", + "requires": { + "colors": "1.0.3" + } + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -404,7 +410,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 +417,12 @@ "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==" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" }, "commander": { "version": "2.19.0", @@ -493,8 +502,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 +719,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 +1025,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 +1035,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 +1238,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 +1273,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 +1364,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 +1373,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 +1418,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 +1431,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 +1444,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 +1470,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 +1516,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 3886420..38fee11 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "better-npm-audit", - "version": "1.12.1", + "version": "2.0.4", "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.", + "description": "Reshape npm audit into the way the community would like, by the community itself, to encourage more people to do security audits.", "license": "MIT", "repository": { "type": "git", @@ -13,6 +13,8 @@ "audit", "skip", "ignore", + "exclude", + "exceptions", "node", "security", "advisory", @@ -30,14 +32,17 @@ "better-npm-audit": "index.js" }, "dependencies": { + "cli-table": "^0.3.6", "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__/exception-table-data.json b/test/__mocks__/exception-table-data.json new file mode 100644 index 0000000..fb0e4a4 --- /dev/null +++ b/test/__mocks__/exception-table-data.json @@ -0,0 +1,80 @@ +[ + [ + "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" + ] +] \ No newline at end of file diff --git a/test/__mocks__/exception-table.js b/test/__mocks__/exception-table.js new file mode 100644 index 0000000..f2d045a --- /dev/null +++ b/test/__mocks__/exception-table.js @@ -0,0 +1,20 @@ +module.exports = +`╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ +β•‘ === list of exceptions === β•‘ +β•‘ β•‘ +β•‘ ID β”‚ Status β”‚ Expiry β”‚ Notes β•‘ +β•‘ 1165 β”‚ \u001b[32mactive\u001b[39m β”‚ β”‚ β•‘ +β•‘ 1890 β”‚ \u001b[32mactive\u001b[39m β”‚ β”‚ β•‘ +β•‘ 975 β”‚ \u001b[31mexpired\u001b[39m β”‚ Thu, 11 Mar 2021 11:28:54 GMT β”‚ β•‘ +β•‘ 976 β”‚ \u001b[33minactive\u001b[39m β”‚ β”‚ β•‘ +β•‘ 985 β”‚ \u001b[32mactive\u001b[39m β”‚ β”‚ β•‘ +β•‘ 1084 β”‚ \u001b[31mexpired\u001b[39m β”‚ Thu, 11 Mar 2021 11:28:54 GMT β”‚ Inactive package; consider replacing it. β•‘ +β•‘ 1179 β”‚ \u001b[31mexpired\u001b[39m β”‚ Thu, 11 Mar 2021 11:28:54 GMT β”‚ β•‘ +β•‘ 1213 β”‚ \u001b[32mactive\u001b[39m β”‚ β”‚ Ignored since we don't use xxx method β•‘ +β•‘ 1556 β”‚ \u001b[31mexpired\u001b[39m β”‚ Thu, 11 Mar 2021 11:28:54 GMT β”‚ Issue: https://github.com/jeemok/better-npm-audit/issues/28 β•‘ +β•‘ 1651 β”‚ \u001b[31mexpired\u001b[39m β”‚ Thu, 11 Mar 2021 11:28:54 GMT β”‚ This will be fixed by the maintainers by June 14 β•‘ +β•‘ 1654 β”‚ \u001b[32mactive\u001b[39m β”‚ Fri, 31 Dec 2021 16:00:00 GMT β”‚ β•‘ +β•‘ 2100 β”‚ \u001b[32mactive\u001b[39m β”‚ β”‚ Unused β•‘ +β•‘ Note β”‚ \u001b[31minvalid\u001b[39m β”‚ β”‚ personal note β•‘ +β•šβ•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +`; 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__/v6-security-report-table-data.json b/test/__mocks__/v6-security-report-table-data.json new file mode 100644 index 0000000..f47dddc --- /dev/null +++ b/test/__mocks__/v6-security-report-table-data.json @@ -0,0 +1,90 @@ +[ + [ + "\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" + ] +] \ 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/__mocks__/v7-security-report-table-data.json b/test/__mocks__/v7-security-report-table-data.json new file mode 100644 index 0000000..ccdae02 --- /dev/null +++ b/test/__mocks__/v7-security-report-table-data.json @@ -0,0 +1,90 @@ +[ + [ + "\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" + ] +] \ No newline at end of file diff --git a/test/__mocks__/v7-security-report-table.js b/test/__mocks__/v7-security-report-table.js new file mode 100644 index 0000000..3535450 --- /dev/null +++ b/test/__mocks__/v7-security-report-table.js @@ -0,0 +1,18 @@ +module.exports = +`╔═══════════════════════════════════════════════════════════════════════════════════════════════════╗ +β•‘ === npm audit security report === β•‘ +β•‘ β•‘ +β•‘ ID β”‚ Module β”‚ Title β”‚ Sev. β”‚ URL β”‚ Ex. β•‘ +β•‘ \u001b[33m1555\u001b[39m β”‚ \u001b[33mbl\u001b[39m β”‚ \u001b[33mRemote Memory Exposure\u001b[39m β”‚ \u001b[33m\u001b[41mcritical\u001b[39m\u001b[49m β”‚ \u001b[33mhttps://npmjs.com/advisories/1555\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1213\u001b[39m β”‚ \u001b[33mdot-prop\u001b[39m β”‚ \u001b[33mPrototype Pollution\u001b[39m β”‚ \u001b[33m\u001b[41mhigh\u001b[39m\u001b[49m β”‚ \u001b[33mhttps://npmjs.com/advisories/1213\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1589\u001b[39m β”‚ \u001b[33mini\u001b[39m β”‚ \u001b[33mPrototype Pollution\u001b[39m β”‚ \u001b[33mlow\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/1589\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1523\u001b[39m β”‚ \u001b[33mlodash\u001b[39m β”‚ \u001b[33mPrototype Pollution\u001b[39m β”‚ \u001b[33mlow\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/1523\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1084\u001b[39m β”‚ \u001b[33mmem\u001b[39m β”‚ \u001b[33mDenial of Service\u001b[39m β”‚ \u001b[33mlow\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/1084\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1179\u001b[39m β”‚ \u001b[33mminimist\u001b[39m β”‚ \u001b[33mPrototype Pollution\u001b[39m β”‚ \u001b[33mlow\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/1179\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1556\u001b[39m β”‚ \u001b[33mnode-fetch\u001b[39m β”‚ \u001b[33mDenial of Service\u001b[39m β”‚ \u001b[33mlow\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/1556\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m975\u001b[39m β”‚ \u001b[33mswagger-ui\u001b[39m β”‚ \u001b[33mReverse Tabnapping\u001b[39m β”‚ \u001b[33mmoderate\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/975\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m976\u001b[39m β”‚ \u001b[33mswagger-ui\u001b[39m β”‚ \u001b[33mCross-Site Scripting\u001b[39m β”‚ \u001b[33mmoderate\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/976\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m985\u001b[39m β”‚ \u001b[33mswagger-ui\u001b[39m β”‚ \u001b[33mCross-Site Scripting\u001b[39m β”‚ \u001b[33mmoderate\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/985\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•‘ \u001b[33m1500\u001b[39m β”‚ \u001b[33myargs-parser\u001b[39m β”‚ \u001b[33mPrototype Pollution\u001b[39m β”‚ \u001b[33mlow\u001b[39m β”‚ \u001b[33mhttps://npmjs.com/advisories/1500\u001b[39m β”‚ \u001b[31mn\u001b[39m β•‘ +β•šβ•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β• +`; diff --git a/test/flags.js b/test/flags.js new file mode 100644 index 0000000..0860b08 --- /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('--exclude', () => { + 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 = { exclude: '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.exclude = '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.exclude = '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.exclude = '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.exclude = '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 = { exclude: '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..3c381bb 100644 --- a/test/index.js +++ b/test/index.js @@ -1,558 +1,129 @@ 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); - - 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 }, - ]); +const { handleFinish } = require('../index'); - 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); - - // 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); + expect(consoleStub.called).to.equal(false); + handleFinish(jsonBuffer, auditLevel, exceptionIds); + expect(consoleStub.called).to.equal(true); + expect(consoleStub.calledWith('🀝 All good!')).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, - }; + const jsonBuffer = JSON.stringify(V6_JSON_BUFFER); + const auditLevel = 'info'; + const exceptionIds = [975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]; - expect(stub.called).to.equal(false); - handleUserInput(options, stub); - expect(stub.called).to.equal(true); - - 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(); - }); + expect(processStub.called).to.equal(true); + expect(consoleErrorStub.called).to.equal(true); + expect(consoleInfoStub.called).to.equal(true); // Print security report - it('should be able to handle default command correctly', () => { - const stub = sinon.stub(); - const options = {}; + expect(processStub.calledWith(1)).to.equal(true); + expect(consoleErrorStub.calledWith('2 vulnerabilities found. Node security advisories: 1556, 1589')).to.equal(true); - 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 = []; - 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); - // 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 = []; + 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'; - let expectedDisplay = V6_LOG_REPORT.substring(0, maxLength); - expectedDisplay += '\n\n'; - expectedDisplay += '...'; - expectedDisplay += '\n\n'; - expectedDisplay += LOGS_EXCEEDED_MESSAGE; - expectedDisplay += '\n\n'; + let exceptionIds = [975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 2001]; - 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([]); - }); + expect(processStub.called).to.equal(false); + expect(consoleErrorStub.called).to.equal(false); + expect(consoleWarnStub.called).to.equal(false); + expect(consoleInfoStub.called).to.equal(false); - 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); + handleFinish(jsonBuffer, auditLevel, exceptionIds); - expect(result).to.have.length(11).and.to.deep.equal([975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 1556, 1589]); - }); + 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); - 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(consoleInfoStub.called).to.equal(true); // Print security report + expect(consoleWarnStub.called).to.equal(true); - 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]); - }); + // Message for one unused exception + // eslint-disable-next-line max-len + let message = `1 of the excluded vulnerabilities did not match any of the found vulnerabilities: 2001. It can be removed from the .nsprc file or --exclude -x flags.`; + expect(consoleWarnStub.calledWith(message)).to.equal(true); - 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); + // Message for multiple unused exceptions + exceptionIds = [975, 976, 985, 1084, 1179, 1213, 1500, 1523, 1555, 2001, 2002]; + handleFinish(jsonBuffer, auditLevel, exceptionIds); + // eslint-disable-next-line max-len + message = `2 of the excluded vulnerabilities did not match any of the found vulnerabilities: 2001, 2002. They can be removed from the .nsprc file or --exclude -x 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/print.js b/test/utils/print.js new file mode 100644 index 0000000..728b195 --- /dev/null +++ b/test/utils/print.js @@ -0,0 +1,44 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const { expect } = chai; +const { printSecurityReport, printExceptionReport } = require('../../utils/print'); +const EXCEPTION_TABLE_DATA = require('../__mocks__/exception-table-data.json'); +const EXCEPTION_TABLE = require('../__mocks__/exception-table'); +const V7_SECURITY_REPORT_TABLE_DATA = require('../__mocks__/v7-security-report-table-data.json'); +const V7_SECURITY_REPORT_TABLE = require('../__mocks__/v7-security-report-table'); + +describe('Print utils', () => { + it('should display the exception table correctly', () => { + const consoleStub = sinon.stub(console, 'info'); + + expect(consoleStub.called).to.equal(false); + + printExceptionReport(EXCEPTION_TABLE_DATA); + + expect(consoleStub.called).to.equal(true); + expect(consoleStub.firstCall.args[0]).to.equal(EXCEPTION_TABLE); + + consoleStub.restore(); + }); + + it('exception table visual', () => { + printExceptionReport(EXCEPTION_TABLE_DATA); + }); + + it('should display the security report table correctly', () => { + const consoleStub = sinon.stub(console, 'info'); + + expect(consoleStub.called).to.equal(false); + + printSecurityReport(V7_SECURITY_REPORT_TABLE_DATA); + + expect(consoleStub.called).to.equal(true); + expect(consoleStub.firstCall.args[0]).to.equal(V7_SECURITY_REPORT_TABLE); + + consoleStub.restore(); + }); + + it('security report table visual', () => { + printSecurityReport(V7_SECURITY_REPORT_TABLE_DATA); + }); +}); diff --git a/test/utils/vulnerability.js b/test/utils/vulnerability.js new file mode 100644 index 0000000..d3560a5 --- /dev/null +++ b/test/utils/vulnerability.js @@ -0,0 +1,328 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const { expect } = chai; + +const NSPRC = require('../__mocks__/nsprc'); +const EXCEPTION_TABLE_DATA = require('../__mocks__/exception-table-data.json'); +const V6_SECURITY_REPORT_TABLE_DATA = require('../__mocks__/v6-security-report-table-data.json'); +const V6_JSON_BUFFER = require('../__mocks__/v6-json-buffer.json'); +const V6_JSON_BUFFER_EMPTY = require('../__mocks__/v6-json-buffer-empty.json'); +const V7_SECURITY_REPORT_TABLE_DATA = require('../__mocks__/v7-security-report-table-data.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'); + expect(result.report).to.have.length(13).and.to.deep.equal(EXCEPTION_TABLE_DATA); + }); + + 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'); + expect(result.report).to.have.length(11).and.to.deep.equal(V6_SECURITY_REPORT_TABLE_DATA); + }); + + 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'); + + expect(result.report).to.have.length(11).and.to.deep.equal(V7_SECURITY_REPORT_TABLE_DATA); + }); + + 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..54471db --- /dev/null +++ b/utils/color.js @@ -0,0 +1,84 @@ +const get = require('lodash.get'); + +const RESET = '\x1b[0m'; +const COLORS = { + reset: { + fg: '\x1b[0m', + bg: '\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, + 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, +};