Skip to content

Commit

Permalink
format(full): consistified full report with install report
Browse files Browse the repository at this point in the history
  • Loading branch information
zkat committed May 16, 2018
1 parent ea05c43 commit d5e6756
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 57 deletions.
46 changes: 31 additions & 15 deletions 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')

Expand Down Expand Up @@ -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.`)
}
}
}

Expand Down Expand Up @@ -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
}
}
Expand Down
33 changes: 21 additions & 12 deletions reporters/install.js
Expand Up @@ -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'
}
Expand All @@ -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 = []
Expand All @@ -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
75 changes: 45 additions & 30 deletions test/detail-report-test.js
Expand Up @@ -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')
})
})
54 changes: 54 additions & 0 deletions 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
}
}
64 changes: 64 additions & 0 deletions 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
}
}

0 comments on commit d5e6756

Please sign in to comment.