mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
369 lines
9.1 KiB
JavaScript
369 lines
9.1 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import os from "os";
|
|
import { ui } from "../environment/userInteraction.js";
|
|
import { getEcoSystem } from "./settings.js";
|
|
import { getSafeChainBaseDir } from "./safeChainDir.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 | string} logFile
|
|
* @property {unknown | string} logFileFormat
|
|
* @property {unknown | string} logFileVerbosity
|
|
* @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 log file path from the config file
|
|
* @returns {string | undefined}
|
|
*/
|
|
export function getLogFile() {
|
|
const config = readConfigFile();
|
|
if (config.logFile && typeof config.logFile === "string") {
|
|
return config.logFile;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Gets the log file format from the config file
|
|
* @returns {string | undefined}
|
|
*/
|
|
export function getLogFileFormat() {
|
|
const config = readConfigFile();
|
|
if (config.logFileFormat && typeof config.logFileFormat === "string") {
|
|
return config.logFileFormat;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Gets the log file verbosity from the config file
|
|
* @returns {string | undefined}
|
|
*/
|
|
export function getLogFileVerbosity() {
|
|
const config = readConfigFile();
|
|
if (config.logFileVerbosity && typeof config.logFileVerbosity === "string") {
|
|
return config.logFileVerbosity;
|
|
}
|
|
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,
|
|
logFile: undefined,
|
|
logFileFormat: undefined,
|
|
logFileVerbosity: 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 safeChainDir = getSafeChainBaseDir();
|
|
|
|
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;
|
|
}
|