import fs from "fs"; import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; import { getEcoSystem } from "./settings.js"; /** * @typedef {Object} SafeChainConfig * * 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 * * @typedef {Object} SafeChainRegistryConfiguration * We cannot trust the input and should add the necessary validations. * @property {unknown | string[]} customRegistries * @property {unknown | string[]} minimumPackageAgeExclusions */ /** * @returns {number} */ export function getScanTimeout() { const config = readConfigFile(); 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 {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 !== undefined) { const validated = validateMinimumPackageAgeHours( config.minimumPackageAgeHours ); if (validated !== undefined) { return validated; } } 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[]} */ export function getNpmCustomRegistries() { const config = readConfigFile(); if (!config || !config.npm) { return []; } // TypeScript needs help understanding that config.npm exists and has customRegistries const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm); const customRegistries = npmConfig.customRegistries; if (!Array.isArray(customRegistries)) { return []; } return customRegistries.filter((item) => typeof item === "string"); } /** * Gets the custom npm registries from the config file (format parsing only, no validation) * @returns {string[]} */ export function getPipCustomRegistries() { const config = readConfigFile(); if (!config || !config.pip) { return []; } // TypeScript needs help understanding that config.pip exists and has customRegistries const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip); const customRegistries = pipConfig.customRegistries; if (!Array.isArray(customRegistries)) { return []; } return customRegistries.filter((item) => typeof item === "string"); } /** * Gets the minimum package age exclusions from the config file for the current ecosystem * @returns {string[]} */ export function getMinimumPackageAgeExclusions() { const config = readConfigFile(); const ecosystem = getEcoSystem(); const registryConfig = ecosystem === "py" ? config.pip : config.npm; if (!config || !registryConfig) { return []; } const typedRegistryConfig = /** @type {SafeChainRegistryConfiguration} */ (registryConfig); const exclusions = typedRegistryConfig.minimumPackageAgeExclusions; if (!Array.isArray(exclusions)) { return []; } return exclusions.filter((item) => typeof item === "string"); } /** * @param {import("../api/aikido.js").MalwarePackage[]} data * @param {string | number} version * * @returns {void} */ export function writeDatabaseToLocalCache(data, version) { try { const databasePath = getDatabasePath(); const versionPath = getDatabaseVersionPath(); fs.writeFileSync(databasePath, JSON.stringify(data)); fs.writeFileSync(versionPath, version.toString()); } catch { ui.writeWarning( "Failed to write malware database to local cache, next time the database will be fetched from the server again." ); } } /** * @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}} */ export function readDatabaseFromLocalCache() { try { const databasePath = getDatabasePath(); if (!fs.existsSync(databasePath)) { return { malwareDatabase: null, version: null, }; } const data = fs.readFileSync(databasePath, "utf8"); const malwareDatabase = JSON.parse(data); const versionPath = getDatabaseVersionPath(); let version = null; if (fs.existsSync(versionPath)) { version = fs.readFileSync(versionPath, "utf8").trim(); } return { malwareDatabase: malwareDatabase, version: version, }; } catch { ui.writeWarning( "Failed to read malware database from local cache. Continuing without local cache." ); return { malwareDatabase: null, version: null, }; } } /** * @returns {SafeChainConfig} */ function readConfigFile() { /** @type {SafeChainConfig} */ const emptyConfig = { scanTimeout: undefined, minimumPackageAgeHours: undefined, malwareListBaseUrl: undefined, npm: { customRegistries: undefined, }, pip: { customRegistries: undefined, }, }; const configFilePath = getConfigFilePath(); if (!fs.existsSync(configFilePath)) { return emptyConfig; } try { const data = fs.readFileSync(configFilePath, "utf8"); return JSON.parse(data); } catch { return emptyConfig; } } /** * @returns {string} */ function getDatabasePath() { const aikidoDir = getAikidoDirectory(); const ecosystem = getEcoSystem(); return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`); } function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); const ecosystem = getEcoSystem(); return path.join(aikidoDir, `version_${ecosystem}.txt`); } /** * @returns {string} */ export function getNewPackagesListPath() { const safeChainDir = getSafeChainDirectory(); const ecosystem = getEcoSystem(); return path.join(safeChainDir, `newPackagesList_${ecosystem}.json`); } /** * @returns {string} */ export function getNewPackagesListVersionPath() { const safeChainDir = getSafeChainDirectory(); const ecosystem = getEcoSystem(); return path.join(safeChainDir, `newPackagesList_version_${ecosystem}.txt`); } /** * @returns {string} */ function getConfigFilePath() { const primaryPath = path.join(getSafeChainDirectory(), "config.json"); if (fs.existsSync(primaryPath)) { return primaryPath; } const legacyPath = path.join(getAikidoDirectory(), "config.json"); if (fs.existsSync(legacyPath)) { return legacyPath; } return primaryPath; } /** * @returns {string} */ export function getSafeChainDirectory() { const homeDir = os.homedir(); const safeChainDir = path.join(homeDir, ".safe-chain"); if (!fs.existsSync(safeChainDir)) { fs.mkdirSync(safeChainDir, { recursive: true }); } return safeChainDir; } /** * @returns {string} */ function getAikidoDirectory() { const homeDir = os.homedir(); const aikidoDir = path.join(homeDir, ".aikido"); if (!fs.existsSync(aikidoDir)) { fs.mkdirSync(aikidoDir, { recursive: true }); } return aikidoDir; }