mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Add installer build scripts and configuration
This commit is contained in:
parent
fb3a8582a2
commit
3420290ea9
22 changed files with 1377 additions and 7 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
43
packages/safe-chain/src/config/settings.spec.js
Normal file
43
packages/safe-chain/src/config/settings.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue