@npmcli/arborist 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ const { minimatch } = require('minimatch')
8
8
  const npa = require('npm-package-arg')
9
9
  const pacote = require('pacote')
10
10
  const semver = require('semver')
11
+ const fetch = require('npm-registry-fetch')
11
12
 
12
13
  // handle results for parsed query asts, results are stored in a map that has a
13
14
  // key that points to each ast selector node and stores the resulting array of
@@ -18,6 +19,7 @@ class Results {
18
19
  #initialItems
19
20
  #inventory
20
21
  #outdatedCache = new Map()
22
+ #vulnCache
21
23
  #pendingCombinator
22
24
  #results = new Map()
23
25
  #targetNode
@@ -26,6 +28,7 @@ class Results {
26
28
  this.#currentAstSelector = opts.rootAstNode.nodes[0]
27
29
  this.#inventory = opts.inventory
28
30
  this.#initialItems = opts.initialItems
31
+ this.#vulnCache = opts.vulnCache
29
32
  this.#targetNode = opts.targetNode
30
33
 
31
34
  this.currentResults = this.#initialItems
@@ -211,6 +214,7 @@ class Results {
211
214
  inventory: this.#inventory,
212
215
  rootAstNode: this.currentAstNode.nestedNode,
213
216
  targetNode: item,
217
+ vulnCache: this.#vulnCache,
214
218
  })
215
219
  if (res.size > 0) {
216
220
  found.push(item)
@@ -239,6 +243,7 @@ class Results {
239
243
  inventory: this.#inventory,
240
244
  rootAstNode: this.currentAstNode.nestedNode,
241
245
  targetNode: this.currentAstNode,
246
+ vulnCache: this.#vulnCache,
242
247
  })
243
248
  return [...res]
244
249
  }
@@ -266,6 +271,7 @@ class Results {
266
271
  inventory: this.#inventory,
267
272
  rootAstNode: this.currentAstNode.nestedNode,
268
273
  targetNode: this.currentAstNode,
274
+ vulnCache: this.#vulnCache,
269
275
  })
270
276
  const internalSelector = new Set(res)
271
277
  return this.initialItems.filter(node =>
@@ -432,6 +438,75 @@ class Results {
432
438
  return this.initialItems.filter(node => node.target.edgesIn.size > 1)
433
439
  }
434
440
 
441
+ async vulnPseudo () {
442
+ if (!this.initialItems.length) {
443
+ return this.initialItems
444
+ }
445
+ if (!this.#vulnCache) {
446
+ const packages = {}
447
+ // We have to map the items twice, once to get the request, and a second time to filter out the results of that request
448
+ this.initialItems.map((node) => {
449
+ if (node.isProjectRoot || node.package.private) {
450
+ return
451
+ }
452
+ if (!packages[node.name]) {
453
+ packages[node.name] = []
454
+ }
455
+ if (!packages[node.name].includes(node.version)) {
456
+ packages[node.name].push(node.version)
457
+ }
458
+ })
459
+ const res = await fetch('/-/npm/v1/security/advisories/bulk', {
460
+ ...this.flatOptions,
461
+ registry: this.flatOptions.auditRegistry || this.flatOptions.registry,
462
+ method: 'POST',
463
+ gzip: true,
464
+ body: packages,
465
+ })
466
+ this.#vulnCache = await res.json()
467
+ }
468
+ const advisories = this.#vulnCache
469
+ const { vulns } = this.currentAstNode
470
+ return this.initialItems.filter(item => {
471
+ const vulnerable = advisories[item.name]?.filter(advisory => {
472
+ // This could be for another version of this package elsewhere in the tree
473
+ if (!semver.intersects(advisory.vulnerable_versions, item.version)) {
474
+ return false
475
+ }
476
+ if (!vulns) {
477
+ return true
478
+ }
479
+ // vulns are OR with each other, if any one matches we're done
480
+ for (const vuln of vulns) {
481
+ if (vuln.severity && !vuln.severity.includes('*')) {
482
+ if (!vuln.severity.includes(advisory.severity)) {
483
+ continue
484
+ }
485
+ }
486
+
487
+ if (vuln?.cwe) {
488
+ // * is special, it means "has a cwe"
489
+ if (vuln.cwe.includes('*')) {
490
+ if (!advisory.cwe.length) {
491
+ continue
492
+ }
493
+ } else if (!vuln.cwe.every(cwe => advisory.cwe.includes(`CWE-${cwe}`))) {
494
+ continue
495
+ }
496
+ }
497
+ return true
498
+ }
499
+ })
500
+ if (vulnerable?.length) {
501
+ item.queryContext = {
502
+ advisories: vulnerable,
503
+ }
504
+ return true
505
+ }
506
+ return false
507
+ })
508
+ }
509
+
435
510
  async outdatedPseudo () {
436
511
  const { outdatedKind = 'any' } = this.currentAstNode
437
512
 
@@ -445,6 +520,11 @@ class Results {
445
520
  return false
446
521
  }
447
522
 
523
+ // private packages can't be published, skip them
524
+ if (node.package.private) {
525
+ return false
526
+ }
527
+
448
528
  // we cache the promise representing the full versions list, this helps reduce the
449
529
  // number of requests we send by keeping population of the cache in a single tick
450
530
  // making it less likely that multiple requests for the same package will be inflight
@@ -839,8 +919,6 @@ const retrieveNodesFromParsedAst = async (opts) => {
839
919
  return results.collect(rootAstNode)
840
920
  }
841
921
 
842
- // We are keeping this async in the event that we do add async operators, we
843
- // won't have to have a breaking change on this function signature.
844
922
  const querySelectorAll = async (targetNode, query, flatOptions) => {
845
923
  // This never changes ever we just pass it around. But we can't scope it to
846
924
  // this whole file if we ever want to support concurrent calls to this
package/lib/tracker.js CHANGED
@@ -1,47 +1,46 @@
1
- const _progress = Symbol('_progress')
2
- const _onError = Symbol('_onError')
3
- const _setProgress = Symbol('_setProgess')
4
1
  const npmlog = require('npmlog')
5
2
 
6
3
  module.exports = cls => class Tracker extends cls {
4
+ #progress = new Map()
5
+ #setProgress
6
+
7
7
  constructor (options = {}) {
8
8
  super(options)
9
- this[_setProgress] = !!options.progress
10
- this[_progress] = new Map()
9
+ this.#setProgress = !!options.progress
11
10
  }
12
11
 
13
12
  addTracker (section, subsection = null, key = null) {
14
13
  if (section === null || section === undefined) {
15
- this[_onError](`Tracker can't be null or undefined`)
14
+ this.#onError(`Tracker can't be null or undefined`)
16
15
  }
17
16
 
18
17
  if (key === null) {
19
18
  key = subsection
20
19
  }
21
20
 
22
- const hasTracker = this[_progress].has(section)
23
- const hasSubtracker = this[_progress].has(`${section}:${key}`)
21
+ const hasTracker = this.#progress.has(section)
22
+ const hasSubtracker = this.#progress.has(`${section}:${key}`)
24
23
 
25
24
  if (hasTracker && subsection === null) {
26
25
  // 0. existing tracker, no subsection
27
- this[_onError](`Tracker "${section}" already exists`)
26
+ this.#onError(`Tracker "${section}" already exists`)
28
27
  } else if (!hasTracker && subsection === null) {
29
28
  // 1. no existing tracker, no subsection
30
29
  // Create a new tracker from npmlog
31
30
  // starts progress bar
32
- if (this[_setProgress] && this[_progress].size === 0) {
31
+ if (this.#setProgress && this.#progress.size === 0) {
33
32
  npmlog.enableProgress()
34
33
  }
35
34
 
36
- this[_progress].set(section, npmlog.newGroup(section))
35
+ this.#progress.set(section, npmlog.newGroup(section))
37
36
  } else if (!hasTracker && subsection !== null) {
38
37
  // 2. no parent tracker and subsection
39
- this[_onError](`Parent tracker "${section}" does not exist`)
38
+ this.#onError(`Parent tracker "${section}" does not exist`)
40
39
  } else if (!hasTracker || !hasSubtracker) {
41
40
  // 3. existing parent tracker, no subsection tracker
42
- // Create a new subtracker in this[_progress] from parent tracker
43
- this[_progress].set(`${section}:${key}`,
44
- this[_progress].get(section).newGroup(`${section}:${subsection}`)
41
+ // Create a new subtracker in this.#progress from parent tracker
42
+ this.#progress.set(`${section}:${key}`,
43
+ this.#progress.get(section).newGroup(`${section}:${subsection}`)
45
44
  )
46
45
  }
47
46
  // 4. existing parent tracker, existing subsection tracker
@@ -50,22 +49,22 @@ module.exports = cls => class Tracker extends cls {
50
49
 
51
50
  finishTracker (section, subsection = null, key = null) {
52
51
  if (section === null || section === undefined) {
53
- this[_onError](`Tracker can't be null or undefined`)
52
+ this.#onError(`Tracker can't be null or undefined`)
54
53
  }
55
54
 
56
55
  if (key === null) {
57
56
  key = subsection
58
57
  }
59
58
 
60
- const hasTracker = this[_progress].has(section)
61
- const hasSubtracker = this[_progress].has(`${section}:${key}`)
59
+ const hasTracker = this.#progress.has(section)
60
+ const hasSubtracker = this.#progress.has(`${section}:${key}`)
62
61
 
63
62
  // 0. parent tracker exists, no subsection
64
- // Finish parent tracker and remove from this[_progress]
63
+ // Finish parent tracker and remove from this.#progress
65
64
  if (hasTracker && subsection === null) {
66
65
  // check if parent tracker does
67
66
  // not have any remaining children
68
- const keys = this[_progress].keys()
67
+ const keys = this.#progress.keys()
69
68
  for (const key of keys) {
70
69
  if (key.match(new RegExp(section + ':'))) {
71
70
  this.finishTracker(section, key)
@@ -73,28 +72,28 @@ module.exports = cls => class Tracker extends cls {
73
72
  }
74
73
 
75
74
  // remove parent tracker
76
- this[_progress].get(section).finish()
77
- this[_progress].delete(section)
75
+ this.#progress.get(section).finish()
76
+ this.#progress.delete(section)
78
77
 
79
78
  // remove progress bar if all
80
79
  // trackers are finished
81
- if (this[_setProgress] && this[_progress].size === 0) {
80
+ if (this.#setProgress && this.#progress.size === 0) {
82
81
  npmlog.disableProgress()
83
82
  }
84
83
  } else if (!hasTracker && subsection === null) {
85
84
  // 1. no existing parent tracker, no subsection
86
- this[_onError](`Tracker "${section}" does not exist`)
85
+ this.#onError(`Tracker "${section}" does not exist`)
87
86
  } else if (!hasTracker || hasSubtracker) {
88
87
  // 2. subtracker exists
89
- // Finish subtracker and remove from this[_progress]
90
- this[_progress].get(`${section}:${key}`).finish()
91
- this[_progress].delete(`${section}:${key}`)
88
+ // Finish subtracker and remove from this.#progress
89
+ this.#progress.get(`${section}:${key}`).finish()
90
+ this.#progress.delete(`${section}:${key}`)
92
91
  }
93
92
  // 3. existing parent tracker, no subsection
94
93
  }
95
94
 
96
- [_onError] (msg) {
97
- if (this[_setProgress]) {
95
+ #onError (msg) {
96
+ if (this.#setProgress) {
98
97
  npmlog.disableProgress()
99
98
  }
100
99
  throw new Error(msg)
@@ -1,22 +1,21 @@
1
- /* eslint node/no-deprecated-api: "off" */
2
1
  const semver = require('semver')
3
2
  const { basename } = require('path')
4
- const { parse } = require('url')
3
+ const { URL } = require('url')
5
4
  module.exports = (name, tgz) => {
6
5
  const base = basename(tgz)
7
6
  if (!base.endsWith('.tgz')) {
8
7
  return null
9
8
  }
10
9
 
11
- const u = parse(tgz)
12
- if (/^https?:/.test(u.protocol)) {
10
+ if (tgz.startsWith('http:/') || tgz.startsWith('https:/')) {
11
+ const u = new URL(tgz)
13
12
  // registry url? check for most likely pattern.
14
13
  // either /@foo/bar/-/bar-1.2.3.tgz or
15
14
  // /foo/-/foo-1.2.3.tgz, and fall through to
16
15
  // basename checking. Note that registries can
17
16
  // be mounted below the root url, so /a/b/-/x/y/foo/-/foo-1.2.3.tgz
18
17
  // is a potential option.
19
- const tfsplit = u.path.slice(1).split('/-/')
18
+ const tfsplit = u.pathname.slice(1).split('/-/')
20
19
  if (tfsplit.length > 1) {
21
20
  const afterTF = tfsplit.pop()
22
21
  if (afterTF === base) {
package/lib/vuln.js CHANGED
@@ -16,24 +16,23 @@ const semverOpt = { loose: true, includePrerelease: true }
16
16
 
17
17
  const localeCompare = require('@isaacs/string-locale-compare')('en')
18
18
  const npa = require('npm-package-arg')
19
- const _range = Symbol('_range')
20
- const _simpleRange = Symbol('_simpleRange')
21
- const _fixAvailable = Symbol('_fixAvailable')
22
19
 
23
20
  const severities = new Map([
24
- ['info', 0],
25
- ['low', 1],
26
- ['moderate', 2],
27
- ['high', 3],
28
- ['critical', 4],
29
- [null, -1],
21
+ ['info', 0], [0, 'info'],
22
+ ['low', 1], [1, 'low'],
23
+ ['moderate', 2], [2, 'moderate'],
24
+ ['high', 3], [3, 'high'],
25
+ ['critical', 4], [4, 'critical'],
26
+ [null, -1], [-1, null],
30
27
  ])
31
28
 
32
- for (const [name, val] of severities.entries()) {
33
- severities.set(val, name)
34
- }
35
-
36
29
  class Vuln {
30
+ #range = null
31
+ #simpleRange = null
32
+ // assume a fix is available unless it hits a top node
33
+ // that locks it in place, setting this false or {isSemVerMajor, version}.
34
+ #fixAvailable = true
35
+
37
36
  constructor ({ name, advisory }) {
38
37
  this.name = name
39
38
  this.via = new Set()
@@ -41,23 +40,18 @@ class Vuln {
41
40
  this.severity = null
42
41
  this.effects = new Set()
43
42
  this.topNodes = new Set()
44
- this[_range] = null
45
- this[_simpleRange] = null
46
43
  this.nodes = new Set()
47
- // assume a fix is available unless it hits a top node
48
- // that locks it in place, setting this false or {isSemVerMajor, version}.
49
- this[_fixAvailable] = true
50
44
  this.addAdvisory(advisory)
51
45
  this.packument = advisory.packument
52
46
  this.versions = advisory.versions
53
47
  }
54
48
 
55
49
  get fixAvailable () {
56
- return this[_fixAvailable]
50
+ return this.#fixAvailable
57
51
  }
58
52
 
59
53
  set fixAvailable (f) {
60
- this[_fixAvailable] = f
54
+ this.#fixAvailable = f
61
55
  // if there's a fix available for this at the top level, it means that
62
56
  // it will also fix the vulns that led to it being there. to get there,
63
57
  // we set the vias to the most "strict" of fix availables.
@@ -131,7 +125,7 @@ class Vuln {
131
125
  effects: [...this.effects].map(v => v.name).sort(localeCompare),
132
126
  range: this.simpleRange,
133
127
  nodes: [...this.nodes].map(n => n.location).sort(localeCompare),
134
- fixAvailable: this[_fixAvailable],
128
+ fixAvailable: this.#fixAvailable,
135
129
  }
136
130
  }
137
131
 
@@ -151,8 +145,8 @@ class Vuln {
151
145
  this.advisories.delete(advisory)
152
146
  // make sure we have the max severity of all the vulns causing this one
153
147
  this.severity = null
154
- this[_range] = null
155
- this[_simpleRange] = null
148
+ this.#range = null
149
+ this.#simpleRange = null
156
150
  // refresh severity
157
151
  for (const advisory of this.advisories) {
158
152
  this.addAdvisory(advisory)
@@ -170,27 +164,30 @@ class Vuln {
170
164
  addAdvisory (advisory) {
171
165
  this.advisories.add(advisory)
172
166
  const sev = severities.get(advisory.severity)
173
- this[_range] = null
174
- this[_simpleRange] = null
167
+ this.#range = null
168
+ this.#simpleRange = null
175
169
  if (sev > severities.get(this.severity)) {
176
170
  this.severity = advisory.severity
177
171
  }
178
172
  }
179
173
 
180
174
  get range () {
181
- return this[_range] ||
182
- (this[_range] = [...this.advisories].map(v => v.range).join(' || '))
175
+ if (!this.#range) {
176
+ this.#range = [...this.advisories].map(v => v.range).join(' || ')
177
+ }
178
+ return this.#range
183
179
  }
184
180
 
185
181
  get simpleRange () {
186
- if (this[_simpleRange] && this[_simpleRange] === this[_range]) {
187
- return this[_simpleRange]
182
+ if (this.#simpleRange && this.#simpleRange === this.#range) {
183
+ return this.#simpleRange
188
184
  }
189
185
 
190
186
  const versions = [...this.advisories][0].versions
191
187
  const range = this.range
192
- const simple = simplifyRange(versions, range, semverOpt)
193
- return this[_simpleRange] = this[_range] = simple
188
+ this.#simpleRange = simplifyRange(versions, range, semverOpt)
189
+ this.#range = this.#simpleRange
190
+ return this.#simpleRange
194
191
  }
195
192
 
196
193
  isVulnerable (node) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -11,7 +11,7 @@
11
11
  "@npmcli/name-from-folder": "^2.0.0",
12
12
  "@npmcli/node-gyp": "^3.0.0",
13
13
  "@npmcli/package-json": "^5.0.0",
14
- "@npmcli/query": "^3.0.1",
14
+ "@npmcli/query": "^3.1.0",
15
15
  "@npmcli/run-script": "^7.0.2",
16
16
  "bin-links": "^4.0.1",
17
17
  "cacache": "^18.0.0",
@@ -30,7 +30,7 @@
30
30
  "parse-conflict-json": "^3.0.0",
31
31
  "proc-log": "^3.0.0",
32
32
  "promise-all-reject-late": "^1.0.0",
33
- "promise-call-limit": "^1.0.2",
33
+ "promise-call-limit": "^3.0.1",
34
34
  "read-package-json-fast": "^3.0.2",
35
35
  "semver": "^7.3.7",
36
36
  "ssri": "^10.0.5",
@@ -1,51 +0,0 @@
1
- // mixin implementing the audit method
2
-
3
- const AuditReport = require('../audit-report.js')
4
-
5
- // shared with reify
6
- const _global = Symbol.for('global')
7
- const _workspaces = Symbol.for('workspaces')
8
- const _includeWorkspaceRoot = Symbol.for('includeWorkspaceRoot')
9
-
10
- module.exports = cls => class Auditor extends cls {
11
- async audit (options = {}) {
12
- this.addTracker('audit')
13
- if (this[_global]) {
14
- throw Object.assign(
15
- new Error('`npm audit` does not support testing globals'),
16
- { code: 'EAUDITGLOBAL' }
17
- )
18
- }
19
-
20
- // allow the user to set options on the ctor as well.
21
- // XXX: deprecate separate method options objects.
22
- options = { ...this.options, ...options }
23
-
24
- process.emit('time', 'audit')
25
- let tree
26
- if (options.packageLock === false) {
27
- // build ideal tree
28
- await this.loadActual(options)
29
- await this.buildIdealTree()
30
- tree = this.idealTree
31
- } else {
32
- tree = await this.loadVirtual()
33
- }
34
- if (this[_workspaces] && this[_workspaces].length) {
35
- options.filterSet = this.workspaceDependencySet(
36
- tree,
37
- this[_workspaces],
38
- this[_includeWorkspaceRoot]
39
- )
40
- }
41
- if (!options.workspacesEnabled) {
42
- options.filterSet =
43
- this.excludeWorkspacesDependencySet(tree)
44
- }
45
- this.auditReport = await AuditReport.load(tree, options)
46
- const ret = options.fix ? this.reify(options) : this.auditReport
47
- process.emit('timeEnd', 'audit')
48
- this.finishTracker('audit')
49
- return ret
50
- }
51
- }
@@ -1,19 +0,0 @@
1
- module.exports = cls => class Deduper extends cls {
2
- async dedupe (options = {}) {
3
- // allow the user to set options on the ctor as well.
4
- // XXX: deprecate separate method options objects.
5
- options = { ...this.options, ...options }
6
- const tree = await this.loadVirtual().catch(() => this.loadActual())
7
- const names = []
8
- for (const name of tree.inventory.query('name')) {
9
- if (tree.inventory.query('name', name).size > 1) {
10
- names.push(name)
11
- }
12
- }
13
- return this.reify({
14
- ...options,
15
- preferDedupe: true,
16
- update: { names },
17
- })
18
- }
19
- }
@@ -1,30 +0,0 @@
1
- const _idealTreePrune = Symbol.for('idealTreePrune')
2
- const _workspacesEnabled = Symbol.for('workspacesEnabled')
3
- const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
4
-
5
- module.exports = cls => class Pruner extends cls {
6
- async prune (options = {}) {
7
- // allow the user to set options on the ctor as well.
8
- // XXX: deprecate separate method options objects.
9
- options = { ...this.options, ...options }
10
-
11
- await this.buildIdealTree(options)
12
-
13
- this[_idealTreePrune]()
14
-
15
- if (!this[_workspacesEnabled]) {
16
- const excludeNodes = this.excludeWorkspacesDependencySet(this.idealTree)
17
- for (const node of this.idealTree.inventory.values()) {
18
- if (
19
- node.parent !== null
20
- && !node.isProjectRoot
21
- && !excludeNodes.has(node)
22
- ) {
23
- this[_addNodeToTrashList](node)
24
- }
25
- }
26
- }
27
-
28
- return this.reify(options)
29
- }
30
- }
@@ -1,19 +0,0 @@
1
- const mapWorkspaces = require('@npmcli/map-workspaces')
2
-
3
- // shared ref used by other mixins/Arborist
4
- const _setWorkspaces = Symbol.for('setWorkspaces')
5
-
6
- module.exports = cls => class MapWorkspaces extends cls {
7
- async [_setWorkspaces] (node) {
8
- const workspaces = await mapWorkspaces({
9
- cwd: node.path,
10
- pkg: node.package,
11
- })
12
-
13
- if (node && workspaces.size) {
14
- node.workspaces = workspaces
15
- }
16
-
17
- return node
18
- }
19
- }
@@ -1,36 +0,0 @@
1
- // Get the actual nodes corresponding to a root node's child workspaces,
2
- // given a list of workspace names.
3
-
4
- const log = require('proc-log')
5
- const relpath = require('./relpath.js')
6
-
7
- const getWorkspaceNodes = (tree, workspaces) => {
8
- const wsMap = tree.workspaces
9
- if (!wsMap) {
10
- log.warn('workspaces', 'filter set, but no workspaces present')
11
- return []
12
- }
13
-
14
- const nodes = []
15
- for (const name of workspaces) {
16
- const path = wsMap.get(name)
17
- if (!path) {
18
- log.warn('workspaces', `${name} in filter set, but not in workspaces`)
19
- continue
20
- }
21
-
22
- const loc = relpath(tree.realpath, path)
23
- const node = tree.inventory.get(loc)
24
-
25
- if (!node) {
26
- log.warn('workspaces', `${name} in filter set, but no workspace folder present`)
27
- continue
28
- }
29
-
30
- nodes.push(node)
31
- }
32
-
33
- return nodes
34
- }
35
-
36
- module.exports = getWorkspaceNodes