Socket
Socket
Sign inDemoInstall

@npmcli/arborist

Package Overview
Dependencies
Maintainers
6
Versions
192
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@npmcli/arborist - npm Package Compare versions

Comparing version 0.0.24 to 0.0.25

421

lib/audit-report.js

@@ -5,22 +5,9 @@ // an object representing the set of vulnerabilities in a tree

const pickManifest = require('npm-pick-manifest')
const pacote = require('pacote')
const {intersects, satisfies, simplifyRange} = require('semver')
const semverOpt = { loose: true, includePrerelease: true }
const Vuln = require('./vuln.js')
const Calculator = require('@npmcli/metavuln-calculator')
const _getReport = Symbol('getReport')
const _processDeps = Symbol('processDeps')
const _processDependent = Symbol('processDependent')
const _metaVulnSeen = Symbol('metaVulnSeen')
const _specVulnerableMemo = Symbol('specVulnerableMemo')
const _addVulnerability = Symbol('addVulnerability')
const _vulnDependents = Symbol('vulnDependents')
const _isVulnerable = Symbol('isVulnerable')
const _specVulnerable = Symbol('specVulnerable')
const _specVulnCheck = Symbol('specVulnCheck')
const _fixAvailable = Symbol('fixAvailable')
const _packument = Symbol('packument')
const _packuments = Symbol('packuments')
const _getDepSpec = Symbol('getDepSpec')
const _checkTopNode = Symbol('checkTopNode')
const _init = Symbol('init')

@@ -87,7 +74,15 @@ const _omit = Symbol('omit')

// require a semver major update.
const vulnerabilities = []
for (const [name, vuln] of this.entries()) {
obj.vulnerabilities[name] = vuln.toJSON()
vulnerabilities.push([name, vuln.toJSON()])
obj.metadata.vulnerabilities[vuln.severity]++
}
obj.vulnerabilities = vulnerabilities
.sort(([a], [b]) => a.localeCompare(b))
.reduce((set, [name, vuln]) => {
set[name] = vuln
return set
}, {})
return obj

@@ -98,11 +93,6 @@ }

super()
this[_metaVulnSeen] = new Set()
this[_specVulnerableMemo] = new Map()
this[_vulnDependents] = new Set()
this[_packuments] = new Map()
this[_omit] = new Set(opts.omit || [])
this.topVulns = new Map()
this.advisoryVulns = new Map()
this.dependencyVulns = new Map()
this.calculator = new Calculator(opts)
this.error = null

@@ -116,9 +106,12 @@ this.options = opts

this.report = await this[_getReport]()
if (this.report) {
if (this.report)
await this[_init]()
await this[_processDeps]()
}
return this
}
isVulnerable (node) {
const vuln = this.get(node.package.name)
return !!(vuln && vuln.isVulnerable(node))
}
async [_init] () {

@@ -130,95 +123,122 @@ process.emit('time', 'auditReport:init')

for (const advisory of advisories) {
const { vulnerable_versions: range } = advisory
promises.push(this[_addVulnerability](name, range, advisory))
promises.push(this.calculator.calculate(name, advisory))
}
}
await Promise.all(promises)
process.emit('timeEnd', 'auditReport:init')
}
// 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
// for each node P
// for each vulnerable dep Q
// pickManifest(Q, P's dep on Q, {avoid})
// if resulting version is vunlerable, then P@version is vulnerable
// find all versions of P depending on unsafe Q
async [_processDeps] () {
process.emit('time', 'auditReport:process')
for (const p of this[_vulnDependents]) {
await this[_processDependent](p)
}
process.emit('timeEnd', 'auditReport:process')
}
// 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.
const k = `${name}@${range}`
if (seen.has(k)) {
continue
}
seen.add(k)
isVulnerable (node) {
return node && this.has(node.name) &&
this.get(node.name).isVulnerable(node)
}
const vuln = this.get(name) || new Vuln({ name, advisory })
if (this.has(name))
vuln.addAdvisory(advisory)
super.set(name, vuln)
[_specVulnCheck] (paku, spec) {
// if it's not a thing that came from the registry, and for whatever
// reason, it's vulnerable, and we have to assume we can't fix that.
if (!paku || !paku.versions || typeof paku.versions !== 'object')
return false
const p = []
for (const node of this.tree.inventory.query('name', name)) {
if (shouldOmit(node, this[_omit])) {
continue
}
// similarly, even if we HAVE a packument, but we're looking for a version
// that doesn't come out of that packument, and what we've got is
// vulnerable, then we're stuck with it.
const specObj = npa(spec)
if (!specObj.registry)
return false
// if not vulnerable by this advisory, keep searching
if (!advisory.testVersion(node.version)) {
continue
}
return spec
}
// we will have loaded the source already if this is a metavuln
if (advisory.type === 'metavuln') {
vuln.addVia(this.get(advisory.dependency))
}
// pass in the packument for the vulnerable dep, the spec that is
// depended upon, and the range of dep versions to avoid.
// returns true if every satisfying version is vulnerable.
[_specVulnerable] (paku, spec, avoid, bundled) {
spec = this[_specVulnCheck](paku, spec)
if (spec === false)
return true
// already marked this one, no need to do it again
if (vuln.nodes.has(node)) {
continue
}
const key = `${paku.name}@${spec} !${avoid} bundled=${bundled}`
if (this[_specVulnerableMemo].has(key)) {
return this[_specVulnerableMemo].get(key)
// 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)) {
this[_checkTopNode](dep, vuln, spec)
} else {
// calculate a metavuln, if necessary
p.push(this.calculator.calculate(dep.name, advisory).then(meta => {
if (meta.testVersion(dep.version, spec))
advisories.add(meta)
}))
}
}
}
await Promise.all(p)
// 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)
}
}
process.emit('timeEnd', 'auditReport:init')
}
// if it's a bundle dep, then we must avoid it if the vulnerable
// range and the dep range intersect, since we can't update in-place.
if (bundled) {
const val = intersects(spec, avoid, { includePrerelease: true })
if (val)
this[_specVulnerableMemo].set(key, val)
return val
[_checkTopNode] (topNode, vuln, spec) {
vuln.fixAvailable = this[_fixAvailable](topNode, 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(topNode)
}
}
// if we can't avoid the vulnerable version range within the spec
// required, then the dep range is entirely vulnerable.
try {
const val = pickManifest(paku, spec, {
...this.options,
before: null,
avoid,
})._shouldAvoid
if (val)
this[_specVulnerableMemo].set(key, val)
return val
} catch (er) {
// not vulnerable per se, but also not installable, so best avoided
// this can happen when dep versions are unpublished.
/* istanbul ignore next */
this[_specVulnerableMemo].set(key, true)
/* istanbul ignore next */
// check whether the top node is vulnerable.
// check whether we can get out of the bad place with --force, and if
// so, whether that update is SemVer Major
[_fixAvailable] (topNode, vuln, spec) {
// this will always be set to at least {name, versions:{}}
const paku = vuln.packument
if (!vuln.testSpec(spec)) {
return true
}
}
// see if the top node CAN be fixed, even with a semver major update
// if not, then the user just has to find a different thing to use.
[_fixAvailable] (paku, spec, avoid) {
spec = this[_specVulnCheck](paku, spec)
if (spec === false)
// similarly, even if we HAVE a packument, but we're looking for it
// somewhere other than the registry, and we got something vulnerable,
// then we're stuck with it.
const specObj = npa(spec)
if (!specObj.registry)
return false
// 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 if that is a semver major bump.
try {

@@ -232,3 +252,3 @@ const {

before: null,
avoid,
avoid: vuln.range,
avoidStrict: true,

@@ -242,125 +262,2 @@ })

// p is a node that depends on a known-vulnerable node q in the tree
// loop over versions of p to figure out which ones are dependent exclusively
// upon vulnerable versions of q
// add those p versions to a metavuln list, and add the new vuln on p
async [_processDependent] (p) {
const loc = p.location || '#ROOT'
process.emit('time', `auditReport:dep:${loc}`)
// remove it from the queue so we can process it again if another
// vulnerability will affect it.
this[_vulnDependents].delete(p)
const bd = p.package.bundleDependencies || []
for (const edge of p.edgesOut.values()) {
const { to: dep, spec } = edge
// if this isn't a dependency on a vulnerable module, skip it
if (!this.isVulnerable(dep)) {
continue
}
const vuln = this.get(dep.name)
// stop me if you've heard this one before
const key = `${p.package.name} -> ${dep.name}@${vuln.simpleRange}`
if (this[_metaVulnSeen].has(key)) {
continue
}
this[_metaVulnSeen].add(key)
const bundled = bd.includes(dep.name)
const {packument: depPaku, range: avoid} = vuln
// if the range is not entirely vulnerable, this can be fixed
// no metavuln required
if (!this[_specVulnerable](depPaku, spec, avoid, bundled)) {
continue
}
process.emit('time', `auditReport:dep:${loc}:${edge.to.location}`)
this.log.silly('audit', 'processing', key)
if (p.isTop) {
// this indicates that the root 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(dep.name, vuln)
vuln.topNodes.add(p)
// We don't provide fixes for top nodes other than root, but we
// still check to see if the node is fixable, and if semver major
vuln.fixAvailable = this[_fixAvailable](depPaku, spec, avoid)
} else {
// p is vulnerable!
// mark all versions with this problem, and then add the
// vulnerability for the dependent
const paku = await this[_packument](p.package.name)
const metaVuln = []
const versions = []
if (!paku) {
// not a dep that comes from the registry, apparently
metaVuln.push(p.version)
} else {
for (const [version, pmani] of Object.entries(paku.versions)) {
this.log.silly('audit', 'processing', dep.name, `${p.name}@${version}`)
// XXX if version already vulnerable, don't bother checking it
versions.push(version)
const spec = this[_getDepSpec](pmani, dep.name)
// if we don't even depend on the thing, we're in the clear
if (typeof spec !== 'string')
continue
const bd = pmani.bundleDependencies || []
const bundled = bd.includes(dep.name)
const specVuln = this[_specVulnerable](depPaku, spec, avoid, bundled)
if (specVuln) {
metaVuln.push(version)
}
}
}
const mvr = simplifyRange(versions, metaVuln.join(' || '), {
includePrerelease:true,
})
await this[_addVulnerability](p.name, mvr, vuln)
}
process.emit('timeEnd', `auditReport:dep:${loc}:${edge.to.location}`)
}
process.emit('timeEnd', `auditReport:dep:${loc}`)
}
async [_packument] (name) {
return this[_packuments].has(name) ? this[_packuments].get(name)
: pacote.packument(name, { ...this.options })
.catch(() => null)
.then(packument => {
this[_packuments].set(name, packument)
return packument
})
}
[_getDepSpec] (mani, name) {
// skip dev because that only matters at the root,
// where we aren't fetching a manifest from the registry
// with multiple versions anyway.
return mani.dependencies && mani.dependencies[name] ||
mani.optionalDependencies && mani.optionalDependencies[name] ||
mani.peerDependencies && mani.peerDependencies[name]
}
delete (name) {
const vuln = this.get(name)
if (vuln) {
for (const via of vuln.via) {
if (via instanceof Vuln)
via.effects.delete(vuln)
}
}
super.delete(name)
this.topVulns.delete(name)
this.advisoryVulns.delete(name)
this.dependencyVulns.delete(name)
}
set () {

@@ -370,74 +267,2 @@ throw new Error('do not call AuditReport.set() directly')

async [_addVulnerability] (name, range, via) {
this.log.silly('audit', 'add vuln', name, range)
const has = this.has(name)
const vuln = has ? this.get(name) : new Vuln({ name, via })
if (has)
vuln.addVia(via)
else
super.set(name, vuln)
// if we've already seen this exact range, just make sure that
// we have the advisory or source already, but do nothing else,
// because all the matching have already been collected.
if (vuln.hasRange(range))
return
vuln.addRange(range)
// track it in the appropriate maps for reporting on later
super.set(name, vuln)
if (!(via instanceof Vuln)) {
this.dependencyVulns.delete(name)
this.advisoryVulns.set(name, vuln)
} else if (!this.advisoryVulns.has(name))
this.dependencyVulns.set(name, vuln)
// wrap in try/finally to ensure we end the timer properly
// and don't leave it hanging to conflict with a future one.
try {
// we always treat metavulns as relevant. just check advisories
// that might not apply to this tree, given omit options.
let relevant = via instanceof Vuln
process.emit('time', `auditReport:add:${name}@${range}`)
for (const node of this.tree.inventory.query('name', name)) {
if (shouldOmit(node, this[_omit]))
continue
// check if this node would be affected by the advisory or metavuln
if (!relevant) {
const { version } = node.package
relevant = version && satisfies(version, range, semverOpt)
}
if (!vuln.isVulnerable(node))
continue
for (const {from} of node.edgesIn) {
this[_vulnDependents].add(from)
}
}
// if we didn't get anything, then why is this even here??
// 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 (vuln.nodes.size === 0)
return this.delete(name)
// if the vuln is valid, but THIS vuln isn't relevant, remove it
// from the via list. happens when using omit with old endpoint.
if (!relevant)
vuln.deleteVia(via)
if (!vuln.packument)
vuln.packument = await this[_packument](name)
} finally {
process.emit('timeEnd', `auditReport:add:${name}@${range}`)
}
}
// convert a quick-audit into a bulk advisory listing

@@ -472,4 +297,5 @@ static auditToBulk (report) {

// if we're not auditing, just return false
if (this.options.audit === false || this.tree.inventory.size === 0)
if (this.options.audit === false || this.tree.inventory.size === 0) {
return null
}

@@ -520,3 +346,4 @@ process.emit('time', 'auditReport:getReport')

const shouldOmit = (node, omit) =>
omit.size === 0 ? false
!node.version ? true
: omit.size === 0 ? false
: node.dev && omit.has('dev') ||

@@ -523,0 +350,0 @@ node.optional && omit.has('optional') ||

@@ -9,7 +9,6 @@ // An object representing a vulnerability either as the result of an

// - effects: Set of vulns triggered by this one
// - via: Set of advisories or vulnerabilities causing this vuln
//
// These objects are filled in by the operations in the AuditReport
// class, which sets the the packument and calls addRange() with
// the vulnerable range.
// - advisories: Set of advisories (including metavulns) causing this vuln.
// All of the entries in via are vulnerability objects returned by
// @npmcli/metavuln-calculator
// - via: dependency vulns which cause this one

@@ -19,4 +18,4 @@ const {satisfies, simplifyRange} = require('semver')

const npa = require('npm-package-arg')
const _range = Symbol('_range')
const _ranges = Symbol('_ranges')
const _simpleRange = Symbol('_simpleRange')

@@ -39,17 +38,18 @@ const _fixAvailable = Symbol('_fixAvailable')

class Vuln {
constructor ({ name, via }) {
constructor ({ name, advisory }) {
this.name = name
this.via = new Set()
this.advisories = new Set()
this.severity = null
this.addVia(via)
this.effects = new Set()
this.topNodes = new Set()
this[_ranges] = new Set()
this[_range] = null
this[_simpleRange] = null
this.nodes = new Set()
this.packument = null
// assume a fix is available unless it hits a top node
// that locks it in place, setting this to false or {isSemVerMajor, version}.
this[_fixAvailable] = true
this.addAdvisory(advisory)
this.packument = advisory.packument
this.versions = advisory.versions
}

@@ -63,17 +63,50 @@

// if there's a fix available for this at the top level, it means that
// it will also fix the vulns that led to it being there.
// it will also fix the vulns that led to it being there. to get there,
// we set the vias to the most "strict" of fix availables.
// - false: no fix is available
// - {name, version, isSemVerMajor} fix requires -f, is semver major
// - {name, version} fix requires -f, not semver major
// - true: fix does not require -f
for (const v of this.via) {
if (v.fixAvailable === true)
if (f === false)
v.fixAvailable = f
else if (v.fixAvailable === true)
v.fixAvailable = f
else if (typeof f === 'object' && (
typeof v.fixAvailable !== 'object' || !v.fixAvailable.isSemVerMajor))
v.fixAvailable = f
}
}
testSpec (spec) {
const specObj = npa(spec)
if (!specObj.registry)
return true
for (const v of this.versions) {
if (satisfies(v, spec) && !satisfies(v, this.range, semverOpt))
return false
}
return true
}
toJSON () {
// sort so that they're always in a consistent order
return {
name: this.name,
severity: this.severity,
via: [...this.via].map(v => v instanceof Vuln ? v.name : v),
effects: [...this.effects].map(v => v.name),
// just loop over the advisories, since via is only Vuln objects,
// and calculated advisories have all the info we need
via: [...this.advisories].map(v => v.type === 'metavuln' ? v.dependency : {
...v,
versions: undefined,
vulnerableVersions: undefined,
id: undefined,
}).sort((a, b) =>
String(a.source || a).localeCompare(String(b.source || b))),
effects: [...this.effects].map(v => v.name)
.sort(/* istanbul ignore next */(a, b) => a.localeCompare(b)),
range: this.simpleRange,
nodes: [...this.nodes].map(n => n.location),
nodes: [...this.nodes].map(n => n.location)
.sort(/* istanbul ignore next */(a, b) => a.localeCompare(b)),
fixAvailable: this[_fixAvailable],

@@ -83,33 +116,44 @@ }

deleteVia (via) {
this.via.delete(via)
addVia (v) {
this.via.add(v)
v.effects.add(this)
// call the setter since we might add vias _after_ setting fixAvailable
this.fixAvailable = this.fixAvailable
}
deleteVia (v) {
this.via.delete(v)
v.effects.delete(this)
}
deleteAdvisory (advisory) {
this.advisories.delete(advisory)
// make sure we have the max severity of all the vulns causing this one
this.severity = null
this[_range] = null
this[_simpleRange] = null
// refresh severity
for (const advisory of this.advisories) {
this.addAdvisory(advisory)
}
// remove any effects that are no longer relevant
const vias = new Set([...this.advisories].map(a => a.dependency))
for (const via of this.via) {
this.addVia(via)
if (!vias.has(via.name))
this.deleteVia(via)
}
}
addVia (via) {
this.via.add(via)
const sev = severities.get(via.severity)
addAdvisory (advisory) {
this.advisories.add(advisory)
const sev = severities.get(advisory.severity)
this[_range] = null
this[_simpleRange] = null
if (sev > severities.get(this.severity))
this.severity = via.severity
if (via instanceof Vuln)
via.effects.add(this)
this.severity = advisory.severity
}
hasRange (range) {
return this[_ranges].has(range)
}
addRange (range) {
this[_ranges].add(range)
this[_range] = [...this[_ranges]].join(' || ')
this[_simpleRange] = null
}
get range () {
return this[_range] || (this[_range] = [...this[_ranges]].join(' || '))
return this[_range] ||
(this[_range] = [...this.advisories].map(v => v.range).join(' || '))
}

@@ -120,6 +164,5 @@

return this[_simpleRange]
const versions = [...this.advisories][0].versions
const range = this.range
if (!this.packument)
return range
const versions = Object.keys(this.packument.versions)
const simple = simplifyRange(versions, range, semverOpt)

@@ -134,5 +177,10 @@ return this[_simpleRange] = this[_range] = simple

const { version } = node.package
if (version && satisfies(version, this.range, semverOpt)) {
this.nodes.add(node)
return true
if (!version)
return false
for (const v of this.advisories) {
if (v.testVersion(version)) {
this.nodes.add(node)
return true
}
}

@@ -139,0 +187,0 @@

{
"name": "@npmcli/arborist",
"version": "0.0.24",
"version": "0.0.25",
"description": "Manage node_modules trees",

@@ -27,3 +27,4 @@ "dependencies": {

"walk-up-path": "^1.0.0",
"json-parse-even-better-errors": "^2.3.1"
"json-parse-even-better-errors": "^2.3.1",
"@npmcli/metavuln-calculator": "^1.0.0"
},

@@ -30,0 +31,0 @@ "devDependencies": {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc