AikidoSec-safe-chain/installer/scripts/darwin-build-installer.js
2025-11-25 08:21:35 -08:00

334 lines
10 KiB
JavaScript

#!/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);
}
}
}