UNPKG

ncm-cli

Version:

Command-line tool for NodeSource Certified Modules 2.0

279 lines (241 loc) 8.21 kB
'use strict' const chalk = require('chalk') const semver = require('semver') const { COLORS, divider } = require('../ncm-style') const L = console.log const SEVERITY_RMAP = [ 'NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL' ] const SEVERITY_RMAP_NPM = [ 'NONE', 'LOW', 'MODERATE', 'HIGH', 'CRITICAL' ] const W = [ 40, // Module Name 12, // Risk Score 23, // License 20, // Security 23 // Score ] const SEVERITY_COLOR = { CRITICAL: COLORS.red, HIGH: COLORS.orange, MEDIUM: COLORS.yellow, LOW: COLORS.light1, NONE: COLORS.base } module.exports = { W, SEVERITY_RMAP, SEVERITY_RMAP_NPM, SEVERITY_COLOR, severityMeter, severityText, severityTextLabel, shortVulnerabilityList, moduleList, moduleSort } function filterReport (report, options) { const out = [] if (!options.filterCompliance && !options.filterSecurity && options.filterLevel === 0 ) { return report } const minimumSeverity = options.filterLevel for (const pkg of report) { const hasComplianceFailure = pkg.scores.some( s => s.group === 'compliance' && !s.pass ) if (options.filterCompliance && !hasComplianceFailure) { continue } if (options.filterSecurity || options.filterLevel > 0) { if (!pkg.failures) { continue } const hasSecurityFailure = pkg.failures.some( f => f.group === 'security' && SEVERITY_RMAP.indexOf(f.severity) >= minimumSeverity ) if (!hasSecurityFailure) { continue } } out.push(pkg) } return out } function moduleList (report, title, options) { L(divider(W[0] + W[1] + W[2] + W[3] + 7, '─')) L(chalk`{${COLORS.light1} ${title}}`) L(divider(W[0] + W[1] + W[2] + W[3] + 7)) report = moduleSort(report) if (options) { report = filterReport(report, options) if (report.length === 0) { return } } /* Module List */ L(chalk`{${COLORS.light1} Module Name${' '.repeat(W[0] - 9)}Risk${' '.repeat(W[1] - 3)}License${' '.repeat(W[2] - 6)}Security}`) L(chalk`{${COLORS.light1} ┌──${'─'.repeat(W[0])}${'─'.repeat(W[1])}${'─'.repeat(W[2])}${'─'.repeat(W[3])}┐}`) report.forEach((pkg, ind) => { const version = String(pkg.version) /* Module Name */ let mod = pkg.name + ' @ ' + version let leftMod = '' // truncate name text to fit within column const sliceLen = W[0] - 10 - version.length if (mod.length + 3 > W[0]) { leftMod = mod.slice(sliceLen, mod.length) mod = mod.slice(0, sliceLen) + '...' } if (pkg.published) { let license = pkg.license && pkg.license.data && pkg.license.data.spdx ? pkg.license.data.spdx : 'UNKNOWN' // truncate license text to fit within column if (license.length + 3 > W[2]) license = license.slice(0, W[2] - 4) + '…' const licenseBadge = pkg.license && pkg.license.pass ? chalk`{${COLORS.green} ✓}` : 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 securityBadges = [ vulns.reduce((a, b) => a + b, 0) === 0 ? chalk`{${COLORS.green} ✓} 0` : chalk`{${COLORS.red} X} `, vulns[3] > 0 ? chalk`{${COLORS.red} ${vulns[3]}C} ` : '', vulns[2] > 0 ? chalk`{${COLORS.orange} ${vulns[2]}H} ` : '', vulns[1] > 0 ? chalk`{${COLORS.yellow} ${vulns[1]}M} ` : '', vulns[0] > 0 ? chalk`{${COLORS.light1} ${vulns[0]}L} ` : '' ] /* risk badge */ const { quantitativeScore = 0 } = pkg const maxSeverity = quantitativeScore === 0 ? 4 : pkg.maxSeverity const riskMeter = severityMeter(SEVERITY_RMAP[maxSeverity]) const riskLabel = severityTextLabel(SEVERITY_RMAP[maxSeverity]) const riskBadge = riskMeter + ' ' + riskLabel const getScoreColor = quantitativeScore => quantitativeScore === 100 ? COLORS.green : quantitativeScore === 0 ? COLORS.red : COLORS.yellow /* printing */ L( // module label with left and right bar chalk`{${COLORS.light1} │} ${mod} {${getScoreColor(quantitativeScore)} (${quantitativeScore})} ${' '.repeat(Math.max(W[0] - (mod.length + String(quantitativeScore).length + 3), 0))}{${COLORS.light1} │}` + // risk badge with right bar riskBadge + chalk`${' '.repeat(W[1] - 10 + 1)}{${COLORS.light1} │} ` + // license badge with right bar licenseBadge + chalk` ${license}${' '.repeat(W[2] - license.length - 3)}{${COLORS.light1} │} ` + // security badge with right bar securityBadges.join('') + chalk`${' '.repeat(W[3] - 2 - securityBadges.filter(x => x !== '').length * 2)}{${COLORS.light1} │}` + `${leftMod ? `\n| ${leftMod}` : ''}` ) } else { /* printing */ L( // module label with left and right bar chalk`{${COLORS.light1} │} ${mod} ${' '.repeat(W[0] - mod.length)}{${COLORS.light1} │} ` + // spacing with right bar chalk`${' '.repeat(W[1] - 1)}{${COLORS.light1} │}` + // spacing with right bar chalk`${' '.repeat(W[2])}{${COLORS.light1} │}` + // spacing with right bar chalk`${' '.repeat(W[3])}{${COLORS.light1} │}` ) } /* Cell Divider */ if (ind === report.length - 1) { L(chalk`{${COLORS.light1} └──${'─'.repeat(W[0])}${'─'.repeat(W[1])}${'─'.repeat(W[2])}${'─'.repeat(W[3])}┘}`) } // } else if (dividers) { // L(chalk`{${COLORS.light1} ├──${'─'.repeat(W[0])}┼${'─'.repeat(W[1])}┼${'─'.repeat(W[2])}┼${'─'.repeat(W[3])}┤}`) // } }) } function shortVulnerabilityList (report) { const netSecurity = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0, AFFECTED: new Set() } for (const pkg of report) { for (const failure of pkg.failures || []) { if (failure.group === 'security') { netSecurity[failure.severity]++ netSecurity.AFFECTED.add(pkg.name) } } } const vulnerabilityCount = netSecurity.CRITICAL + netSecurity.HIGH + netSecurity.MEDIUM + netSecurity.LOW const securityBadge = vulnerabilityCount > 0 ? `${COLORS.red} !` : `${COLORS.green} ✓` if (netSecurity.AFFECTED.size > 0) { L(chalk` {${securityBadge}} ${vulnerabilityCount} security vulnerabilities found across ${netSecurity.AFFECTED.size} modules`) } else { L(chalk` {${securityBadge}} ${vulnerabilityCount} security vulnerabilities found`) } L(chalk` {${COLORS.red} C} ${netSecurity.CRITICAL} critical severity`) L(chalk` {${COLORS.orange} H} ${netSecurity.HIGH} high severity`) L(chalk` {${COLORS.yellow} M} ${netSecurity.MEDIUM} medium severity`) L(chalk` {${COLORS.light1} L} ${netSecurity.LOW} low severity`) return vulnerabilityCount } function moduleSort (report) { report.sort((a, b) => { if (a.maxSeverity > b.maxSeverity) return -1 if (b.maxSeverity > a.maxSeverity) return 1 if (a.name < b.name) return -1 if (b.name < a.name) return 1 return semver.compare(a.version, b.version) }) return report } function severityMeter (severity) { const risk = SEVERITY_RMAP.indexOf(severity) return [ chalk`{${SEVERITY_COLOR[severity]} ${'|'.repeat(risk)}}`, chalk`{${COLORS.base} ${'|'.repeat(4 - risk)}}` ].join('') } function severityText (severity) { const formatted = severity[0].toUpperCase() + severity.slice(1).toLowerCase() return chalk`{${SEVERITY_COLOR[severity]} ${formatted}}` } function severityTextLabel (severity) { const severityLabel = { CRITICAL: 'Crit', HIGH: 'High', MEDIUM: 'Med ', LOW: 'Low ', NONE: 'None' } const color = severity === 'NONE' ? COLORS.base : COLORS.light1 return chalk`{${color} ${severityLabel[severity]}}` }