mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Allow to exclude packages from the minimum package age
This commit is contained in:
parent
5898fc851a
commit
6815b62019
9 changed files with 387 additions and 1 deletions
|
|
@ -16,6 +16,7 @@ import { getEcoSystem } from "./settings.js";
|
|||
* @typedef {Object} SafeChainRegistryConfiguration
|
||||
* We cannot trust the input and should add the necessary validations.
|
||||
* @property {unknown | string[]} customRegistries
|
||||
* @property {unknown | string[]} minimumPackageAgeExclusions
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -127,6 +128,27 @@ export function getPipCustomRegistries() {
|
|||
return customRegistries.filter((item) => typeof item === "string");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum package age exclusions from the config file
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getNpmMinimumPackageAgeExclusions() {
|
||||
const config = readConfigFile();
|
||||
|
||||
if (!config || !config.npm) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
|
||||
const exclusions = npmConfig.minimumPackageAgeExclusions;
|
||||
|
||||
if (!Array.isArray(exclusions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return exclusions.filter((item) => typeof item === "string");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
||||
* @param {string | number} version
|
||||
|
|
|
|||
|
|
@ -34,3 +34,13 @@ export function getPipCustomRegistries() {
|
|||
export function getLoggingLevel() {
|
||||
return process.env.SAFE_CHAIN_LOGGING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum package age exclusions from environment variable
|
||||
* Expected format: comma-separated list of package names
|
||||
* Example: "react,@aikidosec/safe-chain,lodash"
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getNpmMinimumPackageAgeExclusions() {
|
||||
return process.env.SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,3 +167,34 @@ export function getPipCustomRegistries() {
|
|||
// Normalize each registry (remove protocol if any)
|
||||
return uniqueRegistries.map(normalizeRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses comma-separated exclusions from environment variable
|
||||
* @param {string | undefined} envValue
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function parseExclusionsFromEnv(envValue) {
|
||||
if (!envValue || typeof envValue !== "string") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return envValue
|
||||
.split(",")
|
||||
.map((exclusion) => exclusion.trim())
|
||||
.filter((exclusion) => exclusion.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum package age exclusions from both environment variable and config file (merged)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getNpmMinimumPackageAgeExclusions() {
|
||||
const envExclusions = parseExclusionsFromEnv(
|
||||
environmentVariables.getNpmMinimumPackageAgeExclusions()
|
||||
);
|
||||
const configExclusions = configFile.getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
// Merge both sources and remove duplicates
|
||||
const allExclusions = [...envExclusions, ...configExclusions];
|
||||
return [...new Set(allExclusions)];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ mock.module("fs", {
|
|||
const {
|
||||
getNpmCustomRegistries,
|
||||
getPipCustomRegistries,
|
||||
getNpmMinimumPackageAgeExclusions,
|
||||
getLoggingLevel,
|
||||
LOGGING_SILENT,
|
||||
LOGGING_NORMAL,
|
||||
|
|
@ -365,3 +366,137 @@ describe("getLoggingLevel", () => {
|
|||
assert.strictEqual(level, LOGGING_NORMAL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNpmMinimumPackageAgeExclusions", () => {
|
||||
let originalEnv;
|
||||
const envVarName = "SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS";
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env[envVarName];
|
||||
delete 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 exclusions configured", () => {
|
||||
configFileContent = undefined;
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, []);
|
||||
});
|
||||
|
||||
it("should return exclusions from config file", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
minimumPackageAgeExclusions: ["react", "@aikidosec/safe-chain"],
|
||||
},
|
||||
});
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
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 = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
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 = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
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 = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, ["lodash", "react", "express"]);
|
||||
});
|
||||
|
||||
it("should trim whitespace from environment variable exclusions", () => {
|
||||
process.env[envVarName] = " lodash , react ";
|
||||
configFileContent = undefined;
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
|
||||
});
|
||||
|
||||
it("should handle scoped packages", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
minimumPackageAgeExclusions: ["@babel/core", "@types/react"],
|
||||
},
|
||||
});
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
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 = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
|
||||
});
|
||||
|
||||
it("should return empty array for empty environment variable", () => {
|
||||
process.env[envVarName] = "";
|
||||
configFileContent = undefined;
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, []);
|
||||
});
|
||||
|
||||
it("should return empty array for whitespace-only environment variable", () => {
|
||||
process.env[envVarName] = " , , ";
|
||||
configFileContent = undefined;
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, []);
|
||||
});
|
||||
|
||||
it("should filter non-string values from config file", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
minimumPackageAgeExclusions: ["react", 123, null, "lodash", undefined],
|
||||
},
|
||||
});
|
||||
|
||||
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||
|
||||
assert.deepStrictEqual(exclusions, ["react", "lodash"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue