mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
add a configuration option for custom malwaredb and newpackagelist urls.
This commit is contained in:
parent
5bc8b39f56
commit
1abe5932ad
8 changed files with 219 additions and 19 deletions
|
|
@ -3,17 +3,18 @@ import {
|
|||
getEcoSystem,
|
||||
ECOSYSTEM_JS,
|
||||
ECOSYSTEM_PY,
|
||||
getMalwareListBaseUrl,
|
||||
} from "../config/settings.js";
|
||||
import { ui } from "../environment/userInteraction.js";
|
||||
|
||||
const malwareDatabaseUrls = {
|
||||
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
|
||||
[ECOSYSTEM_PY]: "https://malware-list.aikido.dev/malware_pypi.json",
|
||||
const malwareDatabasePaths = {
|
||||
[ECOSYSTEM_JS]: "malware_predictions.json",
|
||||
[ECOSYSTEM_PY]: "malware_pypi.json",
|
||||
};
|
||||
|
||||
const newPackagesListUrls = {
|
||||
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/releases/npm.json",
|
||||
[ECOSYSTEM_PY]: "https://malware-list.aikido.dev/releases/pypi.json",
|
||||
const newPackagesListPaths = {
|
||||
[ECOSYSTEM_JS]: "releases/npm.json",
|
||||
[ECOSYSTEM_PY]: "releases/pypi.json",
|
||||
};
|
||||
|
||||
const DEFAULT_FETCH_RETRY_ATTEMPTS = 4;
|
||||
|
|
@ -40,10 +41,11 @@ const DEFAULT_FETCH_RETRY_ATTEMPTS = 4;
|
|||
export async function fetchMalwareDatabase() {
|
||||
return retry(async () => {
|
||||
const ecosystem = getEcoSystem();
|
||||
const malwareDatabaseUrl =
|
||||
malwareDatabaseUrls[
|
||||
/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)
|
||||
];
|
||||
const baseUrl = getMalwareListBaseUrl();
|
||||
const path = malwareDatabasePaths[
|
||||
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
||||
];
|
||||
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
||||
const response = await fetch(malwareDatabaseUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
|
|
@ -69,10 +71,11 @@ export async function fetchMalwareDatabase() {
|
|||
export async function fetchMalwareDatabaseVersion() {
|
||||
return retry(async () => {
|
||||
const ecosystem = getEcoSystem();
|
||||
const malwareDatabaseUrl =
|
||||
malwareDatabaseUrls[
|
||||
/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)
|
||||
];
|
||||
const baseUrl = getMalwareListBaseUrl();
|
||||
const path = malwareDatabasePaths[
|
||||
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
||||
];
|
||||
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
||||
const response = await fetch(malwareDatabaseUrl, {
|
||||
method: "HEAD",
|
||||
});
|
||||
|
|
@ -92,8 +95,9 @@ export async function fetchMalwareDatabaseVersion() {
|
|||
export async function fetchNewPackagesList() {
|
||||
return retry(async () => {
|
||||
const ecosystem = getEcoSystem();
|
||||
const url =
|
||||
newPackagesListUrls[/** @type {keyof typeof newPackagesListUrls} */ (ecosystem)];
|
||||
const baseUrl = getMalwareListBaseUrl();
|
||||
const path = newPackagesListPaths[/** @type {keyof typeof newPackagesListPaths} */ (ecosystem)];
|
||||
const url = `${baseUrl}/${path}`;
|
||||
|
||||
if (!url) {
|
||||
return { newPackagesList: [], version: undefined };
|
||||
|
|
@ -124,8 +128,9 @@ export async function fetchNewPackagesList() {
|
|||
export async function fetchNewPackagesListVersion() {
|
||||
return retry(async () => {
|
||||
const ecosystem = getEcoSystem();
|
||||
const url =
|
||||
newPackagesListUrls[/** @type {keyof typeof newPackagesListUrls} */ (ecosystem)];
|
||||
const baseUrl = getMalwareListBaseUrl();
|
||||
const path = newPackagesListPaths[/** @type {keyof typeof newPackagesListPaths} */ (ecosystem)];
|
||||
const url = `${baseUrl}/${path}`;
|
||||
|
||||
if (!url) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ describe("aikido API", async () => {
|
|||
getEcoSystem: () => ecosystem,
|
||||
ECOSYSTEM_JS: "js",
|
||||
ECOSYSTEM_PY: "py",
|
||||
getMalwareListBaseUrl: () => "https://malware-list.aikido.dev",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { ui } from "../environment/userInteraction.js";
|
||||
|
||||
/**
|
||||
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined}}
|
||||
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, malwareListBaseUrl: string | undefined}}
|
||||
*/
|
||||
const state = {
|
||||
loggingLevel: undefined,
|
||||
skipMinimumPackageAge: undefined,
|
||||
minimumPackageAgeHours: undefined,
|
||||
malwareListBaseUrl: undefined,
|
||||
};
|
||||
|
||||
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
||||
|
|
@ -20,6 +21,7 @@ export function initializeCliArguments(args) {
|
|||
state.loggingLevel = undefined;
|
||||
state.skipMinimumPackageAge = undefined;
|
||||
state.minimumPackageAgeHours = undefined;
|
||||
state.malwareListBaseUrl = undefined;
|
||||
|
||||
const safeChainArgs = [];
|
||||
const remainingArgs = [];
|
||||
|
|
@ -35,6 +37,7 @@ export function initializeCliArguments(args) {
|
|||
setLoggingLevel(safeChainArgs);
|
||||
setSkipMinimumPackageAge(safeChainArgs);
|
||||
setMinimumPackageAgeHours(safeChainArgs);
|
||||
setMalwareListBaseUrl(safeChainArgs);
|
||||
checkDeprecatedPythonFlag(args);
|
||||
return remainingArgs;
|
||||
}
|
||||
|
|
@ -109,6 +112,26 @@ export function getMinimumPackageAgeHours() {
|
|||
return state.minimumPackageAgeHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function setMalwareListBaseUrl(args) {
|
||||
const argName = SAFE_CHAIN_ARG_PREFIX + "malware-list-base-url=";
|
||||
|
||||
const value = getLastArgEqualsValue(args, argName);
|
||||
if (value) {
|
||||
state.malwareListBaseUrl = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getMalwareListBaseUrl() {
|
||||
return state.malwareListBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @param {string} flagName
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getEcoSystem } from "./settings.js";
|
|||
* We cannot trust the input and should add the necessary validations
|
||||
* @property {unknown | Number} scanTimeout
|
||||
* @property {unknown | Number} minimumPackageAgeHours
|
||||
* @property {unknown | string} malwareListBaseUrl
|
||||
* @property {unknown | SafeChainRegistryConfiguration} npm
|
||||
* @property {unknown | SafeChainRegistryConfiguration} pip
|
||||
*
|
||||
|
|
@ -84,6 +85,18 @@ export function getMinimumPackageAgeHours() {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the malware list base URL from config file only
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getMalwareListBaseUrl() {
|
||||
const config = readConfigFile();
|
||||
if (config.malwareListBaseUrl && typeof config.malwareListBaseUrl === "string") {
|
||||
return config.malwareListBaseUrl;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
||||
* @returns {string[]}
|
||||
|
|
@ -214,6 +227,7 @@ function readConfigFile() {
|
|||
const emptyConfig = {
|
||||
scanTimeout: undefined,
|
||||
minimumPackageAgeHours: undefined,
|
||||
malwareListBaseUrl: undefined,
|
||||
npm: {
|
||||
customRegistries: undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,3 +45,13 @@ export function getMinimumPackageAgeExclusions() {
|
|||
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS ||
|
||||
process.env.SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the malware list base URL from environment variable
|
||||
* Expected format: full URL without trailing slash
|
||||
* Example: "https://malware-list.aikido.dev"
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getMalwareListBaseUrl() {
|
||||
return process.env.SAFE_CHAIN_MALWARE_LIST_BASE_URL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,3 +198,30 @@ export function getMinimumPackageAgeExclusions() {
|
|||
const allExclusions = [...envExclusions, ...configExclusions];
|
||||
return [...new Set(allExclusions)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the malware list base URL with priority: CLI argument > environment variable > config file > default
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getMalwareListBaseUrl() {
|
||||
// Priority 1: CLI argument
|
||||
const cliValue = cliArguments.getMalwareListBaseUrl();
|
||||
if (cliValue) {
|
||||
return cliValue;
|
||||
}
|
||||
|
||||
// Priority 2: Environment variable
|
||||
const envValue = environmentVariables.getMalwareListBaseUrl();
|
||||
if (envValue) {
|
||||
return envValue;
|
||||
}
|
||||
|
||||
// Priority 3: Config file
|
||||
const configValue = configFile.getMalwareListBaseUrl();
|
||||
if (configValue) {
|
||||
return configValue;
|
||||
}
|
||||
|
||||
// Default
|
||||
return "https://malware-list.aikido.dev";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const {
|
|||
getNpmCustomRegistries,
|
||||
getPipCustomRegistries,
|
||||
getMinimumPackageAgeExclusions,
|
||||
getMalwareListBaseUrl,
|
||||
setEcoSystem,
|
||||
ECOSYSTEM_JS,
|
||||
ECOSYSTEM_PY,
|
||||
|
|
@ -534,3 +535,87 @@ describe("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([]);
|
||||
});
|
||||
|
||||
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 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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue