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 1. Install the `safe-chain` binary to the system PATH
2. Generate and install the CA certificate in the OS trust store 2. Generate and install the CA certificate in the OS trust store
3. Configure the system for automatic MITM proxy interception 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..." 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 # The binary is installed to the location specified by --install-location
# which is passed as $3 (installation volume/mountpoint) # which is passed as $3 (installation volume/mountpoint)
INSTALL_LOCATION="${3}/tmp/safe-chain-install" INSTALL_LOCATION="${3}/tmp/safe-chain-install"
@ -53,8 +59,6 @@ fi
echo "Starting Safe Chain proxy service..." echo "Starting Safe Chain proxy service..."
# Get the actual user to install the LaunchAgent in their home # 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}") USER_HOME=$(eval echo "~${ACTUAL_USER}")
# Create LaunchAgent for auto-start on login # 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" "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": { "node_modules/@aikidosec/safe-chain": {
"resolved": "packages/safe-chain", "resolved": "packages/safe-chain",
"link": true "link": true

View file

@ -4,15 +4,14 @@
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",
"test/e2e", "test/e2e"
"installer"
], ],
"scripts": { "scripts": {
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun", "test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
"test:e2e": "npm run test --workspace=test/e2e", "test:e2e": "npm run test --workspace=test/e2e",
"lint": "npm run lint --workspace=packages/safe-chain", "lint": "npm run lint --workspace=packages/safe-chain",
"typecheck": "npm run typecheck --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": { "repository": {
"type": "git", "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 { generateCACertificate } from "../registryProxy/certUtils.js";
import { writeFileSync, mkdirSync } from "node:fs"; import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
@ -12,6 +7,7 @@ import chalk from "chalk";
/** /**
* Generate certificate command * Generate certificate command
* Allows us to call this independently, for instance from the installer.
* @param {string[]} args - Command line arguments * @param {string[]} args - Command line arguments
*/ */
export async function generateCertCommand(args) { export async function generateCertCommand(args) {
@ -26,9 +22,6 @@ export async function generateCertCommand(args) {
} }
try { try {
ui.writeInformation(chalk.bold("Generating Safe Chain CA certificate..."));
ui.emptyLine();
// Create output directory // Create output directory
mkdirSync(outputDir, { recursive: true }); mkdirSync(outputDir, { recursive: true });
@ -41,20 +34,7 @@ export async function generateCertCommand(args) {
writeFileSync(certPath, cert); writeFileSync(certPath, cert);
writeFileSync(keyPath, key); 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) { } catch (/** @type {any} */ error) {
ui.writeError(`Failed to generate certificate: ${error.message}`);
process.exit(1); 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 { ui } from "../environment/userInteraction.js";
import chalk from "chalk"; import chalk from "chalk";
import { initializeCliArguments } from "../config/cliArguments.js"; import { initializeCliArguments } from "../config/cliArguments.js";
import { writeProxyState, clearProxyState } from "./proxyState.js";
import { getCaCertPath } from "../registryProxy/certUtils.js"; import { getCaCertPath } from "../registryProxy/certUtils.js";
/** /**
@ -34,15 +33,6 @@ export async function runCommand(args) {
// Setup event listeners // Setup event listeners
service.on("started", ({ port, url }) => { 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.emptyLine();
ui.writeInformation(chalk.green("✔") + " Safe Chain proxy started successfully!"); ui.writeInformation(chalk.green("✔") + " Safe Chain proxy started successfully!");
ui.emptyLine(); ui.emptyLine();
@ -68,9 +58,6 @@ export async function runCommand(args) {
}); });
service.on("stopped", ({ blockedPackages }) => { service.on("stopped", ({ blockedPackages }) => {
// Clear proxy state file
clearProxyState();
ui.emptyLine(); ui.emptyLine();
ui.writeInformation(chalk.yellow("Proxy stopped.")); 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 { createSafeChainProxy } from "./registryProxy/registryProxy.js";
import chalk from "chalk"; import chalk from "chalk";
import { getAuditStats } from "./scanning/audit/index.js"; import { getAuditStats } from "./scanning/audit/index.js";
import { readProxyState } from "./agent/proxyState.js";
/** /**
* @param {string[]} args * @param {string[]} args
@ -18,32 +17,15 @@ export async function main(args) {
process.on("SIGTERM", handleProcessTermination); process.on("SIGTERM", handleProcessTermination);
// Check if a proxy is already running from 'safe-chain run' // Check if a proxy is already running from 'safe-chain run'
const existingProxy = readProxyState(); // In the new agent architecture, we rely on system-wide environment variables
const usingExistingProxy = existingProxy !== null; // 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; let proxy;
if (usingExistingProxy) { // No existing proxy logic needed anymore as we don't wrap commands when using the agent
// 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
proxy = createSafeChainProxy(); proxy = createSafeChainProxy();
await proxy.startServer(); await proxy.startServer();
}
// Global error handlers to log unhandled errors // Global error handlers to log unhandled errors
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
@ -91,20 +73,12 @@ export async function main(args) {
const auditStats = getAuditStats(); const auditStats = getAuditStats();
if (auditStats.totalPackages > 0) { if (auditStats.totalPackages > 0) {
ui.emptyLine(); ui.emptyLine();
if (usingExistingProxy) {
ui.writeInformation(
`${chalk.green("✔")} Safe-chain: Scanned ${
auditStats.totalPackages
} packages via proxy, no malware found.`
);
} else {
ui.writeInformation( ui.writeInformation(
`${chalk.green("✔")} Safe-chain: Scanned ${ `${chalk.green("✔")} Safe-chain: Scanned ${
auditStats.totalPackages auditStats.totalPackages
} packages, no malware found.` } packages, no malware found.`
); );
} }
}
// Returning the exit code back to the caller allows the promise // Returning the exit code back to the caller allows the promise
// to be awaited in the bin files and return the correct exit code // 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 // to be awaited in the bin files and return the correct exit code
return 1; return 1;
} finally { } finally {
// Only stop the proxy if we started it (not using existing proxy)
if (!usingExistingProxy) {
await proxy.stopServer(); await proxy.stopServer();
} }
} }
}
function handleProcessTermination() { function handleProcessTermination() {
ui.writeBufferedLogsAndStopBuffering(); ui.writeBufferedLogsAndStopBuffering();