From 343d47ad51baa568d93f85723025670e29d00be3 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 6 Feb 2024 12:02:46 -0800 Subject: [PATCH 1/2] fix(query-selector): don't look up private packages on :outdated --- workspaces/arborist/lib/query-selector-all.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/workspaces/arborist/lib/query-selector-all.js b/workspaces/arborist/lib/query-selector-all.js index 96c52144060b8..5eb3bcc741de3 100644 --- a/workspaces/arborist/lib/query-selector-all.js +++ b/workspaces/arborist/lib/query-selector-all.js @@ -445,6 +445,11 @@ class Results { return false } + // private packages can't be published, skip them + if (node.package.private) { + return false + } + // we cache the promise representing the full versions list, this helps reduce the // number of requests we send by keeping population of the cache in a single tick // making it less likely that multiple requests for the same package will be inflight From d6ae11d3364a5e7cdc45dd1e4b9875daa0af195d Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 6 Feb 2024 13:54:29 -0800 Subject: [PATCH 2/2] feat(query): add :vuln pseudo selector --- .../content/using-npm/dependency-selectors.md | 9 ++++- workspaces/arborist/lib/query-selector-all.js | 40 +++++++++++++++++++ .../arborist/test/query-selector-all.js | 10 +++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/docs/lib/content/using-npm/dependency-selectors.md b/docs/lib/content/using-npm/dependency-selectors.md index 87a722748c261..fe0ec38cc7fd9 100644 --- a/docs/lib/content/using-npm/dependency-selectors.md +++ b/docs/lib/content/using-npm/dependency-selectors.md @@ -13,7 +13,7 @@ The [`npm query`](/commands/npm-query) command exposes a new dependency selector - Unlocks the ability to answer complex, multi-faceted questions about dependencies, their relationships & associative metadata - Consolidates redundant logic of similar query commands in `npm` (ex. `npm fund`, `npm ls`, `npm outdated`, `npm audit` ...) -### Dependency Selector Syntax `v1.0.0` +### Dependency Selector Syntax #### Overview: @@ -62,6 +62,7 @@ The [`npm query`](/commands/npm-query) command exposes a new dependency selector - `:path()` [glob](https://www.npmjs.com/package/glob) matching based on dependencies path relative to the project - `:type()` [based on currently recognized types](https://github.com/npm/npm-package-arg#result-object) - `:outdated()` when a dependency is outdated +- `:vuln` when a dependency has a known vulnerability ##### `:semver(, [selector], [function])` @@ -101,6 +102,12 @@ Some examples: - `:root > :outdated(major)` returns every direct dependency that has a new semver major release - `.prod:outdated(in-range)` returns production dependencies that have a new release that satisfies at least one of its edges in +##### `:vuln` + +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 `semver@7.6.0` in your tree, a vulnerability for `semver` which affects versions `<=6.3.1` will not match. + +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. + #### [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. diff --git a/workspaces/arborist/lib/query-selector-all.js b/workspaces/arborist/lib/query-selector-all.js index 5eb3bcc741de3..82f5bd9c56de9 100644 --- a/workspaces/arborist/lib/query-selector-all.js +++ b/workspaces/arborist/lib/query-selector-all.js @@ -8,6 +8,7 @@ const { minimatch } = require('minimatch') const npa = require('npm-package-arg') const pacote = require('pacote') const semver = require('semver') +const fetch = require('npm-registry-fetch') // handle results for parsed query asts, results are stored in a map that has a // key that points to each ast selector node and stores the resulting array of @@ -432,6 +433,45 @@ class Results { return this.initialItems.filter(node => node.target.edgesIn.size > 1) } + async vulnPseudo () { + 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() + return this.initialItems.filter(item => { + const vulnerable = advisories[item.name]?.filter(advisory => + semver.intersects(advisory.vulnerable_versions, item.version) + ) + if (vulnerable?.length) { + item.queryContext = { + advisories: vulnerable, + } + return true + } + return false + }) + } + async outdatedPseudo () { const { outdatedKind = 'any' } = this.currentAstNode diff --git a/workspaces/arborist/test/query-selector-all.js b/workspaces/arborist/test/query-selector-all.js index 08d62034479ea..d8a854f429dae 100644 --- a/workspaces/arborist/test/query-selector-all.js +++ b/workspaces/arborist/test/query-selector-all.js @@ -99,6 +99,12 @@ t.test('query-selector-all', async t => { nock.enableNetConnect() }) + nock('https://registry.npmjs.org') + .post('/-/npm/v1/security/advisories/bulk') + .reply(200, { + foo: [{ id: 'test-vuln', vulnerable_versions: '*' }], + moo: [{ id: 'test-vuln', vulnerable_versions: '<1.0.0' }], + }) for (const [pkg, versions] of Object.entries(packumentStubs)) { nock('https://registry.npmjs.org') .persist() @@ -842,6 +848,10 @@ t.test('query-selector-all', async t => { ], { before: yesterday }], [':outdated(nonsense)', [], { before: yesterday }], // again, no results here ever + // vuln pseudo + [':vuln', ['foo@2.2.2']], + ['#nomatch:vuln', []], // no network requests are made if the result set is empty + // attr pseudo [':attr([name=dasher])', ['dasher@2.0.0']], [':attr(dependencies, [bar="^1.0.0"])', ['foo@2.2.2']],