@npmcli/arborist 9.1.2 → 9.1.3
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.
- package/lib/arborist/build-ideal-tree.js +25 -11
- package/lib/arborist/reify.js +4 -15
- package/lib/audit-report.js +55 -72
- package/lib/node.js +9 -0
- package/package.json +3 -3
|
@@ -6,7 +6,7 @@ const pacote = require('pacote')
|
|
|
6
6
|
const cacache = require('cacache')
|
|
7
7
|
const { callLimit: promiseCallLimit } = require('promise-call-limit')
|
|
8
8
|
const realpath = require('../../lib/realpath.js')
|
|
9
|
-
const { resolve, dirname } = require('node:path')
|
|
9
|
+
const { resolve, dirname, sep } = require('node:path')
|
|
10
10
|
const treeCheck = require('../tree-check.js')
|
|
11
11
|
const { readdirScoped } = require('@npmcli/fs')
|
|
12
12
|
const { lstat, readlink } = require('node:fs/promises')
|
|
@@ -192,9 +192,11 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
async #checkEngineAndPlatform () {
|
|
195
|
-
const { engineStrict, npmVersion, nodeVersion } = this.options
|
|
195
|
+
const { engineStrict, npmVersion, nodeVersion, omit = [] } = this.options
|
|
196
|
+
const omitSet = new Set(omit)
|
|
197
|
+
|
|
196
198
|
for (const node of this.idealTree.inventory.values()) {
|
|
197
|
-
if (!node.optional) {
|
|
199
|
+
if (!node.optional && !node.shouldOmit(omitSet)) {
|
|
198
200
|
try {
|
|
199
201
|
// if devEngines is present in the root node we ignore the engines check
|
|
200
202
|
if (!(node.isRoot && node.package.devEngines)) {
|
|
@@ -1224,9 +1226,21 @@ This is a one-time fix-up, please be patient...
|
|
|
1224
1226
|
const { installLinks, legacyPeerDeps } = this
|
|
1225
1227
|
const isWorkspace = this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name)
|
|
1226
1228
|
|
|
1227
|
-
// spec is a directory, link it
|
|
1229
|
+
// spec is a directory, link it if:
|
|
1230
|
+
// - it's a workspace, OR
|
|
1231
|
+
// - it's a project-internal file: dependency (always linked), OR
|
|
1232
|
+
// - it's external and installLinks is false
|
|
1228
1233
|
// TODO post arborist refactor, will need to check for installStrategy=linked
|
|
1229
|
-
|
|
1234
|
+
let isProjectInternalFileSpec = false
|
|
1235
|
+
if (edge?.rawSpec.startsWith('file:../') || edge?.rawSpec.startsWith('file:./')) {
|
|
1236
|
+
const targetPath = resolve(parent.realpath, edge.rawSpec.slice(5))
|
|
1237
|
+
const resolvedProjectRoot = resolve(this.idealTree.realpath)
|
|
1238
|
+
// Check if the target is within the project root
|
|
1239
|
+
isProjectInternalFileSpec = targetPath.startsWith(resolvedProjectRoot + sep) || targetPath === resolvedProjectRoot
|
|
1240
|
+
}
|
|
1241
|
+
// Decide whether to link or copy the dependency
|
|
1242
|
+
const shouldLink = isWorkspace || isProjectInternalFileSpec || !installLinks
|
|
1243
|
+
if (spec.type === 'directory' && shouldLink) {
|
|
1230
1244
|
return this.#linkFromSpec(name, spec, parent, edge)
|
|
1231
1245
|
}
|
|
1232
1246
|
|
|
@@ -1476,11 +1490,6 @@ This is a one-time fix-up, please be patient...
|
|
|
1476
1490
|
const needPrune = metaFromDisk && (mutateTree || flagsSuspect)
|
|
1477
1491
|
if (this.#prune && needPrune) {
|
|
1478
1492
|
this.#idealTreePrune()
|
|
1479
|
-
for (const node of this.idealTree.inventory.values()) {
|
|
1480
|
-
if (node.extraneous) {
|
|
1481
|
-
node.parent = null
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
1493
|
}
|
|
1485
1494
|
|
|
1486
1495
|
timeEnd()
|
|
@@ -1514,7 +1523,12 @@ This is a one-time fix-up, please be patient...
|
|
|
1514
1523
|
|
|
1515
1524
|
#idealTreePrune () {
|
|
1516
1525
|
for (const node of this.idealTree.inventory.values()) {
|
|
1517
|
-
|
|
1526
|
+
// optional peer dependencies are meant to be added to the tree
|
|
1527
|
+
// through an explicit required dependency (most commonly in the
|
|
1528
|
+
// root package.json), at which point they won't be optional so
|
|
1529
|
+
// any dependencies still marked as both optional and peer at
|
|
1530
|
+
// this point can be pruned as a special kind of extraneous
|
|
1531
|
+
if (node.extraneous || (node.peer && node.optional)) {
|
|
1518
1532
|
node.parent = null
|
|
1519
1533
|
}
|
|
1520
1534
|
}
|
package/lib/arborist/reify.js
CHANGED
|
@@ -84,9 +84,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
84
84
|
#bundleUnpacked = new Set() // the nodes we unpack to read their bundles
|
|
85
85
|
#dryRun
|
|
86
86
|
#nmValidated = new Set()
|
|
87
|
-
#
|
|
88
|
-
#omitPeer
|
|
89
|
-
#omitOptional
|
|
87
|
+
#omit
|
|
90
88
|
#retiredPaths = {}
|
|
91
89
|
#retiredUnchanged = {}
|
|
92
90
|
#savePrefix
|
|
@@ -110,10 +108,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
110
108
|
throw er
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
this.#omitDev = omit.has('dev')
|
|
115
|
-
this.#omitOptional = omit.has('optional')
|
|
116
|
-
this.#omitPeer = omit.has('peer')
|
|
111
|
+
this.#omit = new Set(options.omit)
|
|
117
112
|
|
|
118
113
|
// start tracker block
|
|
119
114
|
this.addTracker('reify')
|
|
@@ -562,12 +557,11 @@ module.exports = cls => class Reifier extends cls {
|
|
|
562
557
|
// adding to the trash list will skip reifying, and delete them
|
|
563
558
|
// if they are currently in the tree and otherwise untouched.
|
|
564
559
|
[_addOmitsToTrashList] () {
|
|
565
|
-
if (!this.#
|
|
560
|
+
if (!this.#omit.size) {
|
|
566
561
|
return
|
|
567
562
|
}
|
|
568
563
|
|
|
569
564
|
const timeEnd = time.start('reify:trashOmits')
|
|
570
|
-
|
|
571
565
|
for (const node of this.idealTree.inventory.values()) {
|
|
572
566
|
const { top } = node
|
|
573
567
|
|
|
@@ -583,12 +577,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
583
577
|
}
|
|
584
578
|
|
|
585
579
|
// omit node if the dep type matches any omit flags that were set
|
|
586
|
-
if (
|
|
587
|
-
node.peer && this.#omitPeer ||
|
|
588
|
-
node.dev && this.#omitDev ||
|
|
589
|
-
node.optional && this.#omitOptional ||
|
|
590
|
-
node.devOptional && this.#omitOptional && this.#omitDev
|
|
591
|
-
) {
|
|
580
|
+
if (node.shouldOmit(this.#omit)) {
|
|
592
581
|
this[_addNodeToTrashList](node)
|
|
593
582
|
}
|
|
594
583
|
}
|
package/lib/audit-report.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// an object representing the set of vulnerabilities in a tree
|
|
2
|
-
/* eslint camelcase: "off" */
|
|
3
2
|
|
|
4
3
|
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
|
5
4
|
const npa = require('npm-package-arg')
|
|
@@ -8,16 +7,15 @@ const pickManifest = require('npm-pick-manifest')
|
|
|
8
7
|
const Vuln = require('./vuln.js')
|
|
9
8
|
const Calculator = require('@npmcli/metavuln-calculator')
|
|
10
9
|
|
|
11
|
-
const _getReport = Symbol('getReport')
|
|
12
|
-
const _fixAvailable = Symbol('fixAvailable')
|
|
13
|
-
const _checkTopNode = Symbol('checkTopNode')
|
|
14
|
-
const _init = Symbol('init')
|
|
15
|
-
const _omit = Symbol('omit')
|
|
16
10
|
const { log, time } = require('proc-log')
|
|
17
11
|
|
|
18
12
|
const npmFetch = require('npm-registry-fetch')
|
|
19
13
|
|
|
20
14
|
class AuditReport extends Map {
|
|
15
|
+
#omit
|
|
16
|
+
error = null
|
|
17
|
+
topVulns = new Map()
|
|
18
|
+
|
|
21
19
|
static load (tree, opts) {
|
|
22
20
|
return new AuditReport(tree, opts).run()
|
|
23
21
|
}
|
|
@@ -91,22 +89,18 @@ class AuditReport extends Map {
|
|
|
91
89
|
|
|
92
90
|
constructor (tree, opts = {}) {
|
|
93
91
|
super()
|
|
94
|
-
|
|
95
|
-
this[_omit] = new Set(omit || [])
|
|
96
|
-
this.topVulns = new Map()
|
|
97
|
-
|
|
92
|
+
this.#omit = new Set(opts.omit || [])
|
|
98
93
|
this.calculator = new Calculator(opts)
|
|
99
|
-
this.error = null
|
|
100
94
|
this.options = opts
|
|
101
95
|
this.tree = tree
|
|
102
96
|
this.filterSet = opts.filterSet
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
async run () {
|
|
106
|
-
this.report = await this
|
|
100
|
+
this.report = await this.#getReport()
|
|
107
101
|
log.silly('audit report', this.report)
|
|
108
102
|
if (this.report) {
|
|
109
|
-
await this
|
|
103
|
+
await this.#init()
|
|
110
104
|
}
|
|
111
105
|
return this
|
|
112
106
|
}
|
|
@@ -116,7 +110,7 @@ class AuditReport extends Map {
|
|
|
116
110
|
return !!(vuln && vuln.isVulnerable(node))
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
async
|
|
113
|
+
async #init () {
|
|
120
114
|
const timeEnd = time.start('auditReport:init')
|
|
121
115
|
|
|
122
116
|
const promises = []
|
|
@@ -148,7 +142,7 @@ class AuditReport extends Map {
|
|
|
148
142
|
if (!seen.has(k)) {
|
|
149
143
|
const p = []
|
|
150
144
|
for (const node of this.tree.inventory.query('packageName', name)) {
|
|
151
|
-
if (!shouldAudit(node
|
|
145
|
+
if (!this.shouldAudit(node)) {
|
|
152
146
|
continue
|
|
153
147
|
}
|
|
154
148
|
|
|
@@ -171,7 +165,15 @@ class AuditReport extends Map {
|
|
|
171
165
|
vuln.nodes.add(node)
|
|
172
166
|
for (const { from: dep, spec } of node.edgesIn) {
|
|
173
167
|
if (dep.isTop && !vuln.topNodes.has(dep)) {
|
|
174
|
-
this
|
|
168
|
+
vuln.fixAvailable = this.#fixAvailable(vuln, spec)
|
|
169
|
+
if (vuln.fixAvailable !== true) {
|
|
170
|
+
// now we know the top node is vulnerable, and cannot be
|
|
171
|
+
// upgraded out of the bad place without --force. But, there's
|
|
172
|
+
// no need to add it to the actual vulns list, because nothing
|
|
173
|
+
// depends on root.
|
|
174
|
+
this.topVulns.set(vuln.name, vuln)
|
|
175
|
+
vuln.topNodes.add(dep)
|
|
176
|
+
}
|
|
175
177
|
} else {
|
|
176
178
|
// calculate a metavuln, if necessary
|
|
177
179
|
const calc = this.calculator.calculate(dep.packageName, advisory)
|
|
@@ -214,33 +216,14 @@ class AuditReport extends Map {
|
|
|
214
216
|
timeEnd()
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (vuln.fixAvailable !== true) {
|
|
221
|
-
// now we know the top node is vulnerable, and cannot be
|
|
222
|
-
// upgraded out of the bad place without --force. But, there's
|
|
223
|
-
// no need to add it to the actual vulns list, because nothing
|
|
224
|
-
// depends on root.
|
|
225
|
-
this.topVulns.set(vuln.name, vuln)
|
|
226
|
-
vuln.topNodes.add(topNode)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// check whether the top node is vulnerable.
|
|
231
|
-
// check whether we can get out of the bad place with --force, and if
|
|
232
|
-
// so, whether that update is SemVer Major
|
|
233
|
-
[_fixAvailable] (topNode, vuln, spec) {
|
|
234
|
-
// this will always be set to at least {name, versions:{}}
|
|
235
|
-
const paku = vuln.packument
|
|
236
|
-
|
|
219
|
+
// 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)
|
|
220
|
+
#fixAvailable (vuln, spec) {
|
|
221
|
+
// TODO we return true, false, OR an object here. this is probably a bad pattern.
|
|
237
222
|
if (!vuln.testSpec(spec)) {
|
|
238
223
|
return true
|
|
239
224
|
}
|
|
240
225
|
|
|
241
|
-
//
|
|
242
|
-
// somewhere other than the registry, and we got something vulnerable,
|
|
243
|
-
// then we're stuck with it.
|
|
226
|
+
// 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.
|
|
244
227
|
const specObj = npa(spec)
|
|
245
228
|
if (!specObj.registry) {
|
|
246
229
|
return false
|
|
@@ -250,15 +233,13 @@ class AuditReport extends Map {
|
|
|
250
233
|
spec = specObj.subSpec.rawSpec
|
|
251
234
|
}
|
|
252
235
|
|
|
253
|
-
//
|
|
254
|
-
// still check to see if the node is fixable with a different version,
|
|
255
|
-
// and if that is a semver major bump.
|
|
236
|
+
// 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.
|
|
256
237
|
try {
|
|
257
238
|
const {
|
|
258
239
|
_isSemVerMajor: isSemVerMajor,
|
|
259
240
|
version,
|
|
260
241
|
name,
|
|
261
|
-
} = pickManifest(
|
|
242
|
+
} = pickManifest(vuln.packument, spec, {
|
|
262
243
|
...this.options,
|
|
263
244
|
before: null,
|
|
264
245
|
avoid: vuln.range,
|
|
@@ -274,7 +255,7 @@ class AuditReport extends Map {
|
|
|
274
255
|
throw new Error('do not call AuditReport.set() directly')
|
|
275
256
|
}
|
|
276
257
|
|
|
277
|
-
async
|
|
258
|
+
async #getReport () {
|
|
278
259
|
// if we're not auditing, just return false
|
|
279
260
|
if (this.options.audit === false || this.options.offline === true || this.tree.inventory.size === 1) {
|
|
280
261
|
return null
|
|
@@ -282,7 +263,7 @@ class AuditReport extends Map {
|
|
|
282
263
|
|
|
283
264
|
const timeEnd = time.start('auditReport:getReport')
|
|
284
265
|
try {
|
|
285
|
-
const body = prepareBulkData(
|
|
266
|
+
const body = this.prepareBulkData()
|
|
286
267
|
log.silly('audit', 'bulk request', body)
|
|
287
268
|
|
|
288
269
|
// no sense asking if we don't have anything to audit,
|
|
@@ -309,37 +290,39 @@ class AuditReport extends Map {
|
|
|
309
290
|
timeEnd()
|
|
310
291
|
}
|
|
311
292
|
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// return true if we should audit this one
|
|
315
|
-
const shouldAudit = (node, omit, filterSet) =>
|
|
316
|
-
!node.version ? false
|
|
317
|
-
: node.isRoot ? false
|
|
318
|
-
: filterSet && filterSet.size !== 0 && !filterSet.has(node) ? false
|
|
319
|
-
: omit.size === 0 ? true
|
|
320
|
-
: !( // otherwise, just ensure we're not omitting this one
|
|
321
|
-
node.dev && omit.has('dev') ||
|
|
322
|
-
node.optional && omit.has('optional') ||
|
|
323
|
-
node.devOptional && omit.has('dev') && omit.has('optional') ||
|
|
324
|
-
node.peer && omit.has('peer')
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
const prepareBulkData = (tree, omit, filterSet) => {
|
|
328
|
-
const payload = {}
|
|
329
|
-
for (const name of tree.inventory.query('packageName')) {
|
|
330
|
-
const set = new Set()
|
|
331
|
-
for (const node of tree.inventory.query('packageName', name)) {
|
|
332
|
-
if (!shouldAudit(node, omit, filterSet)) {
|
|
333
|
-
continue
|
|
334
|
-
}
|
|
335
293
|
|
|
336
|
-
|
|
294
|
+
// return true if we should audit this one
|
|
295
|
+
shouldAudit (node) {
|
|
296
|
+
if (
|
|
297
|
+
!node.version ||
|
|
298
|
+
node.isRoot ||
|
|
299
|
+
(this.filterSet && this.filterSet?.size !== 0 && !this.filterSet?.has(node))
|
|
300
|
+
) {
|
|
301
|
+
return false
|
|
337
302
|
}
|
|
338
|
-
if (
|
|
339
|
-
|
|
303
|
+
if (this.#omit.size === 0) {
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
return !node.shouldOmit(this.#omit)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
prepareBulkData () {
|
|
310
|
+
const payload = {}
|
|
311
|
+
for (const name of this.tree.inventory.query('packageName')) {
|
|
312
|
+
const set = new Set()
|
|
313
|
+
for (const node of this.tree.inventory.query('packageName', name)) {
|
|
314
|
+
if (!this.shouldAudit(node)) {
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
set.add(node.version)
|
|
319
|
+
}
|
|
320
|
+
if (set.size) {
|
|
321
|
+
payload[name] = [...set]
|
|
322
|
+
}
|
|
340
323
|
}
|
|
324
|
+
return payload
|
|
341
325
|
}
|
|
342
|
-
return payload
|
|
343
326
|
}
|
|
344
327
|
|
|
345
328
|
module.exports = AuditReport
|
package/lib/node.js
CHANGED
|
@@ -489,6 +489,15 @@ class Node {
|
|
|
489
489
|
return false
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
shouldOmit (omitSet) {
|
|
493
|
+
return (
|
|
494
|
+
this.peer && omitSet.has('peer') ||
|
|
495
|
+
this.dev && omitSet.has('dev') ||
|
|
496
|
+
this.optional && omitSet.has('optional') ||
|
|
497
|
+
this.devOptional && omitSet.has('optional') && omitSet.has('dev')
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
492
501
|
getBundler (path = []) {
|
|
493
502
|
// made a cycle, definitely not bundled!
|
|
494
503
|
if (path.includes(this)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.3",
|
|
4
4
|
"description": "Manage node_modules trees",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@isaacs/string-locale-compare": "^1.1.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@npmcli/eslint-config": "^5.0.1",
|
|
43
43
|
"@npmcli/mock-registry": "^1.0.0",
|
|
44
|
-
"@npmcli/template-oss": "4.
|
|
44
|
+
"@npmcli/template-oss": "4.24.4",
|
|
45
45
|
"benchmark": "^2.1.4",
|
|
46
46
|
"minify-registry-metadata": "^4.0.0",
|
|
47
47
|
"nock": "^13.3.3",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"templateOSS": {
|
|
95
95
|
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
|
|
96
|
-
"version": "4.
|
|
96
|
+
"version": "4.24.4",
|
|
97
97
|
"content": "../../scripts/template-oss/index.js"
|
|
98
98
|
}
|
|
99
99
|
}
|