Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
audit-report.js331 linesDownload Raw Back to lib
1// an object representing the set of vulnerabilities in a tree
2
3const localeCompare = require('@isaacs/string-locale-compare')('en')
4const npa = require('npm-package-arg')
5const pickManifest = require('npm-pick-manifest')
6
7const Vuln = require('./vuln.js')
8const Calculator = require('@npmcli/metavuln-calculator')
9
10const { log, time } = require('proc-log')
11
12const npmFetch = require('npm-registry-fetch')
13
14class AuditReport extends Map {
15  #omit
16  error = null
17  topVulns = new Map()
18
19  static load (tree, opts) {
20    return new AuditReport(tree, opts).run()
21  }
22
23  get auditReportVersion () {
24    return 2
25  }
26
27  toJSON () {
28    const obj = {
29      auditReportVersion: this.auditReportVersion,
30      vulnerabilities: {},
31      metadata: {
32        vulnerabilities: {
33          info: 0,
34          low: 0,
35          moderate: 0,
36          high: 0,
37          critical: 0,
38          total: this.size,
39        },
40        dependencies: {
41          prod: 0,
42          dev: 0,
43          optional: 0,
44          peer: 0,
45          peerOptional: 0,
46          total: this.tree.inventory.size - 1,
47        },
48      },
49    }
50
51    for (const node of this.tree.inventory.values()) {
52      const { dependencies } = obj.metadata
53      let prod = true
54      for (const type of [
55        'dev',
56        'optional',
57        'peer',
58        'peerOptional',
59      ]) {
60        if (node[type]) {
61          dependencies[type]++
62          prod = false
63        }
64      }
65      if (prod) {
66        dependencies.prod++
67      }
68    }
69
70    // if it doesn't have any topVulns, then it's fixable with audit fix
71    // for each topVuln, figure out if it's fixable with audit fix --force,
72    // or if we have to just delete the thing, and if the fix --force will
73    // require a semver major update.
74    const vulnerabilities = []
75    for (const [name, vuln] of this.entries()) {
76      vulnerabilities.push([name, vuln.toJSON()])
77      obj.metadata.vulnerabilities[vuln.severity]++
78    }
79
80    obj.vulnerabilities = vulnerabilities
81      .sort(([a], [b]) => localeCompare(a, b))
82      .reduce((set, [name, vuln]) => {
83        set[name] = vuln
84        return set
85      }, {})
86
87    return obj
88  }
89
90  constructor (tree, opts = {}) {
91    super()
92    this.#omit = new Set(opts.omit || [])
93    this.calculator = new Calculator(opts)
94    this.options = opts
95    this.tree = tree
96    this.filterSet = opts.filterSet
97  }
98
99  async run () {
100    this.report = await this.#getReport()
101    log.silly('audit report', this.report)
102    if (this.report) {
103      await this.#init()
104    }
105    return this
106  }
107
108  isVulnerable (node) {
109    const vuln = this.get(node.packageName)
110    return !!(vuln && vuln.isVulnerable(node))
111  }
112
113  async #init () {
114    const timeEnd = time.start('auditReport:init')
115
116    const promises = []
117    for (const [name, advisories] of Object.entries(this.report)) {
118      for (const advisory of advisories) {
119        promises.push(this.calculator.calculate(name, advisory))
120      }
121    }
122
123    // now the advisories are calculated with a set of versions
124    // and the packument.  turn them into our style of vuln objects
125    // which also have the affected nodes, and also create entries
126    // for all the metavulns that we find from dependents.
127    const advisories = new Set(await Promise.all(promises))
128    const seen = new Set()
129    for (const advisory of advisories) {
130      const { name, range } = advisory
131      const k = `${name}@${range}`
132
133      const vuln = this.get(name) || new Vuln({ name, advisory })
134      if (this.has(name)) {
135        vuln.addAdvisory(advisory)
136      }
137      super.set(name, vuln)
138
139      // don't flag the exact same name/range more than once
140      // adding multiple advisories with the same range is fine, but no
141      // need to search for nodes we already would have added.
142      if (!seen.has(k)) {
143        const p = []
144        for (const node of this.tree.inventory.query('packageName', name)) {
145          if (!this.shouldAudit(node)) {
146            continue
147          }
148
149          // if not vulnerable by this advisory, keep searching
150          if (!advisory.testVersion(node.version)) {
151            continue
152          }
153
154          // we will have loaded the source already if this is a metavuln
155          if (advisory.type === 'metavuln') {
156            vuln.addVia(this.get(advisory.dependency))
157          }
158
159          // already marked this one, no need to do it again
160          if (vuln.nodes.has(node)) {
161            continue
162          }
163
164          // haven't marked this one yet.  get its dependents.
165          vuln.nodes.add(node)
166          for (const { from: dep, spec } of node.edgesIn) {
167            if (dep.isTop && !vuln.topNodes.has(dep)) {
168              vuln.fixAvailable = this.#fixAvailable(vuln, spec)
169              if (vuln.fixAvailable !== true) {
170                // now we know the top node is vulnerable, and cannot be
171                // upgraded out of the bad place without --force.  But, there's
172                // no need to add it to the actual vulns list, because nothing
173                // depends on root.
174                this.topVulns.set(vuln.name, vuln)
175                vuln.topNodes.add(dep)
176              }
177            } else {
178            // calculate a metavuln, if necessary
179              const calc = this.calculator.calculate(dep.packageName, advisory)
180              // eslint-disable-next-line promise/always-return
181              p.push(calc.then(meta => {
182                // eslint-disable-next-line promise/always-return
183                if (meta.testVersion(dep.version, spec)) {
184                  advisories.add(meta)
185                }
186              }))
187            }
188          }
189        }
190        await Promise.all(p)
191        seen.add(k)
192      }
193
194      // make sure we actually got something.  if not, remove it
195      // this can happen if you are loading from a lockfile created by
196      // npm v5, since it lists the current version of all deps,
197      // rather than the range that is actually depended upon,
198      // or if using --omit with the older audit endpoint.
199      if (this.get(name).nodes.size === 0) {
200        this.delete(name)
201        continue
202      }
203
204      // if the vuln is valid, but THIS advisory doesn't apply to any of
205      // the nodes it references, then remove it from the advisory list.
206      // happens when using omit with old audit endpoint.
207      for (const advisory of vuln.advisories) {
208        const relevant = [...vuln.nodes]
209          .some(n => advisory.testVersion(n.version))
210        if (!relevant) {
211          vuln.deleteAdvisory(advisory)
212        }
213      }
214    }
215
216    timeEnd()
217  }
218
219  // given the spec, see if there is a fix available at all, and note whether or not it's a semver major fix or not (i.e. will need --force)
220  #fixAvailable (vuln, spec) {
221    // TODO we return true, false, OR an object here. this is probably a bad pattern.
222    if (!vuln.testSpec(spec)) {
223      return true
224    }
225
226    // even if we HAVE a packument, if we're looking for it somewhere other than the registry and we have something vulnerable then we're stuck with it.
227    const specObj = npa(spec)
228    if (!specObj.registry) {
229      return false
230    }
231
232    if (specObj.subSpec) {
233      spec = specObj.subSpec.rawSpec
234    }
235
236    // we don't provide fixes for top nodes other than root, but we still check to see if the node is fixable with a different version, and note if that is a semver major bump.
237    try {
238      const {
239        _isSemVerMajor: isSemVerMajor,
240        version,
241        name,
242      } = pickManifest(vuln.packument, spec, {
243        ...this.options,
244        before: null,
245        avoid: vuln.range,
246        avoidStrict: true,
247      })
248      return { name, version, isSemVerMajor }
249    } catch (er) {
250      return false
251    }
252  }
253
254  set () {
255    throw new Error('do not call AuditReport.set() directly')
256  }
257
258  async #getReport () {
259    // if we're not auditing, just return false
260    if (this.options.audit === false || this.options.offline === true || this.tree.inventory.size === 1) {
261      return null
262    }
263
264    const timeEnd = time.start('auditReport:getReport')
265    try {
266      const body = this.prepareBulkData()
267      log.silly('audit', 'bulk request', body)
268
269      // no sense asking if we don't have anything to audit,
270      // we know it'll be empty
271      if (!Object.keys(body).length) {
272        return null
273      }
274
275      const res = await npmFetch('/-/npm/v1/security/advisories/bulk', {
276        ...this.options,
277        registry: this.options.auditRegistry || this.options.registry,
278        method: 'POST',
279        gzip: true,
280        body,
281      })
282
283      return await res.json()
284    } catch (er) {
285      log.verbose('audit error', er)
286      log.silly('audit error', String(er.body))
287      this.error = er
288      return null
289    } finally {
290      timeEnd()
291    }
292  }
293
294  // return true if we should audit this one
295  shouldAudit (node) {
296    if (
297      !node.version ||
298      node.isRoot ||
299      node.isLink ||
300      node.linksIn?.size > 0 ||
301      (this.filterSet && this.filterSet?.size !== 0 && !this.filterSet?.has(node))
302    ) {
303      return false
304    }
305    if (this.#omit.size === 0) {
306      return true
307    }
308    return !node.shouldOmit(this.#omit)
309  }
310
311  prepareBulkData () {
312    const payload = {}
313    for (const name of this.tree.inventory.query('packageName')) {
314      const set = new Set()
315      for (const node of this.tree.inventory.query('packageName', name)) {
316        if (!this.shouldAudit(node)) {
317          continue
318        }
319
320        set.add(node.version)
321      }
322      if (set.size) {
323        payload[name] = [...set]
324      }
325    }
326    return payload
327  }
328}
329
330module.exports = AuditReport
331 
codekingpro/portable-devtools · Team Ai