import { fetchMalwareDatabase, fetchMalwareDatabaseVersion, } from "../api/aikido.js"; import { readDatabaseFromLocalCache, writeDatabaseToLocalCache, } from "../config/configFile.js"; import { ui } from "../environment/userInteraction.js"; import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js"; /** * @typedef {Object} MalwareDatabase * @property {function(string, string): string} getPackageStatus * @property {function(string, string): boolean} isMalware */ /** @type {MalwareDatabase | null} */ let cachedMalwareDatabase = null; /** * Normalize package name for comparison. * For Python packages (PEP-503): lowercase and replace _, -, . with - * For js packages: keep as-is (case-sensitive) * @param {string} name * @returns {string} */ function normalizePackageName(name) { const ecosystem = getEcoSystem(); if (ecosystem === ECOSYSTEM_PY) { return name.toLowerCase().replace(/[-_.]+/g, "-"); } return name; } export async function openMalwareDatabase() { if (cachedMalwareDatabase) { return cachedMalwareDatabase; } const malwareDatabase = await getMalwareDatabase(); /** * @param {string} name * @param {string} version * @returns {string} */ function getPackageStatus(name, version) { const normalizedName = normalizePackageName(name); const packageData = malwareDatabase.find( (pkg) => { const normalizedPkgName = normalizePackageName(pkg.package_name); return normalizedPkgName === normalizedName && (pkg.version === version || pkg.version === "*"); } ); if (!packageData) { return MALWARE_STATUS_OK; } return packageData.reason; } // This implicitly caches the malware database // that's closed over by the getPackageStatus function cachedMalwareDatabase = { getPackageStatus, isMalware: (name, version) => { const status = getPackageStatus(name, version); return isMalwareStatus(status); }, }; return cachedMalwareDatabase; } /** * @returns {Promise} */ async function getMalwareDatabase() { const { malwareDatabase: cachedDatabase, version: cachedVersion } = readDatabaseFromLocalCache(); try { if (cachedDatabase) { const currentVersion = await fetchMalwareDatabaseVersion(); if (cachedVersion === currentVersion) { return cachedDatabase; } } const { malwareDatabase, version } = await fetchMalwareDatabase(); if (version) { // Only cache the malware database when we have a version. writeDatabaseToLocalCache(malwareDatabase, version); return malwareDatabase; } else { // We received a valid malware database, but the response // did not contain an etag header with the version ui.writeWarning( "The malware database was downloaded, but could not be cached due to a missing version." ); return malwareDatabase; } } catch (/** @type any */ error) { if (cachedDatabase) { ui.writeWarning( "Failed to fetch the latest malware database. Using cached version." ); return cachedDatabase; } throw error; } } /** * @param {string} status * * @returns {boolean} */ function isMalwareStatus(status) { let malwareStatus = status.toUpperCase(); return malwareStatus === MALWARE_STATUS_MALWARE; } export const MALWARE_STATUS_OK = "OK"; export const MALWARE_STATUS_MALWARE = "MALWARE"; export const MALWARE_STATUS_TELEMETRY = "TELEMETRY"; export const MALWARE_STATUS_PROTESTWARE = "PROTESTWARE";