Skip to content

Commit 8d5a6ec

Browse files
committed
report: concept for html report feature (needs design)
1 parent 83c2a81 commit 8d5a6ec

4 files changed

Lines changed: 242 additions & 33 deletions

File tree

commands/report.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
} = require('../lib/report/util')
1515
const longReport = require('../lib/report/long')
1616
const shortReport = require('../lib/report/short')
17+
const htmlReport = require('../lib/report/html')
1718
const { helpHeader } = require('../lib/help')
1819
const {
1920
COLORS,
@@ -30,7 +31,8 @@ module.exports.optionsList = optionsList
3031

3132
async function report (argv, _dir) {
3233
const {
33-
long
34+
long,
35+
html
3436
} = argv
3537
let { dir = _dir } = argv
3638
if (!dir) dir = process.cwd()
@@ -162,6 +164,7 @@ async function report (argv, _dir) {
162164

163165
if (!long) shortReport(pkgScores, whitelisted, dir, argv)
164166
if (long) longReport(pkgScores, whitelisted, dir, argv)
167+
if (html) htmlReport(pkgScores, whitelisted, dir, html, argv)
165168
if (hasFailures) process.exitCode = 1
166169
}
167170

lib/report/html.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
'use strict'
2+
3+
const path = require('path')
4+
const { promisify } = require('util')
5+
const writeFile = promisify(require('fs').writeFile)
6+
const {
7+
summaryInfo,
8+
filterVulns,
9+
SEVERITY_RMAP
10+
} = require('./util')
11+
const {
12+
COLORS,
13+
success,
14+
formatError
15+
} = require('../ncm-style')
16+
const L = console.log
17+
18+
module.exports = htmlReport
19+
20+
async function htmlReport (report, whitelist, dir, output) {
21+
if (output === true) output = path.join(process.cwd(), `ncm-report-${Date.now()}.html`)
22+
23+
const { riskCount, insecureModules, complianceCount, securityCount } = summaryInfo(report)
24+
25+
/* Define embedded CSS report styles */
26+
const reportStyles = `
27+
body {
28+
background: ${COLORS.base.match(/(#[a-zA-Z0-9]*)/)[0]};
29+
padding: 10px;
30+
}
31+
p {
32+
color: white;
33+
}
34+
.summary {
35+
text-align: left;
36+
font-size: 14pt;
37+
color: #ffffff;
38+
}
39+
.module-list {
40+
41+
}
42+
.module-element {
43+
padding: 0px;
44+
margin: 0px;
45+
font-size: 10pt;
46+
}
47+
`
48+
49+
/* Begin body with report summary */
50+
let reportBody = `
51+
<h1 class="">${path.basename(dir) || 'NCM'} Report</h1>
52+
<h6>Powered by <a href="https://docs.nodesource.com/ncmv2/docs">Node Certified Modules v2</a></h6>
53+
<div class="summary">
54+
<div style=""><b>${report.length}</b> packages checked</div>
55+
<br>
56+
<div style="color:${COLORS.red.match(/(#[a-zA-Z0-9]*)/)[0]}"><b>${riskCount[4]}</b> Critical Risk</div>
57+
<div style="color:${COLORS.orange.match(/(#[a-zA-Z0-9]*)/)[0]}"><b>${riskCount[3]}</b> High Risk</div>
58+
<div style="color:${COLORS.yellow.match(/(#[a-zA-Z0-9]*)/)[0]}"><b>${riskCount[2]}</b> Medium Risk</div>
59+
<div style="color:${COLORS.light1.match(/(#[a-zA-Z0-9]*)/)[0]}"><b>${riskCount[1]}</b> Low Risk</div>
60+
<br>
61+
<div><b>${securityCount}</b> security vulnerabilities found across <b>${insecureModules}</b> modules</div>
62+
<div><b>${complianceCount}</b> noncompliant modules found</div>
63+
${(whitelist.length > 0 ? `<div><b>${whitelist.length}</b> used modules whitelisted </div>` : '')}
64+
</div>
65+
`
66+
67+
/* Whitelisted Modules */
68+
reportBody += `
69+
<a data-toggle="collapse" href="#collapseWL" data-target="#collapseWL">
70+
<h2>Whitelisted Modules</h2>
71+
</a>
72+
<div class="module-list collapse" id="collapseWL">`
73+
for (const pkg of whitelist) {
74+
reportBody += formatSegment(pkg)
75+
}
76+
reportBody += '</div>'
77+
78+
/* Non-whitelisted Modules */
79+
reportBody += `
80+
<h2>Non-whitelisted Modules</h2>
81+
<div class="module-list">`
82+
83+
/* Critical risk */
84+
reportBody += `
85+
<a data-toggle="collapse" href="#collapseCritRisk" data-target="#collapseCritRisk">
86+
<h4>Critical Risk Modules</h4>
87+
</a>
88+
<div class="module-list collapse" id="collapseCritRisk">`
89+
for (const pkg of report.filter(pkg => pkg.maxSeverity === 4)) {
90+
reportBody += formatSegment(pkg)
91+
}
92+
reportBody += '</div>'
93+
94+
/* High risk */
95+
reportBody += `
96+
<a data-toggle="collapse" href="#collapseHighRisk" data-target="#collapseHighRisk">
97+
<h4>High Risk Modules</h4>
98+
</a>
99+
<div class="module-list collapse" id="collapseHighRisk">`
100+
for (const pkg of report.filter(pkg => pkg.maxSeverity === 3)) {
101+
reportBody += formatSegment(pkg)
102+
}
103+
reportBody += '</div>'
104+
105+
/* Medium risk */
106+
reportBody += `
107+
<a data-toggle="collapse" href="#collapseMedRisk" data-target="#collapseMedRisk">
108+
<h4>Medium Risk Modules</h4>
109+
</a>
110+
<div class="module-list collapse" id="collapseMedRisk">`
111+
for (const pkg of report.filter(pkg => pkg.maxSeverity === 2)) {
112+
reportBody += formatSegment(pkg)
113+
}
114+
reportBody += '</div>'
115+
116+
/* Low risk */
117+
reportBody += `
118+
<a data-toggle="collapse" href="#collapseLowRisk" data-target="#collapseLowRisk">
119+
<h4>Low Risk Modules</h4>
120+
</a>
121+
<div class="module-list collapse" id="collapseLowRisk">`
122+
for (const pkg of report.filter(pkg => pkg.maxSeverity === 1)) {
123+
reportBody += formatSegment(pkg)
124+
}
125+
reportBody += '</div>'
126+
127+
/* None risk */
128+
reportBody += `
129+
<a data-toggle="collapse" href="#collapseNoneRisk" data-target="#collapseNoneRisk">
130+
<h4>No Risk Modules</h4>
131+
</a>
132+
<div class="module-list collapse" id="collapseNoneRisk">`
133+
for (const pkg of report.filter(pkg => pkg.maxSeverity === 0)) {
134+
reportBody += formatSegment(pkg)
135+
}
136+
reportBody += '</div>'
137+
138+
reportBody += '</div>'
139+
140+
/* Format final HTML report */
141+
const html = `
142+
<!DOCTYPE html>
143+
<html>
144+
<head>
145+
<title>${path.basename(dir) || 'NCM'} Report</title>
146+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
147+
<style type="text/css">
148+
${reportStyles}
149+
</style>
150+
</head>
151+
<body>
152+
${reportBody}
153+
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
154+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
155+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
156+
</body>
157+
</html>
158+
`
159+
160+
/* Write report to file */
161+
try {
162+
await writeFile(output, html)
163+
L()
164+
L(success(`Wrote HTML report to: ${output}`))
165+
L()
166+
} catch (error) {
167+
L()
168+
L(formatError(`Unable to write HTML report to: ${output}`, error))
169+
L()
170+
process.exitCode = 1
171+
}
172+
}
173+
174+
function formatSegment (pkg) {
175+
const { name, version, maxSeverity, failures, license } = pkg
176+
const pkgVulns = filterVulns(failures).map(v => v === 0 ? '' : `${v} ${['Critical', 'High', 'Medium', 'Low'][v]}`)
177+
const pkgLicense = license && license.data && license.data.spdx ? license.data.spdx : 'UNKNOWN'
178+
const pkgSeverity = SEVERITY_RMAP[maxSeverity]
179+
180+
const segment = `
181+
<div class="row module-element">
182+
<div class="col-4" style="display:inline-block;">${name}@${version}</div>
183+
<div class="col-2" style="display:inline-block;">Risk: ${pkgSeverity}</div>
184+
<div class="col-3" style="display:inline-block;">License: ${pkgLicense}</div>
185+
<div class="col-3" style="display:inline-block;">
186+
Vulnerabilities: ${pkgVulns.join('').length === 0 ? 'None' : pkgVulns.join(' ')}
187+
</div>
188+
</div>
189+
`
190+
191+
return segment
192+
}

lib/report/summary.js

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {
77
tooltip
88
} = require('../ncm-style')
99
const {
10-
SEVERITY_RMAP
10+
summaryInfo
1111
} = require('./util')
1212
const L = console.log
1313
const chalk = require('chalk')
@@ -19,27 +19,7 @@ function summary (report, dir, filterOptions) {
1919
L(chalk`${report.length} {${COLORS.light1} packages checked}`)
2020
L()
2121

22-
const riskCount = [0, 0, 0, 0, 0]
23-
let insecureModules = 0
24-
let complianceCount = 0
25-
let securityCount = 0
26-
27-
for (const pkg of report) {
28-
let insecure = false
29-
let pkgMaxSeverity = 0
30-
for (const score of pkg.scores) {
31-
if (score.group === 'quality') continue
32-
if (score.group === 'compliance' && !score.pass) complianceCount++
33-
if (score.group === 'security' && !score.pass) {
34-
securityCount++
35-
insecure = true
36-
}
37-
const scoreIndex = SEVERITY_RMAP.indexOf(score.severity)
38-
pkgMaxSeverity = scoreIndex > pkgMaxSeverity ? scoreIndex : pkgMaxSeverity
39-
}
40-
riskCount[pkgMaxSeverity]++
41-
if (insecure) insecureModules++
42-
}
22+
const { riskCount, insecureModules, complianceCount, securityCount } = summaryInfo(report)
4323

4424
L(chalk` {${COLORS.red} ! ${riskCount[4]}} critical risk`)
4525
L(chalk` {${COLORS.orange} ${riskCount[3]}} high risk`)

lib/report/util.js

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ module.exports = {
4040
severityTextLabel,
4141
shortVulnerabilityList,
4242
moduleList,
43-
moduleSort
43+
moduleSort,
44+
summaryInfo,
45+
filterVulns
4446
}
4547

4648
function filterReport (report, options) {
@@ -121,15 +123,7 @@ function moduleList (report, title, options) {
121123
: chalk`{${COLORS.red} X}`
122124

123125
/* security badge */
124-
const vulns = [0, 0, 0, 0]
125-
for (const { group, severity } of pkg.failures) {
126-
if (group === 'security') {
127-
if (severity === 'CRITICAL') vulns[3]++
128-
if (severity === 'HIGH') vulns[2]++
129-
if (severity === 'MEDIUM') vulns[1]++
130-
if (severity === 'LOW') vulns[0]++
131-
}
132-
}
126+
const vulns = filterVulns(pkg.failures)
133127
const securityBadges = [
134128
vulns.reduce((a, b) => a + b, 0) === 0
135129
? chalk`{${COLORS.green} ✓} 0` : chalk`{${COLORS.red} X} `,
@@ -254,3 +248,43 @@ function severityTextLabel (severity) {
254248
const color = severity === 'NONE' ? COLORS.base : COLORS.light1
255249
return chalk`{${color} ${severityLabel[severity]}}`
256250
}
251+
252+
function summaryInfo (report) {
253+
const riskCount = [0, 0, 0, 0, 0]
254+
let insecureModules = 0
255+
let complianceCount = 0
256+
let securityCount = 0
257+
258+
for (const pkg of report) {
259+
let insecure = false
260+
let pkgMaxSeverity = 0
261+
for (const score of pkg.scores) {
262+
if (score.group === 'quality') continue
263+
if (score.group === 'compliance' && !score.pass) complianceCount++
264+
if (score.group === 'security' && !score.pass) {
265+
securityCount++
266+
insecure = true
267+
}
268+
const scoreIndex = SEVERITY_RMAP.indexOf(score.severity)
269+
pkgMaxSeverity = scoreIndex > pkgMaxSeverity ? scoreIndex : pkgMaxSeverity
270+
}
271+
riskCount[pkgMaxSeverity]++
272+
if (insecure) insecureModules++
273+
}
274+
275+
return { riskCount, insecureModules, complianceCount, securityCount }
276+
}
277+
278+
function filterVulns (failures) {
279+
const vulns = [0, 0, 0, 0]
280+
for (const { group, severity } of failures) {
281+
if (group === 'security') {
282+
if (severity === 'CRITICAL') vulns[3]++
283+
if (severity === 'HIGH') vulns[2]++
284+
if (severity === 'MEDIUM') vulns[1]++
285+
if (severity === 'LOW') vulns[0]++
286+
}
287+
}
288+
289+
return vulns
290+
}

0 commit comments

Comments
 (0)