mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Allow to configure the minimum package age
This commit is contained in:
parent
5c3c3399d9
commit
13892efa70
8 changed files with 449 additions and 3 deletions
|
|
@ -1,9 +1,10 @@
|
|||
/**
|
||||
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, includePython: boolean}}
|
||||
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, includePython: boolean}}
|
||||
*/
|
||||
const state = {
|
||||
loggingLevel: undefined,
|
||||
skipMinimumPackageAge: undefined,
|
||||
minimumPackageAgeHours: undefined,
|
||||
includePython: false,
|
||||
};
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ export function initializeCliArguments(args) {
|
|||
// Reset state on each call
|
||||
state.loggingLevel = undefined;
|
||||
state.skipMinimumPackageAge = undefined;
|
||||
state.minimumPackageAgeHours = undefined;
|
||||
|
||||
const safeChainArgs = [];
|
||||
const remainingArgs = [];
|
||||
|
|
@ -31,6 +33,7 @@ export function initializeCliArguments(args) {
|
|||
|
||||
setLoggingLevel(safeChainArgs);
|
||||
setSkipMinimumPackageAge(safeChainArgs);
|
||||
setMinimumPackageAgeHours(safeChainArgs);
|
||||
setIncludePython(args);
|
||||
|
||||
return remainingArgs;
|
||||
|
|
@ -86,6 +89,26 @@ export function getSkipMinimumPackageAge() {
|
|||
return state.skipMinimumPackageAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function setMinimumPackageAgeHours(args) {
|
||||
const argName = SAFE_CHAIN_ARG_PREFIX + "minimum-package-age-hours=";
|
||||
|
||||
const value = getLastArgEqualsValue(args, argName);
|
||||
if (value) {
|
||||
state.minimumPackageAgeHours = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getMinimumPackageAgeHours() {
|
||||
return state.minimumPackageAgeHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
initializeCliArguments,
|
||||
getLoggingLevel,
|
||||
getSkipMinimumPackageAge,
|
||||
getMinimumPackageAgeHours,
|
||||
} from "./cliArguments.js";
|
||||
|
||||
describe("initializeCliArguments", () => {
|
||||
|
|
@ -178,4 +179,96 @@ describe("initializeCliArguments", () => {
|
|||
assert.deepEqual(result, ["install", "lodash"]);
|
||||
assert.strictEqual(getSkipMinimumPackageAge(), true);
|
||||
});
|
||||
|
||||
it("should return undefined when no minimum-package-age-hours argument is passed", () => {
|
||||
const args = ["install", "express", "--save"];
|
||||
initializeCliArguments(args);
|
||||
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), undefined);
|
||||
});
|
||||
|
||||
it("should parse minimum-package-age-hours value and set state", () => {
|
||||
const args = [
|
||||
"--safe-chain-minimum-package-age-hours=48",
|
||||
"install",
|
||||
"lodash",
|
||||
];
|
||||
const result = initializeCliArguments(args);
|
||||
|
||||
assert.deepEqual(result, ["install", "lodash"]);
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "48");
|
||||
});
|
||||
|
||||
it("should handle minimum-package-age-hours with zero value", () => {
|
||||
const args = ["--safe-chain-minimum-package-age-hours=0", "install"];
|
||||
initializeCliArguments(args);
|
||||
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "0");
|
||||
});
|
||||
|
||||
it("should handle minimum-package-age-hours with decimal values", () => {
|
||||
const args = ["--safe-chain-minimum-package-age-hours=1.5", "install"];
|
||||
initializeCliArguments(args);
|
||||
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "1.5");
|
||||
});
|
||||
|
||||
it("should handle minimum-package-age-hours case-insensitively", () => {
|
||||
const args = ["--SAFE-CHAIN-MINIMUM-PACKAGE-AGE-HOURS=72", "install"];
|
||||
initializeCliArguments(args);
|
||||
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "72");
|
||||
});
|
||||
|
||||
it("should use the last minimum-package-age-hours argument when multiple are provided", () => {
|
||||
const args = [
|
||||
"--safe-chain-minimum-package-age-hours=12",
|
||||
"--safe-chain-minimum-package-age-hours=36",
|
||||
"install",
|
||||
];
|
||||
initializeCliArguments(args);
|
||||
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "36");
|
||||
});
|
||||
|
||||
it("should filter out minimum-package-age-hours argument from returned args", () => {
|
||||
const args = [
|
||||
"install",
|
||||
"--safe-chain-minimum-package-age-hours=48",
|
||||
"express",
|
||||
"--save",
|
||||
];
|
||||
const result = initializeCliArguments(args);
|
||||
|
||||
assert.deepEqual(result, ["install", "express", "--save"]);
|
||||
});
|
||||
|
||||
it("should handle minimum-package-age-hours with other safe-chain arguments", () => {
|
||||
const args = [
|
||||
"--safe-chain-logging=verbose",
|
||||
"--safe-chain-minimum-package-age-hours=96",
|
||||
"install",
|
||||
"lodash",
|
||||
];
|
||||
const result = initializeCliArguments(args);
|
||||
|
||||
assert.deepEqual(result, ["install", "lodash"]);
|
||||
assert.strictEqual(getLoggingLevel(), "verbose");
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "96");
|
||||
});
|
||||
|
||||
it("should handle non-numeric values without validation (validation in settings.js)", () => {
|
||||
const args = ["--safe-chain-minimum-package-age-hours=invalid", "install"];
|
||||
initializeCliArguments(args);
|
||||
|
||||
// cliArguments.js just captures the value; validation is in settings.js
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "invalid");
|
||||
});
|
||||
|
||||
it("should handle negative values as strings (validation in settings.js)", () => {
|
||||
const args = ["--safe-chain-minimum-package-age-hours=-24", "install"];
|
||||
initializeCliArguments(args);
|
||||
|
||||
assert.strictEqual(getMinimumPackageAgeHours(), "-24");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { getEcoSystem } from "./settings.js";
|
|||
*
|
||||
* This should be a number, but can be anything because it is user-input.
|
||||
* We cannot trust the input and should add the necessary validations.
|
||||
* @property {any} scanTimeout
|
||||
* @property {unknown} scanTimeout
|
||||
* @property {unknown} minimumPackageAgeHours
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -48,6 +49,35 @@ function validateTimeout(value) {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
function validateMinimumPackageAgeHours(value) {
|
||||
const hours = Number(value);
|
||||
if (!Number.isNaN(hours)) {
|
||||
return hours;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum package age in hours from config file only
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
export function getMinimumPackageAgeHours() {
|
||||
const config = readConfigFile();
|
||||
if (config.minimumPackageAgeHours) {
|
||||
const validated = validateMinimumPackageAgeHours(
|
||||
config.minimumPackageAgeHours
|
||||
);
|
||||
if (validated !== undefined) {
|
||||
return validated;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
||||
* @param {string | number} version
|
||||
|
|
@ -111,6 +141,7 @@ function readConfigFile() {
|
|||
if (!fs.existsSync(configFilePath)) {
|
||||
return {
|
||||
scanTimeout: undefined,
|
||||
minimumPackageAgeHours: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +151,7 @@ function readConfigFile() {
|
|||
} catch {
|
||||
return {
|
||||
scanTimeout: undefined,
|
||||
minimumPackageAgeHours: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,3 +170,116 @@ describe("getScanTimeout", () => {
|
|||
assert.strictEqual(timeout, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMinimumPackageAgeHours", () => {
|
||||
let fsMock;
|
||||
let getMinimumPackageAgeHours;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock fs module
|
||||
fsMock = {
|
||||
existsSync: mock.fn(() => false),
|
||||
readFileSync: mock.fn(() => "{}"),
|
||||
writeFileSync: mock.fn(),
|
||||
mkdirSync: mock.fn(),
|
||||
};
|
||||
|
||||
mock.module("fs", {
|
||||
namedExports: fsMock,
|
||||
});
|
||||
|
||||
// Re-import the module to get the mocked version
|
||||
const configFileModule = await import(
|
||||
`./configFile.js?update=${Date.now()}`
|
||||
);
|
||||
getMinimumPackageAgeHours = configFileModule.getMinimumPackageAgeHours;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset all mocks
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
it("should return null when config file doesn't exist", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => false);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, undefined);
|
||||
});
|
||||
|
||||
it("should return null when config file exists but minimumPackageAgeHours is not set", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: 5000 })
|
||||
);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, undefined);
|
||||
});
|
||||
|
||||
it("should return value from config file when set to valid number", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ minimumPackageAgeHours: 48 })
|
||||
);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, 48);
|
||||
});
|
||||
|
||||
it("should handle string numbers in config file", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ minimumPackageAgeHours: "72" })
|
||||
);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, 72);
|
||||
});
|
||||
|
||||
it("should handle decimal values", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ minimumPackageAgeHours: 1.5 })
|
||||
);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, 1.5);
|
||||
});
|
||||
|
||||
it("should return null for non-numeric strings", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ minimumPackageAgeHours: "invalid" })
|
||||
);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, undefined);
|
||||
});
|
||||
|
||||
it("should return null for values with units suffix", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ minimumPackageAgeHours: "48h" })
|
||||
);
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, undefined);
|
||||
});
|
||||
|
||||
it("should handle malformed JSON and return null", () => {
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() => "{ invalid json");
|
||||
|
||||
const hours = getMinimumPackageAgeHours();
|
||||
|
||||
assert.strictEqual(hours, undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
7
packages/safe-chain/src/config/environmentVariables.js
Normal file
7
packages/safe-chain/src/config/environmentVariables.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Gets the minimum package age in hours from environment variable
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getMinimumPackageAgeHours() {
|
||||
return process.env.AIKIDO_MINIMUM_PACKAGE_AGE_HOURS;
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import * as cliArguments from "./cliArguments.js";
|
||||
import * as configFile from "./configFile.js";
|
||||
import * as environmentVariables from "./environmentVariables.js";
|
||||
|
||||
export const LOGGING_SILENT = "silent";
|
||||
export const LOGGING_NORMAL = "normal";
|
||||
|
|
@ -38,10 +40,54 @@ export function setEcoSystem(setting) {
|
|||
}
|
||||
|
||||
const defaultMinimumPackageAge = 24;
|
||||
/** @returns {number} */
|
||||
export function getMinimumPackageAgeHours() {
|
||||
// Priority 1: CLI argument
|
||||
const cliValue = validateMinimumPackageAgeHours(
|
||||
cliArguments.getMinimumPackageAgeHours()
|
||||
);
|
||||
if (cliValue !== undefined) {
|
||||
return cliValue;
|
||||
}
|
||||
|
||||
// Priority 2: Environment variable
|
||||
const envValue = validateMinimumPackageAgeHours(
|
||||
environmentVariables.getMinimumPackageAgeHours()
|
||||
);
|
||||
if (envValue !== undefined) {
|
||||
return envValue;
|
||||
}
|
||||
|
||||
// Priority 3: Config file
|
||||
const configValue = configFile.getMinimumPackageAgeHours();
|
||||
if (configValue !== undefined) {
|
||||
return configValue;
|
||||
}
|
||||
|
||||
return defaultMinimumPackageAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | undefined} value
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
function validateMinimumPackageAgeHours(value) {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const numericValue = Number(value);
|
||||
if (Number.isNaN(numericValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (numericValue > 0) {
|
||||
return numericValue;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultSkipMinimumPackageAge = false;
|
||||
export function skipMinimumPackageAge() {
|
||||
const cliValue = cliArguments.getSkipMinimumPackageAge();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue