From 32660ff07c420044575e7dbd83ab351c43940ad9 Mon Sep 17 00:00:00 2001 From: michaelsterpka Date: Mon, 12 Aug 2019 19:36:56 -0700 Subject: [PATCH 1/4] report: html report feature --- commands/report.js | 5 +- lib/report/html-template.js | 217 ++++++++++++++++++++++++++++++++++++ lib/report/html.js | 56 ++++++++++ lib/report/summary.js | 24 +--- lib/report/util.js | 54 +++++++-- 5 files changed, 323 insertions(+), 33 deletions(-) create mode 100644 lib/report/html-template.js create mode 100644 lib/report/html.js diff --git a/commands/report.js b/commands/report.js index d1dae3f..b17b1e6 100644 --- a/commands/report.js +++ b/commands/report.js @@ -14,6 +14,7 @@ const { } = require('../lib/report/util') const longReport = require('../lib/report/long') const shortReport = require('../lib/report/short') +const htmlReport = require('../lib/report/html') const { helpHeader } = require('../lib/help') const { COLORS, @@ -30,7 +31,8 @@ module.exports.optionsList = optionsList async function report (argv, _dir) { const { - long + long, + html } = argv let { dir = _dir } = argv if (!dir) dir = process.cwd() @@ -162,6 +164,7 @@ async function report (argv, _dir) { if (!long) shortReport(pkgScores, whitelisted, dir, argv) if (long) longReport(pkgScores, whitelisted, dir, argv) + if (html) htmlReport(pkgScores, whitelisted, dir, html, argv) if (hasFailures) process.exitCode = 1 } diff --git a/lib/report/html-template.js b/lib/report/html-template.js new file mode 100644 index 0000000..a2b2dd1 --- /dev/null +++ b/lib/report/html-template.js @@ -0,0 +1,217 @@ +'use strict' + +const { + filterVulns, + SEVERITY_RMAP +} = require('./util') + +module.exports = renderHTML + +function renderHTML (title, summary, report, whitelist) { + const { riskCount, securityCount, insecureModules, complianceCount } = summary + + let alternate = false + let whitelistInfo = '' + for (const pkg of whitelist) { + whitelistInfo += segment(pkg) + } + + alternate = false + let pkgInfo = '' + for (const pkg of report) { + pkgInfo += segment(pkg) + } + + function segment (pkg) { + const { name, version, maxSeverity, failures, license } = pkg + const pkgVulns = filterVulns(failures).map((v, i) => v !== 0 + ? `

+ ${v} ${['Low', 'Medium', 'High', 'Critical'][i]} +

` + : '' + ) + const pkgLicense = license && license.data && license.data.spdx ? license.data.spdx : 'UNKNOWN' + const pkgLicensePass = license && license.pass === true + const pkgSeverity = maxSeverity === 0 + ? `

${SEVERITY_RMAP[maxSeverity]}

` + : `

+ ${SEVERITY_RMAP[maxSeverity]} +

` + + alternate = !alternate + return ` + + +

${name}@${version}

+ + +

${pkgSeverity}

+ +

+ ✓' : 'red">X'} ${pkgLicense} +

+ + +

+ ${pkgVulns.join('').length === 0 + ? 'None' + : pkgVulns.reverse().join(' ')} +

+ + + ` + } + + const template = ` + + + + + NCM Report > ${title} + + + + +
+ + + + + + +
+ +
+
+

+ NCM Project Report +  >  + ${title} +

+
+
+

+ > + Powered by + NodeSource Certified Modules v2 +

+
+
+ +
+ Summary +

${report.length} packages checked

+
+

${riskCount[4]} Critical Risk

+

${riskCount[3]} High Risk

+

${riskCount[2]} Medium Risk

+

${riskCount[1]} Low Risk

+
+

+ ${securityCount} + security vulnerabilities found + ${(securityCount > 0 + ? `across ${insecureModules} modules` + : '')} +

+

${complianceCount} noncompliant modules found

+ ${(whitelist.length > 0 + ? `

${whitelist.length} used modules whitelisted

` + : '')} +
+ + ${(whitelist.length > 0 + ? ` +
+ Whitelisted Modules +
+ + + + + + + + ${whitelistInfo} +
Module NameRiskLicenseSecurity
+
+ ` + : '')} + +
+

${(whitelist.length > 0 ? 'Non-Whitelisted ' : '')}Modules

+
+ + + + + + + + ${pkgInfo} +
Module NameRiskLicenseSecurity
+
+ + + ` + + return template +} diff --git a/lib/report/html.js b/lib/report/html.js new file mode 100644 index 0000000..9c61db7 --- /dev/null +++ b/lib/report/html.js @@ -0,0 +1,56 @@ +'use strict' + +const path = require('path') +const { promisify } = require('util') +const writeFile = promisify(require('fs').writeFile) +const { + success, + formatError +} = require('../ncm-style') +const { + summaryInfo +} = require('./util') +const renderTemplate = require('./html-template') +const L = console.log + +module.exports = htmlReport + +async function htmlReport (report, whitelist, dir, output) { + /* Output may only use the `.html` file format */ + if (output !== true && !(/^.*\.html$/.test(path.basename(output)))) { + L() + L(formatError('Invalid file extension to write the HTML report. Please use `*.html`.')) + L() + process.exitCode = 1 + return + } + + const title = `${path.basename(dir) || 'NCM'}` + const summary = summaryInfo(report) // { riskCount, insecureModules, complianceCount, securityCount } + + const htmlData = renderTemplate( + title, + summary, + report, + whitelist + ) + + /* + No write location was specified. + Setting output location to current working directory with generated report name. + */ + if (output === true) output = path.join(process.cwd(), `${title.toLowerCase()}-report-${Date.now()}.html`) + + /* Write report to file */ + try { + await writeFile(output, htmlData) + L() + L(success(`Wrote HTML report to: ${output}`)) + L() + } catch (error) { + L() + L(formatError(`Unable to write HTML report to: ${output}`, error)) + L() + process.exitCode = 1 + } +} diff --git a/lib/report/summary.js b/lib/report/summary.js index 75d366b..ff59fd4 100644 --- a/lib/report/summary.js +++ b/lib/report/summary.js @@ -7,7 +7,7 @@ const { tooltip } = require('../ncm-style') const { - SEVERITY_RMAP + summaryInfo } = require('./util') const L = console.log const chalk = require('chalk') @@ -19,27 +19,7 @@ function summary (report, dir, filterOptions) { L(chalk`${report.length} {${COLORS.light1} packages checked}`) L() - const riskCount = [0, 0, 0, 0, 0] - let insecureModules = 0 - let complianceCount = 0 - let securityCount = 0 - - for (const pkg of report) { - let insecure = false - let pkgMaxSeverity = 0 - for (const score of pkg.scores) { - if (score.group === 'quality') continue - if (score.group === 'compliance' && !score.pass) complianceCount++ - if (score.group === 'security' && !score.pass) { - securityCount++ - insecure = true - } - const scoreIndex = SEVERITY_RMAP.indexOf(score.severity) - pkgMaxSeverity = scoreIndex > pkgMaxSeverity ? scoreIndex : pkgMaxSeverity - } - riskCount[pkgMaxSeverity]++ - if (insecure) insecureModules++ - } + const { riskCount, insecureModules, complianceCount, securityCount } = summaryInfo(report) L(chalk` {${COLORS.red} ! ${riskCount[4]}} critical risk`) L(chalk` {${COLORS.orange} ${riskCount[3]}} high risk`) diff --git a/lib/report/util.js b/lib/report/util.js index ca65523..b2eb5a9 100644 --- a/lib/report/util.js +++ b/lib/report/util.js @@ -40,7 +40,9 @@ module.exports = { severityTextLabel, shortVulnerabilityList, moduleList, - moduleSort + moduleSort, + summaryInfo, + filterVulns } function filterReport (report, options) { @@ -121,15 +123,7 @@ function moduleList (report, title, options) { : chalk`{${COLORS.red} X}` /* security badge */ - const vulns = [0, 0, 0, 0] - for (const { group, severity } of pkg.failures) { - if (group === 'security') { - if (severity === 'CRITICAL') vulns[3]++ - if (severity === 'HIGH') vulns[2]++ - if (severity === 'MEDIUM') vulns[1]++ - if (severity === 'LOW') vulns[0]++ - } - } + const vulns = filterVulns(pkg.failures) const securityBadges = [ vulns.reduce((a, b) => a + b, 0) === 0 ? chalk`{${COLORS.green} ✓} 0` : chalk`{${COLORS.red} X} `, @@ -254,3 +248,43 @@ function severityTextLabel (severity) { const color = severity === 'NONE' ? COLORS.base : COLORS.light1 return chalk`{${color} ${severityLabel[severity]}}` } + +function summaryInfo (report) { + const riskCount = [0, 0, 0, 0, 0] + let insecureModules = 0 + let complianceCount = 0 + let securityCount = 0 + + for (const pkg of report) { + let insecure = false + let pkgMaxSeverity = 0 + for (const score of pkg.scores) { + if (score.group === 'quality') continue + if (score.group === 'compliance' && !score.pass) complianceCount++ + if (score.group === 'security' && !score.pass) { + securityCount++ + insecure = true + } + const scoreIndex = SEVERITY_RMAP.indexOf(score.severity) + pkgMaxSeverity = scoreIndex > pkgMaxSeverity ? scoreIndex : pkgMaxSeverity + } + riskCount[pkgMaxSeverity]++ + if (insecure) insecureModules++ + } + + return { riskCount, insecureModules, complianceCount, securityCount } +} + +function filterVulns (failures) { + const vulns = [0, 0, 0, 0] + for (const { group, severity } of failures) { + if (group === 'security') { + if (severity === 'CRITICAL') vulns[3]++ + if (severity === 'HIGH') vulns[2]++ + if (severity === 'MEDIUM') vulns[1]++ + if (severity === 'LOW') vulns[0]++ + } + } + + return vulns +} From 6470ea5ac8b8e4025f1c31524e6987e2b6fdc11c Mon Sep 17 00:00:00 2001 From: michaelsterpka Date: Thu, 29 Aug 2019 19:06:31 -0700 Subject: [PATCH 2/4] report: consistency in risk & security information --- lib/report/html-template.js | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/report/html-template.js b/lib/report/html-template.js index a2b2dd1..f251144 100644 --- a/lib/report/html-template.js +++ b/lib/report/html-template.js @@ -32,11 +32,12 @@ function renderHTML (title, summary, report, whitelist) { ) const pkgLicense = license && license.data && license.data.spdx ? license.data.spdx : 'UNKNOWN' const pkgLicensePass = license && license.pass === true - const pkgSeverity = maxSeverity === 0 - ? `

${SEVERITY_RMAP[maxSeverity]}

` - : `

- ${SEVERITY_RMAP[maxSeverity]} -

` + const pkgSeverity = ` +

+ + ${['', '| ', '| | ', '| | | ', '| | | |'][maxSeverity]}${['| | | |', '| | |', '| |', '|', ''][maxSeverity]}  ${SEVERITY_RMAP[maxSeverity]} +

` alternate = !alternate return ` @@ -44,7 +45,7 @@ function renderHTML (title, summary, report, whitelist) {

${name}@${version}

- +

${pkgSeverity}

@@ -54,7 +55,7 @@ function renderHTML (title, summary, report, whitelist) {

${pkgVulns.join('').length === 0 - ? 'None' + ? ' 0' : pkgVulns.reverse().join(' ')}

@@ -88,6 +89,9 @@ function renderHTML (title, summary, report, whitelist) { padding-left: 10px; padding-right: 10px; } + th { + text-align: left; + } table { width: 100%; } @@ -165,14 +169,18 @@ function renderHTML (title, summary, report, whitelist) {

${riskCount[2]} Medium Risk

${riskCount[1]} Low Risk


-

- ${securityCount} - security vulnerabilities found +

${(securityCount > 0 - ? `across ${insecureModules} modules` - : '')} + ? `${securityCount} + security vulnerabilities found across + ${insecureModules} modules` + : ' No security vulnerabilities found')} +

+

+ ${(complianceCount > 0 + ? `${complianceCount} noncompliant modules found` + : ' All modules compliant')}

-

${complianceCount} noncompliant modules found

${(whitelist.length > 0 ? `

${whitelist.length} used modules whitelisted

` : '')} From d35e6490f7fc5d318cead33555a4b70c356313e6 Mon Sep 17 00:00:00 2001 From: michaelsterpka Date: Thu, 29 Aug 2019 20:37:09 -0700 Subject: [PATCH 3/4] refactor, report: migrating filter formatting functions to utils, implement filtering into html report. --- lib/report/html-template.js | 39 +++++++++++++++++++----- lib/report/html.js | 23 ++++++++++++-- lib/report/short.js | 59 +++++------------------------------- lib/report/util.js | 60 ++++++++++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 63 deletions(-) diff --git a/lib/report/html-template.js b/lib/report/html-template.js index f251144..9847c6f 100644 --- a/lib/report/html-template.js +++ b/lib/report/html-template.js @@ -7,7 +7,7 @@ const { module.exports = renderHTML -function renderHTML (title, summary, report, whitelist) { +function renderHTML (title, summary, report, reportLength, whitelist, whitelistLength, formattedFilterOptions) { const { riskCount, securityCount, insecureModules, complianceCount } = summary let alternate = false @@ -162,7 +162,7 @@ function renderHTML (title, summary, report, whitelist) {
Summary -

${report.length} packages checked

+

${reportLength} packages checked


${riskCount[4]} Critical Risk

${riskCount[3]} High Risk

@@ -181,15 +181,27 @@ function renderHTML (title, summary, report, whitelist) { ? `${complianceCount} noncompliant modules found` : ' All modules compliant')}

- ${(whitelist.length > 0 - ? `

${whitelist.length} used modules whitelisted

` + ${(whitelistLength > 0 + ? `

${whitelistLength} used modules whitelisted

` : '')}
- ${(whitelist.length > 0 + ${(whitelistLength > 0 ? `
- Whitelisted Modules +

+ + Whitelisted + ${(formattedFilterOptions.length > 9 + ? 'Filtered' + : '')} + Modules + + ${(formattedFilterOptions.length > 9 + ? `(${formattedFilterOptions})` + : '')} + +


@@ -205,7 +217,20 @@ function renderHTML (title, summary, report, whitelist) { : '')}
-

${(whitelist.length > 0 ? 'Non-Whitelisted ' : '')}Modules

+

+ + ${(whitelistLength > 0 + ? 'Non-Whitelisted' + : '')} + ${(formattedFilterOptions.length > 9 + ? 'Filtered' + : '')} + Modules + + ${(formattedFilterOptions.length > 9 + ? `( ${formattedFilterOptions} )` + : '')} +


diff --git a/lib/report/html.js b/lib/report/html.js index 9c61db7..d5c37c6 100644 --- a/lib/report/html.js +++ b/lib/report/html.js @@ -8,14 +8,18 @@ const { formatError } = require('../ncm-style') const { - summaryInfo + summaryInfo, + moduleSort, + filterReport, + parseFilterOptions, + formatFilterOptions } = require('./util') const renderTemplate = require('./html-template') const L = console.log module.exports = htmlReport -async function htmlReport (report, whitelist, dir, output) { +async function htmlReport (report, whitelist, dir, output, argv) { /* Output may only use the `.html` file format */ if (output !== true && !(/^.*\.html$/.test(path.basename(output)))) { L() @@ -27,12 +31,25 @@ async function htmlReport (report, whitelist, dir, output) { const title = `${path.basename(dir) || 'NCM'}` const summary = summaryInfo(report) // { riskCount, insecureModules, complianceCount, securityCount } + const reportLength = report.length + const whitelistLength = whitelist.length + + const filterOptions = parseFilterOptions(argv) + const formattedfilterOptions = formatFilterOptions(filterOptions) + report = filterReport(report, filterOptions) + report = moduleSort(report) + + whitelist = filterReport(whitelist, filterOptions) + whitelist = moduleSort(whitelist) const htmlData = renderTemplate( title, summary, report, - whitelist + reportLength, + whitelist, + whitelistLength, + formattedfilterOptions ) /* diff --git a/lib/report/short.js b/lib/report/short.js index 243743a..e3bab76 100644 --- a/lib/report/short.js +++ b/lib/report/short.js @@ -3,7 +3,11 @@ module.exports = shortReport const summary = require('./summary') -const { moduleList, SEVERITY_RMAP } = require('./util') +const { + moduleList, + parseFilterOptions, + formatFilterOptions +} = require('./util') const { COLORS, @@ -13,39 +17,7 @@ const chalk = require('chalk') const L = console.log function shortReport (report, whitelist, dir, argv) { - let filterSecurity = argv ? !!argv.security : false - let filterCompliance = argv ? !!argv.compliance : false - let filterLevel = SEVERITY_RMAP.indexOf('NONE') - - if (argv.filter) { - const segments = argv.filter.split(',') - .map(s => s.trim().toLowerCase()) - - if (segments.includes('compliance')) { - filterCompliance = true - } - if (segments.includes('security')) { - filterSecurity = true - } - if (segments.includes('c') || segments.includes('critical')) { - filterLevel = SEVERITY_RMAP.indexOf('CRITICAL') - } - if (segments.includes('h') || segments.includes('high')) { - filterLevel = SEVERITY_RMAP.indexOf('HIGH') - } - if (segments.includes('m') || segments.includes('medium')) { - filterLevel = SEVERITY_RMAP.indexOf('MEDIUM') - } - if (segments.includes('l') || segments.includes('low')) { - filterLevel = SEVERITY_RMAP.indexOf('LOW') - } - } - - const filterOptions = { - filterCompliance: filterCompliance, - filterSecurity: filterSecurity, - filterLevel: filterLevel - } + const filterOptions = parseFilterOptions(argv) summary(report, dir, filterOptions) @@ -55,7 +27,7 @@ function shortReport (report, whitelist, dir, argv) { L() } - if (filterCompliance || filterSecurity || filterLevel > 0) { + if (filterOptions.filterCompliance || filterOptions.filterSecurity || filterOptions.filterLevel > 0) { const filterFormat = formatFilterOptions(filterOptions) if (whitelist.length > 0) { moduleList( @@ -80,20 +52,3 @@ function shortReport (report, whitelist, dir, argv) { moduleList(report.slice(0, 5), 'Top 5: Highest Risk Modules') } } - -function formatFilterOptions (filterOptions) { - let str = '--filter=' - if (filterOptions.filterCompliance) { - str += 'compliance,' - } - if (filterOptions.filterSecurity) { - str += 'security,' - } - if (filterOptions.filterLevel) { - str += SEVERITY_RMAP[filterOptions.filterLevel] + ',' - } - if (str[str.length - 1] === ',') { - str = str.slice(0, str.length - 1) - } - return str -} diff --git a/lib/report/util.js b/lib/report/util.js index b2eb5a9..f465e2e 100644 --- a/lib/report/util.js +++ b/lib/report/util.js @@ -42,7 +42,10 @@ module.exports = { moduleList, moduleSort, summaryInfo, - filterVulns + filterVulns, + filterReport, + parseFilterOptions, + formatFilterOptions } function filterReport (report, options) { @@ -288,3 +291,58 @@ function filterVulns (failures) { return vulns } + +function parseFilterOptions (argv) { + let filterSecurity = argv ? !!argv.security : false + let filterCompliance = argv ? !!argv.compliance : false + let filterLevel = SEVERITY_RMAP.indexOf('NONE') + + if (argv.filter) { + const segments = argv.filter.split(',') + .map(s => s.trim().toLowerCase()) + + if (segments.includes('compliance')) { + filterCompliance = true + } + if (segments.includes('security')) { + filterSecurity = true + } + if (segments.includes('c') || segments.includes('critical')) { + filterLevel = SEVERITY_RMAP.indexOf('CRITICAL') + } + if (segments.includes('h') || segments.includes('high')) { + filterLevel = SEVERITY_RMAP.indexOf('HIGH') + } + if (segments.includes('m') || segments.includes('medium')) { + filterLevel = SEVERITY_RMAP.indexOf('MEDIUM') + } + if (segments.includes('l') || segments.includes('low')) { + filterLevel = SEVERITY_RMAP.indexOf('LOW') + } + } + + const filterOptions = { + filterCompliance: filterCompliance, + filterSecurity: filterSecurity, + filterLevel: filterLevel + } + + return filterOptions +} + +function formatFilterOptions (filterOptions) { + let str = '--filter=' + if (filterOptions.filterCompliance) { + str += 'compliance,' + } + if (filterOptions.filterSecurity) { + str += 'security,' + } + if (filterOptions.filterLevel) { + str += SEVERITY_RMAP[filterOptions.filterLevel] + ',' + } + if (str[str.length - 1] === ',') { + str = str.slice(0, str.length - 1) + } + return str +} From 742b92014f6e60349413ad7c088b47a280c06de4 Mon Sep 17 00:00:00 2001 From: michaelsterpka Date: Fri, 30 Aug 2019 11:40:39 -0700 Subject: [PATCH 4/4] report: use Source Sans Pro font, add '!' badge to summary fields, change font weight, use nbsp in risk badges --- lib/report/html-template.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/report/html-template.js b/lib/report/html-template.js index 9847c6f..77e3f3b 100644 --- a/lib/report/html-template.js +++ b/lib/report/html-template.js @@ -35,8 +35,8 @@ function renderHTML (title, summary, report, reportLength, whitelist, whitelistL const pkgSeverity = `

- ${['', '| ', '| | ', '| | | ', '| | | |'][maxSeverity]}${['| | | |', '| | |', '| |', '|', ''][maxSeverity]}  ${SEVERITY_RMAP[maxSeverity]} + ${['', '| ', '| | ', '| | | ', '| | | |'][maxSeverity]}${['| | | |', '| | |', '| |', '|', ''][maxSeverity]}  ${SEVERITY_RMAP[maxSeverity]}

` alternate = !alternate @@ -69,12 +69,13 @@ function renderHTML (title, summary, report, reportLength, whitelist, whitelistL NCM Report > ${title} +