680 lines
21 KiB
TypeScript
680 lines
21 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||||
|
|
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness";
|
|||
|
|
import { join } from "path";
|
|||
|
|
|
|||
|
|
describe("bun pm scan", () => {
|
|||
|
|
describe("configuration", () => {
|
|||
|
|
test("shows error when no security scanner configured", async () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(`
|
|||
|
|
"bun pm scan <version> (<revision>)
|
|||
|
|
error: missing lockfile, nothing to scan
|
|||
|
|
note: run 'bun install' first"
|
|||
|
|
`);
|
|||
|
|
expect(exitCode).toBe(1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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 () => {
|
|||
|
|
await using dir = tempDir("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");
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|