update
This commit is contained in:
415
package/node_modules/@npmcli/arborist/lib/audit-report.js
generated
vendored
Normal file
415
package/node_modules/@npmcli/arborist/lib/audit-report.js
generated
vendored
Normal file
@@ -0,0 +1,415 @@
|
||||
// an object representing the set of vulnerabilities in a tree
|
||||
/* eslint camelcase: "off" */
|
||||
|
||||
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 _getReport = Symbol('getReport')
|
||||
const _fixAvailable = Symbol('fixAvailable')
|
||||
const _checkTopNode = Symbol('checkTopNode')
|
||||
const _init = Symbol('init')
|
||||
const _omit = Symbol('omit')
|
||||
const { log, time } = require('proc-log')
|
||||
|
||||
const fetch = require('npm-registry-fetch')
|
||||
|
||||
class AuditReport extends 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()
|
||||
const { omit } = opts
|
||||
this[_omit] = new Set(omit || [])
|
||||
this.topVulns = new Map()
|
||||
|
||||
this.calculator = new Calculator(opts)
|
||||
this.error = null
|
||||
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 (!shouldAudit(node, this[_omit], this.filterSet)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// if not vulnerable by this advisory, keep searching
|
||||
if (!advisory.testVersion(node.version)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// we will have loaded the source already if this is a metavuln
|
||||
if (advisory.type === 'metavuln') {
|
||||
vuln.addVia(this.get(advisory.dependency))
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
this[_checkTopNode](dep, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeEnd()
|
||||
}
|
||||
|
||||
[_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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 if that is a semver major bump.
|
||||
try {
|
||||
const {
|
||||
_isSemVerMajor: isSemVerMajor,
|
||||
version,
|
||||
name,
|
||||
} = pickManifest(paku, spec, {
|
||||
...this.options,
|
||||
before: null,
|
||||
avoid: vuln.range,
|
||||
avoidStrict: true,
|
||||
})
|
||||
return { name, version, isSemVerMajor }
|
||||
} catch (er) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
set () {
|
||||
throw new Error('do not call AuditReport.set() directly')
|
||||
}
|
||||
|
||||
// convert a quick-audit into a bulk advisory listing
|
||||
static auditToBulk (report) {
|
||||
if (!report.advisories) {
|
||||
// tack on the report json where the response body would go
|
||||
throw Object.assign(new Error('Invalid advisory report'), {
|
||||
body: JSON.stringify(report),
|
||||
})
|
||||
}
|
||||
|
||||
const bulk = {}
|
||||
const { advisories } = report
|
||||
for (const advisory of Object.values(advisories)) {
|
||||
const {
|
||||
id,
|
||||
url,
|
||||
title,
|
||||
severity = 'high',
|
||||
vulnerable_versions = '*',
|
||||
module_name: name,
|
||||
} = advisory
|
||||
bulk[name] = bulk[name] || []
|
||||
bulk[name].push({ id, url, title, severity, vulnerable_versions })
|
||||
}
|
||||
|
||||
return bulk
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
// first try the super fast bulk advisory listing
|
||||
const body = prepareBulkData(this.tree, this[_omit], this.filterSet)
|
||||
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 fetch('/-/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.silly('audit', 'bulk request failed', String(er.body))
|
||||
// that failed, try the quick audit endpoint
|
||||
const body = prepareData(this.tree, this.options)
|
||||
const res = await fetch('/-/npm/v1/security/audits/quick', {
|
||||
...this.options,
|
||||
registry: this.options.auditRegistry || this.options.registry,
|
||||
method: 'POST',
|
||||
gzip: true,
|
||||
body,
|
||||
})
|
||||
return AuditReport.auditToBulk(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
|
||||
const shouldAudit = (node, omit, filterSet) =>
|
||||
!node.version ? false
|
||||
: node.isRoot ? false
|
||||
: filterSet && filterSet.size !== 0 && !filterSet.has(node) ? false
|
||||
: omit.size === 0 ? true
|
||||
: !( // otherwise, just ensure we're not omitting this one
|
||||
node.dev && omit.has('dev') ||
|
||||
node.optional && omit.has('optional') ||
|
||||
node.devOptional && omit.has('dev') && omit.has('optional') ||
|
||||
node.peer && omit.has('peer')
|
||||
)
|
||||
|
||||
const prepareBulkData = (tree, omit, filterSet) => {
|
||||
const payload = {}
|
||||
for (const name of tree.inventory.query('packageName')) {
|
||||
const set = new Set()
|
||||
for (const node of tree.inventory.query('packageName', name)) {
|
||||
if (!shouldAudit(node, omit, filterSet)) {
|
||||
continue
|
||||
}
|
||||
|
||||
set.add(node.version)
|
||||
}
|
||||
if (set.size) {
|
||||
payload[name] = [...set]
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
const prepareData = (tree, opts) => {
|
||||
const { npmVersion: npm_version } = opts
|
||||
const node_version = process.version
|
||||
const { platform, arch } = process
|
||||
const { NODE_ENV: node_env } = process.env
|
||||
const data = tree.meta.commit()
|
||||
// the legacy audit endpoint doesn't support any kind of pre-filtering
|
||||
// we just have to get the advisories and skip over them in the report
|
||||
return {
|
||||
name: data.name,
|
||||
version: data.version,
|
||||
requires: {
|
||||
...(tree.package.devDependencies || {}),
|
||||
...(tree.package.peerDependencies || {}),
|
||||
...(tree.package.optionalDependencies || {}),
|
||||
...(tree.package.dependencies || {}),
|
||||
},
|
||||
dependencies: data.dependencies,
|
||||
metadata: {
|
||||
node_version,
|
||||
npm_version,
|
||||
platform,
|
||||
arch,
|
||||
node_env,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuditReport
|
Reference in New Issue
Block a user