Add installer build scripts and configuration

This commit is contained in:
Reinier Criel 2025-11-25 08:21:35 -08:00
parent fb3a8582a2
commit 3420290ea9
22 changed files with 1377 additions and 7 deletions

View file

@ -1,5 +1,5 @@
import fetch from "make-fetch-happen";
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY, ECOSYSTEM_ALL } from "../config/settings.js";
const malwareDatabaseUrls = {
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
@ -18,6 +18,42 @@ const malwareDatabaseUrls = {
*/
export async function fetchMalwareDatabase() {
const ecosystem = getEcoSystem();
// For ECOSYSTEM_ALL, fetch both databases concurrently
if (ecosystem === ECOSYSTEM_ALL) {
const [jsResponse, pyResponse] = await Promise.all([
fetch(malwareDatabaseUrls[ECOSYSTEM_JS]),
fetch(malwareDatabaseUrls[ECOSYSTEM_PY])
]);
if (!jsResponse.ok) {
throw new Error(`Error fetching JS malware database: ${jsResponse.statusText}`);
}
if (!pyResponse.ok) {
throw new Error(`Error fetching Python malware database: ${pyResponse.statusText}`);
}
try {
const [jsDatabase, pyDatabase] = await Promise.all([
jsResponse.json(),
pyResponse.json()
]);
const mergedDatabase = [...jsDatabase, ...pyDatabase];
// Use JS etag for version (or combine both if needed)
const version = jsResponse.headers.get("etag") || pyResponse.headers.get("etag") || undefined;
return {
malwareDatabase: mergedDatabase,
version: version,
};
} catch (/** @type {any} */ error) {
throw new Error(`Error parsing malware database: ${error.message}`);
}
}
// Single ecosystem mode (existing behavior)
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
const response = await fetch(malwareDatabaseUrl);
if (!response.ok) {
@ -40,6 +76,28 @@ export async function fetchMalwareDatabase() {
*/
export async function fetchMalwareDatabaseVersion() {
const ecosystem = getEcoSystem();
// For ECOSYSTEM_ALL, check both databases
if (ecosystem === ECOSYSTEM_ALL) {
const [jsResponse, pyResponse] = await Promise.all([
fetch(malwareDatabaseUrls[ECOSYSTEM_JS], { method: "HEAD" }),
fetch(malwareDatabaseUrls[ECOSYSTEM_PY], { method: "HEAD" })
]);
if (!jsResponse.ok) {
throw new Error(`Error fetching JS malware database version: ${jsResponse.statusText}`);
}
if (!pyResponse.ok) {
throw new Error(`Error fetching Python malware database version: ${pyResponse.statusText}`);
}
// Combine both etags for version (so cache invalidates if either changes)
const jsEtag = jsResponse.headers.get("etag") || "";
const pyEtag = pyResponse.headers.get("etag") || "";
return jsEtag && pyEtag ? `${jsEtag}|${pyEtag}` : undefined;
}
// Single ecosystem mode (existing behavior)
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
const response = await fetch(malwareDatabaseUrl, {
method: "HEAD",

View file

@ -20,20 +20,24 @@ export function getLoggingLevel() {
export const ECOSYSTEM_JS = "js";
export const ECOSYSTEM_PY = "py";
export const ECOSYSTEM_ALL = "all";
// Default to JavaScript ecosystem
const ecosystemSettings = {
ecoSystem: ECOSYSTEM_JS,
};
/** @returns {string} - The current ecosystem setting (ECOSYSTEM_JS or ECOSYSTEM_PY) */
/** @returns {string} - The current ecosystem setting (ECOSYSTEM_JS, ECOSYSTEM_PY, or ECOSYSTEM_ALL) */
export function getEcoSystem() {
return ecosystemSettings.ecoSystem;
}
/**
* @param {string} setting - The ecosystem to set (ECOSYSTEM_JS or ECOSYSTEM_PY)
* @param {string} setting - The ecosystem to set (ECOSYSTEM_JS, ECOSYSTEM_PY, or ECOSYSTEM_ALL)
*/
export function setEcoSystem(setting) {
if (![ECOSYSTEM_JS, ECOSYSTEM_PY, ECOSYSTEM_ALL].includes(setting)) {
throw new Error(`Invalid ecosystem: ${setting}. Must be one of: ${ECOSYSTEM_JS}, ${ECOSYSTEM_PY}, ${ECOSYSTEM_ALL}`);
}
ecosystemSettings.ecoSystem = setting;
}

View file

@ -0,0 +1,43 @@
import { describe, it } from "node:test";
import * as assert from "node:assert";
import { setEcoSystem, getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY, ECOSYSTEM_ALL } from "./settings.js";
describe("Ecosystem Settings", () => {
it("should default to ECOSYSTEM_JS", () => {
// Reset to default
setEcoSystem(ECOSYSTEM_JS);
assert.strictEqual(getEcoSystem(), ECOSYSTEM_JS);
});
it("should allow setting ECOSYSTEM_PY", () => {
setEcoSystem(ECOSYSTEM_PY);
assert.strictEqual(getEcoSystem(), ECOSYSTEM_PY);
// Reset to default
setEcoSystem(ECOSYSTEM_JS);
});
it("should allow setting ECOSYSTEM_ALL", () => {
setEcoSystem(ECOSYSTEM_ALL);
assert.strictEqual(getEcoSystem(), ECOSYSTEM_ALL);
// Reset to default
setEcoSystem(ECOSYSTEM_JS);
});
it("should throw error for invalid ecosystem", () => {
assert.throws(
() => setEcoSystem("invalid"),
{
name: "Error",
message: /Invalid ecosystem: invalid/
}
);
});
it("should validate all valid ecosystem constants", () => {
assert.doesNotThrow(() => setEcoSystem(ECOSYSTEM_JS));
assert.doesNotThrow(() => setEcoSystem(ECOSYSTEM_PY));
assert.doesNotThrow(() => setEcoSystem(ECOSYSTEM_ALL));
// Reset to default
setEcoSystem(ECOSYSTEM_JS);
});
});

View file

@ -95,16 +95,27 @@ function loadCa() {
return { privateKey, certificate };
}
function generateCa() {
/**
* Generate a CA certificate with optional custom attributes and validity
* @param {Object} options - Certificate options
* @param {Array<{name: string, value: string}>} [options.attrs] - Certificate attributes
* @param {number} [options.validityDays] - Number of days the certificate is valid (default: 1)
* @returns {{privateKey: any, certificate: any}} Private key and certificate objects
*/
export function generateCa(options = {}) {
const {
attrs = [{ name: "commonName", value: "safe-chain proxy" }],
validityDays = 1,
} = options;
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1);
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + validityDays);
const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.setExtensions([

View file

@ -1,6 +1,7 @@
import {
ECOSYSTEM_JS,
ECOSYSTEM_PY,
ECOSYSTEM_ALL,
getEcoSystem,
} from "../../config/settings.js";
import { npmInterceptorForUrl } from "./npm/npmInterceptor.js";
@ -13,6 +14,21 @@ import { pipInterceptorForUrl } from "./pipInterceptor.js";
export function createInterceptorForUrl(url) {
const ecosystem = getEcoSystem();
if (ecosystem === ECOSYSTEM_ALL) {
// Try both ecosystems (npm registries first, then PyPI)
const jsInterceptor = npmInterceptorForUrl(url);
if (jsInterceptor) {
return jsInterceptor;
}
const pyInterceptor = pipInterceptorForUrl(url);
if (pyInterceptor) {
return pyInterceptor;
}
return undefined;
}
if (ecosystem === ECOSYSTEM_JS) {
return npmInterceptorForUrl(url);
}

View file

@ -7,7 +7,7 @@ import {
writeDatabaseToLocalCache,
} from "../config/configFile.js";
import { ui } from "../environment/userInteraction.js";
import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
import { getEcoSystem, ECOSYSTEM_PY, ECOSYSTEM_ALL } from "../config/settings.js";
/**
* @typedef {Object} MalwareDatabase
@ -22,6 +22,7 @@ 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)
* For ECOSYSTEM_ALL: We need to try both normalization strategies
* @param {string} name
* @returns {string}
*/
@ -31,6 +32,8 @@ function normalizePackageName(name) {
return name.toLowerCase().replace(/[-_.]+/g, "-");
}
// For ECOSYSTEM_JS and ECOSYSTEM_ALL, keep as-is
// (ECOSYSTEM_ALL handles both in getPackageStatus)
return name;
}
@ -47,6 +50,37 @@ export async function openMalwareDatabase() {
* @returns {string}
*/
function getPackageStatus(name, version) {
const ecosystem = getEcoSystem();
// For ECOSYSTEM_ALL, try both normalization strategies
if (ecosystem === ECOSYSTEM_ALL) {
// Try JS-style first (exact match)
let packageData = malwareDatabase.find(
(pkg) => {
return pkg.package_name === name &&
(pkg.version === version || pkg.version === "*");
}
);
// If not found, try Python-style normalization
if (!packageData) {
const normalizedName = name.toLowerCase().replace(/[-_.]+/g, "-");
packageData = malwareDatabase.find(
(pkg) => {
const normalizedPkgName = pkg.package_name.toLowerCase().replace(/[-_.]+/g, "-");
return normalizedPkgName === normalizedName &&
(pkg.version === version || pkg.version === "*");
}
);
}
if (!packageData) {
return MALWARE_STATUS_OK;
}
return packageData.reason;
}
// Single ecosystem mode
const normalizedName = normalizePackageName(name);
const packageData = malwareDatabase.find(
(pkg) => {