From d6dda73fb983b5f8aae30992c7c64925eddb0756 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 24 Oct 2025 16:21:14 +0200 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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); From 14c4c4997edbf5b75b81a82e327545a6e4a9dcd1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 13:57:29 +0100 Subject: [PATCH 7/9] Remove @ts-expect-error suppressions --- packages/safe-chain/bin/aikido-bun.js | 1 - packages/safe-chain/bin/aikido-bunx.js | 1 - packages/safe-chain/bin/aikido-npm.js | 1 - packages/safe-chain/bin/aikido-npx.js | 1 - packages/safe-chain/bin/aikido-pnpm.js | 1 - packages/safe-chain/bin/aikido-pnpx.js | 1 - packages/safe-chain/bin/aikido-yarn.js | 1 - packages/safe-chain/src/main.js | 2 +- .../bun/createBunPackageManager.js | 1 - .../src/packagemanager/npm/runNpmCommand.js | 35 ------------------- .../src/packagemanager/npx/runNpxCommand.js | 1 - .../src/packagemanager/pnpm/runPnpmCommand.js | 2 -- .../src/packagemanager/yarn/runYarnCommand.js | 1 - .../src/registryProxy/mitmRequestHandler.js | 19 +++++++--- .../src/registryProxy/plainHttpProxy.js | 18 ++++++++-- .../src/registryProxy/registryProxy.js | 10 +++--- .../src/registryProxy/tunnelRequestHandler.js | 26 +++++++------- packages/safe-chain/src/scanning/index.js | 4 +-- .../src/scanning/malwareDatabase.js | 7 ++-- packages/safe-chain/src/utils/safeSpawn.js | 8 +++-- 20 files changed, 62 insertions(+), 79 deletions(-) diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index 2467512..01e3972 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -7,5 +7,4 @@ const packageManagerName = "bun"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index 6a84ff8..fb378e5 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -7,5 +7,4 @@ const packageManagerName = "bunx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 4a82c34..0e9f302 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -7,5 +7,4 @@ const packageManagerName = "npm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index 40118d4..d3dfdd6 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -7,5 +7,4 @@ const packageManagerName = "npx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index e495568..0a06217 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -7,5 +7,4 @@ const packageManagerName = "pnpm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 75f093d..cdb6504 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -7,5 +7,4 @@ const packageManagerName = "pnpx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index 3ca5b94..fd87606 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -7,5 +7,4 @@ const packageManagerName = "yarn"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index eea9257..3fba24f 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -9,7 +9,7 @@ import chalk from "chalk"; /** * @param {string[]} args - * @returns {Promise} + * @returns {Promise} */ export async function main(args) { process.on("SIGINT", handleProcessTermination); diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 9716261..037a512 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -39,7 +39,6 @@ async function runBunCommand(command, args) { try { const result = await safeSpawn(command, args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index c068240..af57fad 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -11,7 +11,6 @@ export async function runNpm(args) { try { const result = await safeSpawn("npm", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; @@ -24,37 +23,3 @@ export async function runNpm(args) { } } } - -/** - * @param {string[]} args - * @returns {Promise<{status: number, output?: string}>} - */ -export async function dryRunNpmCommandAndOutput(args) { - try { - const result = await safeSpawn( - "npm", - [...args, "--ignore-scripts", "--dry-run"], - { - stdio: "pipe", - // @ts-expect-error values of process.env can be string | undefined - env: mergeSafeChainProxyEnvironmentVariables(process.env), - } - ); - return { - status: result.status, - output: result.status === 0 ? result.stdout : result.stderr, - }; - } catch (/** @type any */ error) { - if (error.status) { - const output = - error.stdout?.toString() ?? - error.stderr?.toString() ?? - error.message ?? - ""; - return { status: error.status, output }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } - } -} diff --git a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index 61adcaa..2501b79 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -11,7 +11,6 @@ export async function runNpx(args) { try { const result = await safeSpawn("npx", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index db4ffa9..d958fb8 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -13,13 +13,11 @@ export async function runPnpmCommand(args, toolName = "pnpm") { if (toolName === "pnpm") { result = await safeSpawn("pnpm", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else if (toolName === "pnpx") { result = await safeSpawn("pnpx", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else { diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 1ba0f5c..04650f7 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -9,7 +9,6 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg */ export async function runYarnCommand(args) { try { - // @ts-expect-error values of process.env can be string | undefined const env = mergeSafeChainProxyEnvironmentVariables(process.env); await fixYarnProxyEnvironmentVariables(env); diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index c990a98..6f7b20e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,7 +5,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {(target: string) => Promise} isAllowed */ export function mitmConnect(req, clientSocket, isAllowed) { @@ -25,7 +25,6 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); - // @ts-expect-error Property 'headersSent' does not exist on type 'Socket' if (!clientSocket.headersSent) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } else if (clientSocket.writable) { @@ -55,7 +54,13 @@ function createHttpsServer(hostname, isAllowed) { * @returns {Promise} */ async function handleRequest(req, res) { - // @ts-expect-error req.url might be undefined + if (!req.url) { + ui.writeError("Safe-chain: Request missing URL"); + res.writeHead(400, "Bad Request"); + res.end("Bad Request: Missing URL"); + return; + } + const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; @@ -163,7 +168,13 @@ function createProxyRequest(hostname, req, res) { } }); - // @ts-expect-error statusCode might be undefined + if (!proxyRes.statusCode) { + ui.writeError("Safe-chain: Proxy response missing status code"); + res.writeHead(500); + res.end("Internal Server Error"); + return; + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 16b305a..75b9d77 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,5 +1,6 @@ import * as http from "http"; import * as https from "https"; +import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req @@ -8,7 +9,13 @@ import * as https from "https"; * @returns {void} */ export function handleHttpProxyRequest(req, res) { - // @ts-expect-error req.url might be undefined + if (!req.url) { + ui.writeError("Safe-chain: Request missing URL"); + res.writeHead(400, "Bad Request"); + res.end("Bad Request: Missing URL"); + return; + } + const url = new URL(req.url); // The protocol for the plainHttpProxy should usually only be http: @@ -27,11 +34,16 @@ export function handleHttpProxyRequest(req, res) { const proxyRequest = protocol .request( - // @ts-expect-error req.url might be undefined req.url, { method: req.method, headers: req.headers }, (proxyRes) => { - // @ts-expect-error statusCode might be undefined + if (!proxyRes.statusCode) { + ui.writeError("Safe-chain: Proxy response missing status code"); + res.writeHead(500); + res.end("Internal Server Error"); + return; + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index ed103b2..9e7bb7b 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -43,7 +43,7 @@ function getSafeChainProxyEnvironmentVariables() { } /** - * @param {Record} env + * @param {Record} env * * @returns {Record} */ @@ -56,7 +56,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { // So we only copy the variable if it's not already set in a different case const upperKey = key.toUpperCase(); - if (!proxyEnv[upperKey]) { + if (!proxyEnv[upperKey] && env[key]) { proxyEnv[key] = env[key]; } } @@ -122,7 +122,7 @@ function stopServer(server) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -130,9 +130,9 @@ function stopServer(server) { function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL + const url = req.url; - // @ts-expect-error req.url might be undefined - if (knownRegistries.some((reg) => req.url.includes(reg))) { + if (url && knownRegistries.some((reg) => url.includes(reg))) { // For npm and yarn registries, we want to intercept and inspect the traffic // so we can block packages with malware mitmConnect(req, clientSocket, isAllowedUrl); diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 452ef15..4b756d7 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -3,7 +3,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -30,7 +30,7 @@ export function tunnelRequest(req, clientSocket, head) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -38,13 +38,16 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); - // @ts-expect-error port from URL is a string but net.connect accepts number - const serverSocket = net.connect(port || 443, hostname, () => { - clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); - serverSocket.write(head); - serverSocket.pipe(clientSocket); - clientSocket.pipe(serverSocket); - }); + const serverSocket = net.connect( + Number.parseInt(port) || 443, + hostname, + () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + } + ); clientSocket.on("error", () => { // This can happen if the client TCP socket sends RST instead of FIN. @@ -66,7 +69,7 @@ function tunnelRequestToDestination(req, clientSocket, head) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * @param {string} proxyUrl */ @@ -75,10 +78,9 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { const proxy = new URL(proxyUrl); // Connect to proxy server - // @ts-expect-error net.connect wants port as number but proxy.port is string const proxySocket = net.connect({ host: proxy.hostname, - port: proxy.port, + port: Number.parseInt(proxy.port) || 80, }); proxySocket.on("connect", () => { diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index d8e817e..44ff57c 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -21,11 +21,11 @@ export function shouldScanCommand(args) { /** * @param {string[]} args * - * @returns {Promise} + * @returns {Promise} */ export async function scanCommand(args) { if (!shouldScanCommand(args)) { - return []; + return 0; } let timedOut = false; diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index b54846e..03c7081 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -71,8 +71,11 @@ async function getMalwareDatabase() { } const { malwareDatabase, version } = await fetchMalwareDatabase(); - // @ts-expect-error version can be undefined - writeDatabaseToLocalCache(malwareDatabase, version); + + if (version) { + // Only cache the malware database when we have a version. + writeDatabaseToLocalCache(malwareDatabase, version); + } return malwareDatabase; } catch (/** @type any */ error) { diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 489d070..e17bdb5 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -67,8 +67,6 @@ function resolveCommandPath(command) { // Use 'command -v' to find the full path const fullPath = execSync(`command -v ${command}`, { encoding: "utf8", - // @ts-expect-error shell is a string option - shell: true, }).trim(); if (!fullPath) { @@ -120,8 +118,12 @@ export async function safeSpawn(command, args, options = {}) { }); child.on("close", (code) => { + // Code is null if it terminated by a signal. This should never + // happen in our code. If this happens, return 1 error code. + + code = code ?? 1; + resolve({ - // @ts-expect-error code can be null status: code, stdout: stdout, stderr: stderr, From 3ea4e82acb6b058f2c504298027e55616573fccc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 4 Nov 2025 11:26:07 +0100 Subject: [PATCH 8/9] Write a warning if no version was returned from the malware download, causing the malware db not to be cached. --- packages/safe-chain/src/scanning/malwareDatabase.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 03c7081..539044b 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -75,9 +75,15 @@ async function getMalwareDatabase() { if (version) { // Only cache the malware database when we have a version. writeDatabaseToLocalCache(malwareDatabase, version); + return malwareDatabase; + } else { + // We received a valid malware database, but the response + // did not contain an etag header with the version + ui.writeWarning( + "The malware database was downloaded, but could not be cached due to a missing version." + ); + return malwareDatabase; } - - return malwareDatabase; } catch (/** @type any */ error) { if (cachedDatabase) { ui.writeWarning( From 497401e8e053c440ea39817bf69e6405437024c8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 4 Nov 2025 13:18:36 +0100 Subject: [PATCH 9/9] Remove yarn version check --- .../src/packagemanager/yarn/runYarnCommand.js | 26 ++----------------- .../yarn/runYarnCommand.spec.js | 4 +-- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 04650f7..2089551 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -35,29 +35,7 @@ export async function runYarnCommand(args) { async function fixYarnProxyEnvironmentVariables(env) { // Yarn ignores standard proxy environment variable HTTPS_PROXY // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. - // Don't use YARN_HTTPS_CA_FILE_PATH though, as it causes to ignore all system CAs + // Don't use YARN_HTTPS_CA_FILE_PATH or YARN_CA_FILE_PATH though, it causes yarn to ignore all system CAs - // Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs - // When setting all variables, yarn returns an error about conflicting variables - // - v2/v3: "Usage Error: Unrecognized or legacy configuration settings found: httpsCaFilePath" - // - v4+: "Usage Error: Unrecognized or legacy configuration settings found: caFilePath" - - const version = await yarnVersion(); - const majorVersion = parseInt(version.split(".")[0]); - - if (majorVersion >= 4) { - env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - } else if (majorVersion === 2 || majorVersion === 3) { - env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - } -} - -async function yarnVersion() { - const result = await safeSpawn("yarn", ["--version"], { - stdio: "pipe", - }); - if (result.status !== 0) { - throw new Error("Failed to get yarn version"); - } - return result.stdout.trim(); + env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; } diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js index bd3d04d..21475f9 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js @@ -103,13 +103,13 @@ describe("runYarnCommand", () => { ); }); - it("should not set Yarn-specific proxy vars for Yarn v1", async () => { + it("should set YARN_HTTPS_PROXY for Yarn v1", async () => { yarnVersion = "1.22.19"; await runYarnCommand(["add", "lodash"]); assert.strictEqual( capturedEnv.YARN_HTTPS_PROXY, - undefined, + "http://localhost:8080", "YARN_HTTPS_PROXY should not be set for Yarn v1" ); assert.strictEqual(