Files
bun.sh/test/cli/install/bun-pm-scan.test.ts
Alistair Smith 3ee477fc5b fix: scanner on update, install, remove, uninstall and add, and introduce the pm scan command (#22193)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-09-09 21:42:01 -07:00

677 lines
21 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles, tmpdirSync } from "harness";
import { join } from "path";
describe("bun pm scan", () => {
describe("configuration", () => {
test("shows error when no security scanner configured", async () => {
const dir = tempDirWithFiles("scan-no-config", {
"package.json": JSON.stringify({ name: "test", dependencies: { "left-pad": "^1.0.0" } }),
"bun.lockb": "",
});
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("error: no security scanner configured");
});
test("shows error when lockfile doesn't exist", async () => {
const dir = tempDirWithFiles("scan-no-lockfile", {
"package.json": JSON.stringify({ name: "test", dependencies: {} }),
"bunfig.toml": `[install.security]\nscanner = "test-scanner"`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Lockfile not found");
expect(stderr).toContain("Run 'bun install' first");
});
test("shows error when package.json doesn't exist", async () => {
const dir = tmpdirSync();
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("No package.json was found");
});
});
describe("scanner execution", () => {
test("scanner receives correct package format", async () => {
const dir = tempDirWithFiles("scan-package-format", {
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
express: "^4.0.0",
},
}),
"bunfig.toml": `[install.security]\nscanner = "./scanner.js"`,
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
// Log the packages we receive
console.error("PACKAGES:", JSON.stringify(payload.packages));
// Verify format
if (!Array.isArray(payload.packages)) {
throw new Error("packages should be an array");
}
for (const pkg of payload.packages) {
if (!pkg.name || !pkg.version || !pkg.requestedRange || !pkg.tarball) {
throw new Error("Invalid package format");
}
}
return [];
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("PACKAGES:");
expect(exitCode).toBe(0);
expect(stdout).toContain("No advisories found");
});
test("scanner version validation", async () => {
const dir = tempDirWithFiles("scan-version-check", {
"package.json": JSON.stringify({ name: "test", dependencies: { "left-pad": "^1.0.0" } }),
"scanner.js": `
module.exports = {
scanner: {
version: "2", // Wrong version
scan: async () => []
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
// Add config after install
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Security scanner must be version 1");
});
});
describe("vulnerability detection", () => {
test("detects fatal vulnerabilities", async () => {
const dir = tempDirWithFiles("scan-fatal", {
"package.json": JSON.stringify({
name: "test-app",
dependencies: { lodash: "^4.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
return [{
package: "lodash",
level: "fatal",
description: "Prototype pollution vulnerability",
url: "https://example.com/CVE-2024-1234"
}];
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stdout).toContain("FATAL: lodash");
expect(stdout).toContain("Prototype pollution vulnerability");
expect(stdout).toContain("https://example.com/CVE-2024-1234");
expect(stdout).toMatch(/1 advisory \(.*1 fatal.*\)/);
});
test("detects warning vulnerabilities", async () => {
const dir = tempDirWithFiles("scan-warn", {
"package.json": JSON.stringify({
name: "test-app",
dependencies: { axios: "^0.21.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
return [{
package: "axios",
level: "warn",
description: "Inefficient regular expression",
url: "https://example.com/advisory/123"
}];
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1); // Still exits with 1 for warnings
expect(stdout).toContain("WARNING: axios");
expect(stdout).toContain("Inefficient regular expression");
expect(stdout).toMatch(/1 advisory \(.*1 warning.*\)/);
});
test("handles mixed vulnerabilities", async () => {
const dir = tempDirWithFiles("scan-mixed", {
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
lodash: "^4.0.0",
axios: "^0.21.0",
express: "^4.0.0",
},
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
const results = [];
for (const pkg of payload.packages) {
if (pkg.name === "lodash") {
results.push({
package: "lodash",
level: "fatal",
description: "Critical vulnerability"
});
}
if (pkg.name === "axios") {
results.push({
package: "axios",
level: "warn",
description: "Minor issue"
});
}
if (pkg.name === "express") {
results.push({
package: "express",
level: "warn",
description: "Another minor issue"
});
}
}
return results;
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stdout).toContain("FATAL: lodash");
expect(stdout).toContain("WARNING: axios");
expect(stdout).toContain("WARNING: express");
expect(stdout).toMatch(/3 advisories \(.*1 fatal.*2 warnings.*\)/);
});
test("no vulnerabilities found", async () => {
const dir = tempDirWithFiles("scan-clean", {
"package.json": JSON.stringify({
name: "test-app",
dependencies: { lodash: "^4.0.0" },
}),
"bunfig.toml": `[install.security]\nscanner = "./scanner.js"`,
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async () => []
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("No advisories found");
});
});
describe("dependency paths", () => {
test("shows correct path for direct dependencies", async () => {
const dir = tempDirWithFiles("scan-direct-dep", {
"package.json": JSON.stringify({
name: "my-app",
dependencies: { express: "^4.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
const results = [];
for (const pkg of payload.packages) {
if (pkg.name === "express") {
results.push({
package: "express",
level: "fatal",
description: "Test vulnerability"
});
}
}
return results;
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("FATAL: express");
expect(stdout).toContain("via my-app express");
});
test("shows correct path for transitive dependencies", async () => {
const dir = tempDirWithFiles("scan-transitive-dep", {
"package.json": JSON.stringify({
name: "my-app",
dependencies: { express: "^4.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
const results = [];
for (const pkg of payload.packages) {
// body-parser is a dependency of express
if (pkg.name === "body-parser") {
results.push({
package: "body-parser",
level: "warn",
description: "Transitive vulnerability"
});
}
}
return results;
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// body-parser might not actually be a dependency of express
// So we check if we found it in the scan
if (stdout.includes("WARNING: body-parser")) {
expect(stdout).toContain("via my-app express body-parser");
} else {
// If body-parser wasn't found, the test passes since we can't verify transitive deps
expect(exitCode).toBeDefined();
}
});
});
describe("error handling", () => {
test("handles scanner crash", async () => {
const dir = tempDirWithFiles("scan-crash", {
"package.json": JSON.stringify({
name: "test",
dependencies: { "left-pad": "^1.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function() {
process.exit(42); // Crash
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Security scanner exited with code 42");
});
test("handles invalid JSON from scanner", async () => {
const dir = tempDirWithFiles("scan-bad-json", {
"package.json": JSON.stringify({
name: "test",
dependencies: { "left-pad": "^1.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function() {
// Return something that's not an array
return { not: "an array" };
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Security scanner must return an array");
});
test("handles missing required fields in advisory", async () => {
const dir = tempDirWithFiles("scan-missing-fields", {
"package.json": JSON.stringify({
name: "test",
dependencies: { lodash: "^4.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function() {
return [{
package: "lodash"
// Missing 'level' field
}];
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("missing required 'level' field");
});
});
describe("output formatting", () => {
test("singular vs plural in summary", async () => {
const dir = tempDirWithFiles("scan-singular", {
"package.json": JSON.stringify({
name: "test",
dependencies: { "left-pad": "^1.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function(payload) {
const results = [];
for (const pkg of payload.packages) {
if (pkg.name === "left-pad") {
results.push({
package: "left-pad",
level: "fatal",
description: "Test"
});
}
}
return results;
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const stdout = await proc.stdout.text();
// Should say "1 advisory" not "1 advisories"
expect(stdout).toContain("1 advisory (");
expect(stdout).not.toContain("1 advisories");
});
test("shows timing for slow scans", async () => {
const dir = tempDirWithFiles("scan-slow", {
"package.json": JSON.stringify({
name: "test",
dependencies: { "left-pad": "^1.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function() {
// Simulate slow scan
await new Promise(resolve => setTimeout(resolve, 1200));
return [];
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "0" }, // Enable timing output
});
const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
// Should show timing information for scans > 1 second
expect(stderr).toMatch(/Scanning \d+ package[s]? took \d+ms/);
});
});
describe("differences from bun add/install", () => {
test("does not show 'installation aborted' message", async () => {
const dir = tempDirWithFiles("scan-no-abort-msg", {
"package.json": JSON.stringify({
name: "test",
dependencies: { lodash: "^4.0.0" },
}),
"scanner.js": `
module.exports = {
scanner: {
version: "1",
scan: async function() {
return [{
package: "lodash",
level: "fatal",
description: "Critical"
}];
}
}
};
`,
});
await Bun.$`${bunExe()} install`.cwd(dir).env(bunEnv).quiet();
await Bun.write(join(dir, "bunfig.toml"), `[install.security]\nscanner = "./scanner.js"`);
const proc = Bun.spawn({
cmd: [bunExe(), "pm", "scan"],
cwd: dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
// Should NOT contain the installation aborted message
expect(stdout).not.toContain("installation aborted");
expect(stdout).not.toContain("Installation aborted");
expect(stderr).not.toContain("installation aborted");
expect(stderr).not.toContain("Installation aborted");
});
});
});