Retry downloading the malware database 3 times

This commit is contained in:
Sander Declerck 2026-01-14 14:02:27 +01:00
parent 045fc1519b
commit d83a381231
No known key found for this signature in database
2 changed files with 77 additions and 34 deletions

View file

@ -1,5 +1,9 @@
import fetch from "make-fetch-happen"; import fetch from "make-fetch-happen";
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; import {
getEcoSystem,
ECOSYSTEM_JS,
ECOSYSTEM_PY,
} from "../config/settings.js";
const malwareDatabaseUrls = { const malwareDatabaseUrls = {
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json", [ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
@ -17,38 +21,77 @@ const malwareDatabaseUrls = {
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>} * @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
*/ */
export async function fetchMalwareDatabase() { export async function fetchMalwareDatabase() {
const ecosystem = getEcoSystem(); return retry(async () => {
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)]; const ecosystem = getEcoSystem();
const response = await fetch(malwareDatabaseUrl); const malwareDatabaseUrl =
if (!response.ok) { malwareDatabaseUrls[
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`); /** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)
} ];
const response = await fetch(malwareDatabaseUrl);
if (!response.ok) {
throw new Error(
`Error fetching ${ecosystem} malware database: ${response.statusText}`
);
}
try { try {
let malwareDatabase = await response.json(); let malwareDatabase = await response.json();
return { return {
malwareDatabase: malwareDatabase, malwareDatabase: malwareDatabase,
version: response.headers.get("etag") || undefined, version: response.headers.get("etag") || undefined,
}; };
} catch (/** @type {any} */ error) { } catch (/** @type {any} */ error) {
throw new Error(`Error parsing malware database: ${error.message}`); throw new Error(`Error parsing malware database: ${error.message}`);
} }
}, 3);
} }
/** /**
* @returns {Promise<string | undefined>} * @returns {Promise<string | undefined>}
*/ */
export async function fetchMalwareDatabaseVersion() { export async function fetchMalwareDatabaseVersion() {
const ecosystem = getEcoSystem(); return retry(async () => {
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)]; const ecosystem = getEcoSystem();
const response = await fetch(malwareDatabaseUrl, { const malwareDatabaseUrl =
method: "HEAD", malwareDatabaseUrls[
}); /** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)
];
const response = await fetch(malwareDatabaseUrl, {
method: "HEAD",
});
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
`Error fetching ${ecosystem} malware database version: ${response.statusText}` `Error fetching ${ecosystem} malware database version: ${response.statusText}`
); );
} }
return response.headers.get("etag") || undefined; return response.headers.get("etag") || undefined;
}, 3);
}
/**
* Retries an asynchronous function multiple times until it succeeds or exhausts all attempts.
*
* @template T
* @param {() => Promise<T>} func - The asynchronous function to retry
* @param {number} times - The number of retry attempts (will execute times + 1 total attempts)
* @returns {Promise<T>} The return value of the function if successful
* @throws {Error} The last error encountered if all retry attempts fail
*/
async function retry(func, times) {
let lastError;
for (let i = 0; i <= times; i++) {
try {
return await func();
} catch (error) {
lastError = error;
}
if (i < times) {
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 500));
}
}
throw lastError;
} }

View file

@ -48,13 +48,13 @@ export async function openMalwareDatabase() {
*/ */
function getPackageStatus(name, version) { function getPackageStatus(name, version) {
const normalizedName = normalizePackageName(name); const normalizedName = normalizePackageName(name);
const packageData = malwareDatabase.find( const packageData = malwareDatabase.find((pkg) => {
(pkg) => { const normalizedPkgName = normalizePackageName(pkg.package_name);
const normalizedPkgName = normalizePackageName(pkg.package_name); return (
return normalizedPkgName === normalizedName && normalizedPkgName === normalizedName &&
(pkg.version === version || pkg.version === "*"); (pkg.version === version || pkg.version === "*")
} );
); });
if (!packageData) { if (!packageData) {
return MALWARE_STATUS_OK; return MALWARE_STATUS_OK;