mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
131 lines
3.5 KiB
JavaScript
131 lines
3.5 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
|
|
*/
|
|
|
|
/** @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<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";
|