@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
529 lines (509 loc) • 15.6 kB
JavaScript
import { strict as assert } from "node:assert";
import { describe, test } from "poku";
import { parseNpmrc, parseNpmrcFromEnv } from "./npmrc.js";
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: Test data
const VALID_NPMRC_CASES = [
{
name: "basic key=value",
input: "registry = https://registry.npmjs.org/",
expected: { registry: "https://registry.npmjs.org/" },
},
{
name: "key=value without spaces",
input: "cache=/tmp/npm-cache",
expected: { cache: "/tmp/npm-cache" },
},
{
name: "value containing equals sign",
input: "init-author-name=John=Doe",
expected: { "init-author-name": "John=Doe" },
},
{
name: "hash comment",
input: "# this is a comment\nregistry=https://example.com",
expected: { registry: "https://example.com" },
},
{
name: "semicolon comment",
input: "; another comment\nproxy=http://proxy.local",
expected: { proxy: "http://proxy.local" },
},
{
name: "inline comment (treated as value)",
input: "registry=https://example.com # comment",
expected: { registry: "https://example.com # comment" },
},
{
name: "double-quoted value",
input: 'description = "A package with spaces"',
expected: { description: "A package with spaces" },
},
{
name: "single-quoted value",
input: "description = 'Single quoted'",
expected: { description: "Single quoted" },
},
{
name: "quoted value with inner quotes",
input: 'note = "He said \\"hello\\""',
expected: { note: 'He said \\"hello\\"' },
},
{
name: "array values with []",
input: "proxy[] = http://proxy1.local\nproxy[] = http://proxy2.local",
expected: { proxy: ["http://proxy1.local", "http://proxy2.local"] },
},
{
name: "single array value",
input: "registry[] = https://registry.example.com",
expected: { registry: ["https://registry.example.com"] },
},
{
name: "scoped registry",
input: "@myscope:registry = https://custom.example.com",
expected: { "@myscope:registry": "https://custom.example.com" },
},
{
name: "URI-fragment auth config",
input: "//registry.npmjs.org/:_authToken = abc123xyz",
expected: { "//registry.npmjs.org/:_authToken": "abc123xyz" },
},
{
name: "scoped auth with quoted token",
input: '//registry.example.com/:_authToken = "secret-token"',
expected: { "//registry.example.com/:_authToken": "secret-token" },
},
{
name: "extra whitespace around =",
input: " key = value ",
expected: { key: "value" },
},
{
name: "empty lines and mixed whitespace",
input: "\n\nregistry=https://example.com\n\n \nproxy=http://local\n",
expected: {
registry: "https://example.com",
proxy: "http://local",
},
},
{
name: "env var substitution syntax",
input: "cache = ${HOME}/.npm-cache",
expected: { cache: "${HOME}/.npm-cache" },
},
{
name: "env var with default",
input: 'prefix = "${NPM_PREFIX:-/usr/local}"',
expected: { prefix: "${NPM_PREFIX:-/usr/local}" },
},
{
name: "unicode in value",
input: "description = 日本語パッケージ",
expected: { description: "日本語パッケージ" },
},
{
name: "emoji in value",
input: 'note = "Test 🚀 emoji"',
expected: { note: "Test 🚀 emoji" },
},
{
name: "unicode key (unusual but valid)",
input: "キー = 値",
expected: { キー: "値" },
},
{
name: "mixed unicode and ascii",
input: "registry = https://例え.jp/npm",
expected: { registry: "https://例え.jp/npm" },
},
{
name: "path with special chars",
input: "prefix = /usr/local/bin:$HOME/bin",
expected: { prefix: "/usr/local/bin:$HOME/bin" },
},
{
name: "url with query params",
input: "registry = https://example.com/npm?token=abc&scope=private",
expected: { registry: "https://example.com/npm?token=abc&scope=private" },
},
];
const MALICIOUS_NPMRC_CASES = [
{
name: "command injection via git config",
input: "git = ./pwn.sh\nregistry=https://registry.npmjs.org/",
expected: { git: "./pwn.sh", registry: "https://registry.npmjs.org/" },
note: "Parser returns raw value; filtering happens elsewhere",
},
{
name: "script-shell injection",
input: "script-shell = /bin/bash -c 'malicious'",
expected: { "script-shell": "/bin/bash -c 'malicious'" },
},
{
name: "path traversal in value",
input: "cache = ../../../etc/passwd",
expected: { cache: "../../../etc/passwd" },
},
{
name: "null byte injection attempt",
input: "key = value\u0000injection",
expected: { key: "value\u0000injection" },
},
{
name: "newline injection in value",
input: "key = value\ninjected = true",
expected: { key: "value", injected: "true" },
},
{
name: "very long value (potential DoS)",
input: `longkey = ${"a".repeat(100000)}`,
expected: { longkey: "a".repeat(100000) },
},
{
name: "many repeated keys",
input: Array(1000).fill("duplicate = value").join("\n"),
expected: { duplicate: "value" },
},
{
name: "proxy with credentials",
input: "proxy = http://user:pass@evil.com:8080",
expected: { proxy: "http://user:pass@evil.com:8080" },
},
{
name: "cafile pointing to malicious cert",
input: "cafile = /tmp/evil-cert.pem",
expected: { cafile: "/tmp/evil-cert.pem" },
},
{
name: "node-options with code execution flags",
input: 'node-options = "--eval "require("child_process").execSync("id")""',
expected: {
"node-options": '--eval "require("child_process").execSync("id")"',
},
},
];
const EDGE_CASE_NPMRC = [
{
name: "empty input",
input: "",
expected: {},
},
{
name: "only comments",
input: "# comment\n; another\n \n",
expected: {},
},
{
name: "line without equals sign",
input: "invalid-line\nregistry=https://example.com",
expected: { registry: "https://example.com" },
},
{
name: "key without value",
input: "emptykey =\nvalid = value",
expected: { emptykey: "", valid: "value" },
},
{
name: "value without key (should skip)",
input: "=novalue\nregistry=https://example.com",
expected: { registry: "https://example.com" },
},
{
name: "multiple equals in line",
input: "a=b=c=d",
expected: { a: "b=c=d" },
},
{
name: "tabs as whitespace",
input: "key\t=\tvalue",
expected: { key: "value" },
},
{
name: "mixed line endings",
input: "win=1\r\nunix=2\rmac=3",
expected: { win: "1", unix: "2", mac: "3" },
},
{
name: "unmatched quotes (treated as literal)",
input: 'broken = "unclosed quote',
expected: { broken: '"unclosed quote' },
},
{
name: "array with mixed quoted/unquoted",
input: 'items[] = "quoted"\nitems[] = unquoted',
expected: { items: ["quoted", "unquoted"] },
},
];
const REDOS_RESILIENCE_TESTS = [
{
name: "very long key name",
input: `${"a".repeat(50000)} = value`,
},
{
name: "many array entries",
input: Array(10000).fill("list[] = item").join("\n"),
},
{
name: "repeated = characters",
input: `key = ${"=".repeat(50000)}`,
},
{
name: "deeply nested looking scoped key",
input: `${"/".repeat(1000)}registry.example.com${"/".repeat(1000)}:token = abc`,
},
{
name: "alternating comment/value lines",
input: Array(5000).fill("# comment\nkey=value").join("\n"),
},
];
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: Test data
describe("npmrc Parser - Valid Cases", () => {
for (const tc of VALID_NPMRC_CASES) {
test(`should parse: ${tc.name}`, () => {
const result = parseNpmrc(tc.input);
assert.deepStrictEqual(result, tc.expected, `Failed for: ${tc.name}`);
});
}
});
describe("npmrc Parser - Malicious Inputs", () => {
for (const tc of MALICIOUS_NPMRC_CASES) {
test(`should safely parse (no crash): ${tc.name}`, () => {
let result;
assert.doesNotThrow(() => {
result = parseNpmrc(tc.input);
}, `Parser threw on: ${tc.name}`);
assert.deepStrictEqual(
result,
tc.expected,
`Output mismatch for: ${tc.name}`,
);
});
}
});
describe("npmrc Parser - Edge Cases", () => {
for (const tc of EDGE_CASE_NPMRC) {
test(`should handle: ${tc.name}`, () => {
const result = parseNpmrc(tc.input);
assert.deepStrictEqual(result, tc.expected, `Failed for: ${tc.name}`);
});
}
});
describe("npmrc Parser - ReDoS Resilience", () => {
for (const tc of REDOS_RESILIENCE_TESTS) {
test(`should handle quickly: ${tc.name}`, () => {
const start = Date.now();
let result;
assert.doesNotThrow(() => {
result = parseNpmrc(tc.input);
});
const duration = Date.now() - start;
assert.ok(
duration < 100,
`Parsing took too long (${duration}ms): ${tc.name}`,
);
assert.ok(
typeof result === "object" && result !== null,
`Should return object for: ${tc.name}`,
);
});
}
});
describe("npmrc Parser - Unicode Handling", () => {
test("should preserve unicode characters", () => {
const input = "desc = 测试🔐\nregistry = https://例え.日本/";
const result = parseNpmrc(input);
assert.strictEqual(result.desc, "测试🔐");
assert.strictEqual(result.registry, "https://例え.日本/");
});
test("should handle unicode in keys", () => {
const input = "キー🔑 = 値🔐";
const result = parseNpmrc(input);
assert.strictEqual(result["キー🔑"], "値🔐");
});
});
describe("npmrc Parser - Security Separation", () => {
test("parser does not filter - that's caller's responsibility", () => {
const malicious = "git = ./pwn.sh\nregistry = https://safe.com";
const result = parseNpmrc(malicious);
assert.strictEqual(result.git, "./pwn.sh");
assert.strictEqual(result.registry, "https://safe.com");
const DANGEROUS = new Set(["git", "script-shell"]);
const safe = Object.fromEntries(
Object.entries(result).filter(([key]) => !DANGEROUS.has(key)),
);
assert.strictEqual(safe.git, undefined);
assert.strictEqual(safe.registry, "https://safe.com");
});
});
const VALID_ENV_CASES = [
{
name: "basic npm_config_ prefix",
env: { npm_config_registry: "https://example.com" },
expected: { registry: "https://example.com" },
},
{
name: "case-insensitive prefix",
env: { NPM_CONFIG_PROXY: "http://proxy.local" },
expected: { proxy: "http://proxy.local" },
},
{
name: "dash-to-underscore conversion (user provides underscore)",
env: { npm_config_allow_same_version: "true" },
expected: { allow_same_version: "true" },
},
{
name: "scoped registry auth preserves URL case",
env: { "npm_config_//registry.example.com/:_authToken": "secret123" },
expected: { "//registry.example.com/:_authToken": "secret123" },
},
{
name: "scoped package registry preserves scope case",
env: { "NPM_CONFIG_@MyScope:registry": "https://custom.example.com" },
expected: { "@MyScope:registry": "https://custom.example.com" },
},
{
name: "simple keys are lowercased regardless of env var case",
env: { NPM_CONFIG_REGISTRY: "https://example.com" },
expected: { registry: "https://example.com" },
},
{
name: "mixed: simple + scoped keys",
env: {
NPM_CONFIG_REGISTRY: "https://public.com",
"npm_config_//private.example.com/:_authToken": "token123",
},
expected: {
registry: "https://public.com",
"//private.example.com/:_authToken": "token123",
},
},
{
name: "boolean flag with empty value → true",
env: { npm_config_foo: "" },
expected: { foo: "true" },
},
{
name: "boolean flag with undefined value → true",
env: { npm_config_bar: undefined },
expected: { bar: "true" },
},
{
name: "multiple config vars",
env: {
npm_config_registry: "https://a.com",
npm_config_proxy: "http://b.com",
npm_config_cache: "/tmp/cache",
},
expected: {
registry: "https://a.com",
proxy: "http://b.com",
cache: "/tmp/cache",
},
},
{
name: "unicode values preserved",
env: { npm_config_description: "测试🔐" },
expected: { description: "测试🔐" },
},
{
name: "basic pnpm_config_ prefix",
env: { pnpm_config_registry: "https://pnpm-registry.example.com" },
expected: { registry: "https://pnpm-registry.example.com" },
},
{
name: "case-insensitive PNPM_CONFIG_ prefix",
env: { PNPM_CONFIG_PROXY: "http://proxy.local" },
expected: { proxy: "http://proxy.local" },
},
{
name: "pnpm_config_ simple key lowercased",
env: { PNPM_CONFIG_STORE_DIR: "/custom/store" },
expected: { store_dir: "/custom/store" },
},
{
name: "pnpm_config_ boolean flag with empty value → true",
env: { pnpm_config_shamefully_hoist: "" },
expected: { shamefully_hoist: "true" },
},
{
name: "pnpm_config_ overrides npm_config_ for same key",
env: {
npm_config_registry: "https://npm-registry.com",
pnpm_config_registry: "https://pnpm-registry.com",
},
expected: { registry: "https://pnpm-registry.com" },
},
{
name: "pnpm_config_ and npm_config_ for different keys are both included",
env: {
npm_config_cache: "/npm-cache",
pnpm_config_store_dir: "/pnpm-store",
},
expected: { cache: "/npm-cache", store_dir: "/pnpm-store" },
},
{
name: "empty config key after pnpm_config_ prefix ignored",
env: { pnpm_config_: "value" },
expected: {},
},
];
const EDGE_ENV_CASES = [
{
name: "empty env object",
env: {},
expected: {},
},
{
name: "non-npm env vars ignored",
env: { PATH: "/usr/bin", HOME: "/home/user", npm_config_foo: "bar" },
expected: { foo: "bar" },
},
{
name: "prefix substring not matched",
env: { my_npm_config_foo: "bar" },
expected: {},
},
{
name: "empty config key after prefix",
env: { npm_config_: "value" },
expected: {},
},
{
name: "value with special chars preserved",
env: { npm_config_prefix: "/path:with:colons$VAR" },
expected: { prefix: "/path:with:colons$VAR" },
},
];
describe("parseNpmrcFromEnv - Valid Cases", () => {
for (const tc of VALID_ENV_CASES) {
test(`should parse: ${tc.name}`, () => {
const result = parseNpmrcFromEnv(tc.env);
assert.deepStrictEqual(result, tc.expected, `Failed for: ${tc.name}`);
});
}
});
describe("parseNpmrcFromEnv - Edge Cases", () => {
for (const tc of EDGE_ENV_CASES) {
test(`should handle: ${tc.name}`, () => {
const result = parseNpmrcFromEnv(tc.env);
assert.deepStrictEqual(result, tc.expected, `Failed for: ${tc.name}`);
});
}
});
describe("parseNpmrcFromEnv - Security", () => {
test("parser returns raw values - filtering is caller's responsibility", () => {
const maliciousEnv = {
npm_config_git: "./pwn.sh",
npm_config_registry: "https://safe.com",
};
const result = parseNpmrcFromEnv(maliciousEnv);
assert.strictEqual(result.git, "./pwn.sh");
assert.strictEqual(result.registry, "https://safe.com");
const DANGEROUS = new Set(["git", "script-shell", "shell"]);
const safe = Object.fromEntries(
Object.entries(result).filter(([key]) => !DANGEROUS.has(key)),
);
assert.strictEqual(safe.git, undefined);
assert.strictEqual(safe.registry, "https://safe.com");
});
});