• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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