Merge remote-tracking branch 'origin/main' into feature/pypi

This commit is contained in:
Reinier Criel 2025-11-03 06:49:53 -08:00
commit 548d416996
64 changed files with 1689 additions and 381 deletions

View file

@ -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=";

View file

@ -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");

View 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);
});
});

View file

@ -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;
}