From 5e27893612ac34a808f488de57f73135819f6f77 Mon Sep 17 00:00:00 2001 From: Lars Willighagen Date: Thu, 16 Aug 2018 01:32:20 +0200 Subject: [PATCH] fix(parseable): add support for critical vulns and more resolves on update/install action (#28) Fixes: https://npm.community/t/1419 --- reporters/parseable.js | 23 +++-- test/fixtures/some-same-action.json | 127 +++++++++++++++++++++++++ test/fixtures/some-vulns-critical.json | 78 +++++++++++++++ test/parseable-report-test.js | 31 ++++-- 4 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 test/fixtures/some-same-action.json create mode 100644 test/fixtures/some-vulns-critical.json diff --git a/reporters/parseable.js b/reporters/parseable.js index 3633597..9a6e8cd 100644 --- a/reporters/parseable.js +++ b/reporters/parseable.js @@ -11,6 +11,7 @@ const report = function (data, options) { const actions = function (data, config) { let accumulator = { + critical: '', high: '', moderate: '', low: '' @@ -25,16 +26,18 @@ const report = function (data, options) { l.recommendation = recommendation.cmd l.breaking = recommendation.isBreaking ? 'Y' : 'N' - // TODO: Verify: The advisory seems to repeat and be the same for all the 'resolves'. Is it true? - const advisory = data.advisories[action.resolves[0].id] - l.sevLevel = advisory.severity - l.severity = advisory.title - l.package = advisory.module_name - l.moreInfo = `https://nodesecurity.io/advisories/${advisory.id}` - l.path = action.resolves[0].path + action.resolves.forEach((resolution) => { + const advisory = data.advisories[resolution.id] - accumulator[advisory.severity] += [action.action, l.package, l.sevLevel, l.recommendation, l.severity, l.moreInfo, l.path, l.breaking] - .join('\t') + '\n' + l.sevLevel = advisory.severity + l.severity = advisory.title + l.package = advisory.module_name + l.moreInfo = `https://nodesecurity.io/advisories/${advisory.id}` + l.path = resolution.path + + accumulator[advisory.severity] += [action.action, l.package, l.sevLevel, l.recommendation, l.severity, l.moreInfo, l.path, l.breaking] + .join('\t') + '\n' + }) // forEach resolves } if (action.action === 'review') { @@ -53,7 +56,7 @@ const report = function (data, options) { } // is review }) // forEach actions } - return accumulator['high'] + accumulator['moderate'] + accumulator['low'] + return accumulator['critical'] + accumulator['high'] + accumulator['moderate'] + accumulator['low'] } const exitCode = function (metadata) { diff --git a/test/fixtures/some-same-action.json b/test/fixtures/some-same-action.json new file mode 100644 index 0000000..7b0772a --- /dev/null +++ b/test/fixtures/some-same-action.json @@ -0,0 +1,127 @@ +{ + "actions": [ + { + "action": "install", + "module": "mocha-jenkins-reporter", + "target": "0.3.12", + "isMajor": false, + "resolves": [ + { + "id": 534, + "path": "mocha-jenkins-reporter>mocha>debug", + "dev": true, + "optional": false, + "bundled": false + }, + { + "id": 146, + "path": "mocha-jenkins-reporter>mocha>growl", + "dev": true, + "optional": false, + "bundled": false + } + ] + } + ], + "advisories": { + "146": { + "findings": [ + { + "version": "1.9.2", + "paths": [ + "mocha-jenkins-reporter>mocha>growl" + ], + "dev": true, + "optional": false, + "bundled": false + } + ], + "id": 146, + "created": "2016-09-06T12:49:40.000Z", + "updated": "2018-03-02T21:07:28.071Z", + "deleted": null, + "title": "Command Injection", + "found_by": { + "name": "Cristian-Alexandru Staicu" + }, + "reported_by": { + "name": "Cristian-Alexandru Staicu" + }, + "module_name": "growl", + "cves": [ + "CVE-2017-16042" + ], + "vulnerable_versions": "<1.10.2", + "patched_versions": ">=1.10.2", + "overview": "Affected versions of `growl` do not properly sanitize input prior to passing it into a shell command, allowing for arbitrary command execution.", + "recommendation": "Update to version 1.10.2 or later.", + "references": "[Issue #60](https://github.com/tj/node-growl/issues/60)\n[PR #61](https://github.com/tj/node-growl/pull/61)", + "access": "public", + "severity": "critical", + "cwe": "CWE-94", + "metadata": { + "module_type": "CLI.Library", + "exploitability": 5, + "affected_components": "" + }, + "url": "https://nodesecurity.io/advisories/146" + }, + "534": { + "findings": [ + { + "version": "2.6.8", + "paths": [ + "mocha-jenkins-reporter>mocha>debug" + ], + "dev": true, + "optional": false, + "bundled": false + } + ], + "id": 534, + "created": "2017-09-25T18:55:55.956Z", + "updated": "2018-05-16T19:37:43.686Z", + "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": "Affected versions of `debug` are vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. \n\nAs it takes 50,000 characters to block the event loop for 2 seconds, this issue is a low severity issue.", + "recommendation": "Version 2.x.x: Update to version 2.6.9 or later.\nVersion 3.x.x: Update to version 3.1.0 or later.\n", + "references": "- [Issue #501](https://github.com/visionmedia/debug/issues/501)\n- [PR #504](https://github.com/visionmedia/debug/pull/504)", + "access": "public", + "severity": "low", + "cwe": "CWE-400", + "metadata": { + "module_type": "", + "exploitability": 5, + "affected_components": "" + }, + "url": "https://nodesecurity.io/advisories/534" + } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 1, + "moderate": 0, + "high": 0, + "critical": 1 + }, + "dependencies": 0, + "devDependencies": 43, + "optionalDependencies": 0, + "totalDependencies": 43 + }, + "runId": "ab9f276f-15b6-4034-a7a2-f0af6d4420f3" +} diff --git a/test/fixtures/some-vulns-critical.json b/test/fixtures/some-vulns-critical.json new file mode 100644 index 0000000..8639329 --- /dev/null +++ b/test/fixtures/some-vulns-critical.json @@ -0,0 +1,78 @@ +{ + "actions": [ + { + "action": "install", + "module": "mocha-jenkins-reporter", + "target": "0.3.12", + "isMajor": false, + "resolves": [ + { + "id": 146, + "path": "mocha-jenkins-reporter>mocha>growl", + "dev": true, + "optional": false, + "bundled": false + } + ] + } + ], + "advisories": { + "146": { + "findings": [ + { + "version": "1.9.2", + "paths": [ + "mocha-jenkins-reporter>mocha>growl" + ], + "dev": true, + "optional": false, + "bundled": false + } + ], + "id": 146, + "created": "2016-09-06T12:49:40.000Z", + "updated": "2018-03-02T21:07:28.071Z", + "deleted": null, + "title": "Command Injection", + "found_by": { + "name": "Cristian-Alexandru Staicu" + }, + "reported_by": { + "name": "Cristian-Alexandru Staicu" + }, + "module_name": "growl", + "cves": [ + "CVE-2017-16042" + ], + "vulnerable_versions": "<1.10.2", + "patched_versions": ">=1.10.2", + "overview": "Affected versions of `growl` do not properly sanitize input prior to passing it into a shell command, allowing for arbitrary command execution.", + "recommendation": "Update to version 1.10.2 or later.", + "references": "[Issue #60](https://github.com/tj/node-growl/issues/60)\n[PR #61](https://github.com/tj/node-growl/pull/61)", + "access": "public", + "severity": "critical", + "cwe": "CWE-94", + "metadata": { + "module_type": "CLI.Library", + "exploitability": 5, + "affected_components": "" + }, + "url": "https://nodesecurity.io/advisories/146" + } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 1, + "moderate": 0, + "high": 0, + "critical": 1 + }, + "dependencies": 0, + "devDependencies": 43, + "optionalDependencies": 0, + "totalDependencies": 43 + }, + "runId": "ab9f276f-15b6-4034-a7a2-f0af6d4420f3" +} diff --git a/test/parseable-report-test.js b/test/parseable-report-test.js index c0fe221..4cbe584 100644 --- a/test/parseable-report-test.js +++ b/test/parseable-report-test.js @@ -6,21 +6,21 @@ 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) { +tap.test('it generates a parseable report with no vulns', function (t) { return Report(fixtures['no-vulns'], {reporter: 'parseable'}).then((report) => { t.match(report.exitCode, 0, 'successful exit code') t.equal(report.report.length, 0, 'no vulns reported') }) }) -tap.test('it generates a detail report with one vuln (update action)', function (t) { +tap.test('it generates a parseable report with one vuln (update action)', function (t) { return Report(fixtures['one-vuln-one-pkg'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /\tnpm update tough-cookie --depth 6/, 'recommends update command with --depth') }) }) -tap.test('it generates a detail report with one vuln (update action)', function (t) { +tap.test('it generates a parseable report with one vuln (update action)', function (t) { return Report(fixtures['one-vuln'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /^update/) @@ -28,7 +28,7 @@ tap.test('it generates a detail report with one vuln (update action)', function }) }) -tap.test('it generates a detail report with one vuln (install action)', function (t) { +tap.test('it generates a parseable report with one vuln (install action)', function (t) { return Report(fixtures['one-vuln-install'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /^install/) @@ -50,14 +50,14 @@ tap.test('it adds a message if a dep isMajor (multiple vulns)', function (t) { }) }) -tap.test('it generates a detail report with one vuln (install dev dep)', function (t) { +tap.test('it generates a parseable report with one vuln (install dev dep)', function (t) { return Report(fixtures['one-vuln-dev'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /npm install --save-dev knex@3.0.0/) }) }) -tap.test('it generates a detail report with one vuln (review dev dep)', function (t) { +tap.test('it generates a parseable report with one vuln (review dev dep)', function (t) { return Report(fixtures['one-vuln-dev-review'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /review\t/, 'expects manual review') @@ -65,7 +65,7 @@ tap.test('it generates a detail report with one vuln (review dev dep)', function }) }) -tap.test('it generates a detail report with some vulns', function (t) { +tap.test('it generates a parseable report with some vulns', function (t) { return Report(fixtures['some-vulns'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /review\t/, 'expects manual review') @@ -76,9 +76,24 @@ tap.test('it generates a detail report with some vulns', function (t) { }) }) -tap.test('it generates a detail report with review vulns', function (t) { +tap.test('it generates a parseable report with review vulns', function (t) { return Report(fixtures['update-review'], {reporter: 'parseable'}).then((report) => { t.equal(report.exitCode, 1, 'non-zero exit code') t.match(report.report, /review\t/, 'expects manual review') }) }) + +tap.test('it generates a parseable report with critical vulns', function (t) { + return Report(fixtures['some-vulns-critical'], {reporter: 'parseable'}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /\tcritical/) + }) +}) + +tap.test('it generates a parseable report with multiple resolves on the same update/install action', function (t) { + return Report(fixtures['some-same-action'], {reporter: 'parseable'}).then((report) => { + t.equal(report.exitCode, 1, 'non-zero exit code') + t.match(report.report, /\tcritical/) + t.match(report.report, /\tlow/) + }) +})