File Explorer

/var/lang/lib/node_modules/npm/node_modules/@npmcli/metavuln-calculator/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 /.

advisory.js13.1 KB · 436 lines
const hash = require('./hash.js')const semver = require('semver')const semverOpt = { includePrerelease: true, loose: true }const getDepSpec = require('./get-dep-spec.js') // any fields that we don't want in the cache need to be hiddenconst _source = Symbol('source')const _packument = Symbol('packument')const _versionVulnMemo = Symbol('versionVulnMemo')const _updated = Symbol('updated')const _options = Symbol('options')const _specVulnMemo = Symbol('specVulnMemo')const _testVersion = Symbol('testVersion')const _testVersions = Symbol('testVersions')const _calculateRange = Symbol('calculateRange')const _markVulnerable = Symbol('markVulnerable')const _testSpec = Symbol('testSpec') class Advisory {  constructor (name, source, options = {}) {    this.source = source.id    this[_source] = source    this[_options] = options    this.name = name    if (!source.name) {      source.name = name    }     this.dependency = source.name     if (this.type === 'advisory') {      this.title = source.title      this.url = source.url    } else {      this.title = `Depends on vulnerable versions of ${source.name}`      this.url = null    }     this.severity = source.severity || 'high'    this.versions = []    this.vulnerableVersions = []    this.cwe = source.cwe    this.cvss = source.cvss     // advisories have the range, metavulns do not    // if an advisory doesn't specify range, assume all are vulnerable    this.range = this.type === 'advisory' ? source.vulnerable_versions || '*'      : null     this.id = hash(this)     this[_packument] = null    // memoized list of which versions are vulnerable    this[_versionVulnMemo] = new Map()    // memoized list of which dependency specs are vulnerable    this[_specVulnMemo] = new Map()    this[_updated] = false  }   // true if we updated from what we had in cache  get updated () {    return this[_updated]  }   get type () {    return this.dependency === this.name ? 'advisory' : 'metavuln'  }   get packument () {    return this[_packument]  }   // load up the data from a cache entry and a fetched packument  load (cached, packument) {    // basic data integrity gutcheck    if (!cached || typeof cached !== 'object') {      throw new TypeError('invalid cached data, expected object')    }     if (!packument || typeof packument !== 'object') {      throw new TypeError('invalid packument data, expected object')    }     if (cached.id && cached.id !== this.id) {      throw Object.assign(new Error('loading from incorrect cache entry'), {        expected: this.id,        actual: cached.id,      })    }    if (packument.name !== this.name) {      throw Object.assign(new Error('loading from incorrect packument'), {        expected: this.name,        actual: packument.name,      })    }    if (this[_packument]) {      throw new Error('advisory object already loaded')    }     // if we have a range from the initialization, and the cached    // data has a *different* range, then we know we have to recalc.    // just don't use the cached data, so we will definitely not match later    if (!this.range || cached.range && cached.range === this.range) {      Object.assign(this, cached)    }     this[_packument] = packument     const pakuVersions = Object.keys(packument.versions || {})    const allVersions = new Set([...pakuVersions, ...this.versions])    const versionsAdded = []    const versionsRemoved = []    for (const v of allVersions) {      if (!this.versions.includes(v)) {        versionsAdded.push(v)        this.versions.push(v)      } else if (!pakuVersions.includes(v)) {        versionsRemoved.push(v)      }    }     // strip out any removed versions from our lists, and sort by semver    this.versions = semver.sort(this.versions.filter(v =>      !versionsRemoved.includes(v)), semverOpt)     // if no changes, then just return what we got from cache    // versions added or removed always means we changed    // otherwise, advisories change if the range changes, and    // metavulns change if the source was updated    const unchanged = this.type === 'advisory'      ? this.range && this.range === cached.range      : !this[_source].updated     // if the underlying source changed, by an advisory updating the    // range, or a source advisory being updated, then we have to re-check    // otherwise, only recheck the new ones.    this.vulnerableVersions = !unchanged ? []      : semver.sort(this.vulnerableVersions.filter(v =>        !versionsRemoved.includes(v)), semverOpt)     if (unchanged && !versionsAdded.length && !versionsRemoved.length) {      // nothing added or removed, nothing to do here.  use the cached copy.      return this    }     this[_updated] = true     // test any versions newly added    if (!unchanged || versionsAdded.length) {      this[_testVersions](unchanged ? versionsAdded : this.versions)    }    this.vulnerableVersions = semver.sort(this.vulnerableVersions, semverOpt)     // metavulns have to calculate their range, since cache is invalidated    // advisories just get their range from the advisory above    if (this.type === 'metavuln') {      this[_calculateRange]()    }     return this  }   [_calculateRange] () {    // calling semver.simplifyRange with a massive list of versions, and those    // versions all concatenated with `||` is a geometric CPU explosion!    // we can try to be a *little* smarter up front by doing x-y for all    // contiguous version sets in the list    const ranges = []    this.versions = semver.sort(this.versions, semverOpt)    this.vulnerableVersions = semver.sort(this.vulnerableVersions, semverOpt)    for (let v = 0, vulnVer = 0; v < this.versions.length; v++) {      // figure out the vulnerable subrange      const vr = [this.versions[v]]      while (v < this.versions.length) {        if (this.versions[v] !== this.vulnerableVersions[vulnVer]) {          // we don't test prerelease versions, so just skip past it          if (/-/.test(this.versions[v])) {            v++            continue          }          break        }        if (vr.length > 1) {          vr[1] = this.versions[v]        } else {          vr.push(this.versions[v])        }        v++        vulnVer++      }      // it'll either be just the first version, which means no overlap,      // or the start and end versions, which might be the same version      if (vr.length > 1) {        const tail = this.versions[this.versions.length - 1]        ranges.push(vr[1] === tail ? `>=${vr[0]}`          : vr[0] === vr[1] ? vr[0]          : vr.join(' - '))      }    }    const metavuln = ranges.join(' || ').trim()    this.range = !metavuln ? '<0.0.0-0'      : semver.simplifyRange(this.versions, metavuln, semverOpt)  }   // returns true if marked as vulnerable, false if ok  // spec is a dependency specifier, for metavuln cases  // where the version might not be in the packument.  if  // we have the packument and spec is not provided, then  // we use the dependency version from the manifest.  testVersion (version, spec = null) {    const sv = String(version)    if (this[_versionVulnMemo].has(sv)) {      return this[_versionVulnMemo].get(sv)    }     const result = this[_testVersion](version, spec)    if (result) {      this[_markVulnerable](version)    }    this[_versionVulnMemo].set(sv, !!result)    return result  }   [_markVulnerable] (version) {    const sv = String(version)    if (!this.vulnerableVersions.includes(sv)) {      this.vulnerableVersions.push(sv)    }  }   [_testVersion] (version, spec) {    const sv = String(version)    if (this.vulnerableVersions.includes(sv)) {      return true    }     if (this.type === 'advisory') {      // advisory, just test range      return semver.satisfies(version, this.range, semverOpt)    }     // check the dependency of this version on the vulnerable dep    // if we got a version that's not in the packument, fall back on    // the spec provided, if possible.    const mani = this[_packument]?.versions?.[version] || {      dependencies: {        [this.dependency]: spec,      },    }     if (!spec) {      spec = getDepSpec(mani, this.dependency)    }     // no dep, no vuln    if (spec === null) {      return false    }     if (!semver.validRange(spec, semverOpt)) {      // not a semver range, nothing we can hope to do about it      return true    }     const bd = mani.bundleDependencies    const bundled = bd && bd.includes(this[_source].name)    // XXX if bundled, then semver.intersects() means vulnerable    // else, pick a manifest and see if it can't be avoided    // try to pick a version of the dep that isn't vulnerable    const avoid = this[_source].range     if (bundled) {      return semver.intersects(spec, avoid, semverOpt)    }     return this[_source].testSpec(spec)  }   testSpec (spec) {    // testing all the versions is a bit costly, and the spec tends to stay    // consistent across multiple versions, so memoize this as well, in case    // we're testing lots of versions.    const memo = this[_specVulnMemo]    if (memo.has(spec)) {      return memo.get(spec)    }     const res = this[_testSpec](spec)    memo.set(spec, res)    return res  }   [_testSpec] (spec) {    for (const v of this.versions) {      const satisfies = semver.satisfies(v, spec, semverOpt)      if (!satisfies) {        continue      }      if (!this.testVersion(v)) {        return false      }    }    // either vulnerable, or not installable because nothing satisfied    // either way, best avoided.    return true  }   [_testVersions] (versions) {    if (!versions.length) {      return    }     // set of lists of versions    const versionSets = new Set()    versions = semver.sort(versions.map(v => semver.parse(v, semverOpt)))     // start out with the versions grouped by major and minor    let last = versions[0].major + '.' + versions[0].minor    let list = []    versionSets.add(list)    for (const v of versions) {      const k = v.major + '.' + v.minor      if (k !== last) {        last = k        list = []        versionSets.add(list)      }      list.push(v)    }     for (const set of versionSets) {      // it's common to have version lists like:      // 1.0.0      // 1.0.1-alpha.0      // 1.0.1-alpha.1      // ...      // 1.0.1-alpha.999      // 1.0.1      // 1.0.2-alpha.0      // ...      // 1.0.2-alpha.99      // 1.0.2      // with a huge number of prerelease versions that are not installable      // anyway.      // If mid has a prerelease tag, and set[0] does not, then walk it      // back until we hit a non-prerelease version      // If mid has a prerelease tag, and set[set.length-1] does not,      // then walk it forward until we hit a version without a prerelease tag      // Similarly, if the head/tail is a prerelease, but there is a non-pr      // version in the set, then start there instead.      let h = 0      const origHeadVuln = this.testVersion(set[h])      while (h < set.length && /-/.test(String(set[h]))) {        h++      }       // don't filter out the whole list!  they might all be pr's      if (h === set.length) {        h = 0      } else if (origHeadVuln) {        // if the original was vulnerable, assume so are all of these        for (let hh = 0; hh < h; hh++) {          this[_markVulnerable](set[hh])        }      }       let t = set.length - 1      const origTailVuln = this.testVersion(set[t])      while (t > h && /-/.test(String(set[t]))) {        t--      }       // don't filter out the whole list!  might all be pr's      if (t === h) {        t = set.length - 1      } else if (origTailVuln) {        // if original tail was vulnerable, assume these are as well        for (let tt = set.length - 1; tt > t; tt--) {          this[_markVulnerable](set[tt])        }      }       const headVuln = h === 0 ? origHeadVuln        : this.testVersion(set[h])       const tailVuln = t === set.length - 1 ? origTailVuln        : this.testVersion(set[t])       // if head and tail both vulnerable, whole list is thrown out      if (headVuln && tailVuln) {        for (let v = h; v < t; v++) {          this[_markVulnerable](set[v])        }        continue      }       // if length is 2 or 1, then we marked them all already      if (t < h + 2) {        continue      }       const mid = Math.floor(set.length / 2)      const pre = set.slice(0, mid)      const post = set.slice(mid)       // if the parent list wasn't prereleases, then drop pr tags      // from end of the pre list, and beginning of the post list,      // marking as vulnerable if the midpoint item we picked is.      if (!/-/.test(String(pre[0]))) {        const midVuln = this.testVersion(pre[pre.length - 1])        while (/-/.test(String(pre[pre.length - 1]))) {          const v = pre.pop()          if (midVuln) {            this[_markVulnerable](v)          }        }      }       if (!/-/.test(String(post[post.length - 1]))) {        const midVuln = this.testVersion(post[0])        while (/-/.test(String(post[0]))) {          const v = post.shift()          if (midVuln) {            this[_markVulnerable](v)          }        }      }       versionSets.add(pre)      versionSets.add(post)    }  }} module.exports = Advisory