Add installer build scripts and configuration

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

17
installer/.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,2 @@
# Installer build configuration
package-lock=false

3
installer/README.md Normal file
View 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.

View 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
View 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();

View 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"
}

View 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
View 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"
}

View 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);
}
}
}

View 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>

View 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>

View 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>

View 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

View 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

View 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."

View 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>