Skip to content

Commit

Permalink
feat: add selectors to :vuln
Browse files Browse the repository at this point in the history
This will require npm/query#65 before it will
work
  • Loading branch information
wraithgar committed Feb 13, 2024
1 parent d6ae11d commit 0cd2149
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 27 deletions.
11 changes: 10 additions & 1 deletion docs/lib/content/using-npm/dependency-selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The [`npm query`](/commands/npm-query) command exposes a new dependency selector
- `:path(<path>)` [glob](https://www.npmjs.com/package/glob) matching based on dependencies path relative to the project
- `:type(<type>)` [based on currently recognized types](https://github.com/npm/npm-package-arg#result-object)
- `:outdated(<type>)` when a dependency is outdated
- `:vuln` when a dependency has a known vulnerability
- `:vuln(<selector>)` when a dependency has a known vulnerability

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

Expand Down Expand Up @@ -106,8 +106,17 @@ Some examples:

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.

You can also filter results by certain attributes in advisories. Currently that includes `severity` and `cwe`. Note that severity filtering is done per severity, it does not include severities "higher" or "lower" than the one specified.

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.

Some examples:

- `:root > .prod:vuln` returns direct production dependencies with any known vulnerability
- `:vuln([severity=high])` returns only dependencies with a vulnerability with a `high` severity.
- `:vuln([severity=high],[severity=moderate])` returns only dependencies with a vulnerability with a `high` or `moderate` severity.
- `:vuln([cwe=1333])` returns only dependencies with a vulnerability that includes CWE-1333 (ReDoS)

#### [Attribute Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)

The attribute selector evaluates the key/value pairs in `package.json` if they are `String`s.
Expand Down
79 changes: 55 additions & 24 deletions workspaces/arborist/lib/query-selector-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Results {
#initialItems
#inventory
#outdatedCache = new Map()
#vulnCache
#pendingCombinator
#results = new Map()
#targetNode
Expand Down Expand Up @@ -437,31 +438,61 @@ class Results {
if (!this.initialItems.length) {
return this.initialItems
}
const packages = {}
// We have to map the items twice, once to get the request, and a second time to filter off the results of that request
this.initialItems.map((node) => {
if (node.isProjectRoot || node.package.private) {
return
}
if (!packages[node.name]) {
packages[node.name] = []
}
if (!packages[node.name].includes(node.version)) {
packages[node.name].push(node.version)
}
})
const res = await fetch('/-/npm/v1/security/advisories/bulk', {
...this.flatOptions,
registry: this.flatOptions.auditRegistry || this.flatOptions.registry,
method: 'POST',
gzip: true,
body: packages,
})
const advisories = await res.json()
if (!this.#vulnCache) {
const packages = {}
// We have to map the items twice, once to get the request, and a second time to filter out the results of that request
this.initialItems.map((node) => {
if (node.isProjectRoot || node.package.private) {
return
}
if (!packages[node.name]) {
packages[node.name] = []
}
if (!packages[node.name].includes(node.version)) {
packages[node.name].push(node.version)
}
})
const res = await fetch('/-/npm/v1/security/advisories/bulk', {
...this.flatOptions,
registry: this.flatOptions.auditRegistry || this.flatOptions.registry,
method: 'POST',
gzip: true,
body: packages,
})
this.#vulnCache = await res.json()
}
const advisories = this.#vulnCache
const { vulns } = this.currentAstNode
return this.initialItems.filter(item => {
const vulnerable = advisories[item.name]?.filter(advisory =>
semver.intersects(advisory.vulnerable_versions, item.version)
)
const vulnerable = advisories[item.name]?.filter(advisory => {
// This could be for another version of this package elsewhere in the tree
if (!semver.intersects(advisory.vulnerable_versions, item.version)) {
return false
}
if (!vulns) {
return true
}
// vulns are OR with each other, if any one matches we're done
for (const vuln of vulns) {
if (vuln.severity && !vuln.severity.includes('*')) {
if (!vuln.severity.includes(advisory.severity)) {
continue
}
}

if (vuln?.cwe) {
// * is special, it means "has a cwe"
if (vuln.cwe.includes('*')) {
if (!advisory.cwe.length) {
continue
}
} else if (!vuln.cwe.every(cwe => advisory.cwe.includes(`CWE-${cwe}`))) {
continue
}
}
return true
}
})
if (vulnerable?.length) {
item.queryContext = {
advisories: vulnerable,
Expand Down
10 changes: 8 additions & 2 deletions workspaces/arborist/test/query-selector-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,11 @@ t.test('query-selector-all', async t => {
})

nock('https://registry.npmjs.org')
.persist()
.post('/-/npm/v1/security/advisories/bulk')
.reply(200, {
foo: [{ id: 'test-vuln', vulnerable_versions: '*' }],
foo: [{ id: 'test-vuln', vulnerable_versions: '*', severity: 'high', cwe: [] }],
sive: [{ id: 'test-vuln', vulnerable_versions: '*', severity: 'low', cwe: ['CWE-123'] }],
moo: [{ id: 'test-vuln', vulnerable_versions: '<1.0.0' }],
})
for (const [pkg, versions] of Object.entries(packumentStubs)) {
Expand Down Expand Up @@ -849,7 +851,11 @@ t.test('query-selector-all', async t => {
[':outdated(nonsense)', [], { before: yesterday }], // again, no results here ever

// vuln pseudo
[':vuln', ['[email protected]']],
[':vuln', ['[email protected]', '[email protected]']],
[':vuln([severity=high])', ['[email protected]']],
[':vuln([cwe])', ['[email protected]']],
[':vuln([cwe=123])', ['[email protected]']],
[':vuln([severity=critical])', []],
['#nomatch:vuln', []], // no network requests are made if the result set is empty

// attr pseudo
Expand Down

0 comments on commit 0cd2149

Please sign in to comment.