diff --git a/.travis.yml b/.travis.yml index 0a0acee..2f442b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,5 +25,5 @@ addons: token: "${SONAR_TOKEN}" script: - npm run test - - sonar-scanner + - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sonar-scanner; fi # sonar only on non-PRs - npm run stryker \ No newline at end of file diff --git a/README.md b/README.md index 76d9c96..2466b0c 100755 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Code Coverage](https://sonarcloud.io/api/project_badges/measure?project=com.zanclus%3Anpm-audit-ci-wrapper&metric=coverage)](https://sonarcloud.io/dashboard?id=com.zanclus%3Anpm-audit-ci-wrapper) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=com.zanclus%3Anpm-audit-ci-wrapper&metric=bugs)](https://sonarcloud.io/dashboard?id=com.zanclus%3Anpm-audit-ci-wrapper) [![Quality](https://sonarcloud.io/api/project_badges/measure?project=com.zanclus%3Anpm-audit-ci-wrapper&metric=sqale_index)](https://sonarcloud.io/dashboard?id=com.zanclus%3Anpm-audit-ci-wrapper) +[![Known Vulnerabilities](https://snyk.io/test/github/InfoSec812/npm-audit-ci-wrapper/badge.svg?targetFile=package.json)](https://snyk.io/test/github/InfoSec812/npm-audit-ci-wrapper?targetFile=package.json) This utility is a wrapper around `npm audit --json` which allows for finer grained control over what will cause a CI build to fail. Options include setting the severity threshold and ignoring dev dependencies. diff --git a/bin/index.js b/bin/index.js index 9c093db..d35f915 100755 --- a/bin/index.js +++ b/bin/index.js @@ -27,6 +27,11 @@ if (!check_npm_version()) { process.exit(1); } +if (threshold === -1) { + console.error(`Invalid threshold provided. Threshold must be one of the following: ${validThresholds.join(', ')}`); + process.exit(1); +} + // Build the npm audit command command = 'npm audit --json' if( registry !== null ) { diff --git a/lib/parse_args.js b/lib/parse_args.js index 69eda46..0cf15ba 100644 --- a/lib/parse_args.js +++ b/lib/parse_args.js @@ -89,10 +89,7 @@ function parse_args(cli_args) { // Define which threshold this script should cause a non-zero exit status let threshold = validThresholds.indexOf('critical'); - if ( - args.options.hasOwnProperty('threshold') && - validThresholds.indexOf(args.options.threshold.toLocaleLowerCase()) > -1 - ) { + if (args.options.hasOwnProperty('threshold')) { threshold = validThresholds.indexOf(args.options.threshold.toLocaleLowerCase()); // Set the threshold } diff --git a/lib/parse_args.test.js b/lib/parse_args.test.js index 47c799e..6aab514 100644 --- a/lib/parse_args.test.js +++ b/lib/parse_args.test.js @@ -40,7 +40,7 @@ test('Validate help output', async () => { test('Test undefined threshold', () => { const argv = ['-t', 'undef']; const { threshold } = parse_args(argv); - expect(threshold).toBe(validThresholds.indexOf('critical')); + expect(threshold).toBe(-1); }); test('Test with MODERATE threshold', () => { diff --git a/lib/parser.js b/lib/parser.js index 827437b..3f4a679 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -27,20 +27,27 @@ const { validThresholds } = require('./parse_args'); function parse_audit_results(err, stdout, threshold, ignoreDev, json_output = false, whitelist = []) { let exitCode = 0; let cli_output = ""; + const data = JSON.parse(stdout); if (err === null) { - cli_output += 'No vulnerabilities found.\n'; + if (json_output) { + data['advisories'] = {}; + data['actions'] = []; + data['muted'] = []; + cli_output = JSON.stringify(data, null, 2) + "\n"; + } else { + cli_output += 'No vulnerabilities found.\n'; + } } else { - let data = JSON.parse(stdout); let advisories = Object.entries(data.advisories); let flaggedDepenencies = filter_advisories(advisories, ignoreDev, threshold, whitelist); // If `-j` or `--json` passed, return the json data with the appropriate filters applied if (json_output) { - var retVal = data; + var retVal = JSON.parse(JSON.stringify(data)); retVal.advisories = {}; retVal.advisories = flaggedDepenencies; - cli_output = JSON.stringify(retVal) + '\n'; + cli_output = JSON.stringify(retVal, null, 2) + '\n'; } else { // If any vulnerabilities exceed the threshold and are not filtered, print the details and fail the build. if (flaggedDepenencies.length > 0) { cli_output += 'There are vulnerable dependencies which exceed the selected threshold and scope:\n'; diff --git a/lib/parser.test.js b/lib/parser.test.js index 443469d..8e9e1d9 100644 --- a/lib/parser.test.js +++ b/lib/parser.test.js @@ -32,6 +32,48 @@ test('Validate when err is NULL', () => { expect(exitCode).toBe(0); }); +/* + * When err is NULL, and JSON is requested, expect default JSON output as well + */ +test('Validate when err is NULL and JSON output is desired', () => { + const test_data = readFileSync('test_data/zero_vulnerabilities.json', 'utf8'); + let { exitCode, cli_output } = parse_audit_results(null, test_data, LOW_THRESHOLD, false, true); + const expectedOutput = { + "actions": [], + "advisories": {}, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 0, + "high": 0, + "critical": 0 + }, + "dependencies": 879278, + "devDependencies": 387, + "optionalDependencies": 9709, + "totalDependencies": 889374 + }, + "runId": "3fdcb3d6-c9f3-4e6f-9e4f-c77d1e0dac86" + } + const actualObject = JSON.parse(cli_output); + expect(actualObject.actions).toEqual([]); + expect(actualObject.advisories).toEqual({}); + expect(actualObject.muted).toEqual([]); + expect(actualObject.metadata.vulnerabilities.info).toEqual(0); + expect(actualObject.metadata.vulnerabilities.low).toEqual(0); + expect(actualObject.metadata.vulnerabilities.moderate).toEqual(0); + expect(actualObject.metadata.vulnerabilities.high).toEqual(0); + expect(actualObject.metadata.vulnerabilities.critical).toEqual(0); + expect(actualObject.metadata.dependencies).toBeDefined(); + expect(actualObject.metadata.devDependencies).toBeDefined(); + expect(actualObject.metadata.optionalDependencies).toBeDefined(); + expect(actualObject.metadata.totalDependencies).toBeDefined(); + expect(actualObject.runId).toBeDefined(); + expect(exitCode).toBe(0); +}); + /* * When there are 0 vulnerable dependencies, expect a 0 exit code */ @@ -61,10 +103,17 @@ test('Validate run with 7 vulnerabilities', () => { test('Validate run with 7 vulnerabilities and JSON output', () => { const test_data = readFileSync('test_data/vue_js_app.json', 'utf8'); let { exitCode, cli_output } = parse_audit_results("", test_data, LOW_THRESHOLD, true, true); + const actualObject = JSON.parse(cli_output); + console.log(cli_output+"\n\n"); expect(cli_output).toContain('"https-proxy-agent"'); expect(cli_output).toContain('"version"'); expect(cli_output).toContain('"module_name"'); expect(cli_output.substring((cli_output.length - 1), cli_output.length)).toBe('\n'); + expect(actualObject.metadata.dependencies).toBeDefined(); + expect(actualObject.metadata.devDependencies).toBeDefined(); + expect(actualObject.metadata.optionalDependencies).toBeDefined(); + expect(actualObject.metadata.totalDependencies).toBeDefined(); + expect(actualObject.runId).toBeDefined(); expect(exitCode).toBe(0); }); diff --git a/package.json b/package.json index 62e76d8..54f395a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "npm-audit-ci-wrapper", - "version": "2.1.8", + "version": "2.2.1", "description": "A wrapper for 'npm audit' which can be configurable for use in a CI/CD tool like Jenkins", "keywords": [ "npm", @@ -36,6 +36,7 @@ "@stryker-mutator/jest-runner": "^1.0.2", "capture-stdout": "^1.0.0", "jest": "^24.1.0", + "jest-cli": "^24.8.0", "jest-html-reporter": "^2.5.0", "stryker-cli": "^1.0.0", "stryker-jest-runner": "^1.4.1" diff --git a/test_data/vue_js_app.json b/test_data/vue_js_app.json index a73d7bd..5ea811a 100644 --- a/test_data/vue_js_app.json +++ b/test_data/vue_js_app.json @@ -312,5 +312,20 @@ }, "url": "https://npmjs.com/advisories/755" } - } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 0, + "high": 0, + "critical": 0 + }, + "dependencies": 1, + "devDependencies": 74642, + "optionalDependencies": 396, + "totalDependencies": 74643 + }, + "runId": "13b24789-ae58-468a-83ff-7eb3faafa8ae" } \ No newline at end of file