Skip to content

Commit d6ae11d

Browse files
committed
feat(query): add :vuln pseudo selector
1 parent 343d47a commit d6ae11d

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

docs/lib/content/using-npm/dependency-selectors.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The [`npm query`](/commands/npm-query) command exposes a new dependency selector
1313
- Unlocks the ability to answer complex, multi-faceted questions about dependencies, their relationships & associative metadata
1414
- Consolidates redundant logic of similar query commands in `npm` (ex. `npm fund`, `npm ls`, `npm outdated`, `npm audit` ...)
1515

16-
### Dependency Selector Syntax `v1.0.0`
16+
### Dependency Selector Syntax
1717

1818
#### Overview:
1919

@@ -62,6 +62,7 @@ The [`npm query`](/commands/npm-query) command exposes a new dependency selector
6262
- `:path(<path>)` [glob](https://www.npmjs.com/package/glob) matching based on dependencies path relative to the project
6363
- `:type(<type>)` [based on currently recognized types](https://github.com/npm/npm-package-arg#result-object)
6464
- `:outdated(<type>)` when a dependency is outdated
65+
- `:vuln` when a dependency has a known vulnerability
6566

6667
##### `:semver(<spec>, [selector], [function])`
6768

@@ -101,6 +102,12 @@ Some examples:
101102
- `:root > :outdated(major)` returns every direct dependency that has a new semver major release
102103
- `.prod:outdated(in-range)` returns production dependencies that have a new release that satisfies at least one of its edges in
103104

105+
##### `:vuln`
106+
107+
The `:vuln` pseudo selector retrieves data from the registry and returns information about which if your dependencies has a known vulnerability. Only dependencies whose current version matches a vulnerability will be returned. For example if you have `[email protected]` in your tree, a vulnerability for `semver` which affects versions `<=6.3.1` will not match.
108+
109+
In addition to the filtering performed by the pseudo selector, info about each relevant advisory will be added to the `queryContext` attribute of each node under the `advisories` attribute.
110+
104111
#### [Attribute Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)
105112

106113
The attribute selector evaluates the key/value pairs in `package.json` if they are `String`s.

workspaces/arborist/lib/query-selector-all.js

+40
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { minimatch } = require('minimatch')
88
const npa = require('npm-package-arg')
99
const pacote = require('pacote')
1010
const semver = require('semver')
11+
const fetch = require('npm-registry-fetch')
1112

1213
// handle results for parsed query asts, results are stored in a map that has a
1314
// key that points to each ast selector node and stores the resulting array of
@@ -432,6 +433,45 @@ class Results {
432433
return this.initialItems.filter(node => node.target.edgesIn.size > 1)
433434
}
434435

436+
async vulnPseudo () {
437+
if (!this.initialItems.length) {
438+
return this.initialItems
439+
}
440+
const packages = {}
441+
// We have to map the items twice, once to get the request, and a second time to filter off the results of that request
442+
this.initialItems.map((node) => {
443+
if (node.isProjectRoot || node.package.private) {
444+
return
445+
}
446+
if (!packages[node.name]) {
447+
packages[node.name] = []
448+
}
449+
if (!packages[node.name].includes(node.version)) {
450+
packages[node.name].push(node.version)
451+
}
452+
})
453+
const res = await fetch('/-/npm/v1/security/advisories/bulk', {
454+
...this.flatOptions,
455+
registry: this.flatOptions.auditRegistry || this.flatOptions.registry,
456+
method: 'POST',
457+
gzip: true,
458+
body: packages,
459+
})
460+
const advisories = await res.json()
461+
return this.initialItems.filter(item => {
462+
const vulnerable = advisories[item.name]?.filter(advisory =>
463+
semver.intersects(advisory.vulnerable_versions, item.version)
464+
)
465+
if (vulnerable?.length) {
466+
item.queryContext = {
467+
advisories: vulnerable,
468+
}
469+
return true
470+
}
471+
return false
472+
})
473+
}
474+
435475
async outdatedPseudo () {
436476
const { outdatedKind = 'any' } = this.currentAstNode
437477

workspaces/arborist/test/query-selector-all.js

+10
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ t.test('query-selector-all', async t => {
9999
nock.enableNetConnect()
100100
})
101101

102+
nock('https://registry.npmjs.org')
103+
.post('/-/npm/v1/security/advisories/bulk')
104+
.reply(200, {
105+
foo: [{ id: 'test-vuln', vulnerable_versions: '*' }],
106+
moo: [{ id: 'test-vuln', vulnerable_versions: '<1.0.0' }],
107+
})
102108
for (const [pkg, versions] of Object.entries(packumentStubs)) {
103109
nock('https://registry.npmjs.org')
104110
.persist()
@@ -842,6 +848,10 @@ t.test('query-selector-all', async t => {
842848
], { before: yesterday }],
843849
[':outdated(nonsense)', [], { before: yesterday }], // again, no results here ever
844850

851+
// vuln pseudo
852+
[':vuln', ['[email protected]']],
853+
['#nomatch:vuln', []], // no network requests are made if the result set is empty
854+
845855
// attr pseudo
846856
[':attr([name=dasher])', ['[email protected]']],
847857
[':attr(dependencies, [bar="^1.0.0"])', ['[email protected]']],

0 commit comments

Comments
 (0)