AikidoSec-safe-chain/packages/safe-chain/src/scanning/malwareDatabase.js

134 lines
4.1 KiB
JavaScript

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<MalwareDatabase> | 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<import("../api/aikido.js").MalwarePackage[]>}
*/
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";