UNPKG

ncm-cli

Version:

Command-line tool for NodeSource Certified Modules 2.0

222 lines (192 loc) 6.41 kB
'use strict' module.exports = moduleReport const { W, SEVERITY_RMAP, SEVERITY_COLOR, severityMeter, shortVulnerabilityList } = require('./util') const { COLORS, boxbox } = require('../ncm-style') const chalk = require('chalk') const L = console.log function moduleReport (report) { const { scores, failures = [], license, requirePaths } = report let moduleRisks = [] const securityVulnerabilities = [] const codeQuality = [] for (const score of scores) { if (score.group === 'risk' && score.pass === false) { moduleRisks.push(score) } if (score.group === 'quality' && score.pass === false) { codeQuality.push(score) } if (score.group === 'security' && score.pass === false) { securityVulnerabilities.push(score) } } moduleRisks = moduleRisks.sort((a, b) => { if (SEVERITY_RMAP.indexOf(a.severity) > SEVERITY_RMAP.indexOf(b.severity)) return -1 if (SEVERITY_RMAP.indexOf(b.severity) > SEVERITY_RMAP.indexOf(a.severity)) return 1 return 0 }) const maxLength = W.reduce((a, b) => a + b) /* Module Risk */ let moduleRisk = 'NONE' for (const { severity } of failures) { if (SEVERITY_RMAP.indexOf(severity) > SEVERITY_RMAP.indexOf(moduleRisk)) { moduleRisk = severity } } const riskColor = SEVERITY_COLOR[moduleRisk] L() L(boxbox( severityMeter(moduleRisk), `${moduleRisk[0].toUpperCase() + moduleRisk.slice(1).toLowerCase()} Risk`, riskColor, 4 // Need to override the symbol length because unicode. )) /* Security Overview */ L() L(chalk`{bold Security Risk:}`) shortVulnerabilityList([report]) L() /* Security Detail */ if (securityVulnerabilities.length > 0) { for (const { severity, title, data } of securityVulnerabilities) { L(boxbox( severity[0].toUpperCase(), title, SEVERITY_COLOR[severity] )) L(chalk`{${COLORS.light1} Versions ${data.vulnerable.join(' ')} (Source: Synk)}`) L(chalk`{${COLORS.blue} |➔ https://snyk.io/vuln/${data.id}}`) } } else { L(boxbox('✓', 'No Security Vulnerabilities', COLORS.green)) } /* License */ L() L(chalk`{bold License Risk:}`) const licenseName = license && license.data && license.data.spdx ? license.data.spdx : 'UNKNOWN' if (license && license.pass === true) { L(boxbox('✓', licenseName, COLORS.green)) } else { let msg = 'Unknown license' if (licenseName) { msg = `Noncompliant license: ${licenseName}` } L(boxbox('X', msg, COLORS.red)) } /* Module Risk */ L() L(chalk`{bold Module Risk:}`) if (moduleRisks.length > 0) { moduleRisks.forEach(({ title, severity }, ind) => { if (ind === 0) { L(chalk`{${COLORS.light1} ┌──────┬─${'─'.repeat(maxLength)}─┐}`) } severity = severityTextLabel(severity) const lines = lineWrap(title, maxLength) { const line = lines.shift() L(chalk`{${COLORS.light1} │} {${COLORS.red} ${severity}} {${COLORS.light1} │}` + chalk`{white ${line}} ${' '.repeat(maxLength - line.length)}{${COLORS.light1} │}`) } for (const line of lines) { L(chalk`{${COLORS.light1} │} {${COLORS.light1} │}` + chalk`{white ${line}} ${' '.repeat(maxLength - line.length)}{${COLORS.light1} │}`) } if (ind === moduleRisks.length - 1) { L(chalk`{${COLORS.light1} └──────┴─${'─'.repeat(maxLength)}─┘}`) } else { L(chalk`{${COLORS.light1} ├──────┼─${'─'.repeat(maxLength)}─┤}`) } }) } else { L(boxbox('✓', 'No Module Risk', COLORS.green)) } /* Code Quality */ L() L(chalk`{bold Code Quality} (does not affect risk score){bold :}`) if (codeQuality.length > 0) { codeQuality.forEach(({ title }, ind) => { if (ind === 0) { L(chalk`{${COLORS.light1} ┌───┬─${'─'.repeat(maxLength)}─┐}`) } const lines = lineWrap(title, maxLength) { const line = lines.shift() L(chalk`{${COLORS.light1} │} {${COLORS.yellow} !} {${COLORS.light1} │}` + chalk`{white ${line}} ${' '.repeat(maxLength - line.length)}{${COLORS.light1} │}`) } for (const line of lines) { L(chalk`{${COLORS.light1} │} {${COLORS.light1} │}` + chalk`{white ${line}} ${' '.repeat(maxLength - line.length)}{${COLORS.light1} │}`) } if (ind === codeQuality.length - 1) { L(chalk`{${COLORS.light1} └───┴─${'─'.repeat(maxLength)}─┘}`) } else { L(chalk`{${COLORS.light1} ├───┼─${'─'.repeat(maxLength)}─┤}`) } }) } else { L(boxbox('✓', 'Passes all criteria', COLORS.green)) } if (requirePaths.length > 0) { /* Required By */ L() L(chalk`{bold Required By} (leftmost is directly in your package){bold :}`) requirePaths.sort().forEach((path, ind) => { if (ind === 0) { L(chalk`{${COLORS.light1} ┌─${'─'.repeat(maxLength)}─┐}`) } let formatted = path.map(({ data }) => `${data.name} @ ${data.version}`).join(' / ') if (!formatted) formatted = '(Directly in your package)' const lines = lineWrap(formatted, maxLength, '/') for (const line of lines) { L(chalk`{${COLORS.light1} │} ${line} ${' '.repeat(maxLength - line.length)}{${COLORS.light1} │}`) } if (ind === requirePaths.length - 1) { L(chalk`{${COLORS.light1} └─${'─'.repeat(maxLength)}─┘}`) } else { L(chalk`{${COLORS.light1} ├─${'─'.repeat(maxLength)}─┤}`) } }) } L() } function severityTextLabel (severity) { const severityLabel = { CRITICAL: 'Crit', HIGH: 'High', MEDIUM: 'Med ', LOW: 'Low ', NONE: 'None' } return chalk`{${SEVERITY_COLOR[severity]} ${severityLabel[severity]}}` } function lineWrap (str, maxLength, split = ' ') { const words = str.split(split) const lines = [] let lineIdx = 0 for (const word of words) { while ((lines[lineIdx] + word).length >= maxLength) { lineIdx++ } if (!lines[lineIdx]) lines[lineIdx] = lineIdx > 0 ? [''] : [] lines[lineIdx].push(word) } return lines.map(line => line.join(split).trim()) }