From d6dda73fb983b5f8aae30992c7c64925eddb0756 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 24 Oct 2025 16:21:14 +0200 Subject: [PATCH 1/6] WIP --- .../src/environment/userInteraction.js | 5 +++++ .../src/registryProxy/mitmRequestHandler.js | 18 ++++++++++++++++-- .../src/registryProxy/registryProxy.js | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 829afa1..1b4eae8 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -26,6 +26,10 @@ function writeError(message, ...optionalParams) { console.error(message, ...optionalParams); } +function writeVerboseInformation(message, ...optionalParams) { + writeInformation(message, ...optionalParams); +} + function startProcess(message) { if (isCi()) { return { @@ -89,6 +93,7 @@ async function confirm(config) { export const ui = { writeInformation, + writeVerboseInformation, writeWarning, writeError, emptyLine, diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 63a8168..b0c0af7 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -1,11 +1,16 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; +import { ui } from "../environment/userInteraction.js"; export function mitmConnect(req, clientSocket, isAllowed) { + ui.writeVerboseInformation(`Safe-chain: Set up MITM tunnel for ${req.url}`); const { hostname } = new URL(`http://${req.url}`); - clientSocket.on("error", () => { + clientSocket.on("error", (err) => { + ui.writeVerboseInformation( + `Safe-chain: Client socket error for ${req.url}: ${err.message}` + ); // NO-OP // This can happen if the client TCP socket sends RST instead of FIN. // Not subscribing to 'close' event will cause node to throw and crash. @@ -28,6 +33,9 @@ function createHttpsServer(hostname, isAllowed) { const targetUrl = `https://${hostname}${pathAndQuery}`; if (!(await isAllowed(targetUrl))) { + ui.writeVerboseInformation( + `Safe-chain: Blocking request to ${targetUrl}` + ); res.writeHead(403, "Forbidden - blocked by safe-chain"); res.end("Blocked by safe-chain"); return; @@ -57,7 +65,10 @@ function getRequestPathAndQuery(url) { function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); - proxyReq.on("error", () => { + proxyReq.on("error", (err) => { + ui.writeVerboseInformation( + `Safe-chain: Error occurred while proxying request: ${err.message}` + ); res.writeHead(502); res.end("Bad Gateway"); }); @@ -67,6 +78,9 @@ function forwardRequest(req, hostname, res) { }); req.on("end", () => { + ui.writeVerboseInformation( + `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` + ); proxyReq.end(); }); } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index b0e8dd1..b5227b4 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -109,6 +109,7 @@ function handleConnect(req, clientSocket, head) { mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket + ui.writeVerboseInformation(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } } From c5e25f4813d1a7c2952e0c8e52998dc88272ffce Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 17:09:28 +0100 Subject: [PATCH 2/6] Add verbose logging setting + setup buffering of logs to prevent interleaving logs with the package manager. --- packages/safe-chain/src/config/settings.js | 5 ++ .../src/environment/userInteraction.js | 51 ++++++++++++++++--- packages/safe-chain/src/main.js | 8 +++ .../safe-chain/src/scanning/audit/index.js | 7 +++ 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ad150b2..7f932a7 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -7,8 +7,13 @@ export function getLoggingLevel() { return LOGGING_SILENT; } + if (level === LOGGING_VERBOSE) { + return LOGGING_VERBOSE; + } + return LOGGING_NORMAL; } export const LOGGING_SILENT = "silent"; export const LOGGING_NORMAL = "normal"; +export const LOGGING_VERBOSE = "verbose"; diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index a6a2253..d81ebc9 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -2,12 +2,25 @@ import chalk from "chalk"; import ora from "ora"; import { isCi } from "./environment.js"; -import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js"; +import { + getLoggingLevel, + LOGGING_SILENT, + LOGGING_VERBOSE, +} from "../config/settings.js"; + +const state = { + bufferOutput: false, + bufferedMessages: [], +}; function isSilentMode() { return getLoggingLevel() === LOGGING_SILENT; } +function isVerboseMode() { + return getLoggingLevel() === LOGGING_VERBOSE; +} + function emptyLine() { if (isSilentMode()) return; @@ -17,7 +30,7 @@ function emptyLine() { function writeInformation(message, ...optionalParams) { if (isSilentMode()) return; - console.log(message, ...optionalParams); + writeOrBuffer(() => console.log(message, ...optionalParams)); } function writeWarning(message, ...optionalParams) { @@ -26,14 +39,14 @@ function writeWarning(message, ...optionalParams) { if (!isCi()) { message = chalk.yellow(message); } - console.warn(message, ...optionalParams); + writeOrBuffer(() => console.warn(message, ...optionalParams)); } function writeError(message, ...optionalParams) { if (!isCi()) { message = chalk.red(message); } - console.error(message, ...optionalParams); + writeOrBuffer(() => console.error(message, ...optionalParams)); } function writeExitWithoutInstallingMaliciousPackages() { @@ -41,12 +54,21 @@ function writeExitWithoutInstallingMaliciousPackages() { if (!isCi()) { message = chalk.red(message); } - console.error(message); + writeOrBuffer(() => console.error(message)); } function writeVerboseInformation(message, ...optionalParams) { - // TODO: Correctly implement verbose logging - writeInformation(message, ...optionalParams); + if (!isVerboseMode()) return; + + writeOrBuffer(() => console.log(message, ...optionalParams)); +} + +function writeOrBuffer(messageFunction) { + if (state.bufferOutput) { + state.bufferedMessages.push(messageFunction); + } else { + messageFunction(); + } } function startProcess(message) { @@ -91,6 +113,19 @@ function startProcess(message) { } } +function startBufferingLogs() { + state.bufferOutput = true; + state.bufferedMessages = []; +} + +function writeBufferedLogsAndStopBuffering() { + state.bufferOutput = false; + for (const log of state.bufferedMessages) { + log(); + } + state.bufferedMessages = []; +} + export const ui = { writeInformation, writeVerboseInformation, @@ -99,4 +134,6 @@ export const ui = { writeExitWithoutInstallingMaliciousPackages, emptyLine, startProcess, + startBufferingLogs, + writeBufferedLogsAndStopBuffering, }; diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index e106e83..3c7103a 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -25,8 +25,16 @@ export async function main(args) { } } + // Buffer logs during package manager execution, this avoids interleaving + // of logs from the package manager and safe-chain + // Not doing this could cause bugs to disappear when cursor movement codes + // are written by the package manager while safe-chain is writing logs + ui.startBufferingLogs(); const packageManagerResult = await getPackageManager().runCommand(args); + // Write all buffered logs + ui.writeBufferedLogsAndStopBuffering(); + if (!proxy.verifyNoMaliciousPackages()) { return 1; } diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 215bfa0..6bd1dec 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -1,3 +1,4 @@ +import { ui } from "../../environment/userInteraction.js"; import { MALWARE_STATUS_MALWARE, openMalwareDatabase, @@ -19,8 +20,14 @@ export async function auditChanges(changes) { ); if (malwarePackage) { + ui.writeVerboseInformation( + `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}` + ); disallowedChanges.push({ ...change, reason: malwarePackage.status }); } else { + ui.writeVerboseInformation( + `Safe-chain: Package ${change.name}@${change.version} is clean` + ); allowedChanges.push(change); } } From ddc8218a2d49291b9ca3dce11c3c87e3ca38485f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 17:14:45 +0100 Subject: [PATCH 3/6] Rename writeVerboseInformation to writeVerbose --- .../safe-chain/src/environment/userInteraction.js | 4 ++-- .../src/registryProxy/mitmRequestHandler.js | 12 +++++------- .../safe-chain/src/registryProxy/registryProxy.js | 2 +- packages/safe-chain/src/scanning/audit/index.js | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index d81ebc9..0a47959 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -57,7 +57,7 @@ function writeExitWithoutInstallingMaliciousPackages() { writeOrBuffer(() => console.error(message)); } -function writeVerboseInformation(message, ...optionalParams) { +function writeVerbose(message, ...optionalParams) { if (!isVerboseMode()) return; writeOrBuffer(() => console.log(message, ...optionalParams)); @@ -127,8 +127,8 @@ function writeBufferedLogsAndStopBuffering() { } export const ui = { + writeVerbose, writeInformation, - writeVerboseInformation, writeWarning, writeError, writeExitWithoutInstallingMaliciousPackages, diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index b0c0af7..eec59e8 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -4,11 +4,11 @@ import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; export function mitmConnect(req, clientSocket, isAllowed) { - ui.writeVerboseInformation(`Safe-chain: Set up MITM tunnel for ${req.url}`); + ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`); const { hostname } = new URL(`http://${req.url}`); clientSocket.on("error", (err) => { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Client socket error for ${req.url}: ${err.message}` ); // NO-OP @@ -33,9 +33,7 @@ function createHttpsServer(hostname, isAllowed) { const targetUrl = `https://${hostname}${pathAndQuery}`; if (!(await isAllowed(targetUrl))) { - ui.writeVerboseInformation( - `Safe-chain: Blocking request to ${targetUrl}` - ); + ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); res.writeHead(403, "Forbidden - blocked by safe-chain"); res.end("Blocked by safe-chain"); return; @@ -66,7 +64,7 @@ function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); proxyReq.on("error", (err) => { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Error occurred while proxying request: ${err.message}` ); res.writeHead(502); @@ -78,7 +76,7 @@ function forwardRequest(req, hostname, res) { }); req.on("end", () => { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` ); proxyReq.end(); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3c8b902..3822639 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -109,7 +109,7 @@ function handleConnect(req, clientSocket, head) { mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket - ui.writeVerboseInformation(`Safe-chain: Tunneling request to ${req.url}`); + ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } } diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 6bd1dec..16c54b6 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -20,12 +20,12 @@ export async function auditChanges(changes) { ); if (malwarePackage) { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}` ); disallowedChanges.push({ ...change, reason: malwarePackage.status }); } else { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Package ${change.name}@${change.version} is clean` ); allowedChanges.push(change); From 5eeb68e35501dd9de60e62a11ab001f309bffcad Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 17:20:14 +0100 Subject: [PATCH 4/6] Add documentation for verbose log level --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e385ec4..05ea2a4 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,19 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin - `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit. -Example usage: + Example usage: -```shell -npm install express --safe-chain-logging=silent -``` + ```shell + npm install express --safe-chain-logging=silent + ``` + +- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes. + + Example usage: + + ```shell + npm install express --safe-chain-logging=verbose + ``` # Usage in CI/CD From 088c215569a8c9f02407043e0cdea623d33fc76d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 10:39:24 +0100 Subject: [PATCH 5/6] Write logs on SIGTERM and SIGINT --- packages/safe-chain/src/main.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 3c7103a..00a6e7f 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -8,6 +8,9 @@ import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; import chalk from "chalk"; export async function main(args) { + process.on("SIGINT", handleProcessTermination); + process.on("SIGTERM", handleProcessTermination); + const proxy = createSafeChainProxy(); await proxy.startServer(); @@ -59,3 +62,7 @@ export async function main(args) { await proxy.stopServer(); } } + +function handleProcessTermination() { + ui.writeBufferedLogsAndStopBuffering(); +} From 932ea6b8f9306be03f0c0a1f7c3a7aa5dc499754 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 11:47:59 +0100 Subject: [PATCH 6/6] Add type information for new functions. --- package.json | 3 ++- .../safe-chain/src/environment/userInteraction.js | 12 ++++++++++++ packages/safe-chain/src/main.js | 2 -- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0193a82..6a5dec3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "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" + "lint": "npm run lint --workspace=packages/safe-chain", + "typecheck": "npm run typecheck --workspace=packages/safe-chain" }, "repository": { "type": "git", diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 14fbc4a..3222874 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -8,6 +8,9 @@ import { LOGGING_VERBOSE, } from "../config/settings.js"; +/** + * @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}} + */ const state = { bufferOutput: false, bufferedMessages: [], @@ -72,12 +75,21 @@ function writeExitWithoutInstallingMaliciousPackages() { writeOrBuffer(() => console.error(message)); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeVerbose(message, ...optionalParams) { if (!isVerboseMode()) return; writeOrBuffer(() => console.log(message, ...optionalParams)); } +/** + * + * @param {() => void} messageFunction + */ function writeOrBuffer(messageFunction) { if (state.bufferOutput) { state.bufferedMessages.push(messageFunction); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index feb5156..eea9257 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -21,7 +21,6 @@ export async function main(args) { // Global error handlers to log unhandled errors process.on("uncaughtException", (error) => { ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); - // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${error.stack}`); process.exit(1); }); @@ -29,7 +28,6 @@ export async function main(args) { process.on("unhandledRejection", (reason) => { ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); if (reason instanceof Error) { - // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${reason.stack}`); } process.exit(1);