File Explorer

/proc/4/task/7/root/var/lang/lib/node_modules/npm/node_modules/@npmcli/arborist/lib

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

audit-report.js11.9 KB · 390 lines
// an object representing the set of vulnerabilities in a tree const localeCompare = require('@isaacs/string-locale-compare')('en')const npa = require('npm-package-arg')const pickManifest = require('npm-pick-manifest') const Vuln = require('./vuln.js')const Calculator = require('@npmcli/metavuln-calculator')const { isReleaseAgeExcluded } = require('./release-age-exclude.js') const { log, time } = require('proc-log') const npmFetch = require('npm-registry-fetch') class AuditReport extends Map {  #omit  error = null  topVulns = new Map()   static load (tree, opts) {    return new AuditReport(tree, opts).run()  }   get auditReportVersion () {    return 2  }   toJSON () {    const obj = {      auditReportVersion: this.auditReportVersion,      vulnerabilities: {},      metadata: {        vulnerabilities: {          info: 0,          low: 0,          moderate: 0,          high: 0,          critical: 0,          total: this.size,        },        dependencies: {          prod: 0,          dev: 0,          optional: 0,          peer: 0,          peerOptional: 0,          total: this.tree.inventory.size - 1,        },      },    }     for (const node of this.tree.inventory.values()) {      const { dependencies } = obj.metadata      let prod = true      for (const type of [        'dev',        'optional',        'peer',        'peerOptional',      ]) {        if (node[type]) {          dependencies[type]++          prod = false        }      }      if (prod) {        dependencies.prod++      }    }     // if it doesn't have any topVulns, then it's fixable with audit fix    // for each topVuln, figure out if it's fixable with audit fix --force,    // or if we have to just delete the thing, and if the fix --force will    // require a semver major update.    const vulnerabilities = []    for (const [name, vuln] of this.entries()) {      vulnerabilities.push([name, vuln.toJSON()])      obj.metadata.vulnerabilities[vuln.severity]++    }     obj.vulnerabilities = vulnerabilities      .sort(([a], [b]) => localeCompare(a, b))      .reduce((set, [name, vuln]) => {        set[name] = vuln        return set      }, {})     return obj  }   constructor (tree, opts = {}) {    super()    this.#omit = new Set(opts.omit || [])    this.calculator = new Calculator(opts)    this.options = opts    this.tree = tree    this.filterSet = opts.filterSet  }   async run () {    this.report = await this.#getReport()    log.silly('audit report', this.report)    if (this.report) {      await this.#init()    }    return this  }   isVulnerable (node) {    const vuln = this.get(node.packageName)    return !!(vuln && vuln.isVulnerable(node))  }   async #init () {    const timeEnd = time.start('auditReport:init')     const promises = []    for (const [name, advisories] of Object.entries(this.report)) {      for (const advisory of advisories) {        promises.push(this.calculator.calculate(name, advisory))      }    }     // now the advisories are calculated with a set of versions    // and the packument.  turn them into our style of vuln objects    // which also have the affected nodes, and also create entries    // for all the metavulns that we find from dependents.    const advisories = new Set(await Promise.all(promises))    const seen = new Set()    for (const advisory of advisories) {      const { name, range } = advisory      const k = `${name}@${range}`       const vuln = this.get(name) || new Vuln({ name, advisory })      if (this.has(name)) {        vuln.addAdvisory(advisory)      }      super.set(name, vuln)       // don't flag the exact same name/range more than once      // adding multiple advisories with the same range is fine, but no      // need to search for nodes we already would have added.      if (!seen.has(k)) {        const p = []        for (const node of this.tree.inventory.query('packageName', name)) {          if (!this.shouldAudit(node)) {            continue          }           // if not vulnerable by this advisory, keep searching          if (!advisory.testVersion(node.version)) {            continue          }           // already marked this one, no need to do it again          if (vuln.nodes.has(node)) {            continue          }           // haven't marked this one yet.  get its dependents.          vuln.nodes.add(node)          for (const { from: dep, spec } of node.edgesIn) {            if (dep.isTop && !vuln.topNodes.has(dep)) {              vuln.fixAvailable = this.#fixAvailable(vuln, spec)              if (vuln.fixAvailable !== true) {                // now we know the top node is vulnerable, and cannot be                // upgraded out of the bad place without --force.  But, there's                // no need to add it to the actual vulns list, because nothing                // depends on root.                this.topVulns.set(vuln.name, vuln)                vuln.topNodes.add(dep)              } else {                // An in-range fix exists, but a `min-release-age`/`before`                // window may put the patched version out of reach, leaving the                // vulnerable version installed.                vuln.fixBlockedByReleaseAge = this.#fixBlockedByReleaseAge(vuln, spec)              }            } else {            // calculate a metavuln, if necessary              const calc = this.calculator.calculate(dep.packageName, advisory)              // eslint-disable-next-line promise/always-return              p.push(calc.then(meta => {                // eslint-disable-next-line promise/always-return                if (meta.testVersion(dep.version, spec)) {                  advisories.add(meta)                }              }))            }          }        }        await Promise.all(p)        seen.add(k)      }       // make sure we actually got something.  if not, remove it      // this can happen if you are loading from a lockfile created by      // npm v5, since it lists the current version of all deps,      // rather than the range that is actually depended upon,      // or if using --omit with the older audit endpoint.      if (this.get(name).nodes.size === 0) {        this.delete(name)        continue      }       // if the vuln is valid, but THIS advisory doesn't apply to any of      // the nodes it references, then remove it from the advisory list.      // happens when using omit with old audit endpoint.      for (const advisory of vuln.advisories) {        const relevant = [...vuln.nodes]          .some(n => advisory.testVersion(n.version))        if (!relevant) {          vuln.deleteAdvisory(advisory)        }      }    }     // post-loop reconciliation: establish all via links    // done here to ensure all vulns exist, avoid redundant setter triggers,    // and correctly handle multiple paths and omission cleanups.    for (const vuln of this.values()) {      for (const advisory of vuln.advisories) {        if (advisory.type === 'metavuln') {          vuln.addVia(this.get(advisory.dependency))        }      }    }     timeEnd()  }   // 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)  #fixAvailable (vuln, spec) {    // TODO we return true, false, OR an object here. this is probably a bad pattern.    if (!vuln.testSpec(spec)) {      return true    }     // 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.    const specObj = npa(spec)    if (!specObj.registry) {      return false    }     if (specObj.subSpec) {      spec = specObj.subSpec.rawSpec    }     // 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.    try {      const {        _isSemVerMajor: isSemVerMajor,        version,        name,      } = pickManifest(vuln.packument, spec, {        ...this.options,        before: null,        avoid: vuln.range,        avoidStrict: true,      })      return { name, version, isSemVerMajor }    } catch (er) {      return false    }  }   // A fix that `#fixAvailable` reports as installable in-range can still be  // unreachable when a release-age window (`before` / `min-release-age`) is set,  // because the patched version was published after the cutoff. `npm audit fix`  // then resolves back to a version that is still vulnerable, so detect that  // here and let callers warn about it. Only meaningful when an in-range fix  // exists (fixAvailable === true). Returns the blocked fix as  // `{ version, before }`, or `false` when nothing is blocked.  #fixBlockedByReleaseAge (vuln, spec) {    const { before, minReleaseAgeExclude } = this.options    // No window, or this package is explicitly exempt from it.    if (!before || isReleaseAgeExcluded(vuln.name, minReleaseAgeExclude)) {      return false    }     // For `npm:` aliases the fix resolves against the alias target, so feed    // pickManifest the underlying range rather than the alias spec (which it    // rejects). Mirrors `#fixAvailable`.    const specObj = npa(spec)    if (specObj.subSpec) {      spec = specObj.subSpec.rawSpec    }     // The version that would be installed if the window were lifted.    const { version } = pickManifest(vuln.packument, spec, {      ...this.options,      before: null,      avoid: vuln.range,    })     // What resolution can actually reach within the window. This mirrors how    // `build-ideal-tree` re-resolves the edge (non-strict avoid + `before`).    let windowed    try {      windowed = pickManifest(vuln.packument, spec, {        ...this.options,        avoid: vuln.range,      })    } catch {      // Nothing is old enough to install at all: the fix is blocked.      return { version, before }    }     // `_shouldAvoid` means the best version available within the window is still    // in the vulnerable range, so the only non-vulnerable fix is too new.    return windowed._shouldAvoid ? { version, before } : false  }   set () {    throw new Error('do not call AuditReport.set() directly')  }   async #getReport () {    // if we're not auditing, just return false    if (this.options.audit === false || this.options.offline === true || this.tree.inventory.size === 1) {      return null    }     const timeEnd = time.start('auditReport:getReport')    try {      const body = this.prepareBulkData()      log.silly('audit', 'bulk request', body)       // no sense asking if we don't have anything to audit,      // we know it'll be empty      if (!Object.keys(body).length) {        return null      }       const res = await npmFetch('/-/npm/v1/security/advisories/bulk', {        ...this.options,        registry: this.options.auditRegistry || this.options.registry,        method: 'POST',        gzip: true,        body,      })       return await res.json()    } catch (er) {      log.verbose('audit error', er)      log.silly('audit error', String(er.body))      this.error = er      return null    } finally {      timeEnd()    }  }   // return true if we should audit this one  shouldAudit (node) {    if (      !node.version ||      node.isRoot ||      node.isLink ||      node.linksIn?.size > 0 ||      (this.filterSet && this.filterSet?.size !== 0 && !this.filterSet?.has(node))    ) {      return false    }    if (this.#omit.size === 0) {      return true    }    return !node.shouldOmit(this.#omit)  }   prepareBulkData () {    const payload = {}    for (const name of this.tree.inventory.query('packageName')) {      const set = new Set()      for (const node of this.tree.inventory.query('packageName', name)) {        if (!this.shouldAudit(node)) {          continue        }         set.add(node.version)      }      if (set.size) {        payload[name] = [...set]      }    }    return payload  }} module.exports = AuditReport