From 6df6810663dffd4170b9c0ee83511cf440b24e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Wed, 16 May 2018 16:47:46 -0700 Subject: [PATCH] feat(detail): consistified full report with install report (#15) --- reporters/detail.js | 46 ++- reporters/install.js | 33 ++- test/detail-report-test.js | 75 +++-- test/fixtures/one-vuln-install-ismajor.json | 54 ++++ test/fixtures/one-vuln-one-pkg.json | 64 ++++ test/fixtures/some-vulns-ismajor.json | 310 ++++++++++++++++++++ 6 files changed, 525 insertions(+), 57 deletions(-) create mode 100644 test/fixtures/one-vuln-install-ismajor.json create mode 100644 test/fixtures/one-vuln-one-pkg.json create mode 100644 test/fixtures/some-vulns-ismajor.json diff --git a/reporters/detail.js b/reporters/detail.js index 7d02bb9..f8a462b 100644 --- a/reporters/detail.js +++ b/reporters/detail.js @@ -1,5 +1,6 @@ 'use strict' +const summary = require('./install.js').summary const Table = require('cli-table2') const Utils = require('../lib/utils') @@ -35,31 +36,46 @@ const report = function (data, options) { output = output + value + '\n' } - const footer = function (metadata) { + const footer = function (data) { let total = 0 const sev = [] - const keys = Object.keys(metadata.vulnerabilities) + const keys = Object.keys(data.metadata.vulnerabilities) for (let key of keys) { - const value = metadata.vulnerabilities[key] + const value = data.metadata.vulnerabilities[key] total = total + value if (value > 0) { sev.push([key, value]) } } - const severities = sev.map((value) => { - return `${value[1]} ${Utils.severityLabel(value[0], false)}` - }).join(' | ') - if (total > 0) { exit = 1 } - if (total === 0) { - log(`${Utils.color('[+]', 'brightGreen', config.withColor)} no known vulnerabilities found`) - log(` Packages audited: ${data.metadata.totalDependencies} (${data.metadata.devDependencies} dev, ${data.metadata.optionalDependencies} optional)`) - } else { - log(`\n${Utils.color('[!]', 'brightRed', config.withColor)} ${total} ${total === 1 ? 'vulnerability' : 'vulnerabilities'} found - Packages audited: ${data.metadata.totalDependencies} (${data.metadata.devDependencies} dev, ${data.metadata.optionalDependencies} optional)`) - log(` Severity: ${severities}`) + log(`${summary(data, config)} in ${data.metadata.totalDependencies} scanned package${data.metadata.totalDependencies === 1 ? '' : 's'}`) + if (total) { + const counts = data.actions.reduce((acc, {action, isMajor, resolves}) => { + if (action === 'update' || (action === 'install' && !isMajor)) { + resolves.forEach(({id}) => acc.advisories.add(id)) + } + if (isMajor) { + resolves.forEach(({id}) => acc.major.add(id)) + } + if (action === 'review') { + resolves.forEach(({id}) => acc.review.add(id)) + } + return acc + }, {advisories: new Set(), major: new Set(), review: new Set()}) + if (counts.advisories.size) { + log(` run \`npm audit fix\` to fix ${counts.advisories.size} of them.`) + } + if (counts.major.size) { + const maj = counts.major.size + log(` ${maj} vulnerabilit${maj === 1 ? 'y' : 'ies'} require${maj === 1 ? 's' : ''} semver-major dependency updates.`) + } + if (counts.review.size) { + const rev = counts.review.size + log(` ${rev} vulnerabilit${rev === 1 ? 'y' : 'ies'} require${rev === 1 ? 's' : ''} manual review. See the full report for details.`) + } } } @@ -163,10 +179,10 @@ const report = function (data, options) { } actions(data, config) - footer(data.metadata) + footer(data) return { - report: output, + report: output.trim(), exitCode: exit } } diff --git a/reporters/install.js b/reporters/install.js index 014420c..00d3583 100644 --- a/reporters/install.js +++ b/reporters/install.js @@ -2,7 +2,25 @@ const Utils = require('../lib/utils') -const report = function (data, options) { +module.exports = report +function report (data, options) { + let msg = summary(data, options) + if (!Object.keys(data.advisories).length) { + return { + report: msg, + exitCode: 0 + } + } else { + msg += '\n run `npm audit fix` to fix them, or `npm audit` for details' + return { + report: msg, + exitCode: 1 + } + } +} + +module.exports.summary = summary +function summary (data, options) { const defaults = { severityThreshold: 'info' } @@ -23,10 +41,7 @@ const report = function (data, options) { if (Object.keys(data.advisories).length === 0) { log(`${green('0')} vulnerabilities`) - return { - report: output.trim(), - exitCode: 0 - } + return output } else { let total = 0 const sev = [] @@ -50,12 +65,6 @@ const report = function (data, options) { const vulnLabel = Utils.severityLabel(sev[0][0], config.withColor).toLowerCase() log(`${vulnCount} ${vulnLabel} severity vulnerabilit${vulnCount === 1 ? 'y' : 'ies'}`) } - log(' run `npm audit fix` to fix them, or `npm audit` for details') - return { - report: output.trim(), - exitCode: 1 - } } + return output.trim() } - -module.exports = report diff --git a/test/detail-report-test.js b/test/detail-report-test.js index ef14c1b..b00fce4 100644 --- a/test/detail-report-test.js +++ b/test/detail-report-test.js @@ -7,72 +7,87 @@ const Keyfob = require('keyfob') const fixtures = Keyfob.load({ path: 'test/fixtures', fn: require }) tap.test('it generates a detail report with no vulns', function (t) { - return Report(fixtures['no-vulns'], {reporter: 'detail'}).then((report) => { - t.match(report.exitCode, 0) - t.match(report.report, /no known vulnerabilities found/) - t.match(report.report, /Packages audited: 918 \(466 dev, 77 optional\)/) + return Report(fixtures['no-vulns'], {reporter: 'detail', withColor: false}).then((report) => { + t.match(report.exitCode, 0, 'successful exit code') + t.match(report.report, /found 0 vulnerabilities/, 'no vulns reported') + t.match(report.report, /918 scanned packages/, 'reports scanned count') }) }) tap.test('it generates a detail report with one vuln (update action)', function (t) { - return Report(fixtures['one-vuln'], {reporter: 'detail'}).then((report) => { - t.match(report.exitCode, 1) - t.match(report.report, /npm update tough-cookie --depth 6/) + return Report(fixtures['one-vuln-one-pkg'], {reporter: 'detail'}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /npm update tough-cookie --depth 6/, 'recommends update command with --depth') + t.match(report.report, /1 scanned package/, 'reports a single scanned pkg') }) }) tap.test('it generates a detail report with one vuln (install action)', function (t) { return Report(fixtures['one-vuln-install'], {reporter: 'detail'}).then((report) => { - t.match(report.exitCode, 1) - t.match(report.report, /npm install knex@3.0.0/) + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /npm install knex@3\.0\.0/, 'recommends install command') + }) +}) + +tap.test('it adds a message if a dep isMajor (one vuln)', function (t) { + return Report(fixtures['one-vuln-install-ismajor'], {reporter: 'detail', withColor: false}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /1 vulnerability requires semver-major dependency updates/, 'reports one semver-major bump') + }) +}) + +tap.test('it adds a message if a dep isMajor (multiple vulns)', function (t) { + return Report(fixtures['some-vulns-ismajor'], {reporter: 'detail', withColor: false}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /2 vulnerabilities require semver-major dependency updates/, 'reports multiple semver-major bumps') }) }) tap.test('it generates a detail report with one vuln (install dev dep)', function (t) { return Report(fixtures['one-vuln-dev'], {reporter: 'detail'}).then((report) => { - t.match(report.exitCode, 1) - t.match(report.report, /npm install --save-dev knex@3.0.0/) + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /npm install --save-dev knex@3\.0\.0/, 'adds --save-dev to recommendation') }) }) tap.test('it generates a detail report with one vuln (review dev dep)', function (t) { return Report(fixtures['one-vuln-dev-review'], {reporter: 'detail'}).then((report) => { - t.match(report.exitCode, 1) - t.match(report.report, /knex \[dev\]/) - t.match(report.report, /Manual Review/) + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /knex \[dev\]/, 'mentions the dep and tags it as dev') + t.match(report.report, /Manual Review/, 'reports a manual review requirement') }) }) tap.test('it generates a detail report with one vuln, no color', function (t) { return Report(fixtures['one-vuln'], {reporter: 'detail', withColor: false}).then((report) => { - t.match(report.exitCode, 1) - t.match(report.report, /# Run {2}npm update tough-cookie --depth 6 {2}to resolve 1 vulnerability/) + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /# Run {2}npm update tough-cookie --depth 6 {2}to resolve 1 vulnerability/, 'individual update command printed') }) }) tap.test('it generates a detail report with one vuln, no unicode', function (t) { return Report(fixtures['one-vuln'], {reporter: 'detail', withUnicode: false}).then((report) => { - t.match(report.exitCode, 1) - t.notMatch(report.report, /┬/) + t.equal(report.exitCode, 1, 'non-zero exit code') + t.notMatch(report.report, /┬/, 'prints a fancy table') }) }) tap.test('it generates a detail report with some vulns', function (t) { - return Report(fixtures['some-vulns'], {reporter: 'detail'}).then((report) => { - t.match(report.exitCode, 1) - t.match(report.report, /Manual Review/) - t.match(report.report, /12 vulnerabilities found/) - t.match(report.report, /9 Low \| 3 High/) - t.match(report.report, /Denial of Service/) - t.match(report.report, /Cryptographically Weak PRNG/) + return Report(fixtures['some-vulns'], {reporter: 'detail', withColor: false}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /Manual Review/, 'expects manual review') + t.match(report.report, /found 12 vulnerabilities/, 'reports vuln count') + t.match(report.report, /9 low, 3 high/, 'severity breakdown reported') + t.match(report.report, /Denial of Service/, 'mentions one vuln title') + t.match(report.report, /Cryptographically Weak PRNG/, 'mentions the other vuln title') }) }) tap.test('it generates a detail report with review vulns, no unicode', function (t) { - return Report(fixtures['update-review'], {reporter: 'detail', withUnicode: false}).then((report) => { - t.match(report.exitCode, 1) - t.notMatch(report.report, /┬/) - t.match(report.report, /Manual Review/) - t.match(report.report, /1 Low | 1 Moderate | 1 Critical/) + return Report(fixtures['update-review'], {reporter: 'detail', withUnicode: false, withColor: false}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.notMatch(report.report, /┬/, 'unicode table not printed') + t.match(report.report, /Manual Review/, 'manual review reported') + t.match(report.report, /1 low, 1 moderate, 1 critical/, 'severity breakdown reported') }) }) diff --git a/test/fixtures/one-vuln-install-ismajor.json b/test/fixtures/one-vuln-install-ismajor.json new file mode 100644 index 0000000..284b8a7 --- /dev/null +++ b/test/fixtures/one-vuln-install-ismajor.json @@ -0,0 +1,54 @@ +{ + "actions": [{ + "action": "install", + "module": "knex", + "target": "3.0.0", + "isMajor": true, + "resolves": [{ + "id": 525, + "path": "knex>liftoff>findup-sync>micromatch>braces>expand-range>fill-range>randomatic", + "dev": false, + "optional": false + }] + }], + "advisories": { + "525": { + "id": 525, + "created": "2017-09-08T18:07:02.061Z", + "updated": "2017-09-22T16:26:08.422Z", + "deleted": null, + "title": "Regular Expression Denial of Service", + "found_by": { + "name": "nobody" + }, + "reported_by": { + "name": "testdata" + }, + "module_name": "knex", + "cves": [ + "CVE-2017-16112" + ], + "vulnerable_versions": "<3.0.0", + "patched_versions": ">=3.0.0", + "overview": "something here", + "recommendation": "Please update to version 3.0.0 or greater", + "references": "- https://github.com/salesforce/tough-cookie/issues/92", + "access": "public", + "severity": "high", + "cwe": "CWE-400" + } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "low": 0, + "moderate": 0, + "high": 1, + "critical": 0 + }, + "dependencies": 375, + "devDependencies": 466, + "optionalDependencies": 87, + "totalDependencies": 918 + } +} \ No newline at end of file diff --git a/test/fixtures/one-vuln-one-pkg.json b/test/fixtures/one-vuln-one-pkg.json new file mode 100644 index 0000000..fdd0668 --- /dev/null +++ b/test/fixtures/one-vuln-one-pkg.json @@ -0,0 +1,64 @@ +{ + "actions": [ + { + "action": "update", + "module": "tough-cookie", + "depth": 6, + "target": "2.3.4", + "resolves": [ + { + "id": 525, + "path": "@npm/spife>chokidar>fsevents>node-pre-gyp>request>tough-cookie", + "dev": false, + "optional": false + } + ] + } + ], + "advisories": { + "525": { + "id": 525, + "created": "2017-09-08T18:07:02.061Z", + "updated": "2017-09-22T16:26:08.422Z", + "deleted": null, + "title": "Regular Expression Denial of Service", + "found_by": { + "name": "Cristian-Alexandru Staicu" + }, + "reported_by": { + "name": "Cristian-Alexandru Staicu" + }, + "module_name": "tough-cookie", + "cves": [ + "CVE-2017-16112" + ], + "vulnerable_versions": "<2.3.3", + "patched_versions": ">=2.3.3", + "overview": "The tough-cookie module is vulnerable to regular expression denial of service. Input of around 50k characters is required for a slow down of around 2 seconds.\n\nUnless node was compiled using the -DHTTP_MAX_HEADER_SIZE= option the default header max length is 80kb so the impact of the ReDoS is limited to around 7.3 seconds of blocking.\n\nAt the time of writing all version <=2.3.2 are vulnerable", + "recommendation": "Please update to version 2.3.3 or greater", + "references": "- https://github.com/salesforce/tough-cookie/issues/92", + "access": "public", + "severity": "high", + "cwe": "CWE-400", + "metadata": { + "module_type": "", + "exploitability": 5, + "affected_components": "" + } + } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 0, + "high": 1, + "critical": 0 + }, + "dependencies": 1, + "devDependencies": 0, + "optionalDependencies": 0, + "totalDependencies": 1 + } +} diff --git a/test/fixtures/some-vulns-ismajor.json b/test/fixtures/some-vulns-ismajor.json new file mode 100644 index 0000000..ac84d38 --- /dev/null +++ b/test/fixtures/some-vulns-ismajor.json @@ -0,0 +1,310 @@ +{ + "actions": [ + { + "action": "install", + "module": "tough-cookie", + "target": "2.3.4", + "isMajor": true, + "resolves": [ + { + "id": 525, + "path": "@npm/spife>chokidar>fsevents>node-pre-gyp>request>tough-cookie", + "dev": false, + "optional": false + } + ] + }, + { + "action": "install", + "module": "debug", + "isMajor": true, + "target": "2.6.9", + "resolves": [ + { + "id": 534, + "path": "standard>eslint>debug", + "dev": true, + "optional": false + }, + { + "id": 534, + "path": "standard>eslint-plugin-import>debug", + "dev": true, + "optional": false + }, + { + "id": 534, + "path": "standard>eslint-plugin-import>eslint-import-resolver-node>debug", + "dev": true, + "optional": false + }, + { + "id": 534, + "path": "tap>tap-mocha-reporter>debug", + "dev": true, + "optional": false + }, + { + "id": 534, + "path": "@npm/spife>chokidar>fsevents>node-pre-gyp>tar-pack>debug", + "dev": false, + "optional": false + } + ] + }, + { + "action": "update", + "module": "tunnel-ssh", + "depth": 2, + "target": "4.1.4", + "resolves": [ + { + "id": 534, + "path": "db-migrate>tunnel-ssh>debug", + "dev": true, + "optional": false + } + ] + }, + { + "action": "update", + "module": "test-exclude", + "depth": 3, + "target": "4.2.1", + "resolves": [ + { + "id": 157, + "path": "tap>nyc>test-exclude>micromatch>braces>expand-range>fill-range>randomatic", + "dev": true, + "optional": false + } + ] + }, + { + "action": "review", + "module": "ws", + "resolves": [ + { + "id": 550, + "path": "@npm/spife>numbat-emitter>ws", + "dev": false, + "optional": false + }, + { + "id": 550, + "path": "@npm/spife>numbat-process>numbat-emitter>ws", + "dev": false, + "optional": false + } + ] + }, + { + "action": "review", + "module": "randomatic", + "resolves": [ + { + "id": 157, + "path": "@npm/spife>chokidar>anymatch>micromatch>braces>expand-range>fill-range>randomatic", + "dev": false, + "optional": false + } + ] + } + ], + "advisories": { + "157": { + "findings": [ + { + "version": "1.1.7", + "paths": [ + "tap>nyc>micromatch>braces>expand-range>fill-range>randomatic", + "tap>nyc>test-exclude>micromatch>braces>expand-range>fill-range>randomatic" + ], + "dev": true, + "optional": false + }, + { + "version": "1.1.7", + "paths": [ + "@npm/spife>chokidar>anymatch>micromatch>braces>expand-range>fill-range>randomatic" + ], + "dev": false, + "optional": false + } + ], + "id": 157, + "created": "2016-11-09T20:03:19.000Z", + "updated": "2018-03-02T23:21:42.009Z", + "deleted": null, + "title": "Cryptographically Weak PRNG", + "found_by": { + "name": "Sven Slootweg" + }, + "reported_by": { + "name": "Sven Slootweg" + }, + "module_name": "randomatic", + "cves": [ + "CVE-2017-16028" + ], + "vulnerable_versions": "<3.0.0", + "patched_versions": ">=3.0.0", + "overview": "Affected versions of `randomatic` generate random values using a cryptographically weak psuedo-random number generator. This may result in predictable values instead of random values as intended.\r\n\r\n", + "recommendation": "Update to version 3.0.0 or later.\r\n", + "references": "[Commit #4a52695](https://github.com/jonschlinkert/randomatic/commit/4a526959b3a246ae8e4a82f9c182180907227fe1#diff-b9cfc7f2cdf78a7f4b91a753d10865a2)", + "access": "public", + "severity": "low", + "cwe": "CWE-330", + "metadata": { + "module_type": "Multi.Library", + "exploitability": 5, + "affected_components": "" + } + }, + "525": { + "findings": [ + { + "version": "2.3.2", + "paths": [ + "@npm/spife>chokidar>fsevents>node-pre-gyp>request>tough-cookie" + ], + "dev": false, + "optional": false + } + ], + "id": 525, + "created": "2017-09-08T18:07:02.061Z", + "updated": "2017-09-22T16:26:08.422Z", + "deleted": null, + "title": "Regular Expression Denial of Service", + "found_by": { + "name": "Cristian-Alexandru Staicu" + }, + "reported_by": { + "name": "Cristian-Alexandru Staicu" + }, + "module_name": "tough-cookie", + "cves": [ + "CVE-2017-16112" + ], + "vulnerable_versions": "<2.3.3", + "patched_versions": ">=2.3.3", + "overview": "The tough-cookie module is vulnerable to regular expression denial of service. Input of around 50k characters is required for a slow down of around 2 seconds.\n\nUnless node was compiled using the -DHTTP_MAX_HEADER_SIZE= option the default header max length is 80kb so the impact of the ReDoS is limited to around 7.3 seconds of blocking.\n\nAt the time of writing all version <=2.3.2 are vulnerable", + "recommendation": "Please update to version 2.3.3 or greater", + "references": "- https://github.com/salesforce/tough-cookie/issues/92", + "access": "public", + "severity": "high", + "cwe": "CWE-400", + "metadata": { + "module_type": "", + "exploitability": 5, + "affected_components": "" + } + }, + "534": { + "findings": [ + { + "version": "2.6.0", + "paths": [ + "db-migrate>tunnel-ssh>debug", + "standard>eslint>debug", + "standard>eslint-plugin-import>debug", + "standard>eslint-plugin-import>eslint-import-resolver-node>debug", + "tap>tap-mocha-reporter>debug" + ], + "dev": true, + "optional": false + }, + { + "version": "2.6.8", + "paths": [ + "@npm/spife>chokidar>fsevents>node-pre-gyp>tar-pack>debug" + ], + "dev": false, + "optional": false + } + ], + "id": 534, + "created": "2017-09-25T18:55:55.956Z", + "updated": "2017-09-27T18:24:24.491Z", + "deleted": null, + "title": "Regular Expression Denial of Service", + "found_by": { + "name": "Cristian-Alexandru Staicu" + }, + "reported_by": { + "name": "Cristian-Alexandru Staicu" + }, + "module_name": "debug", + "cves": [ + "CVE-2017-16137" + ], + "vulnerable_versions": "<= 2.6.8 || >= 3.0.0 <= 3.0.1", + "patched_versions": ">= 2.6.9 < 3.0.0 || >= 3.1.0", + "overview": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.", + "recommendation": "Upgrade to version 2.6.9 or greater if you are on the 2.6.x series or 3.1.0 or greater.", + "references": "- https://github.com/visionmedia/debug/issues/501\n- https://github.com/visionmedia/debug/pull/504", + "access": "public", + "severity": "low", + "cwe": "CWE-400", + "metadata": { + "module_type": "", + "exploitability": 5, + "affected_components": "" + } + }, + "550": { + "findings": [ + { + "version": "3.1.0", + "paths": [ + "@npm/spife>numbat-emitter>ws", + "@npm/spife>numbat-process>numbat-emitter>ws" + ], + "dev": false, + "optional": false + } + ], + "id": 550, + "created": "2017-11-08T19:25:17.211Z", + "updated": "2017-11-10T17:26:26.645Z", + "deleted": null, + "title": "Denial of Service", + "found_by": { + "name": "Nick Starke, Ryan Knell" + }, + "reported_by": { + "name": "Nick Starke, Ryan Knell" + }, + "module_name": "ws", + "cves": null, + "vulnerable_versions": "<=99.999.99999", + "patched_versions": "< 0.0.0", + "overview": "A specially crafted value of the `Sec-WebSocket-Extensions` header that used `Object.prototype` property names as extension or parameter names could be used to make a ws server crash.\n\nProof of concept:\n\n```\nconst WebSocket = require('ws');\nconst net = require('net');\n\nconst wss = new WebSocket.Server({ port: 3000 }, function () {\n const payload = 'constructor'; // or ',;constructor'\n\n const request = [\n 'GET / HTTP/1.1',\n 'Connection: Upgrade',\n 'Sec-WebSocket-Key: test',\n 'Sec-WebSocket-Version: 8',\n `Sec-WebSocket-Extensions: ${payload}`,\n 'Upgrade: websocket',\n '\\r\\n'\n ].join('\\r\\n');\n\n const socket = net.connect(3000, function () {\n socket.resume();\n socket.write(request);\n });\n});\n```", + "recommendation": "Upgrade to version 3.3.1 or greater", + "references": "- https://github.com/websockets/ws/commit/c4fe46608acd61fbf7397eadc47378903f95b78a\n- https://github.com/websockets/ws/releases/tag/3.3.1", + "access": "public", + "severity": "high", + "cwe": "CWE-20", + "metadata": { + "module_type": "", + "exploitability": 5, + "affected_components": "" + } + } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 9, + "moderate": 0, + "high": 3, + "critical": 0 + }, + "dependencies": 375, + "devDependencies": 466, + "optionalDependencies": 87, + "totalDependencies": 918 + } +}