Some cleanup

This commit is contained in:
Reinier Criel 2025-11-19 13:54:12 -08:00
parent 97bbc77162
commit c71320386e
13 changed files with 1601 additions and 240 deletions

View file

@ -9,3 +9,26 @@ The installer bundles the Safe Chain Node.js application into a standalone binar
1. Install the `safe-chain` binary to the system PATH
2. Generate and install the CA certificate in the OS trust store
3. Configure the system for automatic MITM proxy interception
## Building the Installer
To build the installer for the current platform, run the following command from the root of the workspace:
```bash
npm run build:installer
```
To build for a specific platform, you can pass arguments to the script:
```bash
# macOS
npm run build:installer -- --platform=macos
# Linux
npm run build:installer -- --platform=linux
# Windows
npm run build:installer -- --platform=windows
```
The build artifacts (binaries and installer packages) will be created in the `installer/dist` directory.

View file

@ -1,46 +0,0 @@
#!/usr/bin/env node
/**
* Wrapper script for certificate generation during installer build
* This re-exports the certificate generation functionality from the main package
*/
import { generateCACertificate } from '../packages/safe-chain/src/registryProxy/certUtils.js';
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
/**
* Generate certificate files (simple version for installer build)
* For the full CLI version with nice output, use: safe-chain _generate-cert
*
* @param {string} outputDir - Directory to save certificate files
* @returns {Promise<{certPath: string, keyPath: string}>}
*/
export async function generateCertificates(outputDir) {
console.log('Generating Safe Chain CA certificate...');
mkdirSync(outputDir, { recursive: true });
const { cert, key } = generateCACertificate();
const certPath = join(outputDir, 'ca-cert.pem');
const keyPath = join(outputDir, 'ca-key.pem');
writeFileSync(certPath, cert);
writeFileSync(keyPath, key);
console.log('✓ Certificate generated:');
console.log(` Certificate: ${certPath}`);
console.log(` Private Key: ${keyPath}`);
return { certPath, keyPath };
}
// CLI usage - when run directly
if (import.meta.url === `file://${process.argv[1]}`) {
const outputDir = process.argv[2] || './certs';
generateCertificates(outputDir).catch(error => {
console.error('Error generating certificates:', error);
process.exit(1);
});
}

1545
installer/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
#!/bin/bash
set -e
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
INSTALLER_ROOT="${SCRIPT_DIR}/.."
echo "=== Building Safe Chain Installer for macOS ==="
# Ensure we are in the installer directory
cd "${INSTALLER_ROOT}"
# Install dependencies
echo "Installing build dependencies..."
npm install
# Build the binary and installer using the Node.js build script
echo "Building binary and installer..."
node build.js --platform=macos
echo "Done."

View file

@ -3,6 +3,12 @@ set -e
echo "Installing Safe Chain..."
# Get the actual user (console user)
ACTUAL_USER=$(stat -f '%Su' /dev/console)
if [ -z "$ACTUAL_USER" ] || [ "$ACTUAL_USER" = "root" ]; then
echo "Warning: Could not detect console user, defaulting to root ownership might cause issues."
fi
# The binary is installed to the location specified by --install-location
# which is passed as $3 (installation volume/mountpoint)
INSTALL_LOCATION="${3}/tmp/safe-chain-install"
@ -53,8 +59,6 @@ fi
echo "Starting Safe Chain proxy service..."
# Get the actual user to install the LaunchAgent in their home
# (We still need this for the LaunchAgent, but not for file permissions)
ACTUAL_USER=$(stat -f '%Su' /dev/console)
USER_HOME=$(eval echo "~${ACTUAL_USER}")
# Create LaunchAgent for auto-start on login

View file

@ -1,5 +0,0 @@
#!/bin/bash
echo "=== Building Safe Chain Installer for Linux ==="
echo "TODO: Implement Linux installer build"
echo "This is a placeholder script."
exit 0

View file

@ -1,5 +0,0 @@
@echo off
echo === Building Safe Chain Installer for Windows ===
echo TODO: Implement Windows installer build
echo This is a placeholder script.
exit /b 0

11
package-lock.json generated
View file

@ -15,6 +15,17 @@
"oxlint": "^1.22.0"
}
},
"installer": {
"name": "@aikidosec/safe-chain-installer",
"version": "1.0.0",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"@yao-pkg/pkg": "^5.15.0",
"esbuild": "^0.24.0"
},
"devDependencies": {}
},
"node_modules/@aikidosec/safe-chain": {
"resolved": "packages/safe-chain",
"link": true

View file

@ -4,15 +4,14 @@
"type": "module",
"workspaces": [
"packages/*",
"test/e2e",
"installer"
"test/e2e"
],
"scripts": {
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
"test:e2e": "npm run test --workspace=test/e2e",
"lint": "npm run lint --workspace=packages/safe-chain",
"typecheck": "npm run typecheck --workspace=packages/safe-chain",
"build:installer": "npm run build --workspace=installer"
"build:installer": "cd installer && npm install && npm run build"
},
"repository": {
"type": "git",

View file

@ -1,8 +1,3 @@
/**
* Generate certificate command for Safe Chain
* Creates CA certificate and key for MITM proxy
*/
import { generateCACertificate } from "../registryProxy/certUtils.js";
import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
@ -12,6 +7,7 @@ import chalk from "chalk";
/**
* Generate certificate command
* Allows us to call this independently, for instance from the installer.
* @param {string[]} args - Command line arguments
*/
export async function generateCertCommand(args) {
@ -26,9 +22,6 @@ export async function generateCertCommand(args) {
}
try {
ui.writeInformation(chalk.bold("Generating Safe Chain CA certificate..."));
ui.emptyLine();
// Create output directory
mkdirSync(outputDir, { recursive: true });
@ -41,20 +34,7 @@ export async function generateCertCommand(args) {
writeFileSync(certPath, cert);
writeFileSync(keyPath, key);
ui.writeInformation(chalk.green("✓") + " Certificate generated successfully!");
ui.emptyLine();
ui.writeInformation(chalk.bold("Files created:"));
ui.writeInformation(` Certificate: ${chalk.cyan(certPath)}`);
ui.writeInformation(` Private Key: ${chalk.cyan(keyPath)}`);
ui.emptyLine();
ui.writeInformation(chalk.dim("To install this certificate in your system trust store:"));
ui.writeInformation(chalk.dim(" macOS: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain " + certPath));
ui.writeInformation(chalk.dim(" Linux: sudo cp " + certPath + " /usr/local/share/ca-certificates/ && sudo update-ca-certificates"));
ui.writeInformation(chalk.dim(" Windows: certutil -addstore -f ROOT " + certPath));
ui.emptyLine();
} catch (/** @type {any} */ error) {
ui.writeError(`Failed to generate certificate: ${error.message}`);
process.exit(1);
}
}

View file

@ -1,82 +0,0 @@
import fs from "fs";
import path from "path";
import os from "os";
/**
* Get the path to the proxy state file
* @returns {string}
*/
export function getProxyStateFilePath() {
const homeDir = os.homedir();
const safeChainDir = path.join(homeDir, ".safe-chain");
// Ensure directory exists
if (!fs.existsSync(safeChainDir)) {
fs.mkdirSync(safeChainDir, { recursive: true });
}
return path.join(safeChainDir, "proxy-state.json");
}
/**
* Write the proxy state to a file that shell scripts can read
* @param {{port: number, url: string, pid: number, ecosystem: string, certPath: string}} state
*/
export function writeProxyState(state) {
const statePath = getProxyStateFilePath();
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
}
/**
* Read the current proxy state
* @returns {{port: number, url: string, pid: number, ecosystem: string, certPath: string} | null}
*/
export function readProxyState() {
const statePath = getProxyStateFilePath();
if (!fs.existsSync(statePath)) {
return null;
}
try {
const content = fs.readFileSync(statePath, "utf-8");
const state = JSON.parse(content);
// Verify the process is still running
if (state.pid) {
try {
// Sending signal 0 checks if process exists without actually sending a signal
process.kill(state.pid, 0);
return state;
} catch {
// Process doesn't exist, clean up state file
clearProxyState();
return null;
}
}
return state;
} catch {
return null;
}
}
/**
* Clear the proxy state file
*/
export function clearProxyState() {
const statePath = getProxyStateFilePath();
if (fs.existsSync(statePath)) {
fs.unlinkSync(statePath);
}
}
/**
* Check if a proxy is currently running
* @returns {boolean}
*/
export function isProxyRunning() {
const state = readProxyState();
return state !== null;
}

View file

@ -2,7 +2,6 @@ import { StandaloneProxyService } from "./standaloneProxy.js";
import { ui } from "../environment/userInteraction.js";
import chalk from "chalk";
import { initializeCliArguments } from "../config/cliArguments.js";
import { writeProxyState, clearProxyState } from "./proxyState.js";
import { getCaCertPath } from "../registryProxy/certUtils.js";
/**
@ -34,15 +33,6 @@ export async function runCommand(args) {
// Setup event listeners
service.on("started", ({ port, url }) => {
// Write proxy state to file so shell integration can detect it
writeProxyState({
port,
url,
pid: process.pid,
ecosystem: 'all',
certPath: getCaCertPath(),
});
ui.emptyLine();
ui.writeInformation(chalk.green("✔") + " Safe Chain proxy started successfully!");
ui.emptyLine();
@ -68,9 +58,6 @@ export async function runCommand(args) {
});
service.on("stopped", ({ blockedPackages }) => {
// Clear proxy state file
clearProxyState();
ui.emptyLine();
ui.writeInformation(chalk.yellow("Proxy stopped."));

View file

@ -7,7 +7,6 @@ import { initializeCliArguments } from "./config/cliArguments.js";
import { createSafeChainProxy } from "./registryProxy/registryProxy.js";
import chalk from "chalk";
import { getAuditStats } from "./scanning/audit/index.js";
import { readProxyState } from "./agent/proxyState.js";
/**
* @param {string[]} args
@ -18,32 +17,15 @@ export async function main(args) {
process.on("SIGTERM", handleProcessTermination);
// Check if a proxy is already running from 'safe-chain run'
const existingProxy = readProxyState();
const usingExistingProxy = existingProxy !== null;
// In the new agent architecture, we rely on system-wide environment variables
// so we don't need to detect or connect to an existing proxy here.
// The 'main' function is now only used when running 'aikido-npm' etc. directly
// (legacy wrapper mode) or when running 'safe-chain run' (which doesn't call main() directly)
let proxy;
if (usingExistingProxy) {
// Use the existing proxy - don't start a new one
ui.writeInformation(`Safe-chain: Using existing proxy at ${existingProxy.url}`);
// Create a proxy object that uses the existing proxy
// We need to set the environment variables to point to the existing proxy
const url = new URL(existingProxy.url);
const port = parseInt(url.port);
// Import and set the proxy state so getSafeChainProxyEnvironmentVariables works
const { setProxyState } = await import("./registryProxy/registryProxy.js");
setProxyState(port, existingProxy.certPath);
proxy = {
verifyNoMaliciousPackages: () => true, // Existing proxy handles this
getBlockedRequests: () => [], // Can't access blocked requests from existing proxy
stopServer: async () => {}, // Don't stop the existing proxy
};
} else {
// No existing proxy, start one inline
// No existing proxy logic needed anymore as we don't wrap commands when using the agent
proxy = createSafeChainProxy();
await proxy.startServer();
}
// Global error handlers to log unhandled errors
process.on("uncaughtException", (error) => {
@ -91,20 +73,12 @@ export async function main(args) {
const auditStats = getAuditStats();
if (auditStats.totalPackages > 0) {
ui.emptyLine();
if (usingExistingProxy) {
ui.writeInformation(
`${chalk.green("✔")} Safe-chain: Scanned ${
auditStats.totalPackages
} packages via proxy, no malware found.`
);
} else {
ui.writeInformation(
`${chalk.green("✔")} Safe-chain: Scanned ${
auditStats.totalPackages
} packages, no malware found.`
);
}
}
// Returning the exit code back to the caller allows the promise
// to be awaited in the bin files and return the correct exit code
@ -116,12 +90,9 @@ export async function main(args) {
// to be awaited in the bin files and return the correct exit code
return 1;
} finally {
// Only stop the proxy if we started it (not using existing proxy)
if (!usingExistingProxy) {
await proxy.stopServer();
}
}
}
function handleProcessTermination() {
ui.writeBufferedLogsAndStopBuffering();