UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

573 lines (514 loc) 18.7 kB
import { strict as assert } from "node:assert"; import { afterEach, describe, test } from "poku"; import { auditEnvironment } from "./envAudit.js"; const NODE_OPTIONS_ATTACK_VECTORS = [ { name: "--require flag", value: "--require ./evil.js", expectedMatch: true, }, { name: "--require with uppercase", value: "--REQUIRE ./evil.js", expectedMatch: true, }, { // -r alone is ambiguous and not matched to avoid false-positives from legitimate short opts name: "-r short flag (not matched)", value: "-r ./evil.js", expectedMatch: false, }, { name: "--eval flag", value: "--eval \"console.log('pwned')\"", expectedMatch: true, }, { name: "--eval with complex payload", value: "--eval \"require('child_process').execSync('id')\"", expectedMatch: true, }, { // -e alone is not matched to avoid false-positives name: "-e short flag (not matched)", value: "-e \"console.log('test')\"", expectedMatch: false, }, { name: "--import flag (Node 18+)", value: "--import ./malicious.mjs", expectedMatch: true, }, { name: "--loader flag", value: "--loader ./hook-loader.js", expectedMatch: true, }, { name: "--inspect flag", value: "--inspect=0.0.0.0:9229", expectedMatch: true, }, { name: "--inspect-brk flag", value: "--inspect-brk=9229", expectedMatch: true, }, { name: "--inspect with host", value: "--inspect 127.0.0.1:9229", expectedMatch: true, }, { // --test runs the built-in test runner and is not an exploit vector name: "--test flag (safe, not matched)", value: "--test", expectedMatch: false, }, { name: "safe memory flag", value: "--max-old-space-size=4096", expectedMatch: false, }, { name: "safe GC flag", value: "--expose-gc", expectedMatch: false, }, { name: "safe trace flag", value: "--trace-warnings", expectedMatch: false, }, { name: "multiple flags with one malicious", value: "--max-old-space-size=4096 --require ./evil.js", expectedMatch: true, }, { name: "empty string", value: "", expectedMatch: false, }, { name: "whitespace only", value: " ", expectedMatch: false, }, ]; const DANGEROUS_ENV_VAR_CASES = [ { name: "NODE_NO_WARNINGS set", env: { NODE_NO_WARNINGS: "1" }, expectedWarnings: 1, expectedVar: "NODE_NO_WARNINGS", }, { name: "NODE_PENDING_DEPRECATION set", env: { NODE_PENDING_DEPRECATION: "1" }, expectedWarnings: 1, expectedVar: "NODE_PENDING_DEPRECATION", }, { name: "UV_THREADPOOL_SIZE set", env: { UV_THREADPOOL_SIZE: "128" }, expectedWarnings: 1, expectedVar: "UV_THREADPOOL_SIZE", }, { name: "all dangerous vars set", env: { NODE_NO_WARNINGS: "1", NODE_PENDING_DEPRECATION: "1", UV_THREADPOOL_SIZE: "128", }, expectedWarnings: 3, expectedVar: null, }, { name: "no dangerous vars", env: { PATH: "/usr/bin", HOME: "/home/user" }, expectedWarnings: 0, expectedVar: null, }, { name: "dangerous var with empty value (falsy)", env: { NODE_NO_WARNINGS: "" }, expectedWarnings: 0, expectedVar: null, }, ]; const COMBINED_ATTACK_CASES = [ { name: "NODE_OPTIONS attack + dangerous vars", env: { NODE_OPTIONS: "--require ./evil.js", NODE_NO_WARNINGS: "1", UV_THREADPOOL_SIZE: "128", }, minWarnings: 3, }, { name: "multiple NODE_OPTIONS patterns", env: { NODE_OPTIONS: '--require ./a.js --eval "code" --inspect', }, minWarnings: 3, }, { name: "clean environment", env: {}, minWarnings: 0, }, ]; describe("auditEnvironment - NODE_OPTIONS Detection", () => { for (const tc of NODE_OPTIONS_ATTACK_VECTORS) { test(`should detect ${tc.name}`, () => { const env = { NODE_OPTIONS: tc.value }; const warnings = auditEnvironment(env); const hasSuspiciousWarning = warnings.some((w) => w.message.includes("NODE_OPTIONS contains a code-execution flag"), ); if (tc.expectedMatch) { assert.ok( hasSuspiciousWarning, `Expected warning for ${tc.name} but got: ${warnings.map((w) => `${w.variable}: ${w.message}`).join(", ")}`, ); } else { assert.ok( !hasSuspiciousWarning, `Unexpected warning for ${tc.name}: ${warnings.map((w) => `${w.variable}: ${w.message}`).join(", ")}`, ); } }); } }); describe("auditEnvironment - Dangerous Env Vars", () => { for (const tc of DANGEROUS_ENV_VAR_CASES) { test(`should handle ${tc.name}`, () => { const warnings = auditEnvironment(tc.env); assert.strictEqual( warnings.length, tc.expectedWarnings, `Expected ${tc.expectedWarnings} warnings, got ${warnings.length}: ${warnings.map((w) => `${w.variable}: ${w.message}`).join(", ")}`, ); if (tc.expectedVar) { assert.ok( warnings.some((w) => w.message.includes(tc.expectedVar)), `Expected warning about ${tc.expectedVar} but got: ${warnings.map((w) => `${w.variable}: ${w.message}`).join(", ")}`, ); } }); } }); describe("auditEnvironment - Combined Attacks", () => { for (const tc of COMBINED_ATTACK_CASES) { test(`should handle ${tc.name}`, () => { const warnings = auditEnvironment(tc.env); assert.ok( warnings.length >= tc.minWarnings, `Expected at least ${tc.minWarnings} warnings, got ${warnings.length}: ${warnings.map((w) => `${w.variable}: ${w.message}`).join(", ")}`, ); }); } }); describe("auditEnvironment - Edge Cases", () => { test("should handle undefined NODE_OPTIONS", () => { const warnings = auditEnvironment({}); const hasSuspiciousWarning = warnings.some((w) => w.message.includes("NODE_OPTIONS contains a code-execution flag"), ); assert.ok(!hasSuspiciousWarning); }); test("should handle null env (uses process.env)", () => { const warnings = auditEnvironment(); assert.ok(Array.isArray(warnings)); }); test("should return empty array for completely clean env", () => { const warnings = auditEnvironment({ PATH: "/usr/bin", HOME: "/home/user", LANG: "en_US.UTF-8", }); assert.deepStrictEqual(warnings, []); }); test("should detect all dangerous vars individually", () => { const warnings1 = auditEnvironment({ NODE_NO_WARNINGS: "1" }); const warnings2 = auditEnvironment({ NODE_PENDING_DEPRECATION: "1" }); const warnings3 = auditEnvironment({ UV_THREADPOOL_SIZE: "128" }); assert.strictEqual(warnings1.length, 1); assert.strictEqual(warnings2.length, 1); assert.strictEqual(warnings3.length, 1); assert.ok(warnings1[0].message.includes("NODE_NO_WARNINGS")); assert.ok(warnings2[0].message.includes("NODE_PENDING_DEPRECATION")); assert.ok(warnings3[0].message.includes("UV_THREADPOOL_SIZE")); }); test("should be case-sensitive for env var names", () => { const warnings = auditEnvironment({ node_no_warnings: "1", Node_Options: "--require ./evil.js", }); assert.strictEqual(warnings.length, 0); }); }); describe("auditEnvironment - Warning Message Format", () => { test("dangerous var warning should mention unsetting", () => { const warnings = auditEnvironment({ NODE_NO_WARNINGS: "1" }); assert.ok(warnings[0].mitigation.includes("Unset")); assert.ok(warnings[0].mitigation.includes("NODE_NO_WARNINGS")); }); test("NODE_OPTIONS warning should mention the pattern", () => { const warnings = auditEnvironment({ NODE_OPTIONS: "--require ./evil.js" }); assert.ok(warnings[0].message.includes("NODE_OPTIONS")); }); test("warnings should be human-readable strings", () => { const warnings = auditEnvironment({ NODE_OPTIONS: "--eval test", NODE_NO_WARNINGS: "1", }); for (const w of warnings) { assert.strictEqual(typeof w.message, "string"); assert.ok(w.message.length > 0); } }); }); describe("auditEnvironment - NODE_TLS_REJECT_UNAUTHORIZED", () => { test("should flag when set to '0' (TLS disabled)", () => { const warnings = auditEnvironment({ NODE_TLS_REJECT_UNAUTHORIZED: "0" }); assert.strictEqual(warnings.length, 1); assert.strictEqual(warnings[0].severity, "high"); assert.ok( warnings[0].message.includes("TLS certificate verification is disabled"), ); }); test("should not flag when set to '1' (TLS enabled)", () => { const warnings = auditEnvironment({ NODE_TLS_REJECT_UNAUTHORIZED: "1" }); assert.strictEqual(warnings.length, 0); }); test("should not flag when unset", () => { const warnings = auditEnvironment({}); const hasTlsWarning = warnings.some( (w) => w.variable === "NODE_TLS_REJECT_UNAUTHORIZED", ); assert.ok(!hasTlsWarning); }); }); describe("auditEnvironment - JVM Code Execution", () => { test("should flag -javaagent in JAVA_TOOL_OPTIONS", () => { const warnings = auditEnvironment({ JAVA_TOOL_OPTIONS: "-javaagent:/evil/agent.jar", }); assert.ok(warnings.some((w) => w.variable === "JAVA_TOOL_OPTIONS")); assert.ok(warnings.some((w) => w.type === "code-execution")); }); test("should flag -javaagent in JDK_JAVA_OPTIONS", () => { const warnings = auditEnvironment({ JDK_JAVA_OPTIONS: "-javaagent:/evil/agent.jar", }); assert.ok(warnings.some((w) => w.variable === "JDK_JAVA_OPTIONS")); assert.ok(warnings.some((w) => w.type === "code-execution")); }); test("should flag --add-opens in JAVA_TOOL_OPTIONS", () => { const warnings = auditEnvironment({ JAVA_TOOL_OPTIONS: "--add-opens java.base/java.lang=ALL-UNNAMED", }); assert.ok(warnings.some((w) => w.type === "code-execution")); }); test("should not flag safe JVM options", () => { const warnings = auditEnvironment({ JAVA_TOOL_OPTIONS: "-Xmx4g -Xms512m", }); assert.ok(!warnings.some((w) => w.variable === "JAVA_TOOL_OPTIONS")); }); test("should not flag empty JAVA_TOOL_OPTIONS", () => { const warnings = auditEnvironment({ JAVA_TOOL_OPTIONS: "" }); assert.ok(!warnings.some((w) => w.variable === "JAVA_TOOL_OPTIONS")); }); }); describe("auditEnvironment - Proxy Interception", () => { test("should flag HTTP_PROXY when set", () => { const warnings = auditEnvironment({ HTTP_PROXY: "http://proxy:3128" }); assert.ok(warnings.some((w) => w.type === "network-interception")); assert.ok(warnings.some((w) => w.variable === "HTTP_PROXY")); }); test("should flag https_proxy (lowercase) when set", () => { const warnings = auditEnvironment({ https_proxy: "http://proxy:3128" }); assert.ok(warnings.some((w) => w.type === "network-interception")); }); test("should deduplicate network-interception findings when multiple proxy vars are set", () => { const warnings = auditEnvironment({ HTTP_PROXY: "http://proxy:3128", HTTPS_PROXY: "http://proxy:3128", }); assert.strictEqual( warnings.filter((w) => w.type === "network-interception").length, 1, ); }); test("should not flag proxy vars when unset", () => { const warnings = auditEnvironment({ PATH: "/usr/bin" }); assert.ok(!warnings.some((w) => w.type === "network-interception")); }); }); describe("auditEnvironment - Credential Exposure", () => { test("should flag GITHUB_TOKEN (matches _TOKEN suffix pattern)", () => { const warnings = auditEnvironment({ GITHUB_TOKEN: "ghp_test1234" }); assert.ok(warnings.some((w) => w.variable === "GITHUB_TOKEN")); assert.ok(warnings.some((w) => w.type === "credential-exposure")); assert.ok( warnings.find((w) => w.variable === "GITHUB_TOKEN")?.severity === "low", ); }); test("should flag NPM_TOKEN (matches _TOKEN suffix pattern)", () => { const warnings = auditEnvironment({ NPM_TOKEN: "npm_secret" }); assert.ok(warnings.some((w) => w.variable === "NPM_TOKEN")); assert.ok(warnings.some((w) => w.type === "credential-exposure")); }); test("should flag vars matching _KEY, _SECRET, _PASS, _PASSWORD patterns", () => { const envs = { MY_API_KEY: "key123", DEPLOY_SECRET: "shhh", DB_PASS: "hunter2", APP_PASSWORD: "p@ssw0rd", }; const warnings = auditEnvironment(envs); assert.ok(warnings.some((w) => w.variable === "MY_API_KEY")); assert.ok(warnings.some((w) => w.variable === "DEPLOY_SECRET")); assert.ok(warnings.some((w) => w.variable === "DB_PASS")); assert.ok(warnings.some((w) => w.variable === "APP_PASSWORD")); }); test("should flag vars matching _CREDENTIAL and _CREDENTIALS patterns", () => { const warnings = auditEnvironment({ SVC_CREDENTIAL: "cred1", CLOUD_CREDENTIALS: "cred2", }); assert.ok(warnings.some((w) => w.variable === "SVC_CREDENTIAL")); assert.ok(warnings.some((w) => w.variable === "CLOUD_CREDENTIALS")); }); test("should NOT flag common system vars with credential-like substrings mid-name", () => { // SSH_AUTH_SOCK contains _AUTH but does NOT end with _AUTH → should not match // __CF_USER_TEXT_ENCODING contains _USER but does NOT end with _USER → should not match const warnings = auditEnvironment({ SSH_AUTH_SOCK: "/tmp/ssh-agent", __CF_USER_TEXT_ENCODING: "0x1F4:0x8000100", }); assert.ok(!warnings.some((w) => w.variable === "SSH_AUTH_SOCK")); assert.ok(!warnings.some((w) => w.variable === "__CF_USER_TEXT_ENCODING")); }); test("should not flag vars that do not match a credential pattern", () => { const warnings = auditEnvironment({ PATH: "/usr/bin", HOME: "/home/user" }); assert.ok(!warnings.some((w) => w.type === "credential-exposure")); }); test("should not flag credential-named vars with empty value", () => { const warnings = auditEnvironment({ GITHUB_TOKEN: "" }); assert.ok(!warnings.some((w) => w.variable === "GITHUB_TOKEN")); }); }); describe("auditEnvironment - Debug Mode Exposure", () => { test("should flag CDXGEN_DEBUG_MODE=verbose", () => { const warnings = auditEnvironment({ CDXGEN_DEBUG_MODE: "verbose" }); assert.ok(warnings.some((w) => w.type === "debug-exposure")); assert.strictEqual( warnings.find((w) => w.type === "debug-exposure")?.severity, "low", ); }); test("should flag CDXGEN_DEBUG_MODE=debug", () => { const warnings = auditEnvironment({ CDXGEN_DEBUG_MODE: "debug" }); assert.ok(warnings.some((w) => w.type === "debug-exposure")); }); test("should flag SCAN_DEBUG_MODE=debug", () => { const warnings = auditEnvironment({ SCAN_DEBUG_MODE: "debug" }); assert.ok(warnings.some((w) => w.type === "debug-exposure")); }); test("should not flag when CDXGEN_DEBUG_MODE is not set", () => { const warnings = auditEnvironment({ PATH: "/usr/bin" }); assert.ok(!warnings.some((w) => w.type === "debug-exposure")); }); test("should not flag when CDXGEN_DEBUG_MODE is an unrecognised value", () => { const warnings = auditEnvironment({ CDXGEN_DEBUG_MODE: "info" }); assert.ok(!warnings.some((w) => w.type === "debug-exposure")); }); }); describe("auditEnvironment - Deno Certificate", () => { test("should flag DENO_CERT when set to a non-empty value", () => { const warnings = auditEnvironment({ DENO_CERT: "/etc/ssl/private/corp-ca.pem", }); assert.ok(warnings.some((w) => w.variable === "DENO_CERT")); assert.ok(warnings.some((w) => w.type === "environment-variable")); assert.strictEqual( warnings.find((w) => w.variable === "DENO_CERT")?.severity, "high", ); }); test("should not flag DENO_CERT when unset", () => { const warnings = auditEnvironment({ PATH: "/usr/bin" }); assert.ok(!warnings.some((w) => w.variable === "DENO_CERT")); }); test("should not flag DENO_CERT when set to empty string", () => { const warnings = auditEnvironment({ DENO_CERT: "" }); assert.ok(!warnings.some((w) => w.variable === "DENO_CERT")); }); }); describe("auditEnvironment - Deno Permissions", () => { // Save original so we restore correctly even if Deno were already defined. const originalDeno = globalThis.Deno; // Helper to build a minimal Deno mock where only the listed commands have run permission. const createDenoMock = (os, allowedCommands) => ({ build: { os }, permissions: { querySync: (desc) => ({ state: desc.name === "run" && allowedCommands.includes(desc.command) ? "granted" : "denied", }), }, }); // afterEach restores globalThis.Deno after each test so mocks cannot leak. afterEach(() => { if (originalDeno === undefined) { delete globalThis.Deno; } else { globalThis.Deno = originalDeno; } }); test("should flag permission-misuse when Deno shell execution is broadly granted (Unix)", () => { globalThis.Deno = createDenoMock("linux", ["sh", "bash"]); const warnings = auditEnvironment({}); assert.ok(warnings.some((w) => w.variable === "DENO_PERMISSIONS")); assert.ok(warnings.some((w) => w.type === "permission-misuse")); assert.strictEqual( warnings.find((w) => w.variable === "DENO_PERMISSIONS")?.severity, "high", ); }); test("should flag permission-misuse when Deno shell execution is broadly granted (Windows)", () => { globalThis.Deno = createDenoMock("windows", ["cmd", "powershell"]); const warnings = auditEnvironment({}); assert.ok(warnings.some((w) => w.variable === "DENO_PERMISSIONS")); }); test("should NOT flag permission-misuse when Deno restricts shell execution", () => { // Only npm and node are allowed; sh/bash are not — no false positive expected. globalThis.Deno = createDenoMock("linux", ["npm", "node"]); const warnings = auditEnvironment({}); assert.ok(!warnings.some((w) => w.variable === "DENO_PERMISSIONS")); }); test("should silently skip Deno permission check when querySync throws", () => { globalThis.Deno = { build: { os: "linux" }, permissions: { querySync: () => { throw new Error("querySync not available"); }, }, }; assert.doesNotThrow(() => auditEnvironment({})); }); test("should not flag when globalThis.Deno is undefined (Node.js environment)", () => { // globalThis.Deno is already undefined in the test runtime (Node.js) const warnings = auditEnvironment({}); assert.ok(!warnings.some((w) => w.variable === "DENO_PERMISSIONS")); }); });