diff --git a/installer/.gitignore b/installer/.gitignore new file mode 100644 index 0000000..1640705 --- /dev/null +++ b/installer/.gitignore @@ -0,0 +1,17 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +build/ +dist/ + +# Logs +*.log + +# macOS +.DS_Store + +# Temporary files +*.tmp +*.temp diff --git a/installer/.npmrc b/installer/.npmrc new file mode 100644 index 0000000..bfd8d9c --- /dev/null +++ b/installer/.npmrc @@ -0,0 +1,2 @@ +# Installer build configuration +package-lock=false diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000..24ee67f --- /dev/null +++ b/installer/README.md @@ -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. diff --git a/installer/agent/configure-proxy.js b/installer/agent/configure-proxy.js new file mode 100644 index 0000000..8684ac4 --- /dev/null +++ b/installer/agent/configure-proxy.js @@ -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(); diff --git a/installer/agent/index.js b/installer/agent/index.js new file mode 100644 index 0000000..2bc2e4d --- /dev/null +++ b/installer/agent/index.js @@ -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(); diff --git a/installer/agent/package.json b/installer/agent/package.json new file mode 100644 index 0000000..3d24149 --- /dev/null +++ b/installer/agent/package.json @@ -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" +} diff --git a/installer/agent/settings.js b/installer/agent/settings.js new file mode 100644 index 0000000..b57fb74 --- /dev/null +++ b/installer/agent/settings.js @@ -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"; diff --git a/installer/package.json b/installer/package.json new file mode 100644 index 0000000..f4abaa0 --- /dev/null +++ b/installer/package.json @@ -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" +} diff --git a/installer/scripts/darwin-build-installer.js b/installer/scripts/darwin-build-installer.js new file mode 100644 index 0000000..a9a7bda --- /dev/null +++ b/installer/scripts/darwin-build-installer.js @@ -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); + } + } +} diff --git a/installer/scripts/templates/conclusion.html b/installer/scripts/templates/conclusion.html new file mode 100644 index 0000000..6980cee --- /dev/null +++ b/installer/scripts/templates/conclusion.html @@ -0,0 +1,11 @@ + + +
+ +blablabla.
+To uninstall:
+sudo bash "/Library/Application Support/AikidoSafety/uninstall.sh"+
For support, visit: aikido.dev
+ + diff --git a/installer/scripts/templates/dev.aikido.safe-chain.plist b/installer/scripts/templates/dev.aikido.safe-chain.plist new file mode 100644 index 0000000..e228056 --- /dev/null +++ b/installer/scripts/templates/dev.aikido.safe-chain.plist @@ -0,0 +1,31 @@ + + +blablabla
+Note: This installer requires administrator privileges to:
+