1'use strict' 2 3const summary = require('./install.js').summary 4const Table = require('cli-table3') 5const Utils = require('../lib/utils') 6 7const report = function (data, options) { 8 const defaults = { 9 severityThreshold: 'info' 10 } 11 12 const blankChars = { 13 'top': ' ', 14 'top-mid': ' ', 15 'top-left': ' ', 16 'top-right': ' ', 17 'bottom': ' ', 18 'bottom-mid': ' ', 19 'bottom-left': ' ', 20 'bottom-right': ' ', 21 'left': ' ', 22 'left-mid': ' ', 23 'mid': ' ', 24 'mid-mid': ' ', 25 'right': ' ', 26 'right-mid': ' ', 27 'middle': ' ' 28 } 29 30 const config = Object.assign({}, defaults, options) 31 32 let output = '' 33 let exit = 0 34 35 const log = function (value) { 36 output = output + value + '\n' 37 } 38 39 const footer = function (data) { 40 const total = Utils.totalVulnCount(data.metadata.vulnerabilities) 41 42 if (total > 0) { 43 exit = 1 44 } 45 log(`${summary(data, config)} in ${data.metadata.totalDependencies} scanned package${data.metadata.totalDependencies === 1 ? '' : 's'}`) 46 if (total) { 47 const counts = data.actions.reduce((acc, {action, isMajor, resolves}) => { 48 if (action === 'update' || (action === 'install' && !isMajor)) { 49 resolves.forEach(({id, path}) => acc.advisories.add(`${id}::${path}`)) 50 } 51 if (isMajor) { 52 resolves.forEach(({id, path}) => acc.major.add(`${id}::${path}`)) 53 } 54 if (action === 'review') { 55 resolves.forEach(({id, path}) => acc.review.add(`${id}::${path}`)) 56 } 57 return acc 58 }, {advisories: new Set(), major: new Set(), review: new Set()}) 59 if (counts.advisories.size) { 60 log(` run \`npm audit fix\` to fix ${counts.advisories.size} of them.`) 61 } 62 if (counts.major.size) { 63 const maj = counts.major.size 64 log(` ${maj} vulnerabilit${maj === 1 ? 'y' : 'ies'} require${maj === 1 ? 's' : ''} semver-major dependency updates.`) 65 } 66 if (counts.review.size) { 67 const rev = counts.review.size 68 log(` ${rev} vulnerabilit${rev === 1 ? 'y' : 'ies'} require${rev === 1 ? 's' : ''} manual review. See the full report for details.`) 69 } 70 } 71 } 72 73 const reportTitle = function () { 74 const tableOptions = { 75 colWidths: [78] 76 } 77 tableOptions.chars = blankChars 78 const table = new Table(tableOptions) 79 table.push([{ 80 content: '=== npm audit security report ===', 81 vAlign: 'center', 82 hAlign: 'center' 83 }]) 84 log(table.toString()) 85 } 86 87 const actions = function (data, config) { 88 reportTitle() 89 90 if (Object.keys(data.advisories).length !== 0) { 91 // vulns found display a report. 92 93 let reviewFlag = false 94 95 data.actions.forEach((action) => { 96 if (action.action === 'update' || action.action === 'install') { 97 const recommendation = getRecommendation(action, config) 98 const label = action.resolves.length === 1 ? 'vulnerability' : 'vulnerabilities' 99 log(`# Run ${Utils.color(' ' + recommendation.cmd + ' ', 'inverse', config.withColor)} to resolve ${action.resolves.length} ${label}`) 100 if (recommendation.isBreaking) { 101 log(`SEMVER WARNING: Recommended action is a potentially breaking change`) 102 } 103 104 action.resolves.forEach((resolution) => { 105 const advisory = data.advisories[resolution.id] 106 const tableOptions = { 107 colWidths: [15, 62], 108 wordWrap: true 109 } 110 if (!config.withUnicode) { 111 tableOptions.chars = blankChars 112 } 113 const table = new Table(tableOptions) 114 115 table.push( 116 {[Utils.severityLabel(advisory.severity, config.withColor, true)]: Utils.color(advisory.title, 'bold', config.withColor)}, 117 {'Package': advisory.module_name}, 118 {'Dependency of': `${resolution.path.split('>')[0]} ${resolution.dev ? '[dev]' : ''}`}, 119 {'Path': `${resolution.path.split('>').join(Utils.color(' > ', 'grey', config.withColor))}`}, 120 {'More info': advisory.url || `https://www.npmjs.com/advisories/${advisory.id}`} 121 ) 122 123 log(table.toString() + '\n\n') 124 }) 125 } 126 if (action.action === 'review') { 127 if (!reviewFlag) { 128 const tableOptions = { 129 colWidths: [78] 130 } 131 if (!config.withUnicode) { 132 tableOptions.chars = blankChars 133 } 134 const table = new Table(tableOptions) 135 table.push([{ 136 content: 'Manual Review\nSome vulnerabilities require your attention to resolve\n\nVisit https://go.npm.me/audit-guide for additional guidance', 137 vAlign: 'center', 138 hAlign: 'center' 139 }]) 140 141 log(table.toString()) 142 } 143 reviewFlag = true 144 145 action.resolves.forEach((resolution) => { 146 const advisory = data.advisories[resolution.id] 147 const tableOptions = { 148 colWidths: [15, 62], 149 wordWrap: true 150 } 151 if (!config.withUnicode) { 152 tableOptions.chars = blankChars 153 } 154 const table = new Table(tableOptions) 155 const patchedIn = advisory.patched_versions.replace(' ', '') === '<0.0.0' ? 'No patch available' : advisory.patched_versions 156 157 table.push( 158 {[Utils.severityLabel(advisory.severity, config.withColor, true)]: Utils.color(advisory.title, 'bold', config.withColor)}, 159 {'Package': advisory.module_name}, 160 {'Patched in': patchedIn}, 161 {'Dependency of': `${resolution.path.split('>')[0]} ${resolution.dev ? '[dev]' : ''}`}, 162 {'Path': `${resolution.path.split('>').join(Utils.color(' > ', 'grey', config.withColor))}`}, 163 {'More info': advisory.url || `https://www.npmjs.com/advisories/${advisory.id}`} 164 ) 165 log(table.toString()) 166 }) 167 } 168 }) 169 } 170 } 171 172 actions(data, config) 173 footer(data) 174 175 return { 176 report: output.trim(), 177 exitCode: exit 178 } 179} 180 181const getRecommendation = function (action, config) { 182 if (action.action === 'install') { 183 const isDev = action.resolves[0].dev 184 185 return { 186 cmd: `npm install ${isDev ? '--save-dev ' : ''}${action.module}@${action.target}`, 187 isBreaking: action.isMajor 188 } 189 } else { 190 return { 191 cmd: `npm update ${action.module} --depth ${action.depth}`, 192 isBreaking: false 193 } 194 } 195} 196 197module.exports = report 198