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 */ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields // control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all // concurrent callers see it immediately and share a single fetch. /** @type {Promise | null} */ let cachedMalwareDatabasePromise = 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 function openMalwareDatabase() { if (!cachedMalwareDatabasePromise) { cachedMalwareDatabasePromise = getMalwareDatabase().then((malwareDatabase) => { /** * @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; } return { getPackageStatus, isMalware: (/** @type {string} */ name, /** @type {string} */ version) => { const status = getPackageStatus(name, version); return isMalwareStatus(status); }, }; }).catch((error) => { cachedMalwareDatabasePromise = null; throw error; }); } return cachedMalwareDatabasePromise; } /** * @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";