mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge remote-tracking branch 'origin/main' into feature/pypi
This commit is contained in:
commit
548d416996
64 changed files with 1689 additions and 381 deletions
|
|
@ -1,9 +1,16 @@
|
|||
/**
|
||||
* @type {{loggingLevel: string | undefined}}
|
||||
*/
|
||||
const state = {
|
||||
loggingLevel: undefined,
|
||||
};
|
||||
|
||||
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function initializeCliArguments(args) {
|
||||
// Reset state on each call
|
||||
state.loggingLevel = undefined;
|
||||
|
|
@ -24,6 +31,11 @@ export function initializeCliArguments(args) {
|
|||
return remainingArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @param {string} prefix
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function getLastArgEqualsValue(args, prefix) {
|
||||
for (var i = args.length - 1; i >= 0; i--) {
|
||||
const arg = args[i];
|
||||
|
|
@ -35,6 +47,10 @@ function getLastArgEqualsValue(args, prefix) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function setLoggingLevel(args) {
|
||||
const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging=";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,56 @@ import os from "os";
|
|||
import { ui } from "../environment/userInteraction.js";
|
||||
import { getEcoSystem } from "./settings.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} SafeChainConfig
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getScanTimeout() {
|
||||
const config = readConfigFile();
|
||||
return (
|
||||
parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds
|
||||
);
|
||||
|
||||
if (process.env.AIKIDO_SCAN_TIMEOUT_MS) {
|
||||
const scanTimeout = validateTimeout(process.env.AIKIDO_SCAN_TIMEOUT_MS);
|
||||
if (scanTimeout != null) {
|
||||
return scanTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.scanTimeout) {
|
||||
const scanTimeout = validateTimeout(config.scanTimeout);
|
||||
if (scanTimeout != null) {
|
||||
return scanTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
return 10000; // Default to 10 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {number?}
|
||||
*/
|
||||
function validateTimeout(value) {
|
||||
const timeout = Number(value);
|
||||
if (!Number.isNaN(timeout) && timeout > 0) {
|
||||
return timeout;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
||||
* @param {string | number} version
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function writeDatabaseToLocalCache(data, version) {
|
||||
try {
|
||||
const databasePath = getDatabasePath();
|
||||
|
|
@ -25,6 +68,9 @@ export function writeDatabaseToLocalCache(data, version) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}}
|
||||
*/
|
||||
export function readDatabaseFromLocalCache() {
|
||||
try {
|
||||
const databasePath = getDatabasePath();
|
||||
|
|
@ -56,17 +102,31 @@ export function readDatabaseFromLocalCache() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SafeChainConfig}
|
||||
*/
|
||||
function readConfigFile() {
|
||||
const configFilePath = getConfigFilePath();
|
||||
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
return {};
|
||||
return {
|
||||
scanTimeout: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(configFilePath, "utf8");
|
||||
return JSON.parse(data);
|
||||
try {
|
||||
const data = fs.readFileSync(configFilePath, "utf8");
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return {
|
||||
scanTimeout: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDatabasePath() {
|
||||
const aikidoDir = getAikidoDirectory();
|
||||
const ecosystem = getEcoSystem();
|
||||
|
|
@ -79,10 +139,16 @@ function getDatabaseVersionPath() {
|
|||
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
function getConfigFilePath() {
|
||||
return path.join(getAikidoDirectory(), "config.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
function getAikidoDirectory() {
|
||||
const homeDir = os.homedir();
|
||||
const aikidoDir = path.join(homeDir, ".aikido");
|
||||
|
|
|
|||
172
packages/safe-chain/src/config/configFile.spec.js
Normal file
172
packages/safe-chain/src/config/configFile.spec.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("getScanTimeout", () => {
|
||||
let originalEnv;
|
||||
let fsMock;
|
||||
let getScanTimeout;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Save original environment
|
||||
originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||
|
||||
// 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()}`
|
||||
);
|
||||
getScanTimeout = configFileModule.getScanTimeout;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = originalEnv;
|
||||
} else {
|
||||
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
// Reset all mocks
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
it("should return default timeout of 10000ms when no config or env var is set", () => {
|
||||
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||
// Mock: config file doesn't exist
|
||||
fsMock.existsSync.mock.mockImplementation(() => false);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 10000);
|
||||
});
|
||||
|
||||
it("should return timeout from config file when set", () => {
|
||||
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||
// Mock: config file exists with scanTimeout: 5000
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: 5000 })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 5000);
|
||||
});
|
||||
|
||||
it("should prioritize environment variable over config file", () => {
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000";
|
||||
// Mock: config file exists with scanTimeout: 5000
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: 5000 })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 20000);
|
||||
});
|
||||
|
||||
it("should handle invalid environment variable and fall back to config", () => {
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid";
|
||||
// Mock: config file exists with scanTimeout: 7000
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: 7000 })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 7000);
|
||||
});
|
||||
|
||||
it("should ignore zero and negative values and fall back to default", () => {
|
||||
// Mock: config file doesn't exist
|
||||
fsMock.existsSync.mock.mockImplementation(() => false);
|
||||
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "0";
|
||||
|
||||
let timeout = getScanTimeout();
|
||||
assert.strictEqual(timeout, 10000);
|
||||
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "-5000";
|
||||
|
||||
timeout = getScanTimeout();
|
||||
assert.strictEqual(timeout, 10000);
|
||||
});
|
||||
|
||||
it("should ignore textual non-numeric values in environment variable and fall back to config", () => {
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast";
|
||||
// Mock: config file exists with scanTimeout: 8000
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: 8000 })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 8000);
|
||||
});
|
||||
|
||||
it("should ignore textual non-numeric values in config file and fall back to default", () => {
|
||||
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||
// Mock: config file exists with scanTimeout: "slow"
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: "slow" })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 10000);
|
||||
});
|
||||
|
||||
it("should ignore textual non-numeric values in both env and config, fall back to default", () => {
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick";
|
||||
// Mock: config file exists with scanTimeout: "medium"
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: "medium" })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 10000);
|
||||
});
|
||||
|
||||
it("should ignore mixed alphanumeric strings in environment variable", () => {
|
||||
process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms";
|
||||
// Mock: config file exists with scanTimeout: 6000
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: 6000 })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 6000);
|
||||
});
|
||||
|
||||
it("should ignore mixed alphanumeric strings in config file", () => {
|
||||
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||
// Mock: config file exists with scanTimeout: "3000ms"
|
||||
fsMock.existsSync.mock.mockImplementation(() => true);
|
||||
fsMock.readFileSync.mock.mockImplementation(() =>
|
||||
JSON.stringify({ scanTimeout: "3000ms" })
|
||||
);
|
||||
|
||||
const timeout = getScanTimeout();
|
||||
|
||||
assert.strictEqual(timeout, 10000);
|
||||
});
|
||||
});
|
||||
|
|
@ -24,6 +24,9 @@ const ecosystemSettings = {
|
|||
export function getEcoSystem() {
|
||||
return ecosystemSettings.ecoSystem;
|
||||
}
|
||||
/**
|
||||
* @param {string} setting - The ecosystem to set (ECOSYSTEM_JS or ECOSYSTEM_PY)
|
||||
*/
|
||||
export function setEcoSystem(setting) {
|
||||
ecosystemSettings.ecoSystem = setting;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue