mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
249 lines
6.4 KiB
JavaScript
249 lines
6.4 KiB
JavaScript
#!/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();
|