diff --git a/test/cli/install/__snapshots__/bun-audit.test.ts.snap b/test/cli/install/__snapshots__/bun-audit.test.ts.snap deleted file mode 100644 index 657e3a3faead..000000000000 --- a/test/cli/install/__snapshots__/bun-audit.test.ts.snap +++ /dev/null @@ -1,436 +0,0 @@ -// Bun Snapshot v1, https://bun.sh/docs/test/snapshots - -exports[`\`bun audit\` should exit code 1 when there are vulnerabilities: bun-audit-expect-vulnerabilities-found 1`] = ` -"bun audit () - -base64-url@1.2.1 - express > connect > csurf > csrf > uid-safe > base64-url - high: Out-of-bounds Read in base64-url (<2.0.0) - https://github.com/advisories/GHSA-j4mr-9xw3-c9jx - -basic-auth-connect@1.0.0 - express > connect > basic-auth-connect - high: basic-auth-connect's callback uses time unsafe string comparison (<1.1.0) - https://github.com/advisories/GHSA-7p89-p6hx-q4fw - -body-parser@1.13.3 - express > connect > body-parser - high: body-parser vulnerable to denial of service when url encoding is enabled (<1.20.3) - https://github.com/advisories/GHSA-qwcr-r2fm-qrc7 - -cookie@0.1.3 - express > connect > cookie - low: cookie accepts cookie name, path, and domain with out of bounds characters (<0.7.0) - https://github.com/advisories/GHSA-pxg6-pf52-xh8x - -debug@2.2.0, 2.6.9 - express > connect > compression > debug - high: debug Inefficient Regular Expression Complexity vulnerability (<2.6.9) - https://github.com/advisories/GHSA-9vvw-cc9w-f27h - low: Regular Expression Denial of Service in debug (<2.6.9) - https://github.com/advisories/GHSA-gxpj-cx7g-858c - -express@3.21.2 - (direct dependency) - low: Express Open Redirect vulnerability (>=3.4.5 <4.0.0-rc1) - https://github.com/advisories/GHSA-jj78-5fmv-mv28 - low: express vulnerable to XSS via response.redirect() (<4.20.0) - https://github.com/advisories/GHSA-qw6h-vgh9-j6wx - moderate: Express ressource injection (<=3.21.4) - https://github.com/advisories/GHSA-cm5g-3pgc-8rg4 - moderate: Express.js Open Redirect in malformed URLs (<4.19.2) - https://github.com/advisories/GHSA-rv95-896h-c2vc - -fresh@0.3.0 - express > connect > fresh - high: Regular Expression Denial of Service in fresh (<0.5.2) - https://github.com/advisories/GHSA-9qj9-36jm-prpv - -mime@1.3.4 - express > send > mime - high: mime Regular Expression Denial of Service when MIME lookup performed on untrusted user input (<1.4.1) - https://github.com/advisories/GHSA-wrvr-8mpx-r7pp - -minimist@0.0.8 - express > mkdirp > minimist - critical: Prototype Pollution in minimist (<0.2.4) - https://github.com/advisories/GHSA-xvch-5gv4-984h - moderate: Prototype Pollution in minimist (<0.2.1) - https://github.com/advisories/GHSA-vh95-rmgr-6w4m - -morgan@1.6.1 - express > connect > morgan - critical: Code Injection in morgan (<1.9.1) - https://github.com/advisories/GHSA-gwg9-rgvj-4h5j - -ms@0.7.1, 0.7.2, 2.0.0 - express > connect > serve-favicon > ms - moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability (<2.0.0) - https://github.com/advisories/GHSA-w9mr-4mfr-499f - -negotiator@0.5.3, 0.6.3 - express > connect > serve-index > accepts > negotiator - high: Regular Expression Denial of Service in negotiator (<0.6.1) - https://github.com/advisories/GHSA-7mc5-chhp-fmc3 - -qs@4.0.0 - express > connect > body-parser > qs - high: qs vulnerable to Prototype Pollution (<6.2.4) - https://github.com/advisories/GHSA-hrpp-h998-j3pp - high: Prototype Pollution Protection Bypass in qs (<6.0.4) - https://github.com/advisories/GHSA-gqgv-6jq5-jjj9 - -send@0.13.0, 0.13.2 - express > send - low: send vulnerable to template injection that can lead to XSS (<0.19.0) - https://github.com/advisories/GHSA-m6fv-jmcg-4jfg - -serve-static@1.10.3 - express > connect > serve-static - low: serve-static vulnerable to template injection that can lead to XSS (<1.16.0) - https://github.com/advisories/GHSA-cm22-4g7w-348p - -21 vulnerabilities (2 critical, 9 high, 4 moderate, 6 low) - - bun audit fix upgrade the vulnerable packages within their ranges - bun audit fix --latest also cross major versions" -`; - -exports[`\`bun audit\` should print valid JSON and exit 0 when --json is passed and there are no vulnerabilities: bun-audit-expect-valid-json-stdout-report-no-vulnerabilities 1`] = `{}`; - -exports[`\`bun audit\` should print valid JSON and exit 1 when --json is passed and there are vulnerabilities: bun-audit-expect-valid-json-stdout-report-vulnerabilities 1`] = ` -{ - "base64-url": [ - { - "cvss": { - "score": 0, - "vectorString": null, - }, - "cwe": [ - "CWE-125", - ], - "id": 1090859, - "severity": "high", - "title": "Out-of-bounds Read in base64-url", - "url": "https://github.com/advisories/GHSA-j4mr-9xw3-c9jx", - "vulnerable_versions": "<2.0.0", - }, - ], - "basic-auth-connect": [ - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", - }, - "cwe": [ - "CWE-208", - ], - "id": 1099800, - "severity": "high", - "title": "basic-auth-connect's callback uses time unsafe string comparison", - "url": "https://github.com/advisories/GHSA-7p89-p6hx-q4fw", - "vulnerable_versions": "<1.1.0", - }, - ], - "body-parser": [ - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - }, - "cwe": [ - "CWE-405", - ], - "id": 1099520, - "severity": "high", - "title": "body-parser vulnerable to denial of service when url encoding is enabled", - "url": "https://github.com/advisories/GHSA-qwcr-r2fm-qrc7", - "vulnerable_versions": "<1.20.3", - }, - ], - "cookie": [ - { - "cvss": { - "score": 0, - "vectorString": null, - }, - "cwe": [ - "CWE-74", - ], - "id": 1103907, - "severity": "low", - "title": "cookie accepts cookie name, path, and domain with out of bounds characters", - "url": "https://github.com/advisories/GHSA-pxg6-pf52-xh8x", - "vulnerable_versions": "<0.7.0", - }, - ], - "debug": [ - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - }, - "cwe": [ - "CWE-1333", - ], - "id": 1094457, - "severity": "high", - "title": "debug Inefficient Regular Expression Complexity vulnerability", - "url": "https://github.com/advisories/GHSA-9vvw-cc9w-f27h", - "vulnerable_versions": "<2.6.9", - }, - { - "cvss": { - "score": 3.7, - "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L", - }, - "cwe": [ - "CWE-400", - ], - "id": 1096795, - "severity": "low", - "title": "Regular Expression Denial of Service in debug", - "url": "https://github.com/advisories/GHSA-gxpj-cx7g-858c", - "vulnerable_versions": "<2.6.9", - }, - ], - "express": [ - { - "cvss": { - "score": 4.7, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N", - }, - "cwe": [ - "CWE-601", - ], - "id": 1099969, - "severity": "low", - "title": "Express Open Redirect vulnerability", - "url": "https://github.com/advisories/GHSA-jj78-5fmv-mv28", - "vulnerable_versions": ">=3.4.5 <4.0.0-rc1", - }, - { - "cvss": { - "score": 5, - "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", - }, - "cwe": [ - "CWE-79", - ], - "id": 1100530, - "severity": "low", - "title": "express vulnerable to XSS via response.redirect()", - "url": "https://github.com/advisories/GHSA-qw6h-vgh9-j6wx", - "vulnerable_versions": "<4.20.0", - }, - { - "cvss": { - "score": 4, - "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N", - }, - "cwe": [ - "CWE-74", - ], - "id": 1101381, - "severity": "moderate", - "title": "Express ressource injection", - "url": "https://github.com/advisories/GHSA-cm5g-3pgc-8rg4", - "vulnerable_versions": "<=3.21.4", - }, - { - "cvss": { - "score": 6.1, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", - }, - "cwe": [ - "CWE-601", - "CWE-1286", - ], - "id": 1096820, - "severity": "moderate", - "title": "Express.js Open Redirect in malformed URLs", - "url": "https://github.com/advisories/GHSA-rv95-896h-c2vc", - "vulnerable_versions": "<4.19.2", - }, - ], - "fresh": [ - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - }, - "cwe": [ - "CWE-400", - ], - "id": 1093570, - "severity": "high", - "title": "Regular Expression Denial of Service in fresh", - "url": "https://github.com/advisories/GHSA-9qj9-36jm-prpv", - "vulnerable_versions": "<0.5.2", - }, - ], - "mime": [ - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - }, - "cwe": [ - "CWE-400", - ], - "id": 1093780, - "severity": "high", - "title": "mime Regular Expression Denial of Service when MIME lookup performed on untrusted user input", - "url": "https://github.com/advisories/GHSA-wrvr-8mpx-r7pp", - "vulnerable_versions": "<1.4.1", - }, - ], - "minimist": [ - { - "cvss": { - "score": 9.8, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", - }, - "cwe": [ - "CWE-1321", - ], - "id": 1097677, - "severity": "critical", - "title": "Prototype Pollution in minimist", - "url": "https://github.com/advisories/GHSA-xvch-5gv4-984h", - "vulnerable_versions": "<0.2.4", - }, - { - "cvss": { - "score": 5.6, - "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - }, - "cwe": [ - "CWE-1321", - ], - "id": 1096466, - "severity": "moderate", - "title": "Prototype Pollution in minimist", - "url": "https://github.com/advisories/GHSA-vh95-rmgr-6w4m", - "vulnerable_versions": "<0.2.1", - }, - ], - "morgan": [ - { - "cvss": { - "score": 9.8, - "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", - }, - "cwe": [ - "CWE-94", - ], - "id": 1093804, - "severity": "critical", - "title": "Code Injection in morgan", - "url": "https://github.com/advisories/GHSA-gwg9-rgvj-4h5j", - "vulnerable_versions": "<1.9.1", - }, - ], - "ms": [ - { - "cvss": { - "score": 5.3, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", - }, - "cwe": [ - "CWE-1333", - ], - "id": 1094419, - "severity": "moderate", - "title": "Vercel ms Inefficient Regular Expression Complexity vulnerability", - "url": "https://github.com/advisories/GHSA-w9mr-4mfr-499f", - "vulnerable_versions": "<2.0.0", - }, - ], - "negotiator": [ - { - "cvss": { - "score": 0, - "vectorString": null, - }, - "cwe": [ - "CWE-400", - ], - "id": 1090969, - "severity": "high", - "title": "Regular Expression Denial of Service in negotiator", - "url": "https://github.com/advisories/GHSA-7mc5-chhp-fmc3", - "vulnerable_versions": "<0.6.1", - }, - ], - "qs": [ - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - }, - "cwe": [ - "CWE-1321", - ], - "id": 1104115, - "severity": "high", - "title": "qs vulnerable to Prototype Pollution", - "url": "https://github.com/advisories/GHSA-hrpp-h998-j3pp", - "vulnerable_versions": "<6.2.4", - }, - { - "cvss": { - "score": 7.5, - "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - }, - "cwe": [ - "CWE-20", - ], - "id": 1087527, - "severity": "high", - "title": "Prototype Pollution Protection Bypass in qs", - "url": "https://github.com/advisories/GHSA-gqgv-6jq5-jjj9", - "vulnerable_versions": "<6.0.4", - }, - ], - "send": [ - { - "cvss": { - "score": 5, - "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", - }, - "cwe": [ - "CWE-79", - ], - "id": 1100526, - "severity": "low", - "title": "send vulnerable to template injection that can lead to XSS", - "url": "https://github.com/advisories/GHSA-m6fv-jmcg-4jfg", - "vulnerable_versions": "<0.19.0", - }, - ], - "serve-static": [ - { - "cvss": { - "score": 5, - "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", - }, - "cwe": [ - "CWE-79", - ], - "id": 1100528, - "severity": "low", - "title": "serve-static vulnerable to template injection that can lead to XSS", - "url": "https://github.com/advisories/GHSA-cm22-4g7w-348p", - "vulnerable_versions": "<1.16.0", - }, - ], -} -`; - -exports[`\`bun audit\` should exit 1 and behave exactly the same when there are vulnerabilities when only devDependencies are specified: bun-audit-expect-vulnerabilities-found 1`] = ` -"bun audit () - -ms@0.7.0 - (direct dependency) - moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability (<2.0.0) - https://github.com/advisories/GHSA-w9mr-4mfr-499f - high: Regular Expression Denial of Service in ms (<0.7.1) - https://github.com/advisories/GHSA-3fx5-fwvr-xrjg - -2 vulnerabilities (1 high, 1 moderate) - - bun audit fix upgrade the vulnerable packages within their ranges - bun audit fix --latest also cross major versions" -`; - -exports[`\`bun audit\` when a project has some safe dependencies and some vulnerable dependencies, we should not print the safe dependencies: bun-audit-expect-vulnerabilities-found 1`] = ` -"bun audit () - -ms@0.7.0 - (direct dependency) - moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability (<2.0.0) - https://github.com/advisories/GHSA-w9mr-4mfr-499f - high: Regular Expression Denial of Service in ms (<0.7.1) - https://github.com/advisories/GHSA-3fx5-fwvr-xrjg - -2 vulnerabilities (1 high, 1 moderate) - - bun audit fix upgrade the vulnerable packages within their ranges - bun audit fix --latest also cross major versions" -`; diff --git a/test/cli/install/bun-audit.test.ts b/test/cli/install/bun-audit.test.ts index b0197c013fb3..dea94446df4c 100644 --- a/test/cli/install/bun-audit.test.ts +++ b/test/cli/install/bun-audit.test.ts @@ -11,6 +11,7 @@ import { runBunInstall, tempDir, } from "harness"; +import { mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { resolveBulkAdvisoryFixture } from "./registry/fixtures/audit/audit-fixtures"; @@ -24,31 +25,34 @@ function fixture( return join(import.meta.dirname, "registry", "fixtures", "audit", folder); } -let server: Bun.Server; -const verdaccio = new VerdaccioRegistry(); +// The body of one bulk advisory request: installed versions by package name. +type BulkRequest = Record; -beforeAll(async () => { - server = Bun.serve({ +// Answers the bulk advisory endpoint with the responses recorded in audit-fixtures.json for the projects under +// registry/fixtures/audit, which ship with a bun.lock and are never installed. Every request body lands in `requests`. +function startFixtureRegistry(requests: BulkRequest[] = []) { + return Bun.serve({ port: 0, - fetch: async req => { - const body = await gunzipJsonRequest(req); - - const fixture = resolveBulkAdvisoryFixture(body); - - if (!fixture) { - console.log("No fixture found for", body); - return new Response("No fixture found", { status: 404 }); - } - - return Response.json(fixture); + async fetch(req) { + const body: BulkRequest = await gunzipJsonRequest(req); + requests.push(body); + const response = resolveBulkAdvisoryFixture(body); + return response ? Response.json(response) : new Response("No fixture found", { status: 404 }); }, }); +} + +const verdaccio = new VerdaccioRegistry(); +let templates: ReturnType; + +beforeAll(async () => { + templates = tempDir("bun-audit-templates-", {}); await verdaccio.start(); }); afterAll(() => { - server?.stop(); verdaccio.stop(); + templates[Symbol.dispose](); }); function doAuditTest( @@ -57,37 +61,25 @@ function doAuditTest( args?: string[]; exitCode: number; files: DirectoryTree | string; - fn: (std: { stdout: PromiseLike; stderr: PromiseLike; dir: string }) => Promise; + fn: (result: { stdout: string; stderr: string; dir: string; requests: BulkRequest[] }) => void; }, ) { - test(label, async () => { - await using dir = tempDir("bun-test-audit-" + label.replace(/[^a-zA-Z0-9]/g, "-"), options.files); - - const cmd = [bunExe(), "audit", ...(options.args ?? [])]; - - const url = server.url.toString().slice(0, -1); + test.concurrent(label, async () => { + const requests: BulkRequest[] = []; + await using registry = startFixtureRegistry(requests); + using dir = tempDir("bun-test-audit-", options.files); await using proc = spawn({ - cmd, + cmd: [bunExe(), "audit", ...(options.args ?? [])], stdout: "pipe", stderr: "pipe", cwd: dir, - env: { - ...bunEnv, - NPM_CONFIG_REGISTRY: url, - }, + env: { ...bunEnv, NPM_CONFIG_REGISTRY: registryHref(registry) }, }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - try { - await options.fn({ stdout: Promise.resolve(out), stderr: Promise.resolve(err), dir }); - expect(exitCode).toBe(options.exitCode); - } catch (e) { - console.log("ERR:", err); - console.log("OUT:", out); - throw e; - } + options.fn({ stdout, stderr, dir: String(dir), requests }); + expect(exitCode).toBe(options.exitCode); }); } @@ -116,7 +108,7 @@ type RegistryOptions = { // Incremented on every bulk request; lets a test prove the registry was never contacted. bulkHits?: { count: number }; // Every bulk request's body, in request order. - bulkBodies?: Record[]; + bulkBodies?: BulkRequest[]; // Package names whose manifest requests answer 404; mutable so a test can break the registry after installing. denyManifests?: Set; // Tarball file names (`a-dep-1.0.4.tgz`) whose downloads answer 404. @@ -135,7 +127,7 @@ function startRegistry(advisories: Record, options: Registry if (req.method === "POST" && url.pathname === "/-/npm/v1/security/advisories/bulk") { bulkRequests++; if (options.bulkHits) options.bulkHits.count++; - const body: Record = await gunzipJsonRequest(req); + const body: BulkRequest = await gunzipJsonRequest(req); options.bulkBodies?.push(body); if (options.bulkStatus) return new Response("registry exploded", { status: options.bulkStatus }); if (options.bulkFailAfter !== undefined && bulkRequests > options.bulkFailAfter) { @@ -221,19 +213,67 @@ function installEnv(dir: string) { return { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(dir, ".bun-cache") }; } -async function setup( +// Most tests start from one of a few dozen single-package.json projects, so each distinct project is installed once, +// straight from verdaccio, and then copied like a checked-in fixture: `installedProject` returns a directory holding +// the package.json, bun.lock and node_modules that `steps` (package.json texts installed in order) end up with. +// bun.lock names verdaccio in every tarball URL, which `copyProject` re-points at the registry a test is using; the +// bunfig and the install cache are removed so a copy only ever talks to that registry, which also means a copy starts +// with an empty cache (see `setupWithCachedManifests` for the tests that need a warm one). +const installedProjects = new Map>(); + +function installedProject(steps: string[]) { + const key = JSON.stringify(steps); + let project = installedProjects.get(key); + if (!project) { + const dir = join(templates, String(installedProjects.size)); + project = (async () => { + mkdirSync(dir); + await writeBunfig(dir, verdaccio.registryUrl()); + for (const text of steps) { + await write(join(dir, "package.json"), text); + await runBunInstall(installEnv(dir), dir); + } + rmSync(join(dir, "bunfig.toml")); + rmSync(join(dir, ".bun-cache"), { recursive: true, force: true }); + return dir; + })(); + installedProjects.set(key, project); + } + return project; +} + +async function copyProject(project: string, server: Registry) { + const dir = tempDir("audit-fix-", project); + await write(join(dir, "bun.lock"), (await lock(dir)).replaceAll(verdaccio.registryUrl(), server.url.href)); + await writeBunfig(dir, server); + return dir; +} + +async function installThrough( server: Registry, - pkgJson: object | string, + pkgJsonText: string, extraFiles: Record = {}, scopes?: Record, ) { - const text = typeof pkgJson === "string" ? pkgJson : JSON.stringify(pkgJson); - const dir = tempDir("audit-fix-", { "package.json": text, ...extraFiles }); + const dir = tempDir("audit-fix-", { "package.json": pkgJsonText, ...extraFiles }); await writeBunfig(dir, server, scopes); await runBunInstall(installEnv(dir), dir); return dir; } +// Projects with more files than a package.json (workspaces) or with scoped registries (whose URLs bun.lock would +// also carry) are installed through `server` every time. +async function setup( + server: Registry, + pkgJson: object | string, + extraFiles: Record = {}, + scopes?: Record, +) { + const text = typeof pkgJson === "string" ? pkgJson : JSON.stringify(pkgJson); + if (scopes || Object.keys(extraFiles).length > 0) return installThrough(server, text, extraFiles, scopes); + return copyProject(await installedProject([text]), server); +} + function pkgJson(dir: string, ...segments: string[]) { return file(join(dir, ...segments, "package.json")).json(); } @@ -344,20 +384,104 @@ function scanned(dir: string): Promise { // a-dep@1.0.2 stays installed after the range is widened because the still-satisfied edge is not re-resolved. async function setupVulnerableADep(server: Registry) { - const dir = await setup(server, { name: "foo", dependencies: { "a-dep": "1.0.2" } }); - await reinstall(dir, { name: "foo", dependencies: { "a-dep": "^1.0.2" } }); + const project = await installedProject([ + JSON.stringify({ name: "foo", dependencies: { "a-dep": "1.0.2" } }), + JSON.stringify({ name: "foo", dependencies: { "a-dep": "^1.0.2" } }), + ]); + const dir = await copyProject(project, server); expect(await lock(dir)).toContain('"a-dep@1.0.2"'); return dir; } +// The same kind of project, installed through `server` instead of copied, so that the install cache holds the manifest +// of every dependency. `bun audit fix` turns the manifest cache off and fetches manifests again, which a test that then +// breaks the registry can only prove while the cache holds a manifest that would have answered. `bun install` writes +// those files from its thread pool and does not wait for them before exiting (`save_async` in src/install/npm.rs), so +// an install occasionally ends without them: such a project is thrown away and installed again. The widening step +// fetches nothing, so it runs once the files are there. +async function setupWithCachedManifests( + server: Registry, + dependencies: Record, + widened: Record, +) { + const pinned = JSON.stringify({ name: "foo", dependencies }); + let dir = await installThrough(server, pinned); + for (let attempt = 1; cachedManifests(dir) < Object.keys(dependencies).length; attempt++) { + dir[Symbol.dispose](); + expect(attempt).toBeLessThan(5); + dir = await installThrough(server, pinned); + } + await reinstall(dir, { name: "foo", dependencies: widened }); + return dir; +} + +function cachedManifests(dir: string) { + return Array.from(new Bun.Glob("*.npm").scanSync(join(dir, ".bun-cache"))).length; +} + +describe("setup()", () => { + // Everything in the project by relative path, minus the two things that are per directory: the bunfig (it names + // the directory's own cache) and the cache itself. + async function projectFiles(dir: string) { + const files: Record = {}; + for (const path of new Bun.Glob("**").scanSync({ cwd: dir, dot: true })) { + const name = path.replaceAll("\\", "/"); + if (name === "bunfig.toml" || name.startsWith(".bun-cache/")) continue; + files[name] = await file(join(dir, path)).text(); + } + return files; + } + + test.concurrent("a copied project is what installing through the test's registry produces", async () => { + await using server = startRegistry({}); + const pkg = JSON.stringify({ name: "foo", dependencies: { "one-fixed-dep": "1.0.0" } }); + using copied = await setup(server, pkg); + using installed = await installThrough(server, pkg); + + const files = await projectFiles(copied); + expect(Object.keys(files).sort()).toEqual([ + "bun.lock", + "node_modules/no-deps/index.js", + "node_modules/no-deps/package.json", + "node_modules/one-fixed-dep/index.js", + "node_modules/one-fixed-dep/package.json", + "package.json", + ]); + expect(files["bun.lock"]).toContain(server.url.href); + expect(files["bun.lock"]).not.toContain(verdaccio.registryUrl()); + expect(files).toEqual(await projectFiles(installed)); + }); +}); + describe("`bun audit`", () => { + // The vulnerable package in the vuln-with-only-dev-dependencies and mix-of-safe-and-vulnerable-dependencies fixtures + // is ms@0.7.0, which the registry answers with one moderate and one high advisory. `--json` prints that response as + // it arrived, so --audit-level and --ignore only show up in the exit code; MS_REPORT is how it renders without --json. + const MS_REQUEST: BulkRequest = { ms: ["0.7.0"] }; + const MS_ADVISORIES = resolveBulkAdvisoryFixture(MS_REQUEST); + const MS_REPORT = + AUDIT_HEADER + + "ms@0.7.0\n" + + " (direct dependency)\n" + + " moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability (<2.0.0) - https://github.com/advisories/GHSA-w9mr-4mfr-499f\n" + + " high: Regular Expression Denial of Service in ms (<0.7.1) - https://github.com/advisories/GHSA-3fx5-fwvr-xrjg\n" + + "\n" + + "2 vulnerabilities (1 high, 1 moderate)\n" + + "\n" + + REPORT_FOOTER; + doAuditTest("should fail with no package.json", { exitCode: 1, files: { "README.md": "This place sure is empty...", }, - fn: async ({ stderr }) => { - expect(await stderr).toContain("No package.json was found for directory"); + fn: ({ stdout, stderr, dir, requests }) => { + expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(` + "error: No package.json was found for directory "" + note: Run "bun init" to initialize a project" + `); + expect(stdout).toBe(""); + expect(requests).toEqual([]); }, }); @@ -372,9 +496,10 @@ describe("`bun audit`", () => { }, }), }, - fn: async ({ stdout, stderr }) => { - expect(normalizeBunSnapshot(await stderr)).toBe(MISSING_LOCKFILE); - expect(normalizeBunSnapshot(await stdout)).toBe("bun audit ()"); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stderr)).toBe(MISSING_LOCKFILE); + expect(normalizeBunSnapshot(stdout)).toBe("bun audit ()"); + expect(requests).toEqual([]); }, }); @@ -396,32 +521,108 @@ describe("`bun audit`", () => { "packages": {}, }), }, - fn: async ({ stdout }) => { - expect(normalizeBunSnapshot(await stdout)).toBe(AUDIT_HEADER + noVulnerabilities(0)); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(0)); + expect(stderr).toBe(""); + expect(requests).toEqual([{}]); }, }); doAuditTest("should exit 0 when there are no vulnerabilities", { exitCode: 0, files: fixture("safe-is-number@7"), - fn: async ({ stdout, stderr }) => { - expect(normalizeBunSnapshot(await stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1)); - expect(await stdout).toMatch(DURATION); - expect(await stderr).toBe(""); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1)); + expect(stdout).toMatch(DURATION); + expect(stderr).toBe(""); + expect(requests).toEqual([{ "is-number": ["7.0.0"] }]); }, }); doAuditTest("should exit code 1 when there are vulnerabilities", { exitCode: 1, files: fixture("express@3"), - fn: async ({ stdout, stderr }) => { - const out = normalizeBunSnapshot(await stdout); - expect(out).toStartWith(AUDIT_HEADER); - expect(out).toEndWith("21 vulnerabilities (2 critical, 9 high, 4 moderate, 6 low)\n\n" + REPORT_FOOTER); - expect(out).not.toContain("›"); - expect(out).not.toContain("bun update"); - expect(out).toMatchSnapshot("bun-audit-expect-vulnerabilities-found"); - expect(await stderr).toBe(""); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun audit () + + base64-url@1.2.1 + express > connect > csurf > csrf > uid-safe > base64-url + high: Out-of-bounds Read in base64-url (<2.0.0) - https://github.com/advisories/GHSA-j4mr-9xw3-c9jx + + basic-auth-connect@1.0.0 + express > connect > basic-auth-connect + high: basic-auth-connect's callback uses time unsafe string comparison (<1.1.0) - https://github.com/advisories/GHSA-7p89-p6hx-q4fw + + body-parser@1.13.3 + express > connect > body-parser + high: body-parser vulnerable to denial of service when url encoding is enabled (<1.20.3) - https://github.com/advisories/GHSA-qwcr-r2fm-qrc7 + + cookie@0.1.3 + express > connect > cookie + low: cookie accepts cookie name, path, and domain with out of bounds characters (<0.7.0) - https://github.com/advisories/GHSA-pxg6-pf52-xh8x + + debug@2.2.0, 2.6.9 + express > connect > compression > debug + high: debug Inefficient Regular Expression Complexity vulnerability (<2.6.9) - https://github.com/advisories/GHSA-9vvw-cc9w-f27h + low: Regular Expression Denial of Service in debug (<2.6.9) - https://github.com/advisories/GHSA-gxpj-cx7g-858c + + express@3.21.2 + (direct dependency) + low: Express Open Redirect vulnerability (>=3.4.5 <4.0.0-rc1) - https://github.com/advisories/GHSA-jj78-5fmv-mv28 + low: express vulnerable to XSS via response.redirect() (<4.20.0) - https://github.com/advisories/GHSA-qw6h-vgh9-j6wx + moderate: Express ressource injection (<=3.21.4) - https://github.com/advisories/GHSA-cm5g-3pgc-8rg4 + moderate: Express.js Open Redirect in malformed URLs (<4.19.2) - https://github.com/advisories/GHSA-rv95-896h-c2vc + + fresh@0.3.0 + express > connect > fresh + high: Regular Expression Denial of Service in fresh (<0.5.2) - https://github.com/advisories/GHSA-9qj9-36jm-prpv + + mime@1.3.4 + express > send > mime + high: mime Regular Expression Denial of Service when MIME lookup performed on untrusted user input (<1.4.1) - https://github.com/advisories/GHSA-wrvr-8mpx-r7pp + + minimist@0.0.8 + express > mkdirp > minimist + critical: Prototype Pollution in minimist (<0.2.4) - https://github.com/advisories/GHSA-xvch-5gv4-984h + moderate: Prototype Pollution in minimist (<0.2.1) - https://github.com/advisories/GHSA-vh95-rmgr-6w4m + + morgan@1.6.1 + express > connect > morgan + critical: Code Injection in morgan (<1.9.1) - https://github.com/advisories/GHSA-gwg9-rgvj-4h5j + + ms@0.7.1, 0.7.2, 2.0.0 + express > connect > serve-favicon > ms + moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability (<2.0.0) - https://github.com/advisories/GHSA-w9mr-4mfr-499f + + negotiator@0.5.3, 0.6.3 + express > connect > serve-index > accepts > negotiator + high: Regular Expression Denial of Service in negotiator (<0.6.1) - https://github.com/advisories/GHSA-7mc5-chhp-fmc3 + + qs@4.0.0 + express > connect > body-parser > qs + high: qs vulnerable to Prototype Pollution (<6.2.4) - https://github.com/advisories/GHSA-hrpp-h998-j3pp + high: Prototype Pollution Protection Bypass in qs (<6.0.4) - https://github.com/advisories/GHSA-gqgv-6jq5-jjj9 + + send@0.13.0, 0.13.2 + express > send + low: send vulnerable to template injection that can lead to XSS (<0.19.0) - https://github.com/advisories/GHSA-m6fv-jmcg-4jfg + + serve-static@1.10.3 + express > connect > serve-static + low: serve-static vulnerable to template injection that can lead to XSS (<1.16.0) - https://github.com/advisories/GHSA-cm22-4g7w-348p + + 21 vulnerabilities (2 critical, 9 high, 4 moderate, 6 low) + + bun audit fix upgrade the vulnerable packages within their ranges + bun audit fix --latest also cross major versions" + `); + expect(stdout).toEndWith("also cross major versions\n"); + expect(stderr).toBe(""); + // One request carrying every installed version of the lockfile's 77 packages. + expect(requests).toHaveLength(1); + expect(Object.keys(requests[0])).toHaveLength(77); + expect(requests[0]).toMatchObject({ express: ["3.21.2"], ms: ["0.7.1", "0.7.2", "2.0.0"] }); }, }); @@ -429,11 +630,11 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("vuln-with-only-dev-dependencies"), args: ["--audit-level", "critical"], - fn: async ({ stdout }) => { - expect(normalizeBunSnapshot(await stdout)).toBe( - AUDIT_HEADER + noVulnerabilities(1, "2 below --audit-level=critical"), - ); - expect(await stdout).toMatch(DURATION); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1, "2 below --audit-level=critical")); + expect(stdout).toMatch(DURATION); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }); @@ -441,8 +642,10 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("vuln-with-only-dev-dependencies"), args: ["--ignore", "GHSA-w9mr-4mfr-499f", "--ignore", "GHSA-3fx5-fwvr-xrjg"], - fn: async ({ stdout }) => { - expect(normalizeBunSnapshot(await stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1, "2 ignored")); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1, "2 ignored")); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }); @@ -450,10 +653,12 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("vuln-with-only-dev-dependencies"), args: ["--audit-level", "high", "--ignore", "GHSA-3fx5-fwvr-xrjg"], - fn: async ({ stdout }) => { - expect(normalizeBunSnapshot(await stdout)).toBe( + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toBe( AUDIT_HEADER + noVulnerabilities(1, "1 below --audit-level=high", "1 ignored"), ); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }); @@ -461,10 +666,10 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("safe-is-number@7"), args: ["--json"], - fn: async ({ stdout }) => { - const out = await stdout; - const json = JSON.parse(out); // this would throw making the test fail if the JSON was invalid - expect(json).toMatchSnapshot("bun-audit-expect-valid-json-stdout-report-no-vulnerabilities"); + fn: ({ stdout, stderr, requests }) => { + expect(stdout).toBe("{}\n"); + expect(stderr).toBe(""); + expect(requests).toEqual([{ "is-number": ["7.0.0"] }]); }, }); @@ -472,10 +677,30 @@ describe("`bun audit`", () => { exitCode: 1, files: fixture("express@3"), args: ["--json"], - fn: async ({ stdout }) => { - const out = await stdout; - const json = JSON.parse(out); // this would throw making the test fail if the JSON was invalid - expect(json).toMatchSnapshot("bun-audit-expect-valid-json-stdout-report-vulnerabilities"); + fn: ({ stdout, stderr, requests }) => { + expect(stdout.trim().split("\n")).toHaveLength(1); + const json = JSON.parse(stdout); + expect(json).toEqual(resolveBulkAdvisoryFixture(requests[0])); + // The same 15 packages and 21 advisories the report above lists. + expect(Object.keys(json)).toEqual([ + "base64-url", + "basic-auth-connect", + "body-parser", + "cookie", + "debug", + "express", + "fresh", + "mime", + "minimist", + "morgan", + "ms", + "negotiator", + "qs", + "send", + "serve-static", + ]); + expect(Object.values(json).flat()).toHaveLength(21); + expect(stderr).toBe(""); }, }); @@ -483,9 +708,10 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("vuln-with-only-dev-dependencies"), args: ["--json", "--audit-level", "critical"], - fn: async ({ stdout }) => { - const json = JSON.parse(await stdout); - expect(json.ms.map((a: { severity: string }) => a.severity).sort()).toStrictEqual(["high", "moderate"]); + fn: ({ stdout, stderr, requests }) => { + expect(JSON.parse(stdout)).toEqual(MS_ADVISORIES); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }); @@ -493,9 +719,10 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("vuln-with-only-dev-dependencies"), args: ["--json", "--ignore", "GHSA-w9mr-4mfr-499f", "--ignore", "GHSA-3fx5-fwvr-xrjg"], - fn: async ({ stdout }) => { - const json = JSON.parse(await stdout); - expect(json.ms).toHaveLength(2); + fn: ({ stdout, stderr, requests }) => { + expect(JSON.parse(stdout)).toEqual(MS_ADVISORIES); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }); @@ -503,8 +730,10 @@ describe("`bun audit`", () => { exitCode: 1, files: fixture("vuln-with-only-dev-dependencies"), args: ["--json", "--audit-level", "high", "--ignore", "GHSA-w9mr-4mfr-499f"], - fn: async ({ stdout }) => { - expect(JSON.parse(await stdout).ms).toHaveLength(2); + fn: ({ stdout, stderr, requests }) => { + expect(JSON.parse(stdout)).toEqual(MS_ADVISORIES); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }); @@ -513,8 +742,10 @@ describe("`bun audit`", () => { { exitCode: 1, files: fixture("vuln-with-only-dev-dependencies"), - fn: async ({ stdout }) => { - expect(normalizeBunSnapshot(await stdout)).toMatchSnapshot("bun-audit-expect-vulnerabilities-found"); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stdout)).toBe(MS_REPORT); + expect(stderr).toBe(""); + expect(requests).toEqual([MS_REQUEST]); }, }, ); @@ -524,15 +755,11 @@ describe("`bun audit`", () => { { exitCode: 1, files: fixture("mix-of-safe-and-vulnerable-dependencies"), - fn: async ({ stdout }) => { - // The fixture installs a safe is-number and a vulnerable ms. - - const out = await stdout; - - expect(out).toContain("ms"); - expect(out).not.toContain("is-number"); - - expect(normalizeBunSnapshot(out)).toMatchSnapshot("bun-audit-expect-vulnerabilities-found"); + fn: ({ stdout, stderr, requests }) => { + // is-number is audited but has no advisories, so the report is the one for ms alone. + expect(requests).toEqual([{ "is-number": ["7.0.0"], ...MS_REQUEST }]); + expect(normalizeBunSnapshot(stdout)).toBe(MS_REPORT); + expect(stderr).toBe(""); }, }, ); @@ -566,38 +793,57 @@ describe("`bun audit`", () => { }; } + // Runs `bun audit` with a fixture registry as the default registry and returns the package names it was asked about. async function auditWithDefaultRegistry(dir: string) { + const requests: BulkRequest[] = []; + await using registry = startFixtureRegistry(requests); await using proc = spawn({ cmd: [bunExe(), "audit"], stdout: "pipe", stderr: "pipe", cwd: dir, - env: { ...bunEnv, NPM_CONFIG_REGISTRY: registryHref(server) }, + env: { ...bunEnv, NPM_CONFIG_REGISTRY: registryHref(registry) }, }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { stdout, stderr, exitCode }; + return { stdout, stderr, exitCode, askedAbout: requests.flatMap(Object.keys) }; } - test("packages served by a scoped registry are audited against that registry", async () => { - await using scoped = startRegistry({}, { bulkResponse: { "@foo/bar": [adv("<2.0.0")] } }); + test.concurrent("packages served by a scoped registry are audited against that registry", async () => { + const bulkBodies: BulkRequest[] = []; + await using scoped = startRegistry({}, { bulkResponse: { "@foo/bar": [adv("<2.0.0")] }, bulkBodies }); using dir = tempDir("bun-test-audit-scoped-registry", scopedRegistryProject(scoped)); - const { stdout, stderr, exitCode } = await auditWithDefaultRegistry(String(dir)); - expect(stdout).toContain("@foo/bar@1.0.0\n"); - expect(stdout).toContain("1 vulnerability (1 high)"); + const { stdout, stderr, exitCode, askedAbout } = await auditWithDefaultRegistry(String(dir)); + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun audit () + + @foo/bar@1.0.0 + high: test advisory (<2.0.0) - https://example.invalid/advisory/1 + + 1 vulnerability (1 high) + + bun audit fix upgrade the vulnerable packages within their ranges + bun audit fix --latest also cross major versions" + `); expect(stderr).toBe(""); + expect(bulkBodies).toEqual([{ "@foo/bar": ["1.0.0"], "@foo/baz": ["1.0.0"] }]); + expect(askedAbout).toEqual([]); expect(exitCode).toBe(1); }); - test("packages whose scoped registry does not answer the audit request are listed as skipped", async () => { - await using scoped = startRegistry({}, { bulkStatus: 404 }); - using dir = tempDir("bun-test-audit-scoped-registry-down", scopedRegistryProject(scoped)); + test.concurrent( + "packages whose scoped registry does not answer the audit request are listed as skipped", + async () => { + await using scoped = startRegistry({}, { bulkStatus: 404 }); + using dir = tempDir("bun-test-audit-scoped-registry-down", scopedRegistryProject(scoped)); - const { stdout, stderr, exitCode } = await auditWithDefaultRegistry(String(dir)); - expect(normalizeBunSnapshot(stderr)).toBe(skippedWarning(registryHref(scoped), "404", "@foo/bar", "@foo/baz")); - expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(0, "2 skipped")); - expect(exitCode).toBe(0); - }); + const { stdout, stderr, exitCode, askedAbout } = await auditWithDefaultRegistry(String(dir)); + expect(normalizeBunSnapshot(stderr)).toBe(skippedWarning(registryHref(scoped), "404", "@foo/bar", "@foo/baz")); + expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(0, "2 skipped")); + expect(askedAbout).toEqual([]); + expect(exitCode).toBe(0); + }, + ); doAuditTest("workspaces print the path to the vulnerable package and include workspace:pkg in the name", { exitCode: 1, @@ -634,10 +880,11 @@ describe("`bun audit`", () => { }, }), }, - fn: async ({ stdout }) => { - const out = await stdout; - expect(out).toContain("\nms@0.7.0\n workspace:a > ms\n"); - expect(out).not.toContain("›"); + fn: ({ stdout, stderr, requests }) => { + // The workspace package itself is not sent to the registry. + expect(requests).toEqual([MS_REQUEST]); + expect(normalizeBunSnapshot(stdout)).toBe(MS_REPORT.replace("(direct dependency)", "workspace:a > ms")); + expect(stderr).toBe(""); }, }); @@ -645,13 +892,24 @@ describe("`bun audit`", () => { exitCode: 1, files: fixture("express@3"), args: ["--audit-level", "critical"], - fn: async ({ stdout, stderr }) => { - expect(await stderr).not.toContain("invalid `--audit-level` value"); - const output = await stdout; - expect(output).toContain("critical:"); - expect(output).not.toContain("moderate:"); - expect(output).not.toContain("high:"); - expect(output).not.toContain("low:"); + fn: ({ stdout, stderr }) => { + expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(` + "bun audit () + + minimist@0.0.8 + express > mkdirp > minimist + critical: Prototype Pollution in minimist (<0.2.4) - https://github.com/advisories/GHSA-xvch-5gv4-984h + + morgan@1.6.1 + express > connect > morgan + critical: Code Injection in morgan (<1.9.1) - https://github.com/advisories/GHSA-gwg9-rgvj-4h5j + + 2 vulnerabilities (2 critical) + + bun audit fix upgrade the vulnerable packages within their ranges + bun audit fix --latest also cross major versions" + `); + expect(stderr).toBe(""); }, }); @@ -659,9 +917,12 @@ describe("`bun audit`", () => { exitCode: 1, files: fixture("safe-is-number@7"), args: ["--audit-level", "invalid"], - fn: async ({ stderr }) => { - expect(await stderr).toContain("invalid `--audit-level` value"); - expect(await stderr).toContain("Valid values are: low, moderate, high, critical"); + fn: ({ stdout, stderr, requests }) => { + expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot( + `"error: invalid \`--audit-level\` value: 'invalid'. Valid values are: low, moderate, high, critical"`, + ); + expect(stdout).toBe(""); + expect(requests).toEqual([]); }, }); @@ -669,9 +930,10 @@ describe("`bun audit`", () => { exitCode: 0, files: fixture("safe-is-number@7"), args: ["--audit-level", "moderate"], - fn: async ({ stdout, stderr }) => { - expect(await stderr).toBe(""); - expect(normalizeBunSnapshot(await stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1)); + fn: ({ stdout, stderr, requests }) => { + expect(stderr).toBe(""); + expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1)); + expect(requests).toEqual([{ "is-number": ["7.0.0"] }]); }, }); @@ -679,9 +941,11 @@ describe("`bun audit`", () => { exitCode: 1, files: fixture("mix-of-safe-and-vulnerable-dependencies"), args: ["--prod"], - fn: async ({ stdout, stderr }) => { - expect(await stderr).not.toContain("error"); - expect(await stdout).toContain("vulnerabilities"); + fn: ({ stdout, stderr, requests }) => { + // Both packages are production dependencies, so --prod audits and reports the same set. + expect(requests).toEqual([{ "is-number": ["7.0.0"], ...MS_REQUEST }]); + expect(normalizeBunSnapshot(stdout)).toBe(MS_REPORT); + expect(stderr).toBe(""); }, }); @@ -689,15 +953,20 @@ describe("`bun audit`", () => { exitCode: 1, files: fixture("express@3"), args: ["--ignore", "GHSA-gwg9-rgvj-4h5j"], - fn: async ({ stdout, stderr }) => { - expect(await stderr).not.toContain("error"); - const output = await stdout; - expect(output).not.toContain("GHSA-gwg9-rgvj-4h5j"); - expect(output).toContain("vulnerabilities"); + fn: ({ stdout, stderr }) => { + const out = normalizeBunSnapshot(stdout); + // The ignored advisory was morgan's only one, so its block disappears and the critical count drops with it: + // the header, 14 of the 15 packages, the summary and the footer remain. + expect(out).not.toContain("morgan"); + expect(out).not.toContain("GHSA-gwg9-rgvj-4h5j"); + expect(out.split("\n\n")).toHaveLength(17); + expect(out).toStartWith(AUDIT_HEADER + "base64-url@1.2.1\n"); + expect(out).toEndWith("\n\n20 vulnerabilities (1 critical, 9 high, 4 moderate, 6 low)\n\n" + REPORT_FOOTER); + expect(stderr).toBe(""); }, }); - test("sends a well-formed JSON request body when a package name contains a double quote", async () => { + test.concurrent("sends a well-formed JSON request body when a package name contains a double quote", async () => { const packageName = 'a"b'; using dirHandle = tempDir("bun-test-audit-name-with-quote", { "package.json": JSON.stringify({ @@ -748,6 +1017,7 @@ describe("`bun audit`", () => { expect(JSON.parse(receivedBody)).toStrictEqual({ [packageName]: ["1.0.0"] }); expect(normalizeBunSnapshot(stdout)).toBe(AUDIT_HEADER + noVulnerabilities(1)); + expect(stderr).toBe(""); expect(exitCode).toBe(0); }); }); @@ -850,13 +1120,14 @@ describe("`bun audit` with a secret in the registry URL", () => { // answers 404, so both commands report that registry as skipped; `printed` is how it must be named. async function expectSkippedRegistry(dir: string, printed: string) { const skipped = skippedWarning(printed, "404", "@foo/bar"); + await using defaultRegistry = startFixtureRegistry(); - const report = await auditAgainst(dir, registryHref(server)); + const report = await auditAgainst(dir, registryHref(defaultRegistry)); expect(normalizeBunSnapshot(report.stderr)).toBe(skipped); expect(normalizeBunSnapshot(report.stdout)).toBe(AUDIT_HEADER + noVulnerabilities(0, "1 skipped")); expect(report.exitCode).toBe(0); - const fix = await auditAgainst(dir, registryHref(server), "fix", "--json"); + const fix = await auditAgainst(dir, registryHref(defaultRegistry), "fix", "--json"); expect(normalizeBunSnapshot(fix.stderr)).toBe(skipped); expect(JSON.parse(fix.stdout)).toStrictEqual({ dryRun: false, @@ -2577,8 +2848,9 @@ describe("`bun audit fix`", () => { test.concurrent("a manifest that fails to download is reported, not fixed", async () => { const denyManifests = new Set(); await using server = startRegistry({ "a-dep": [adv("<1.0.4")] }, { denyManifests }); - using dir = await setupVulnerableADep(server); + using dir = await setupWithCachedManifests(server, { "a-dep": "1.0.2" }, { "a-dep": "^1.0.2" }); const lockBefore = await lock(dir); + expect(lockBefore).toContain('"a-dep@1.0.2"'); denyManifests.add("a-dep"); const { stdout, stderr, exitCode } = await auditFix(dir); @@ -2598,8 +2870,11 @@ describe("`bun audit fix`", () => { test.concurrent("a manifest that fails to download does not stop the other fixes", async () => { const denyManifests = new Set(); await using server = startRegistry({ "a-dep": [adv("<1.0.4")], "no-deps": [adv("<1.0.1", 2)] }, { denyManifests }); - using dir = await setup(server, { name: "foo", dependencies: { "a-dep": "1.0.2", "no-deps": "1.0.0" } }); - await reinstall(dir, { name: "foo", dependencies: { "a-dep": "^1.0.2", "no-deps": "^1.0.0" } }); + using dir = await setupWithCachedManifests( + server, + { "a-dep": "1.0.2", "no-deps": "1.0.0" }, + { "a-dep": "^1.0.2", "no-deps": "^1.0.0" }, + ); denyManifests.add("a-dep"); const { stdout, stderr, exitCode } = await auditFix(dir); @@ -3757,7 +4032,7 @@ describe("`bun audit fix`", () => { test.concurrent("--json carries a version whose manifest could not be fetched", async () => { const denyManifests = new Set(); await using server = startRegistry({ "a-dep": [adv("<1.0.4")] }, { denyManifests }); - using dir = await setupVulnerableADep(server); + using dir = await setupWithCachedManifests(server, { "a-dep": "1.0.2" }, { "a-dep": "^1.0.2" }); const lockBefore = await lock(dir); denyManifests.add("a-dep");