AikidoSec-safe-chain/packages/safe-chain/src/config/settings.spec.js

699 lines
20 KiB
JavaScript

import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
let configFileContent = undefined;
let loggedMessages = [];
mock.module("fs", {
namedExports: {
existsSync: () => configFileContent !== undefined,
readFileSync: () => configFileContent,
writeFileSync: (content) => (configFileContent = content),
mkdirSync: () => {},
},
});
mock.module("../environment/userInteraction.js", {
namedExports: {
ui: {
writeInformation: (message) => loggedMessages.push(message),
},
},
});
const {
getNpmCustomRegistries,
getPipCustomRegistries,
getMinimumPackageAgeExclusions,
getMalwareListBaseUrl,
setEcoSystem,
ECOSYSTEM_JS,
ECOSYSTEM_PY,
getLoggingLevel,
LOGGING_SILENT,
LOGGING_NORMAL,
LOGGING_VERBOSE,
} = await import("./settings.js");
const { initializeCliArguments } = await import("./cliArguments.js");
for (const { packageManager, getCustomRegistries, envVarName } of [
{
packageManager: "npm",
getCustomRegistries: getNpmCustomRegistries,
envVarName: "SAFE_CHAIN_NPM_CUSTOM_REGISTRIES",
},
{
packageManager: "pip",
getCustomRegistries: getPipCustomRegistries,
envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES",
},
]) {
describe(getCustomRegistries.name, async () => {
let originalEnv;
beforeEach(() => {
originalEnv = process.env[envVarName];
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env[envVarName] = originalEnv;
} else {
delete process.env[envVarName];
}
configFileContent = undefined;
});
it("should return empty array when no registries configured", () => {
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []);
});
it("should return registries without protocol", () => {
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: [
`${packageManager}.company.com`,
"registry.internal.net",
],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
`${packageManager}.company.com`,
"registry.internal.net",
]);
});
it("should strip https:// protocol from registries", () => {
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: [
`https://${packageManager}.company.com`,
"https://registry.internal.net",
],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
`${packageManager}.company.com`,
"registry.internal.net",
]);
});
it("should strip http:// protocol from registries", () => {
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: [
`http://${packageManager}.company.com`,
"http://registry.internal.net",
],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
`${packageManager}.company.com`,
"registry.internal.net",
]);
});
it("should handle mixed protocols and no protocol", () => {
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: [
`https://${packageManager}.company.com`,
"registry.internal.net",
"http://private.registry.io",
],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
`${packageManager}.company.com`,
"registry.internal.net",
"private.registry.io",
]);
});
it("should preserve registry path after stripping protocol", () => {
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: [
`https://${packageManager}.company.com/custom/path`,
`registry.internal.net/${packageManager}`,
],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
`${packageManager}.company.com/custom/path`,
`registry.internal.net/${packageManager}`,
]);
});
it("should parse comma-separated registries from environment variable", () => {
delete process.env[envVarName];
process.env[envVarName] = "env1.registry.com,env2.registry.net";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
"env1.registry.com",
"env2.registry.net",
]);
});
it("should trim whitespace from environment variable registries", () => {
delete process.env[envVarName];
process.env[envVarName] = " env1.registry.com , env2.registry.net ";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
"env1.registry.com",
"env2.registry.net",
]);
});
it("should merge environment variable and config file registries", () => {
delete process.env[envVarName];
process.env[envVarName] = "env1.registry.com";
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: ["config1.registry.net"],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
"env1.registry.com",
"config1.registry.net",
]);
});
it("should remove duplicate registries when merging env and config", () => {
delete process.env[envVarName];
process.env[
envVarName
] = `${packageManager}.company.com,env.registry.com`;
configFileContent = JSON.stringify({
[packageManager]: {
customRegistries: [
`${packageManager}.company.com`,
"config.registry.net",
],
},
});
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
`${packageManager}.company.com`,
"env.registry.com",
"config.registry.net",
]);
});
it("should normalize protocols from environment variable registries", () => {
delete process.env[envVarName];
process.env[envVarName] =
"https://env1.registry.com,http://env2.registry.net";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
"env1.registry.com",
"env2.registry.net",
]);
});
it("should handle empty strings in comma-separated list", () => {
delete process.env[envVarName];
process.env[envVarName] = "env1.registry.com,,env2.registry.net,";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [
"env1.registry.com",
"env2.registry.net",
]);
});
it("should handle single registry in environment variable", () => {
delete process.env[envVarName];
process.env[envVarName] = "single.registry.com";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, ["single.registry.com"]);
});
it("should return empty array for empty environment variable", () => {
delete process.env[envVarName];
process.env[envVarName] = "";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []);
});
it("should return empty array for whitespace-only environment variable", () => {
delete process.env[envVarName];
process.env[envVarName] = " , , ";
configFileContent = undefined;
const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []);
});
});
}
describe("getLoggingLevel", () => {
let originalEnv;
beforeEach(() => {
originalEnv = process.env.SAFE_CHAIN_LOGGING;
delete process.env.SAFE_CHAIN_LOGGING;
// Reset CLI arguments state
initializeCliArguments([]);
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.SAFE_CHAIN_LOGGING = originalEnv;
} else {
delete process.env.SAFE_CHAIN_LOGGING;
}
});
it("should return normal by default when nothing is configured", () => {
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_NORMAL);
});
it("should return silent from environment variable", () => {
process.env.SAFE_CHAIN_LOGGING = "silent";
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_SILENT);
});
it("should return verbose from environment variable", () => {
process.env.SAFE_CHAIN_LOGGING = "verbose";
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_VERBOSE);
});
it("should handle uppercase environment variable values", () => {
process.env.SAFE_CHAIN_LOGGING = "VERBOSE";
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_VERBOSE);
});
it("should handle mixed case environment variable values", () => {
process.env.SAFE_CHAIN_LOGGING = "Silent";
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_SILENT);
});
it("should return normal for invalid environment variable values", () => {
process.env.SAFE_CHAIN_LOGGING = "invalid";
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_NORMAL);
});
it("should prioritize CLI argument over environment variable", () => {
process.env.SAFE_CHAIN_LOGGING = "verbose";
initializeCliArguments(["--safe-chain-logging=silent"]);
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_SILENT);
});
it("should use environment variable when CLI argument is not set", () => {
process.env.SAFE_CHAIN_LOGGING = "silent";
initializeCliArguments(["install", "express"]);
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_SILENT);
});
it("should return normal when CLI argument is invalid (even if env var is valid)", () => {
process.env.SAFE_CHAIN_LOGGING = "verbose";
initializeCliArguments(["--safe-chain-logging=invalid"]);
const level = getLoggingLevel();
assert.strictEqual(level, LOGGING_NORMAL);
});
});
describe("getMinimumPackageAgeExclusions", () => {
let originalEnv;
let originalLegacyEnv;
const envVarName = "SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS";
const legacyEnvVarName = "SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS";
beforeEach(() => {
originalEnv = process.env[envVarName];
originalLegacyEnv = process.env[legacyEnvVarName];
delete process.env[envVarName];
delete process.env[legacyEnvVarName];
setEcoSystem(ECOSYSTEM_JS);
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env[envVarName] = originalEnv;
} else {
delete process.env[envVarName];
}
if (originalLegacyEnv !== undefined) {
process.env[legacyEnvVarName] = originalLegacyEnv;
} else {
delete process.env[legacyEnvVarName];
}
configFileContent = undefined;
});
it("should return empty array when no exclusions configured", () => {
configFileContent = undefined;
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, []);
});
it("should return exclusions from config file", () => {
configFileContent = JSON.stringify({
npm: {
minimumPackageAgeExclusions: ["react", "@aikidosec/safe-chain"],
},
});
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["react", "@aikidosec/safe-chain"]);
});
it("should parse comma-separated exclusions from environment variable", () => {
process.env[envVarName] = "lodash,express,@types/node";
configFileContent = undefined;
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["lodash", "express", "@types/node"]);
});
it("should merge environment variable and config file exclusions", () => {
process.env[envVarName] = "lodash";
configFileContent = JSON.stringify({
npm: {
minimumPackageAgeExclusions: ["react"],
},
});
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
});
it("should remove duplicate exclusions when merging", () => {
process.env[envVarName] = "lodash,react";
configFileContent = JSON.stringify({
npm: {
minimumPackageAgeExclusions: ["react", "express"],
},
});
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["lodash", "react", "express"]);
});
it("should trim whitespace from environment variable exclusions", () => {
process.env[envVarName] = " lodash , react ";
configFileContent = undefined;
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
});
it("should handle scoped packages", () => {
configFileContent = JSON.stringify({
npm: {
minimumPackageAgeExclusions: ["@babel/core", "@types/react"],
},
});
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["@babel/core", "@types/react"]);
});
it("should handle empty strings in comma-separated list", () => {
process.env[envVarName] = "lodash,,react,";
configFileContent = undefined;
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
});
it("should return empty array for empty environment variable", () => {
process.env[envVarName] = "";
configFileContent = undefined;
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, []);
});
it("should return empty array for whitespace-only environment variable", () => {
process.env[envVarName] = " , , ";
configFileContent = undefined;
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, []);
});
it("should filter non-string values from config file", () => {
configFileContent = JSON.stringify({
npm: {
minimumPackageAgeExclusions: ["react", 123, null, "lodash", undefined],
},
});
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["react", "lodash"]);
});
it("should fall back to the legacy npm environment variable", () => {
process.env[legacyEnvVarName] = "lodash,react";
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
});
it("should read exclusions from the python config when the current ecosystem is py", () => {
setEcoSystem(ECOSYSTEM_PY);
configFileContent = JSON.stringify({
pip: {
minimumPackageAgeExclusions: ["requests", "urllib3"],
},
});
const exclusions = getMinimumPackageAgeExclusions();
assert.deepStrictEqual(exclusions, ["requests", "urllib3"]);
});
});
describe("getMalwareListBaseUrl", () => {
let originalEnv;
const envVarName = "SAFE_CHAIN_MALWARE_LIST_BASE_URL";
beforeEach(() => {
originalEnv = process.env[envVarName];
delete process.env[envVarName];
// Reset CLI arguments state
initializeCliArguments([]);
loggedMessages = [];
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env[envVarName] = originalEnv;
} else {
delete process.env[envVarName];
}
configFileContent = undefined;
});
it("should return default URL when nothing is configured", () => {
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://malware-list.aikido.dev");
});
it("should trim trailing slash from CLI argument", () => {
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com/"]);
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://cli-mirror.com");
});
it("should trim trailing slash from environment variable", () => {
process.env[envVarName] = "https://env-mirror.com/";
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://env-mirror.com");
});
it("should trim trailing slash from config file value", () => {
configFileContent = JSON.stringify({
malwareListBaseUrl: "https://config-mirror.com/",
});
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://config-mirror.com");
});
it("should return CLI argument value with highest priority", () => {
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com"]);
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://cli-mirror.com");
});
it("should return environment variable value when no CLI argument", () => {
process.env[envVarName] = "https://env-mirror.com";
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://env-mirror.com");
});
it("should return config file value when no CLI or env", () => {
configFileContent = JSON.stringify({
malwareListBaseUrl: "https://config-mirror.com",
});
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://config-mirror.com");
});
it("should prioritize CLI over environment variable", () => {
process.env[envVarName] = "https://env-mirror.com";
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com"]);
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://cli-mirror.com");
});
it("should prioritize environment variable over config file", () => {
process.env[envVarName] = "https://env-mirror.com";
configFileContent = JSON.stringify({
malwareListBaseUrl: "https://config-mirror.com",
});
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://env-mirror.com");
});
it("should prioritize CLI over config file", () => {
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com"]);
configFileContent = JSON.stringify({
malwareListBaseUrl: "https://config-mirror.com",
});
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://cli-mirror.com");
});
it("should mask credentials in logged URL from CLI argument", () => {
initializeCliArguments(["--safe-chain-malware-list-base-url=https://user:pass@cli-mirror.com"]);
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://user:pass@cli-mirror.com");
assert.strictEqual(loggedMessages.length, 1);
assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@cli-mirror.com as defined by CLI argument --safe-chain-malware-list-base-url");
});
it("should mask credentials in logged URL from environment variable", () => {
process.env[envVarName] = "https://user:pass@env-mirror.com";
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://user:pass@env-mirror.com");
assert.strictEqual(loggedMessages.length, 1);
assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@env-mirror.com as defined by environment variable SAFE_CHAIN_MALWARE_LIST_BASE_URL");
});
it("should mask credentials in logged URL from config file", () => {
configFileContent = JSON.stringify({
malwareListBaseUrl: "https://user:pass@config-mirror.com",
});
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://user:pass@config-mirror.com");
assert.strictEqual(loggedMessages.length, 1);
assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@config-mirror.com as defined by config file (malwareListBaseUrl)");
});
it("should sanitize control characters in logged URL", () => {
initializeCliArguments(["--safe-chain-malware-list-base-url=https://user:pass@cli-mirror.com\nmalicious"]);
const url = getMalwareListBaseUrl();
assert.strictEqual(url, "https://user:pass@cli-mirror.com\nmalicious");
assert.strictEqual(loggedMessages.length, 1);
assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@cli-mirror.commalicious as defined by CLI argument --safe-chain-malware-list-base-url");
});
});