Skip to content

fix!: remove old audit fallback request #7911

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix!: remove old audit fallback request
BREAKING CHANGE: npm will no longer fall back to the old audit endpoint
if the bulk advisory request fails.

This legacy code has a long tail in npm.  Getting rid of it was
difficult because of how load-bearing some of those requests were in
tests.  This PR removes the old "mock server" that arborist tests spun
up, and moved that logic into the existing mock registry that the cli
uses.  This will allow us to consolidate our logic in tests, and also
outline more granularly which tests actually make registry requests.

A few tests that were testing just the fallback behavior were also
removed.
  • Loading branch information
wraithgar committed Nov 20, 2024
commit 2b3ee8b9aaaae1390d3a3d82c4c0919e57ef461e
75 changes: 72 additions & 3 deletions mock-registry/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
const pacote = require('pacote')
const Arborist = require('@npmcli/arborist')
const npa = require('npm-package-arg')
const Nock = require('nock')
const npa = require('npm-package-arg')
const pacote = require('pacote')
const path = require('node:path')
const stringify = require('json-stringify-safe')

const { createReadStream } = require('node:fs')
const fs = require('node:fs/promises')

const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'

const logReq = (req, ...keys) => {
const obj = JSON.parse(stringify(req))
const res = {}
Expand All @@ -15,6 +21,27 @@ const logReq = (req, ...keys) => {
return stringify(res, null, 2)
}

// helper to convert old audit results to new bulk results
// TODO eventually convert the fixture files themselves
const auditToBulk = audit => {
const bulk = {}
for (const advisory in audit.advisories) {
const {
id,
url,
title,
severity = 'high',
/* eslint-disable-next-line camelcase */
vulnerable_versions = '*',
module_name: name,
} = audit.advisories[advisory]
bulk[name] = bulk[name] || []
/* eslint-disable-next-line camelcase */
bulk[name].push({ id, url, title, severity, vulnerable_versions })
}
return bulk
}

class MockRegistry {
#tap
#nock
Expand Down Expand Up @@ -66,14 +93,14 @@ class MockRegistry {
// find mistakes quicker instead of waiting for the entire test to end
t.afterEach((t) => {
t.strictSame(server.pendingMocks(), [], 'no pending mocks after each')
t.strictSame(server.activeMocks(), [], 'no active mocks after each')
})
}

t.teardown(() => {
Nock.enableNetConnect()
server.done()
Nock.emitter.off('no match', noMatch)
Nock.cleanAll()
})

return server
Expand Down Expand Up @@ -453,6 +480,48 @@ class MockRegistry {
}
}

// bulk advisory audit endpoint
audit ({ responseCode = 200, results = {}, convert = false, times = 1 } = {}) {
this.nock = this.nock
.post(this.fullPath('/-/npm/v1/security/advisories/bulk'))
.times(times)
.reply(
responseCode,
convert ? auditToBulk(results) : results
)
}

// Used in Arborist to mock the registry from fixture data on disk
// Will eat up all GET requests to the entire registry, so it probably doesn't work with the other GET routes very well.
mocks ({ dir }) {
const exists = (p) => fs.stat(p).then((s) => s).catch(() => false)
this.nock = this.nock.get(/.*/).reply(async function () {
const { headers, path: url } = this.req
const isCorgi = headers.accept.includes('application/vnd.npm.install-v1+json')
const encodedUrl = url.replace(/@/g, '').replace(/%2f/gi, '/')
const f = path.join(dir, 'registry-mocks', 'content', encodedUrl)
let file = f
let contentType = 'application/octet-stream'
if (isCorgi && await exists(`${f}.min.json`)) {
file = `${f}.min.json`
contentType = corgiDoc
} else if (await exists(`${f}.json`)) {
file = `${f}.json`
contentType = 'application/json'
} else if (await exists(`${f}/index.json`)) {
file = `${f}index.json`
contentType = 'application/json'
}
const stats = await exists(file)
if (stats) {
const body = createReadStream(file)
body.pause()
return [200, body, { 'content-type': contentType, 'content-length': stats.size }]
}
return [404, { error: 'not found' }]
}).persist()
}

/**
* this is a simpler convience method for creating mockable registry with
* tarballs for specific versions
Expand Down
1 change: 1 addition & 0 deletions package-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -16922,6 +16922,7 @@
},
"devDependencies": {
"@npmcli/eslint-config": "^5.0.1",
"@npmcli/mock-registry": "^1.0.0",
"@npmcli/template-oss": "4.23.3",
"benchmark": "^2.1.4",
"minify-registry-metadata": "^4.0.0",
Expand Down
102 changes: 16 additions & 86 deletions workspaces/arborist/lib/audit-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,33 +274,6 @@ class AuditReport extends Map {
throw new Error('do not call AuditReport.set() directly')
}

// convert a quick-audit into a bulk advisory listing
static auditToBulk (report) {
if (!report.advisories) {
// tack on the report json where the response body would go
throw Object.assign(new Error('Invalid advisory report'), {
body: JSON.stringify(report),
})
}

const bulk = {}
const { advisories } = report
for (const advisory of Object.values(advisories)) {
const {
id,
url,
title,
severity = 'high',
vulnerable_versions = '*',
module_name: name,
} = advisory
bulk[name] = bulk[name] || []
bulk[name].push({ id, url, title, severity, vulnerable_versions })
}

return bulk
}

async [_getReport] () {
// if we're not auditing, just return false
if (this.options.audit === false || this.options.offline === true || this.tree.inventory.size === 1) {
Expand All @@ -309,39 +282,24 @@ class AuditReport extends Map {

const timeEnd = time.start('auditReport:getReport')
try {
try {
// first try the super fast bulk advisory listing
const body = prepareBulkData(this.tree, this[_omit], this.filterSet)
log.silly('audit', 'bulk request', body)

// no sense asking if we don't have anything to audit,
// we know it'll be empty
if (!Object.keys(body).length) {
return null
}
const body = prepareBulkData(this.tree, this[_omit], this.filterSet)
log.silly('audit', 'bulk request', body)

const res = await fetch('/-/npm/v1/security/advisories/bulk', {
...this.options,
registry: this.options.auditRegistry || this.options.registry,
method: 'POST',
gzip: true,
body,
})

return await res.json()
} catch (er) {
log.silly('audit', 'bulk request failed', String(er.body))
// that failed, try the quick audit endpoint
const body = prepareData(this.tree, this.options)
const res = await fetch('/-/npm/v1/security/audits/quick', {
...this.options,
registry: this.options.auditRegistry || this.options.registry,
method: 'POST',
gzip: true,
body,
})
return AuditReport.auditToBulk(await res.json())
// no sense asking if we don't have anything to audit,
// we know it'll be empty
if (!Object.keys(body).length) {
return null
}

const res = await fetch('/-/npm/v1/security/advisories/bulk', {
...this.options,
registry: this.options.auditRegistry || this.options.registry,
method: 'POST',
gzip: true,
body,
})

return await res.json()
} catch (er) {
log.verbose('audit error', er)
log.silly('audit error', String(er.body))
Expand Down Expand Up @@ -384,32 +342,4 @@ const prepareBulkData = (tree, omit, filterSet) => {
return payload
}

const prepareData = (tree, opts) => {
const { npmVersion: npm_version } = opts
const node_version = process.version
const { platform, arch } = process
const { NODE_ENV: node_env } = process.env
const data = tree.meta.commit()
// the legacy audit endpoint doesn't support any kind of pre-filtering
// we just have to get the advisories and skip over them in the report
return {
name: data.name,
version: data.version,
requires: {
...(tree.package.devDependencies || {}),
...(tree.package.peerDependencies || {}),
...(tree.package.optionalDependencies || {}),
...(tree.package.dependencies || {}),
},
dependencies: data.dependencies,
metadata: {
node_version,
npm_version,
platform,
arch,
node_env,
},
}
}

module.exports = AuditReport
3 changes: 2 additions & 1 deletion workspaces/arborist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
},
"devDependencies": {
"@npmcli/eslint-config": "^5.0.1",
"@npmcli/mock-registry": "^1.0.0",
"@npmcli/template-oss": "4.23.3",
"benchmark": "^2.1.4",
"minify-registry-metadata": "^4.0.0",
Expand Down Expand Up @@ -82,7 +83,7 @@
"test-env": [
"LC_ALL=sk"
],
"timeout": "360",
"timeout": "720",
"nyc-arg": [
"--exclude",
"tap-snapshots/**"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159887,6 +159887,7 @@ ArboristNode {
"location": "node_modules/foo",
"name": "foo",
"path": "{CWD}/test/arborist/tap-testdir-build-ideal-tree-workspaces-should-allow-cyclic-peer-dependencies-between-workspaces-and-packages-from-a-repository/node_modules/foo",
"resolved": "https://registry.npmjs.org/foo/-/foo-1.0.0.tgz",
"version": "1.0.0",
},
"workspace-a" => ArboristLink {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ exports[`test/arborist/reify.js TAP add a dep present in the tree, with v1 shrin
{"dependencies":{"once":"^1.4.0","wrappy":"^1.0.2"}}
`

exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be mkdirpd > should output a successful tree in mkdirp folder 1`] = `
exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be mkdirpd not dry run > should output a successful tree in mkdirp folder 1`] = `
ArboristNode {
"children": Map {
"abbrev" => ArboristNode {
Expand All @@ -183,7 +183,7 @@ ArboristNode {
},
"location": "node_modules/abbrev",
"name": "abbrev",
"path": "{CWD}/test/arborist/tap-testdir-reify-add-a-new-pkg-to-a-prefix-that-needs-to-be-mkdirpd/missing/path/to/root/node_modules/abbrev",
"path": "{CWD}/test/arborist/tap-testdir-reify-add-a-new-pkg-to-a-prefix-that-needs-to-be-mkdirpd-not-dry-run/missing/path/to/root/node_modules/abbrev",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"version": "1.1.1",
},
Expand All @@ -199,11 +199,11 @@ ArboristNode {
"isProjectRoot": true,
"location": "",
"name": "root",
"path": "{CWD}/test/arborist/tap-testdir-reify-add-a-new-pkg-to-a-prefix-that-needs-to-be-mkdirpd/missing/path/to/root",
"path": "{CWD}/test/arborist/tap-testdir-reify-add-a-new-pkg-to-a-prefix-that-needs-to-be-mkdirpd-not-dry-run/missing/path/to/root",
}
`

exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be mkdirpd > should place expected lockfile file into place 1`] = `
exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be mkdirpd not dry run > should place expected lockfile file into place 1`] = `
{
"name": "root",
"lockfileVersion": 3,
Expand All @@ -225,7 +225,7 @@ exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be m

`

exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be mkdirpd > should place expected package.json file into place 1`] = `
exports[`test/arborist/reify.js TAP add a new pkg to a prefix that needs to be mkdirpd not dry run > should place expected package.json file into place 1`] = `
{
"dependencies": {
"abbrev": "^1.1.1"
Expand Down Expand Up @@ -17900,6 +17900,7 @@ Object {
"ruy": "bin/index.js",
},
"integrity": "sha512-VYppDTCM6INWUMKlWiKws4nVMuCNU5h+xjF6lj/0y90rLq017/m8aEpNy4zQSZFV2qz66U/hRZwwlSLJ5l5JMQ==",
"license": "ISC",
"resolved": "https://registry.npmjs.org/ruy/-/ruy-1.0.0.tgz",
"version": "1.0.0",
},
Expand Down Expand Up @@ -33046,6 +33047,7 @@ exports[`test/arborist/reify.js TAP save proper lockfile with bins when upgradin
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
Expand Down Expand Up @@ -33073,6 +33075,7 @@ exports[`test/arborist/reify.js TAP save proper lockfile with bins when upgradin
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
Expand Down
Loading