mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10: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
17
installer/.gitignore
vendored
Normal file
17
installer/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
2
installer/.npmrc
Normal file
2
installer/.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Installer build configuration
|
||||||
|
package-lock=false
|
||||||
3
installer/README.md
Normal file
3
installer/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Aikido Safe Chain - macOS Installer
|
||||||
|
|
||||||
|
This directory contains the build system for creating a macOS system-wide agent installer for Aikido Safe Chain.
|
||||||
372
installer/agent/configure-proxy.js
Normal file
372
installer/agent/configure-proxy.js
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System Proxy Configuration Manager for macOS
|
||||||
|
* node configure-proxy.js --install # Configure system proxy
|
||||||
|
* node configure-proxy.js --uninstall # Restore original settings
|
||||||
|
* node configure-proxy.js --status # Check current configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { PROXY_HOST, AGENT_PORT as PROXY_PORT, CONFIG_FILE, BYPASS_DOMAINS } from "./settings.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize service name to prevent command injection
|
||||||
|
* Network service names from networksetup are trusted, but we sanitize as best practice
|
||||||
|
*/
|
||||||
|
function sanitizeServiceName(service) {
|
||||||
|
// Remove any characters that could be used for command injection
|
||||||
|
return service.replace(/[;&|`$()]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all network services on the system
|
||||||
|
*/
|
||||||
|
function getNetworkServices() {
|
||||||
|
try {
|
||||||
|
const output = execSync("networksetup -listallnetworkservices", {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse output, skip first line (header) and disabled services (marked with *)
|
||||||
|
return output
|
||||||
|
.split("\n")
|
||||||
|
.slice(1)
|
||||||
|
.filter((line) => line.trim() && !line.startsWith("*"))
|
||||||
|
.map((line) => line.trim());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get network services:", error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current proxy settings for a network service
|
||||||
|
*/
|
||||||
|
function getCurrentProxySettings(service) {
|
||||||
|
try {
|
||||||
|
const httpProxy = execSync(`networksetup -getwebproxy "${service}"`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
const httpsProxy = execSync(`networksetup -getsecurewebproxy "${service}"`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseProxyOutput = (output) => {
|
||||||
|
const enabled = output.includes("Enabled: Yes");
|
||||||
|
const serverMatch = output.match(/Server: (.+)/);
|
||||||
|
const portMatch = output.match(/Port: (\d+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
server: serverMatch ? serverMatch[1].trim() : "",
|
||||||
|
port: portMatch ? parseInt(portMatch[1]) : 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
http: parseProxyOutput(httpProxy),
|
||||||
|
https: parseProxyOutput(httpsProxy),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get proxy settings for ${service}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current proxy settings to restore later
|
||||||
|
*/
|
||||||
|
function saveOriginalSettings(services) {
|
||||||
|
const settings = {};
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
const current = getCurrentProxySettings(service);
|
||||||
|
if (current) {
|
||||||
|
settings[service] = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
|
if (!fs.existsSync(configDir)) {
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(settings, null, 2));
|
||||||
|
console.log(`Saved original proxy settings to ${CONFIG_FILE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved proxy settings
|
||||||
|
*/
|
||||||
|
function loadOriginalSettings() {
|
||||||
|
if (!fs.existsSync(CONFIG_FILE)) {
|
||||||
|
console.warn("No saved settings found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load saved settings:", error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure proxy for a network service
|
||||||
|
*/
|
||||||
|
function configureProxyForService(service) {
|
||||||
|
try {
|
||||||
|
console.log(`Configuring proxy for: ${service}`);
|
||||||
|
|
||||||
|
// Set HTTP proxy
|
||||||
|
execSync(
|
||||||
|
`networksetup -setwebproxy "${service}" ${PROXY_HOST} ${PROXY_PORT}`,
|
||||||
|
{ stdio: "ignore" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set HTTPS proxy
|
||||||
|
execSync(
|
||||||
|
`networksetup -setsecurewebproxy "${service}" ${PROXY_HOST} ${PROXY_PORT}`,
|
||||||
|
{ stdio: "ignore" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set bypass domains
|
||||||
|
const bypassDomainsStr = BYPASS_DOMAINS.join('" "');
|
||||||
|
execSync(
|
||||||
|
`networksetup -setproxybypassdomains "${service}" "${bypassDomainsStr}"`,
|
||||||
|
{ stdio: "ignore" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable proxies
|
||||||
|
execSync(`networksetup -setwebproxystate "${service}" on`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
execSync(`networksetup -setsecurewebproxystate "${service}" on`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` Configured: ${service}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` Failed to configure ${service}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore original proxy settings for a service
|
||||||
|
*/
|
||||||
|
function restoreProxyForService(service, settings) {
|
||||||
|
try {
|
||||||
|
console.log(`Restoring proxy for: ${service}`);
|
||||||
|
|
||||||
|
// Restore HTTP proxy
|
||||||
|
if (settings.http.enabled && settings.http.server) {
|
||||||
|
execSync(
|
||||||
|
`networksetup -setwebproxy "${service}" ${settings.http.server} ${settings.http.port}`,
|
||||||
|
{ stdio: "ignore" }
|
||||||
|
);
|
||||||
|
execSync(`networksetup -setwebproxystate "${service}" on`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
execSync(`networksetup -setwebproxystate "${service}" off`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore HTTPS proxy
|
||||||
|
if (settings.https.enabled && settings.https.server) {
|
||||||
|
execSync(
|
||||||
|
`networksetup -setsecurewebproxy "${service}" ${settings.https.server} ${settings.https.port}`,
|
||||||
|
{ stdio: "ignore" }
|
||||||
|
);
|
||||||
|
execSync(`networksetup -setsecurewebproxystate "${service}" on`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
execSync(`networksetup -setsecurewebproxystate "${service}" off`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Restored: ${service}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` Failed to restore ${service}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install: Configure system proxy
|
||||||
|
*/
|
||||||
|
function install() {
|
||||||
|
console.log("🔧 Configuring system proxy for Aikido Safe Chain...\n");
|
||||||
|
|
||||||
|
const services = getNetworkServices();
|
||||||
|
|
||||||
|
if (services.length === 0) {
|
||||||
|
console.error("No network services found");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${services.length} network service(s):\n - ${services.join("\n - ")}\n`);
|
||||||
|
|
||||||
|
// Save original settings
|
||||||
|
saveOriginalSettings(services);
|
||||||
|
|
||||||
|
// Configure each service
|
||||||
|
let successCount = 0;
|
||||||
|
for (const service of services) {
|
||||||
|
if (configureProxyForService(service)) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nConfigured proxy for ${successCount}/${services.length} network services`);
|
||||||
|
console.log(`\nProxy settings:`);
|
||||||
|
console.log(` HTTP Proxy: ${PROXY_HOST}:${PROXY_PORT}`);
|
||||||
|
console.log(` HTTPS Proxy: ${PROXY_HOST}:${PROXY_PORT}`);
|
||||||
|
console.log(` Bypass: ${BYPASS_DOMAINS.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall: Restore original proxy settings
|
||||||
|
*/
|
||||||
|
function uninstall() {
|
||||||
|
console.log("Restoring original proxy settings...\n");
|
||||||
|
|
||||||
|
const savedSettings = loadOriginalSettings();
|
||||||
|
|
||||||
|
if (!savedSettings) {
|
||||||
|
console.log("No saved settings found. Disabling proxies...");
|
||||||
|
|
||||||
|
// Just disable proxies for all services
|
||||||
|
const services = getNetworkServices();
|
||||||
|
for (const service of services) {
|
||||||
|
try {
|
||||||
|
execSync(`networksetup -setwebproxystate "${service}" off`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
execSync(`networksetup -setsecurewebproxystate "${service}" off`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
console.log(`Disabled proxy for: ${service}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to disable proxy for ${service}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Restore saved settings
|
||||||
|
let successCount = 0;
|
||||||
|
const serviceNames = Object.keys(savedSettings);
|
||||||
|
|
||||||
|
for (const service of serviceNames) {
|
||||||
|
if (restoreProxyForService(service, savedSettings[service])) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove config file
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(CONFIG_FILE);
|
||||||
|
console.log(`Removed configuration file`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to remove config file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status: Show current proxy configuration
|
||||||
|
*/
|
||||||
|
function status() {
|
||||||
|
const services = getNetworkServices();
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
console.log(`${service}:`);
|
||||||
|
const settings = getCurrentProxySettings(service);
|
||||||
|
|
||||||
|
if (settings) {
|
||||||
|
console.log(` HTTP: ${settings.http.enabled ? "OK" : "NOK"} ${settings.http.server || "None"}:${settings.http.port || 0}`);
|
||||||
|
console.log(` HTTPS: ${settings.https.enabled ? "OK" : "NOK"} ${settings.https.server || "None"}:${settings.https.port || 0}`);
|
||||||
|
} else {
|
||||||
|
console.log("Failed to get settings");
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Aikido proxy is configured
|
||||||
|
const aikidoConfigured = services.some((service) => {
|
||||||
|
const settings = getCurrentProxySettings(service);
|
||||||
|
return (
|
||||||
|
settings &&
|
||||||
|
((settings.http.enabled &&
|
||||||
|
settings.http.server === PROXY_HOST &&
|
||||||
|
settings.http.port === PROXY_PORT) ||
|
||||||
|
(settings.https.enabled &&
|
||||||
|
settings.https.server === PROXY_HOST &&
|
||||||
|
settings.https.port === PROXY_PORT))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (aikidoConfigured) {
|
||||||
|
console.log("Aikido Safe Chain proxy is configured");
|
||||||
|
} else {
|
||||||
|
console.log("Aikido Safe Chain proxy is NOT configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if saved settings exist
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
console.log(`Original settings saved at: ${CONFIG_FILE}`);
|
||||||
|
} else {
|
||||||
|
console.log(`No saved settings found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main
|
||||||
|
*/
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const command = args[0];
|
||||||
|
|
||||||
|
if (!command || command === "--help" || command === "-h") {
|
||||||
|
console.log("Aikido Safe Chain - Proxy Configuration Manager");
|
||||||
|
console.log("\nUsage:");
|
||||||
|
console.log(" node configure-proxy.js --install # Configure system proxy");
|
||||||
|
console.log(" node configure-proxy.js --uninstall # Restore original settings");
|
||||||
|
console.log(" node configure-proxy.js --status # Show current configuration");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "--install":
|
||||||
|
case "install":
|
||||||
|
install();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--uninstall":
|
||||||
|
case "uninstall":
|
||||||
|
uninstall();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--status":
|
||||||
|
case "status":
|
||||||
|
status();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`Unknown command: ${command}`);
|
||||||
|
console.log('Run with --help for usage information');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
249
installer/agent/index.js
Normal file
249
installer/agent/index.js
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aikido Safe Chain Agent - Long Running Daemon (Mac only for now)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as http from "http";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { AGENT_PORT, LOG_DIR, PID_FILE } from "./settings.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Set ecosystem to ALL for system-wide protection (both JS and Python)
|
||||||
|
const { setEcoSystem, ECOSYSTEM_ALL } = await import("./lib/config/settings.js");
|
||||||
|
setEcoSystem(ECOSYSTEM_ALL);
|
||||||
|
|
||||||
|
// Import proxy infrastructure from lib (copied during build)
|
||||||
|
const { createSafeChainProxy } = await import("./lib/registryProxy/registryProxy.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger that writes to both stdout and log files
|
||||||
|
*/
|
||||||
|
class AgentLogger {
|
||||||
|
constructor() {
|
||||||
|
this.ensureLogDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLogDir() {
|
||||||
|
if (!fs.existsSync(LOG_DIR)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(LOG_DIR, { recursive: true, mode: 0o755 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create log directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logMessage = `[${timestamp}] ${message}`;
|
||||||
|
console.log(logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logMessage = `[${timestamp}] ERROR: ${message}`;
|
||||||
|
console.error(logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message) {
|
||||||
|
this.log(`INFO: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message) {
|
||||||
|
this.log(`WARN: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = new AgentLogger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main
|
||||||
|
*/
|
||||||
|
class SafeChainAgent {
|
||||||
|
constructor() {
|
||||||
|
this.proxy = null;
|
||||||
|
this.httpServer = null;
|
||||||
|
this.isShuttingDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the agent
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
logger.info("Starting Aikido POC Safe Chain Agent...");
|
||||||
|
|
||||||
|
// Write PID file
|
||||||
|
this.writePidFile();
|
||||||
|
|
||||||
|
// Setup signal handlers
|
||||||
|
this.setupSignalHandlers();
|
||||||
|
|
||||||
|
// Start proxy server
|
||||||
|
try {
|
||||||
|
await this.startProxyServer();
|
||||||
|
logger.info(`Agent started successfully on port ${AGENT_PORT}`);
|
||||||
|
logger.info("System-wide malware protection is now active");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to start agent: ${error.message}`);
|
||||||
|
logger.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the MITM proxy server
|
||||||
|
*/
|
||||||
|
async startProxyServer() {
|
||||||
|
// Create proxy using existing infrastructure
|
||||||
|
this.proxy = createSafeChainProxy();
|
||||||
|
|
||||||
|
// We need to adapt the proxy to use a fixed port instead of random port
|
||||||
|
// Create custom server that wraps the proxy
|
||||||
|
this.httpServer = await this.createFixedPortProxyServer();
|
||||||
|
|
||||||
|
logger.info(`Proxy server listening on http://127.0.0.1:${AGENT_PORT}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create proxy server on fixed port
|
||||||
|
*
|
||||||
|
* This adapts the existing createSafeChainProxy() which uses random ports
|
||||||
|
* to use our fixed port for system-wide configuration
|
||||||
|
*/
|
||||||
|
async createFixedPortProxyServer() {
|
||||||
|
const { tunnelRequest } = await import("./lib/registryProxy/tunnelRequestHandler.js");
|
||||||
|
const { mitmConnect } = await import("./lib/registryProxy/mitmRequestHandler.js");
|
||||||
|
const { handleHttpProxyRequest } = await import("./lib/registryProxy/plainHttpProxy.js");
|
||||||
|
const { createInterceptorForUrl } = await import("./lib/registryProxy/interceptors/createInterceptorForEcoSystem.js");
|
||||||
|
|
||||||
|
const server = http.createServer(handleHttpProxyRequest);
|
||||||
|
|
||||||
|
// Handle HTTPS CONNECT requests
|
||||||
|
server.on("connect", (req, clientSocket, head) => {
|
||||||
|
const interceptor = createInterceptorForUrl(req.url || "");
|
||||||
|
|
||||||
|
if (interceptor) {
|
||||||
|
// Subscribe to malware blocked events
|
||||||
|
interceptor.on("malwareBlocked", (event) => {
|
||||||
|
logger.warn(
|
||||||
|
`Blocked malicious package: ${event.packageName}@${event.version} from ${event.url}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
mitmConnect(req, clientSocket, interceptor);
|
||||||
|
} else {
|
||||||
|
// For non-registry hosts, just tunnel
|
||||||
|
tunnelRequest(req, clientSocket, head);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server on fixed port
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.listen(AGENT_PORT, "127.0.0.1", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("error", (err) => {
|
||||||
|
if (err.code === "EADDRINUSE") {
|
||||||
|
logger.error(`Port ${AGENT_PORT} is already in use. Is another instance running?`);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write PID file for process management
|
||||||
|
*/
|
||||||
|
writePidFile() {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(PID_FILE, process.pid.toString());
|
||||||
|
logger.info(`PID file written: ${PID_FILE}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to write PID file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove PID file
|
||||||
|
*/
|
||||||
|
removePidFile() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(PID_FILE)) {
|
||||||
|
fs.unlinkSync(PID_FILE);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to remove PID file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup signal handlers for graceful shutdown
|
||||||
|
*/
|
||||||
|
setupSignalHandlers() {
|
||||||
|
const shutdown = async (signal) => {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
logger.warn("Shutdown already in progress...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
logger.info(`Received ${signal}, shutting down gracefully...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop accepting new connections
|
||||||
|
if (this.httpServer) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.httpServer.close(() => {
|
||||||
|
logger.info("HTTP server closed");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove PID file
|
||||||
|
this.removePidFile();
|
||||||
|
|
||||||
|
logger.info("Agent stopped successfully");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error during shutdown: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
logger.error(`Uncaught exception: ${error.message}`);
|
||||||
|
logger.error(error.stack);
|
||||||
|
// Let launchd restart us
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
logger.error(`Unhandled promise rejection: ${reason}`);
|
||||||
|
if (reason instanceof Error) {
|
||||||
|
logger.error(reason.stack);
|
||||||
|
}
|
||||||
|
// Let launchd restart us
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the agent
|
||||||
|
const agent = new SafeChainAgent();
|
||||||
|
await agent.start();
|
||||||
|
|
||||||
|
// Keep process running
|
||||||
|
process.stdin.resume();
|
||||||
20
installer/agent/package.json
Normal file
20
installer/agent/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@aikidosec/safe-chain-agent",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Long-running macOS agent for system-wide malware protection",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "5.4.1",
|
||||||
|
"certifi": "^14.5.15",
|
||||||
|
"https-proxy-agent": "7.0.6",
|
||||||
|
"ini": "^6.0.0",
|
||||||
|
"make-fetch-happen": "14.0.3",
|
||||||
|
"node-forge": "1.3.1",
|
||||||
|
"npm-registry-fetch": "18.0.2",
|
||||||
|
"semver": "7.7.2"
|
||||||
|
},
|
||||||
|
"author": "Aikido Security",
|
||||||
|
"license": "AGPL-3.0-or-later"
|
||||||
|
}
|
||||||
14
installer/agent/settings.js
Normal file
14
installer/agent/settings.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const AGENT_PORT = 8765;
|
||||||
|
export const PROXY_HOST = "127.0.0.1";
|
||||||
|
export const INSTALL_DIR = "/Library/Application Support/AikidoSafety";
|
||||||
|
export const LOG_DIR = "/var/log/aikido-safe-chain";
|
||||||
|
export const PID_FILE = "/var/run/aikido-safe-chain.pid";
|
||||||
|
export const CONFIG_FILE = "/Library/Application Support/AikidoSafety/proxy-config.json";
|
||||||
|
export const LAUNCHD_PLIST = "/Library/LaunchDaemons/dev.aikido.safe-chain.plist";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy bypass domains
|
||||||
|
*/
|
||||||
|
export const BYPASS_DOMAINS = ["*.local", "169.254/16", "127.0.0.1", "localhost"];
|
||||||
|
|
||||||
|
export const LAUNCHD_LABEL = "dev.aikido.safe-chain";
|
||||||
25
installer/package.json
Normal file
25
installer/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@aikidosec/safe-chain-installer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "macOS installer for Aikido Safe Chain system-wide agent",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/darwin-build-installer.js",
|
||||||
|
"clean": "rm -rf build dist node_modules",
|
||||||
|
"bundle-agent": "node scripts/bundle-agent.js",
|
||||||
|
"bundle-node": "node scripts/bundle-node.js",
|
||||||
|
"create-pkg": "node scripts/create-pkg.js",
|
||||||
|
"test-install": "sudo installer -pkg build/AikidoSafeChain.pkg -target /",
|
||||||
|
"test-uninstall": "sudo bash build/uninstall.sh"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-forge": "1.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.19.130",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"author": "Aikido Security",
|
||||||
|
"license": "AGPL-3.0-or-later"
|
||||||
|
}
|
||||||
334
installer/scripts/darwin-build-installer.js
Normal file
334
installer/scripts/darwin-build-installer.js
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main build script for creating the macOS .pkg installer
|
||||||
|
*
|
||||||
|
* 1. Clean previous builds
|
||||||
|
* 2. Bundle Node.js runtime
|
||||||
|
* 3. Bundle agent code and dependencies
|
||||||
|
* 4. Generate CA certificate
|
||||||
|
* 5. Create installer scripts
|
||||||
|
* 6. Build .pkg with pkgbuild and productbuild
|
||||||
|
* 7. Sign the package (if certificates available)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const rootDir = path.join(__dirname, '..');
|
||||||
|
const buildDir = path.join(rootDir, 'build');
|
||||||
|
const distDir = path.join(rootDir, 'dist');
|
||||||
|
|
||||||
|
console.log('🏗️ Building Aikido Safe Chain macOS Installer...\n');
|
||||||
|
|
||||||
|
// Step 1: Clean and create directories
|
||||||
|
console.log('Preparing build directories...');
|
||||||
|
if (fs.existsSync(buildDir)) {
|
||||||
|
fs.rmSync(buildDir, { recursive: true });
|
||||||
|
}
|
||||||
|
if (fs.existsSync(distDir)) {
|
||||||
|
fs.rmSync(distDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(buildDir, { recursive: true });
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
|
||||||
|
const payloadDir = path.join(distDir, 'payload');
|
||||||
|
const installRoot = path.join(payloadDir, 'Library/Application Support/AikidoSafety');
|
||||||
|
fs.mkdirSync(installRoot, { recursive: true });
|
||||||
|
console.log('Directories created\n');
|
||||||
|
|
||||||
|
// Step 2: Bundle Node.js runtime
|
||||||
|
console.log('Bundling Node.js runtime...');
|
||||||
|
await bundleNodeRuntime();
|
||||||
|
console.log('Node.js bundled\n');
|
||||||
|
|
||||||
|
// Step 3: Bundle agent code
|
||||||
|
console.log('Bundling agent code...');
|
||||||
|
await bundleAgentCode();
|
||||||
|
console.log('Agent code bundled\n');
|
||||||
|
|
||||||
|
// Step 4: Generate certificates
|
||||||
|
console.log('Generating CA certificate...');
|
||||||
|
await generateCertificates();
|
||||||
|
console.log('Certificates generated\n');
|
||||||
|
|
||||||
|
// Step 5: Create LaunchDaemon plist
|
||||||
|
console.log('Creating LaunchDaemon configuration...');
|
||||||
|
await createLaunchDaemonPlist();
|
||||||
|
console.log('LaunchDaemon plist created\n');
|
||||||
|
|
||||||
|
// Step 6: Create installer scripts
|
||||||
|
console.log('Creating installer scripts...');
|
||||||
|
await createInstallerScripts();
|
||||||
|
console.log('Installer scripts created\n');
|
||||||
|
|
||||||
|
// Step 7: Create uninstaller
|
||||||
|
console.log('Creating uninstaller...');
|
||||||
|
await createUninstaller();
|
||||||
|
console.log('Uninstaller created\n');
|
||||||
|
|
||||||
|
// Step 8: Build package
|
||||||
|
console.log('Building .pkg installer...');
|
||||||
|
await buildPackage();
|
||||||
|
console.log('Package built\n');
|
||||||
|
|
||||||
|
console.log('Build complete!');
|
||||||
|
console.log(`\nInstaller: ${path.join(buildDir, 'AikidoSafeChain.pkg')}`);
|
||||||
|
console.log(`Uninstaller: ${path.join(buildDir, 'uninstall.sh')}\n`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle Node.js runtime from current installation
|
||||||
|
*/
|
||||||
|
async function bundleNodeRuntime() {
|
||||||
|
const binDir = path.join(installRoot, 'bin');
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
|
||||||
|
// Copy current Node.js binary
|
||||||
|
const nodePath = process.execPath;
|
||||||
|
const targetNodePath = path.join(binDir, 'node');
|
||||||
|
fs.copyFileSync(nodePath, targetNodePath);
|
||||||
|
fs.chmodSync(targetNodePath, 0o755);
|
||||||
|
|
||||||
|
console.log(` Copied Node.js ${process.version} to ${targetNodePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle agent code and dependencies
|
||||||
|
*/
|
||||||
|
async function bundleAgentCode() {
|
||||||
|
const agentDir = path.join(installRoot, 'agent');
|
||||||
|
fs.mkdirSync(agentDir, { recursive: true });
|
||||||
|
|
||||||
|
// Copy agent source files
|
||||||
|
const agentSrc = path.join(rootDir, 'agent');
|
||||||
|
copyDirectory(agentSrc, agentDir);
|
||||||
|
|
||||||
|
// Copy necessary dependencies from packages/safe-chain
|
||||||
|
const safeChainSrc = path.join(rootDir, '../packages/safe-chain/src');
|
||||||
|
const safeChainDest = path.join(agentDir, 'lib');
|
||||||
|
fs.mkdirSync(safeChainDest, { recursive: true });
|
||||||
|
|
||||||
|
// Copy only registry proxy code (reused by agent)
|
||||||
|
copyDirectory(
|
||||||
|
path.join(safeChainSrc, 'registryProxy'),
|
||||||
|
path.join(safeChainDest, 'registryProxy')
|
||||||
|
);
|
||||||
|
|
||||||
|
copyDirectory(
|
||||||
|
path.join(safeChainSrc, 'scanning'),
|
||||||
|
path.join(safeChainDest, 'scanning')
|
||||||
|
);
|
||||||
|
|
||||||
|
copyDirectory(
|
||||||
|
path.join(safeChainSrc, 'api'),
|
||||||
|
path.join(safeChainDest, 'api')
|
||||||
|
);
|
||||||
|
|
||||||
|
copyDirectory(
|
||||||
|
path.join(safeChainSrc, 'config'),
|
||||||
|
path.join(safeChainDest, 'config')
|
||||||
|
);
|
||||||
|
|
||||||
|
copyDirectory(
|
||||||
|
path.join(safeChainSrc, 'environment'),
|
||||||
|
path.join(safeChainDest, 'environment')
|
||||||
|
);
|
||||||
|
|
||||||
|
copyDirectory(
|
||||||
|
path.join(safeChainSrc, 'utils'),
|
||||||
|
path.join(safeChainDest, 'utils')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Install production dependencies
|
||||||
|
const agentPackageJson = path.join(agentDir, 'package.json');
|
||||||
|
if (fs.existsSync(agentPackageJson)) {
|
||||||
|
console.log(' Installing agent dependencies...');
|
||||||
|
execSync('npm install --production --no-optional', {
|
||||||
|
cwd: agentDir,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Agent code bundled to ${agentDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate CA certificate for MITM proxy
|
||||||
|
* Reuses certificate generation code from safe-chain
|
||||||
|
*/
|
||||||
|
async function generateCertificates() {
|
||||||
|
const certsDir = path.join(installRoot, 'certs');
|
||||||
|
fs.mkdirSync(certsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Import certificate generation from safe-chain
|
||||||
|
const certUtilsPath = path.join(rootDir, '../packages/safe-chain/src/registryProxy/certUtils.js');
|
||||||
|
const { generateCa } = await import(certUtilsPath);
|
||||||
|
const { default: forge } = await import('node-forge');
|
||||||
|
|
||||||
|
// Generate CA certificate with system-wide agent attributes
|
||||||
|
// (10 year validity vs 1 day for CLI, full org details for system keychain)
|
||||||
|
const { privateKey, certificate } = generateCa({
|
||||||
|
attrs: [
|
||||||
|
{ name: 'commonName', value: 'Aikido Safe Chain CA' },
|
||||||
|
{ name: 'countryName', value: 'US' },
|
||||||
|
{ shortName: 'ST', value: 'California' },
|
||||||
|
{ name: 'localityName', value: 'San Francisco' },
|
||||||
|
{ name: 'organizationName', value: 'Aikido Security' },
|
||||||
|
{ shortName: 'OU', value: 'Safe Chain' }
|
||||||
|
],
|
||||||
|
validityDays: 3650 // 10 years
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write certificate and key
|
||||||
|
const certPem = forge.pki.certificateToPem(certificate);
|
||||||
|
const keyPem = forge.pki.privateKeyToPem(privateKey);
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(certsDir, 'ca-cert.pem'), certPem);
|
||||||
|
fs.writeFileSync(path.join(certsDir, 'ca-key.pem'), keyPem);
|
||||||
|
fs.chmodSync(path.join(certsDir, 'ca-key.pem'), 0o600);
|
||||||
|
|
||||||
|
console.log(` CA certificate generated in ${certsDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create LaunchDaemon plist file
|
||||||
|
*/
|
||||||
|
async function createLaunchDaemonPlist() {
|
||||||
|
const plistDir = path.join(distDir, 'payload/Library/LaunchDaemons');
|
||||||
|
fs.mkdirSync(plistDir, { recursive: true });
|
||||||
|
|
||||||
|
const templatesDir = path.join(__dirname, 'templates');
|
||||||
|
|
||||||
|
// Read plist template
|
||||||
|
const plist = fs.readFileSync(path.join(templatesDir, 'dev.aikido.safe-chain.plist'), 'utf-8');
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(plistDir, 'dev.aikido.safe-chain.plist'), plist);
|
||||||
|
console.log(` LaunchDaemon plist created`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create installer pre/post install scripts
|
||||||
|
*/
|
||||||
|
async function createInstallerScripts() {
|
||||||
|
const scriptsDir = path.join(distDir, 'scripts');
|
||||||
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
||||||
|
|
||||||
|
const templatesDir = path.join(__dirname, 'templates');
|
||||||
|
|
||||||
|
// Read script templates
|
||||||
|
const postinstall = fs.readFileSync(path.join(templatesDir, 'postinstall.sh'), 'utf-8');
|
||||||
|
const preinstall = fs.readFileSync(path.join(templatesDir, 'preinstall.sh'), 'utf-8');
|
||||||
|
|
||||||
|
// Write scripts to dist directory
|
||||||
|
fs.writeFileSync(path.join(scriptsDir, 'postinstall'), postinstall);
|
||||||
|
fs.writeFileSync(path.join(scriptsDir, 'preinstall'), preinstall);
|
||||||
|
fs.chmodSync(path.join(scriptsDir, 'postinstall'), 0o755);
|
||||||
|
fs.chmodSync(path.join(scriptsDir, 'preinstall'), 0o755);
|
||||||
|
|
||||||
|
console.log(` Installer scripts created in ${scriptsDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create uninstaller script
|
||||||
|
*/
|
||||||
|
async function createUninstaller() {
|
||||||
|
const templatesDir = path.join(__dirname, 'templates');
|
||||||
|
|
||||||
|
// Read uninstaller template
|
||||||
|
const uninstallScript = fs.readFileSync(path.join(templatesDir, 'uninstall.sh'), 'utf-8');
|
||||||
|
|
||||||
|
// Write to both build and payload
|
||||||
|
fs.writeFileSync(path.join(buildDir, 'uninstall.sh'), uninstallScript);
|
||||||
|
fs.chmodSync(path.join(buildDir, 'uninstall.sh'), 0o755);
|
||||||
|
|
||||||
|
const installUninstallPath = path.join(installRoot, 'uninstall.sh');
|
||||||
|
fs.writeFileSync(installUninstallPath, uninstallScript);
|
||||||
|
fs.chmodSync(installUninstallPath, 0o755);
|
||||||
|
|
||||||
|
console.log(` Uninstaller created in ${buildDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the .pkg installer
|
||||||
|
*/
|
||||||
|
async function buildPackage() {
|
||||||
|
const componentPkg = path.join(buildDir, 'component.pkg');
|
||||||
|
const finalPkg = path.join(buildDir, 'AikidoSafeChain.pkg');
|
||||||
|
|
||||||
|
// Build component package
|
||||||
|
const pkgbuildCmd = [
|
||||||
|
'pkgbuild',
|
||||||
|
'--root', `"${path.join(distDir, 'payload')}"`,
|
||||||
|
'--scripts', `"${path.join(distDir, 'scripts')}"`,
|
||||||
|
'--identifier', 'dev.aikido.safe-chain',
|
||||||
|
'--version', '1.0.0',
|
||||||
|
'--install-location', '/',
|
||||||
|
`"${componentPkg}"`
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
console.log(` Running: ${pkgbuildCmd}`);
|
||||||
|
execSync(pkgbuildCmd, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
const templatesDir = path.join(__dirname, 'templates');
|
||||||
|
|
||||||
|
// Read distribution XML template
|
||||||
|
const distribution = fs.readFileSync(path.join(templatesDir, 'distribution.xml'), 'utf-8');
|
||||||
|
|
||||||
|
const distributionPath = path.join(buildDir, 'distribution.xml');
|
||||||
|
fs.writeFileSync(distributionPath, distribution);
|
||||||
|
|
||||||
|
// Create resources
|
||||||
|
const resourcesDir = path.join(buildDir, 'resources');
|
||||||
|
fs.mkdirSync(resourcesDir, { recursive: true });
|
||||||
|
|
||||||
|
// Read HTML templates
|
||||||
|
const welcomeHtml = fs.readFileSync(path.join(templatesDir, 'welcome.html'), 'utf-8');
|
||||||
|
const conclusionHtml = fs.readFileSync(path.join(templatesDir, 'conclusion.html'), 'utf-8');
|
||||||
|
|
||||||
|
// Write HTML files to resources directory
|
||||||
|
fs.writeFileSync(path.join(resourcesDir, 'welcome.html'), welcomeHtml);
|
||||||
|
fs.writeFileSync(path.join(resourcesDir, 'conclusion.html'), conclusionHtml);
|
||||||
|
|
||||||
|
// Build final package
|
||||||
|
const productbuildCmd = [
|
||||||
|
'productbuild',
|
||||||
|
'--distribution', `"${distributionPath}"`,
|
||||||
|
'--resources', `"${resourcesDir}"`,
|
||||||
|
'--package-path', `"${buildDir}"`,
|
||||||
|
`"${finalPkg}"`
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
console.log(` Running: ${productbuildCmd}`);
|
||||||
|
execSync(productbuildCmd, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
console.log(` Package created: ${finalPkg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Copy directory recursively
|
||||||
|
*/
|
||||||
|
function copyDirectory(src, dest) {
|
||||||
|
if (!fs.existsSync(src)) {
|
||||||
|
console.warn(` Warning: Source directory not found: ${src}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = path.join(src, entry.name);
|
||||||
|
const destPath = path.join(dest, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDirectory(srcPath, destPath);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
installer/scripts/templates/conclusion.html
Normal file
11
installer/scripts/templates/conclusion.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="UTF-8"></head>
|
||||||
|
<body>
|
||||||
|
<h1>Installation Complete!</h1>
|
||||||
|
<p>blablabla.</p>
|
||||||
|
<p><strong>To uninstall:</strong></p>
|
||||||
|
<pre>sudo bash "/Library/Application Support/AikidoSafety/uninstall.sh"</pre>
|
||||||
|
<p>For support, visit: <a href="https://aikido.dev">aikido.dev</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
installer/scripts/templates/dev.aikido.safe-chain.plist
Normal file
31
installer/scripts/templates/dev.aikido.safe-chain.plist
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>dev.aikido.safe-chain</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Library/Application Support/AikidoSafety/bin/node</string>
|
||||||
|
<string>/Library/Application Support/AikidoSafety/agent/index.js</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<dict>
|
||||||
|
<key>SuccessfulExit</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/var/log/aikido-safe-chain/stdout.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/var/log/aikido-safe-chain/stderr.log</string>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>NODE_ENV</key>
|
||||||
|
<string>production</string>
|
||||||
|
</dict>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Library/Application Support/AikidoSafety/agent</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
21
installer/scripts/templates/distribution.xml
Normal file
21
installer/scripts/templates/distribution.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<installer-gui-script minSpecVersion="1">
|
||||||
|
<title>Aikido Safe Chain</title>
|
||||||
|
<organization>dev.aikido</organization>
|
||||||
|
<domains enable_localSystem="true"/>
|
||||||
|
<options customize="never" require-scripts="true" rootVolumeOnly="true" />
|
||||||
|
<welcome file="welcome.html"/>
|
||||||
|
<conclusion file="conclusion.html"/>
|
||||||
|
<pkg-ref id="dev.aikido.safe-chain"/>
|
||||||
|
<options customize="never" require-scripts="true"/>
|
||||||
|
<choices-outline>
|
||||||
|
<line choice="default">
|
||||||
|
<line choice="dev.aikido.safe-chain"/>
|
||||||
|
</line>
|
||||||
|
</choices-outline>
|
||||||
|
<choice id="default"/>
|
||||||
|
<choice id="dev.aikido.safe-chain" visible="false">
|
||||||
|
<pkg-ref id="dev.aikido.safe-chain"/>
|
||||||
|
</choice>
|
||||||
|
<pkg-ref id="dev.aikido.safe-chain" version="1.0.0" onConclusion="none">component.pkg</pkg-ref>
|
||||||
|
</installer-gui-script>
|
||||||
40
installer/scripts/templates/postinstall.sh
Normal file
40
installer/scripts/templates/postinstall.sh
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INSTALL_DIR="/Library/Application Support/AikidoSafety"
|
||||||
|
LAUNCHD_PLIST="/Library/LaunchDaemons/dev.aikido.safe-chain.plist"
|
||||||
|
LOG_DIR="/var/log/aikido-safe-chain"
|
||||||
|
|
||||||
|
echo "Installing Aikido Safe Chain Agent..."
|
||||||
|
|
||||||
|
# Create log directory
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
chmod 755 "$LOG_DIR"
|
||||||
|
|
||||||
|
# Install certificate to system keychain
|
||||||
|
echo "Installing CA certificate to system keychain..."
|
||||||
|
security add-trusted-cert -d -r trustRoot \
|
||||||
|
-k /Library/Keychains/System.keychain \
|
||||||
|
"$INSTALL_DIR/certs/ca-cert.pem" || true
|
||||||
|
|
||||||
|
# Configure system proxy
|
||||||
|
echo "Configuring system proxy settings..."
|
||||||
|
"$INSTALL_DIR/bin/node" "$INSTALL_DIR/agent/configure-proxy.js" --install || {
|
||||||
|
echo "Warning: Failed to configure system proxy. You may need to configure manually."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load and start the LaunchDaemon
|
||||||
|
echo "Starting Aikido Safe Chain Agent..."
|
||||||
|
launchctl load -w "$LAUNCHD_PLIST" || {
|
||||||
|
echo "Warning: Failed to start agent. You may need to restart your computer."
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Aikido Safe Chain Agent installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "The agent is now running in the background and will protect"
|
||||||
|
echo "all package installations on this system."
|
||||||
|
echo ""
|
||||||
|
echo "To uninstall, run:"
|
||||||
|
echo " sudo bash '$INSTALL_DIR/uninstall.sh'"
|
||||||
|
|
||||||
|
exit 0
|
||||||
12
installer/scripts/templates/preinstall.sh
Normal file
12
installer/scripts/templates/preinstall.sh
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LAUNCHD_PLIST="/Library/LaunchDaemons/dev.aikido.safe-chain.plist"
|
||||||
|
|
||||||
|
# Stop existing agent if running
|
||||||
|
if [ -f "$LAUNCHD_PLIST" ]; then
|
||||||
|
echo "Stopping existing Aikido Safe Chain Agent..."
|
||||||
|
launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
39
installer/scripts/templates/uninstall.sh
Normal file
39
installer/scripts/templates/uninstall.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Aikido Safe Chain Uninstaller
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Uninstalling Aikido Safe Chain Agent..."
|
||||||
|
|
||||||
|
INSTALL_DIR="/Library/Application Support/AikidoSafety"
|
||||||
|
LAUNCHD_PLIST="/Library/LaunchDaemons/dev.aikido.safe-chain.plist"
|
||||||
|
|
||||||
|
# Stop and remove daemon
|
||||||
|
if [ -f "$LAUNCHD_PLIST" ]; then
|
||||||
|
echo "Stopping agent..."
|
||||||
|
launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true
|
||||||
|
rm "$LAUNCHD_PLIST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove certificate
|
||||||
|
echo "Removing CA certificate..."
|
||||||
|
security delete-certificate -c "Aikido Safe Chain CA" \
|
||||||
|
/Library/Keychains/System.keychain 2>/dev/null || true
|
||||||
|
|
||||||
|
# Restore proxy settings
|
||||||
|
if [ -f "$INSTALL_DIR/agent/configure-proxy.js" ]; then
|
||||||
|
echo "Restoring proxy settings..."
|
||||||
|
"$INSTALL_DIR/bin/node" "$INSTALL_DIR/agent/configure-proxy.js" --uninstall || {
|
||||||
|
echo "Warning: Failed to restore proxy settings. You may need to restore manually."
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove files
|
||||||
|
echo "Removing files..."
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
rm -rf /var/log/aikido-safe-chain
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Aikido Safe Chain has been uninstalled."
|
||||||
|
echo ""
|
||||||
|
echo "Your system proxy settings have been restored to their original state."
|
||||||
14
installer/scripts/templates/welcome.html
Normal file
14
installer/scripts/templates/welcome.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="UTF-8"></head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome to Aikido Safe Chain</h1>
|
||||||
|
<p>blablabla</p>
|
||||||
|
<p><strong>Note:</strong> This installer requires administrator privileges to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Install a trusted certificate in your system keychain</li>
|
||||||
|
<li>Configure system-wide proxy settings</li>
|
||||||
|
<li>Install a background service (LaunchDaemon)</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
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, ECOSYSTEM_ALL } 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",
|
||||||
|
|
@ -18,6 +18,42 @@ const malwareDatabaseUrls = {
|
||||||
*/
|
*/
|
||||||
export async function fetchMalwareDatabase() {
|
export async function fetchMalwareDatabase() {
|
||||||
const ecosystem = getEcoSystem();
|
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 malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
||||||
const response = await fetch(malwareDatabaseUrl);
|
const response = await fetch(malwareDatabaseUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -40,6 +76,28 @@ export async function fetchMalwareDatabase() {
|
||||||
*/
|
*/
|
||||||
export async function fetchMalwareDatabaseVersion() {
|
export async function fetchMalwareDatabaseVersion() {
|
||||||
const ecosystem = getEcoSystem();
|
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 malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
||||||
const response = await fetch(malwareDatabaseUrl, {
|
const response = await fetch(malwareDatabaseUrl, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,24 @@ export function getLoggingLevel() {
|
||||||
|
|
||||||
export const ECOSYSTEM_JS = "js";
|
export const ECOSYSTEM_JS = "js";
|
||||||
export const ECOSYSTEM_PY = "py";
|
export const ECOSYSTEM_PY = "py";
|
||||||
|
export const ECOSYSTEM_ALL = "all";
|
||||||
|
|
||||||
// Default to JavaScript ecosystem
|
// Default to JavaScript ecosystem
|
||||||
const ecosystemSettings = {
|
const ecosystemSettings = {
|
||||||
ecoSystem: ECOSYSTEM_JS,
|
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() {
|
export function getEcoSystem() {
|
||||||
return ecosystemSettings.ecoSystem;
|
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) {
|
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;
|
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 };
|
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 keys = forge.pki.rsa.generateKeyPair(2048);
|
||||||
const cert = forge.pki.createCertificate();
|
const cert = forge.pki.createCertificate();
|
||||||
cert.publicKey = keys.publicKey;
|
cert.publicKey = keys.publicKey;
|
||||||
cert.serialNumber = "01";
|
cert.serialNumber = "01";
|
||||||
cert.validity.notBefore = new Date();
|
cert.validity.notBefore = new Date();
|
||||||
cert.validity.notAfter = 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.setSubject(attrs);
|
||||||
cert.setIssuer(attrs);
|
cert.setIssuer(attrs);
|
||||||
cert.setExtensions([
|
cert.setExtensions([
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
ECOSYSTEM_JS,
|
ECOSYSTEM_JS,
|
||||||
ECOSYSTEM_PY,
|
ECOSYSTEM_PY,
|
||||||
|
ECOSYSTEM_ALL,
|
||||||
getEcoSystem,
|
getEcoSystem,
|
||||||
} from "../../config/settings.js";
|
} from "../../config/settings.js";
|
||||||
import { npmInterceptorForUrl } from "./npm/npmInterceptor.js";
|
import { npmInterceptorForUrl } from "./npm/npmInterceptor.js";
|
||||||
|
|
@ -13,6 +14,21 @@ import { pipInterceptorForUrl } from "./pipInterceptor.js";
|
||||||
export function createInterceptorForUrl(url) {
|
export function createInterceptorForUrl(url) {
|
||||||
const ecosystem = getEcoSystem();
|
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) {
|
if (ecosystem === ECOSYSTEM_JS) {
|
||||||
return npmInterceptorForUrl(url);
|
return npmInterceptorForUrl(url);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
writeDatabaseToLocalCache,
|
writeDatabaseToLocalCache,
|
||||||
} from "../config/configFile.js";
|
} from "../config/configFile.js";
|
||||||
import { ui } from "../environment/userInteraction.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
|
* @typedef {Object} MalwareDatabase
|
||||||
|
|
@ -22,6 +22,7 @@ let cachedMalwareDatabase = null;
|
||||||
* Normalize package name for comparison.
|
* Normalize package name for comparison.
|
||||||
* For Python packages (PEP-503): lowercase and replace _, -, . with -
|
* For Python packages (PEP-503): lowercase and replace _, -, . with -
|
||||||
* For js packages: keep as-is (case-sensitive)
|
* For js packages: keep as-is (case-sensitive)
|
||||||
|
* For ECOSYSTEM_ALL: We need to try both normalization strategies
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -31,6 +32,8 @@ function normalizePackageName(name) {
|
||||||
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For ECOSYSTEM_JS and ECOSYSTEM_ALL, keep as-is
|
||||||
|
// (ECOSYSTEM_ALL handles both in getPackageStatus)
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +50,37 @@ export async function openMalwareDatabase() {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getPackageStatus(name, version) {
|
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 normalizedName = normalizePackageName(name);
|
||||||
const packageData = malwareDatabase.find(
|
const packageData = malwareDatabase.find(
|
||||||
(pkg) => {
|
(pkg) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue