From 9d1f7ac6fdf316bb211e78ebb8c66dd826307823 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 14:15:30 +0100 Subject: [PATCH 01/98] Use ramaproxy if it's available. --- install-scripts/install-safe-chain.sh | 16 ++ .../src/registryProxy/createRamaProxy.js | 102 +++++++ .../src/registryProxy/registryProxy.js | 265 ++++++++++-------- 3 files changed, 264 insertions(+), 119 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/createRamaProxy.js diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 7ee07c2..5d00bdb 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -317,6 +317,22 @@ main() { info "Binary installed to: $FINAL_FILE" + # Download safechain-proxy for macOS only + if [ "$OS" = "macos" ]; then + info "Downloading safechain-proxy..." + + if [ "$ARCH" = "arm64" ]; then + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-arm64" + else + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-amd64" + fi + + PROXY_FILE="${INSTALL_DIR}/safechain-proxy" + download "$PROXY_URL" "$PROXY_FILE" + chmod +x "$PROXY_FILE" || error "Failed to make proxy executable" + info "Proxy installed to: $PROXY_FILE" + fi + # Build setup command based on arguments SETUP_CMD="setup" SETUP_ARGS="" diff --git a/packages/safe-chain/src/registryProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/createRamaProxy.js new file mode 100644 index 0000000..90742ad --- /dev/null +++ b/packages/safe-chain/src/registryProxy/createRamaProxy.js @@ -0,0 +1,102 @@ +import { ChildProcess, spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdtempSync, readFile, writeFile } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { ui } from "../environment/userInteraction.js"; + +const readFilePromise = promisify(readFile); +const writeFilePromise = promisify(writeFile); + +/** + * @typedef {Object} RamaProxyInstance + * @property {ChildProcess} process + * @property {string} proxyAddress + * @property {string} metaAddress + * @property {string} certPath + */ + +/** + * @returns {String | null} + */ +export function getRamaPath() { + const executableDir = dirname(process.execPath); + const ramaPath = join(executableDir, "safechain-proxy"); + + ui.writeWarning(ramaPath); + + if (existsSync(ramaPath)) { + return ramaPath; + } + + return null; +} + +/** + * @param {string} ramaPath + * + * @returns {import("./registryProxy.js").SafeChainProxy} */ +export function createRamaProxy(ramaPath) { + const tempDir = mkdtempSync(join(tmpdir(), "safe-chain-proxy-")); + /** @type {RamaProxyInstance | null} */ + let ramaInstance = null; + + return { + startServer: async () => { + ramaInstance = await startRama(ramaPath, tempDir); + ui.writeInformation( + `Proxy started at address "${ramaInstance.proxyAddress}"` + ); + }, + stopServer: async () => { + if (ramaInstance) { + ramaInstance.process.kill(); + } + return Promise.resolve(); + }, + verifyNoMaliciousPackages: () => true, + hasSuppressedVersions: () => false, + getServerPort: () => { + if (!ramaInstance) return null; + const url = new URL(`http://${ramaInstance.proxyAddress}`); + return url.port ? parseInt(url.port, 10) : null; + }, + getCombinedCaBundlePath: () => ramaInstance?.certPath ?? "", + }; +} + +/** + * @param {string} ramaPath + * @param {string} dataFolder + * @returns {Promise} + */ +async function startRama(ramaPath, dataFolder) { + let process = spawn(ramaPath, ["--secrets", "memory", "--data", dataFolder], { + stdio: "inherit", + }); + + // wait some time to allow the proxy process to start + await new Promise((resolve) => setTimeout(resolve, 5000)); + + const proxyAddress = await readFilePromise( + join(dataFolder, "proxy.addr.txt"), + "utf-8" + ); + const metaAddress = await readFilePromise( + join(dataFolder, "meta.addr.txt"), + "utf-8" + ); + + const certResponse = await fetch(`http://${metaAddress}/ca`); + const cert = await certResponse.text(); + const certPath = join(dataFolder, "cert.ca"); + await writeFilePromise(certPath, cert); + + return { + process, + proxyAddress, + metaAddress, + certPath, + }; +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 47ec256..3f67e6f 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -7,37 +7,47 @@ import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; +import { createRamaProxy, getRamaPath } from "./createRamaProxy.js"; -const SERVER_STOP_TIMEOUT_MS = 1000; /** - * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} + * @typedef {Object} SafeChainProxy + * @prop {() => Promise} startServer + * @prop {() => Promise} stopServer + * @prop {() => boolean} verifyNoMaliciousPackages + * @prop {() => boolean} hasSuppressedVersions + * @prop {() => Number | null} getServerPort + * @prop {() => string} getCombinedCaBundlePath */ -const state = { - port: null, - blockedRequests: [], -}; + +/** @type {SafeChainProxy} */ +let server; export function createSafeChainProxy() { - const server = createProxyServer(); + if (server) { + return server; + } - return { - startServer: () => startServer(server), - stopServer: () => stopServer(server), - verifyNoMaliciousPackages, - hasSuppressedVersions: getHasSuppressedVersions, - }; + let ramaPath = getRamaPath(); + if (ramaPath) { + ui.writeInformation("Starting safe-chain rama proxy"); + server = createRamaProxy(ramaPath); + } else { + server = createBuiltInProxyServer(); + } + + return server; } /** * @returns {Record} */ function getSafeChainProxyEnvironmentVariables() { - if (!state.port) { + if (!server || !server.getServerPort()) { return {}; } - const proxyUrl = `http://localhost:${state.port}`; - const caCertPath = getCombinedCaBundlePath(); + const proxyUrl = `http://localhost:${server.getServerPort()}`; + const caCertPath = server.getCombinedCaBundlePath(); return { HTTPS_PROXY: proxyUrl, @@ -68,7 +78,17 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { return proxyEnv; } -function createProxyServer() { +/** @returns {SafeChainProxy} */ +function createBuiltInProxyServer() { + const SERVER_STOP_TIMEOUT_MS = 1000; + /** + * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} + */ + const state = { + port: null, + blockedRequests: [], + }; + const server = http.createServer( // This handles direct HTTP requests (non-CONNECT requests) // This is normally http-only traffic, but we also handle @@ -79,114 +99,121 @@ function createProxyServer() { // This handles HTTPS requests via the CONNECT method server.on("connect", handleConnect); - return server; -} + return { + startServer: () => startServer(server), + stopServer: () => stopServer(server), + verifyNoMaliciousPackages, + hasSuppressedVersions: getHasSuppressedVersions, + getServerPort: () => state.port, + getCombinedCaBundlePath, + }; -/** - * @param {import("http").Server} server - * - * @returns {Promise} - */ -function startServer(server) { - return new Promise((resolve, reject) => { - // Passing port 0 makes the OS assign an available port - server.listen(0, () => { - const address = server.address(); - if (address && typeof address === "object") { - state.port = address.port; - resolve(); - } else { - reject(new Error("Failed to start proxy server")); - } - }); - - server.on("error", (err) => { - reject(err); - }); - }); -} - -/** - * @param {import("http").Server} server - * - * @returns {Promise} - */ -function stopServer(server) { - return new Promise((resolve) => { - try { - server.close(() => { - resolve(); + /** + * @param {import("http").Server} server + * + * @returns {Promise} + */ + function startServer(server) { + return new Promise((resolve, reject) => { + // Passing port 0 makes the OS assign an available port + server.listen(0, () => { + const address = server.address(); + if (address && typeof address === "object") { + state.port = address.port; + resolve(); + } else { + reject(new Error("Failed to start proxy server")); + } }); - } catch { - resolve(); - } - setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); - }); -} -/** - * @param {import("http").IncomingMessage} req - * @param {import("http").ServerResponse} clientSocket - * @param {Buffer} head - * - * @returns {void} - */ -function handleConnect(req, clientSocket, head) { - // CONNECT method is used for HTTPS requests - // It establishes a tunnel to the server identified by the request URL + server.on("error", (err) => { + reject(err); + }); + }); + } - const interceptor = createInterceptorForUrl(req.url || ""); - - if (interceptor) { - // Subscribe to malware blocked events - interceptor.on( - "malwareBlocked", - ( - /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event - ) => { - onMalwareBlocked(event.packageName, event.version, event.targetUrl); + /** + * @param {import("http").Server} server + * + * @returns {Promise} + */ + function stopServer(server) { + return new Promise((resolve) => { + try { + server.close(() => { + resolve(); + }); + } catch { + resolve(); } + setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); + }); + } + + /** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ + 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 interceptor = createInterceptorForUrl(req.url || ""); + + if (interceptor) { + // Subscribe to malware blocked events + interceptor.on( + "malwareBlocked", + ( + /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event + ) => { + onMalwareBlocked(event.packageName, event.version, event.targetUrl); + } + ); + + mitmConnect(req, clientSocket, interceptor); + } else { + // For other hosts, just tunnel the request to the destination tcp socket + ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); + tunnelRequest(req, clientSocket, head); + } + } + + /** + * + * @param {string} packageName + * @param {string} version + * @param {string} url + */ + function onMalwareBlocked(packageName, version, url) { + state.blockedRequests.push({ packageName, version, url }); + } + + function verifyNoMaliciousPackages() { + if (state.blockedRequests.length === 0) { + // No malicious packages were blocked, so nothing to block + return true; + } + + ui.emptyLine(); + + ui.writeInformation( + `Safe-chain: ${chalk.bold( + `blocked ${state.blockedRequests.length} malicious package downloads` + )}:` ); - mitmConnect(req, clientSocket, interceptor); - } else { - // For other hosts, just tunnel the request to the destination tcp socket - ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); - tunnelRequest(req, clientSocket, head); + for (const req of state.blockedRequests) { + ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); + } + + ui.emptyLine(); + ui.writeExitWithoutInstallingMaliciousPackages(); + ui.emptyLine(); + + return false; } } - -/** - * - * @param {string} packageName - * @param {string} version - * @param {string} url - */ -function onMalwareBlocked(packageName, version, url) { - state.blockedRequests.push({ packageName, version, url }); -} - -function verifyNoMaliciousPackages() { - if (state.blockedRequests.length === 0) { - // No malicious packages were blocked, so nothing to block - return true; - } - - ui.emptyLine(); - - ui.writeInformation( - `Safe-chain: ${chalk.bold( - `blocked ${state.blockedRequests.length} malicious package downloads` - )}:` - ); - - for (const req of state.blockedRequests) { - ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); - } - - ui.emptyLine(); - ui.writeExitWithoutInstallingMaliciousPackages(); - ui.emptyLine(); - - return false; -} From f6abfb8a4ef98225589f72799e4d31ff23a51900 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 15:25:40 +0100 Subject: [PATCH 02/98] Install script for linux x64 as well --- install-scripts/install-safe-chain.sh | 31 +++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 5d00bdb..91f639e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -317,20 +317,33 @@ main() { info "Binary installed to: $FINAL_FILE" - # Download safechain-proxy for macOS only - if [ "$OS" = "macos" ]; then + # Download safechain-proxy + if [ "$OS" = "macos" ] || [ "$OS" = "linux" ] || [ "$OS" = "linuxstatic" ]; then info "Downloading safechain-proxy..." - if [ "$ARCH" = "arm64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-arm64" + if [ "$OS" = "macos" ]; then + if [ "$ARCH" = "arm64" ]; then + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-arm64" + else + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-amd64" + fi else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-amd64" + # Linux (both linux and linuxstatic) + if [ "$ARCH" = "x64" ]; then + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-linux-amd64" + else + # Skip for non-x64 Linux architectures + info "Skipping safechain-proxy download (not available for linux-${ARCH})" + PROXY_URL="" + fi fi - PROXY_FILE="${INSTALL_DIR}/safechain-proxy" - download "$PROXY_URL" "$PROXY_FILE" - chmod +x "$PROXY_FILE" || error "Failed to make proxy executable" - info "Proxy installed to: $PROXY_FILE" + if [ -n "$PROXY_URL" ]; then + PROXY_FILE="${INSTALL_DIR}/safechain-proxy" + download "$PROXY_URL" "$PROXY_FILE" + chmod +x "$PROXY_FILE" || error "Failed to make proxy executable" + info "Proxy installed to: $PROXY_FILE" + fi fi # Build setup command based on arguments From 22580bcf5f2bb5c465a7d829ae5780333cfdef90 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 15:29:43 +0100 Subject: [PATCH 03/98] Set correct version --- install-scripts/install-safe-chain.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 91f639e..f8ae6f5 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -323,14 +323,14 @@ main() { if [ "$OS" = "macos" ]; then if [ "$ARCH" = "arm64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-arm64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-darwin-arm64" else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-darwin-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-darwin-amd64" fi else # Linux (both linux and linuxstatic) if [ "$ARCH" = "x64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/safechain-proxy-linux-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-linux-amd64" else # Skip for non-x64 Linux architectures info "Skipping safechain-proxy download (not available for linux-${ARCH})" From 6006760b672aeccc1365e249a9209358d0f59630 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 15:39:26 +0100 Subject: [PATCH 04/98] Only inherit io when loglevel verbose --- .../safe-chain/src/registryProxy/createRamaProxy.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/createRamaProxy.js index 90742ad..d065357 100644 --- a/packages/safe-chain/src/registryProxy/createRamaProxy.js +++ b/packages/safe-chain/src/registryProxy/createRamaProxy.js @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { ui } from "../environment/userInteraction.js"; +import { getLoggingLevel, LOGGING_VERBOSE } from "../config/settings.js"; const readFilePromise = promisify(readFile); const writeFilePromise = promisify(writeFile); @@ -72,9 +73,13 @@ export function createRamaProxy(ramaPath) { * @returns {Promise} */ async function startRama(ramaPath, dataFolder) { - let process = spawn(ramaPath, ["--secrets", "memory", "--data", dataFolder], { - stdio: "inherit", - }); + const args = ["--secrets", "memory", "--data", dataFolder]; + const process = + getLoggingLevel() === LOGGING_VERBOSE + ? spawn(ramaPath, args, { + stdio: "inherit", + }) + : spawn(ramaPath, args); // wait some time to allow the proxy process to start await new Promise((resolve) => setTimeout(resolve, 5000)); From 0411a579aec2ab65a835f54c4e005923e0689d34 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 13 Jan 2026 10:02:48 +0100 Subject: [PATCH 05/98] Wait and poll until proxy starts for max 60s --- .../src/registryProxy/createRamaProxy.js | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/createRamaProxy.js index d065357..553e201 100644 --- a/packages/safe-chain/src/registryProxy/createRamaProxy.js +++ b/packages/safe-chain/src/registryProxy/createRamaProxy.js @@ -25,8 +25,6 @@ export function getRamaPath() { const executableDir = dirname(process.execPath); const ramaPath = join(executableDir, "safechain-proxy"); - ui.writeWarning(ramaPath); - if (existsSync(ramaPath)) { return ramaPath; } @@ -46,7 +44,7 @@ export function createRamaProxy(ramaPath) { return { startServer: async () => { ramaInstance = await startRama(ramaPath, tempDir); - ui.writeInformation( + ui.writeVerbose( `Proxy started at address "${ramaInstance.proxyAddress}"` ); }, @@ -73,6 +71,7 @@ export function createRamaProxy(ramaPath) { * @returns {Promise} */ async function startRama(ramaPath, dataFolder) { + const startTime = Date.now(); const args = ["--secrets", "memory", "--data", dataFolder]; const process = getLoggingLevel() === LOGGING_VERBOSE @@ -81,13 +80,22 @@ async function startRama(ramaPath, dataFolder) { }) : spawn(ramaPath, args); - // wait some time to allow the proxy process to start - await new Promise((resolve) => setTimeout(resolve, 5000)); + // wait for the proxy process to start (poll for proxy.addr.txt file) + const proxyAddrPath = join(dataFolder, "proxy.addr.txt"); + const maxWaitTime = 60000; // 60 seconds + const pollInterval = 500; // 500 ms - const proxyAddress = await readFilePromise( - join(dataFolder, "proxy.addr.txt"), - "utf-8" - ); + while (!existsSync(proxyAddrPath)) { + if (Date.now() - startTime > maxWaitTime) { + throw new Error("Timeout waiting for proxy to start"); + } + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + + const elapsedTime = Date.now() - startTime; + ui.writeVerbose(`Proxy started in ${elapsedTime}ms`); + + const proxyAddress = await readFilePromise(proxyAddrPath, "utf-8"); const metaAddress = await readFilePromise( join(dataFolder, "meta.addr.txt"), "utf-8" From 67a8f2db52e95b0a3f40ba729dbb12f9a275329c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 13 Jan 2026 13:07:48 +0100 Subject: [PATCH 06/98] Use prroxy version 0.0.4 --- install-scripts/install-safe-chain.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index f8ae6f5..98dccdd 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -323,14 +323,14 @@ main() { if [ "$OS" = "macos" ]; then if [ "$ARCH" = "arm64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-darwin-arm64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.4-linux-proxy-bins/safechain-proxy-darwin-arm64" else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-darwin-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.4-linux-proxy-bins/safechain-proxy-darwin-amd64" fi else # Linux (both linux and linuxstatic) if [ "$ARCH" = "x64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.3-linux-proxy-bins/safechain-proxy-linux-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.4-linux-proxy-bins/safechain-proxy-linux-amd64" else # Skip for non-x64 Linux architectures info "Skipping safechain-proxy download (not available for linux-${ARCH})" From 83b62f9c7abc4b199cb85e95720a7c812717c668 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 13 Jan 2026 13:27:12 +0100 Subject: [PATCH 07/98] v0.0.5-linux-proxy-bins of proxy --- install-scripts/install-safe-chain.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 98dccdd..e7abacf 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -323,14 +323,14 @@ main() { if [ "$OS" = "macos" ]; then if [ "$ARCH" = "arm64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.4-linux-proxy-bins/safechain-proxy-darwin-arm64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.5-linux-proxy-bins/safechain-proxy-darwin-arm64" else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.4-linux-proxy-bins/safechain-proxy-darwin-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.5-linux-proxy-bins/safechain-proxy-darwin-amd64" fi else # Linux (both linux and linuxstatic) if [ "$ARCH" = "x64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.4-linux-proxy-bins/safechain-proxy-linux-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.5-linux-proxy-bins/safechain-proxy-linux-amd64" else # Skip for non-x64 Linux architectures info "Skipping safechain-proxy download (not available for linux-${ARCH})" From c37b0c8265e10698c70b7e61a0911e33be95f1aa Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 15 Jan 2026 10:47:56 +0100 Subject: [PATCH 08/98] Proxy version v0.0.7-linux-proxy-bins --- install-scripts/install-safe-chain.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index e7abacf..4e1ac5e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -323,14 +323,14 @@ main() { if [ "$OS" = "macos" ]; then if [ "$ARCH" = "arm64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.5-linux-proxy-bins/safechain-proxy-darwin-arm64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-darwin-arm64" else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.5-linux-proxy-bins/safechain-proxy-darwin-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-darwin-amd64" fi else # Linux (both linux and linuxstatic) if [ "$ARCH" = "x64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.5-linux-proxy-bins/safechain-proxy-linux-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-linux-amd64" else # Skip for non-x64 Linux architectures info "Skipping safechain-proxy download (not available for linux-${ARCH})" From 6fcca094c582fa94adb2239c8d14ef686f6034c0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 15 Jan 2026 11:13:08 +0100 Subject: [PATCH 09/98] Add link to linux-arm64 proxy --- install-scripts/install-safe-chain.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 4e1ac5e..053e864 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -332,9 +332,7 @@ main() { if [ "$ARCH" = "x64" ]; then PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-linux-amd64" else - # Skip for non-x64 Linux architectures - info "Skipping safechain-proxy download (not available for linux-${ARCH})" - PROXY_URL="" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-linux-arm64" fi fi From 2cba4be1aa554ab5a4a7a7d55d81202363456546 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 13 Jan 2026 10:09:59 -0800 Subject: [PATCH 10/98] Include package name in logging when minimum package age is not met --- .../src/registryProxy/interceptors/npm/modifyNpmInfo.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 2ee4eb8..ae71cb3 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -116,8 +116,10 @@ export function modifyNpmInfoResponse(body, headers) { function deleteVersionFromJson(json, version) { state.hasSuppressedVersions = true; - ui.writeVerbose( - `Safe-chain: ${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` + const packageName = typeof json?.name === "string" ? json.name : "(unknown)"; + + ui.writeInformation( + `Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` ); delete json.time[version]; From a7388bbdcf199377a78d337624ccb5a747951e0d Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Tue, 13 Jan 2026 19:33:00 +0100 Subject: [PATCH 11/98] Update packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js --- .../src/registryProxy/interceptors/npm/modifyNpmInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index ae71cb3..421666a 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -118,7 +118,7 @@ function deleteVersionFromJson(json, version) { const packageName = typeof json?.name === "string" ? json.name : "(unknown)"; - ui.writeInformation( + ui.writeVerbose( `Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` ); From 7377b5577ac9dc0e74f51a9fc834d6157cd9412f Mon Sep 17 00:00:00 2001 From: Robert Slootjes Date: Tue, 13 Jan 2026 08:19:10 +0100 Subject: [PATCH 12/98] Add Bitbucket Pipelines example --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 57d1bf4..17d2515 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,7 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download - ✅ **Azure Pipelines** - ✅ **CircleCI** - ✅ **Jenkins** +- ✅ **Bitbucket Pipelines** ## GitHub Actions Example @@ -360,6 +361,21 @@ pipeline { } ``` +## Bitbucket Pipelines Example + +```yaml +image: node:22 + +steps: + - step: + name: Install + script: + - npm install -g @aikidosec/safe-chain + - safe-chain setup-ci + - export PATH=~/.safe-chain/shims:$PATH + - npm ci +``` + After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. # Troubleshooting From 14e94dcb620ddb60d1d4593f7d7d9eb5d08e9b00 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 14 Jan 2026 14:02:27 +0100 Subject: [PATCH 13/98] Retry downloading the malware database 3 times --- packages/safe-chain/src/api/aikido.js | 97 +++++++++++++------ .../src/scanning/malwareDatabase.js | 14 +-- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 5c04360..26c88ea 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -1,5 +1,9 @@ import fetch from "make-fetch-happen"; -import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { + getEcoSystem, + ECOSYSTEM_JS, + ECOSYSTEM_PY, +} from "../config/settings.js"; const malwareDatabaseUrls = { [ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json", @@ -17,38 +21,77 @@ const malwareDatabaseUrls = { * @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>} */ export async function fetchMalwareDatabase() { - const ecosystem = getEcoSystem(); - const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)]; - const response = await fetch(malwareDatabaseUrl); - if (!response.ok) { - throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`); - } + return retry(async () => { + const ecosystem = getEcoSystem(); + const malwareDatabaseUrl = + malwareDatabaseUrls[ + /** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem) + ]; + const response = await fetch(malwareDatabaseUrl); + if (!response.ok) { + throw new Error( + `Error fetching ${ecosystem} malware database: ${response.statusText}` + ); + } - try { - let malwareDatabase = await response.json(); - return { - malwareDatabase: malwareDatabase, - version: response.headers.get("etag") || undefined, - }; - } catch (/** @type {any} */ error) { - throw new Error(`Error parsing malware database: ${error.message}`); - } + try { + let malwareDatabase = await response.json(); + return { + malwareDatabase: malwareDatabase, + version: response.headers.get("etag") || undefined, + }; + } catch (/** @type {any} */ error) { + throw new Error(`Error parsing malware database: ${error.message}`); + } + }, 3); } /** * @returns {Promise} */ export async function fetchMalwareDatabaseVersion() { - const ecosystem = getEcoSystem(); - const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)]; - const response = await fetch(malwareDatabaseUrl, { - method: "HEAD", - }); + return retry(async () => { + const ecosystem = getEcoSystem(); + const malwareDatabaseUrl = + malwareDatabaseUrls[ + /** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem) + ]; + const response = await fetch(malwareDatabaseUrl, { + method: "HEAD", + }); - if (!response.ok) { - throw new Error( - `Error fetching ${ecosystem} malware database version: ${response.statusText}` - ); - } - return response.headers.get("etag") || undefined; + if (!response.ok) { + throw new Error( + `Error fetching ${ecosystem} malware database version: ${response.statusText}` + ); + } + return response.headers.get("etag") || undefined; + }, 3); +} + +/** + * Retries an asynchronous function multiple times until it succeeds or exhausts all attempts. + * + * @template T + * @param {() => Promise} func - The asynchronous function to retry + * @param {number} times - The number of retry attempts (will execute times + 1 total attempts) + * @returns {Promise} The return value of the function if successful + * @throws {Error} The last error encountered if all retry attempts fail + */ +async function retry(func, times) { + let lastError; + + for (let i = 0; i <= times; i++) { + try { + return await func(); + } catch (error) { + lastError = error; + } + + if (i < times) { + await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 500)); + } + } + + throw lastError; } diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 4aba43c..120c438 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -48,13 +48,13 @@ export async function openMalwareDatabase() { */ function getPackageStatus(name, version) { const normalizedName = normalizePackageName(name); - const packageData = malwareDatabase.find( - (pkg) => { - const normalizedPkgName = normalizePackageName(pkg.package_name); - return normalizedPkgName === normalizedName && - (pkg.version === version || pkg.version === "*"); - } - ); + const packageData = malwareDatabase.find((pkg) => { + const normalizedPkgName = normalizePackageName(pkg.package_name); + return ( + normalizedPkgName === normalizedName && + (pkg.version === version || pkg.version === "*") + ); + }); if (!packageData) { return MALWARE_STATUS_OK; From 4a53a7b20d1a61c148c8b2c578cf797bd090fd42 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 14 Jan 2026 14:41:06 +0100 Subject: [PATCH 14/98] Add tests for malware db retry --- packages/safe-chain/src/api/aikido.spec.js | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/safe-chain/src/api/aikido.spec.js diff --git a/packages/safe-chain/src/api/aikido.spec.js b/packages/safe-chain/src/api/aikido.spec.js new file mode 100644 index 0000000..2191d42 --- /dev/null +++ b/packages/safe-chain/src/api/aikido.spec.js @@ -0,0 +1,125 @@ +import { describe, it, mock, beforeEach } from "node:test"; +import assert from "node:assert"; + +describe("aikido API", async () => { + const mockFetch = mock.fn(); + + mock.module("make-fetch-happen", { + defaultExport: mockFetch, + }); + + mock.module("../config/settings.js", { + namedExports: { + getEcoSystem: () => "js", + ECOSYSTEM_JS: "js", + ECOSYSTEM_PY: "py", + }, + }); + + const { fetchMalwareDatabase, fetchMalwareDatabaseVersion } = + await import("./aikido.js"); + + beforeEach(() => { + mockFetch.mock.resetCalls(); + }); + + describe("fetchMalwareDatabase", () => { + it("should succeed immediately when fetch succeeds on first try", async () => { + const malwareData = [ + { package_name: "malicious-pkg", version: "1.0.0", reason: "test" }, + ]; + mockFetch.mock.mockImplementationOnce(() => ({ + ok: true, + json: async () => malwareData, + headers: { get: () => '"etag-123"' }, + })); + + const result = await fetchMalwareDatabase(); + + assert.strictEqual(mockFetch.mock.calls.length, 1); + assert.deepStrictEqual(result.malwareDatabase, malwareData); + assert.strictEqual(result.version, '"etag-123"'); + }); + + it("should throw error after exhausting all retries", async () => { + mockFetch.mock.mockImplementation(() => { + throw new Error("Network error"); + }); + + await assert.rejects(() => fetchMalwareDatabase(), { + message: "Network error", + }); + + assert.strictEqual(mockFetch.mock.calls.length, 4); + }); + + it("should succeed after failing 3 times and succeeding on 4th attempt", async () => { + const malwareData = [ + { package_name: "bad-pkg", version: "2.0.0", reason: "malware" }, + ]; + let callCount = 0; + mockFetch.mock.mockImplementation(() => { + callCount++; + if (callCount < 4) { + throw new Error("Network error"); + } + return { + ok: true, + json: async () => malwareData, + headers: { get: () => '"etag-456"' }, + }; + }); + + const result = await fetchMalwareDatabase(); + + assert.strictEqual(mockFetch.mock.calls.length, 4); + assert.deepStrictEqual(result.malwareDatabase, malwareData); + assert.strictEqual(result.version, '"etag-456"'); + }); + }); + + describe("fetchMalwareDatabaseVersion", () => { + it("should succeed immediately when fetch succeeds on first try", async () => { + mockFetch.mock.mockImplementationOnce(() => ({ + ok: true, + headers: { get: () => '"version-etag"' }, + })); + + const result = await fetchMalwareDatabaseVersion(); + + assert.strictEqual(mockFetch.mock.calls.length, 1); + assert.strictEqual(result, '"version-etag"'); + }); + + it("should throw error after exhausting all retries", async () => { + mockFetch.mock.mockImplementation(() => { + throw new Error("Connection refused"); + }); + + await assert.rejects(() => fetchMalwareDatabaseVersion(), { + message: "Connection refused", + }); + + assert.strictEqual(mockFetch.mock.calls.length, 4); + }); + + it("should succeed after failing 3 times and succeeding on 4th attempt", async () => { + let callCount = 0; + mockFetch.mock.mockImplementation(() => { + callCount++; + if (callCount < 4) { + throw new Error("Timeout"); + } + return { + ok: true, + headers: { get: () => '"final-etag"' }, + }; + }); + + const result = await fetchMalwareDatabaseVersion(); + + assert.strictEqual(mockFetch.mock.calls.length, 4); + assert.strictEqual(result, '"final-etag"'); + }); + }); +}); From cf8e39c5fd906e38c913a3dd2795d2f54995d0cd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 14 Jan 2026 14:55:11 +0100 Subject: [PATCH 15/98] Handle pr comments --- packages/safe-chain/src/api/aikido.js | 22 ++++++++++++++----- .../src/scanning/malwareDatabase.js | 14 ++++++------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 26c88ea..88dffb2 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -21,6 +21,8 @@ const malwareDatabaseUrls = { * @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>} */ export async function fetchMalwareDatabase() { + const numberOfAttempts = 4; + return retry(async () => { const ecosystem = getEcoSystem(); const malwareDatabaseUrl = @@ -43,13 +45,15 @@ export async function fetchMalwareDatabase() { } catch (/** @type {any} */ error) { throw new Error(`Error parsing malware database: ${error.message}`); } - }, 3); + }, numberOfAttempts); } /** * @returns {Promise} */ export async function fetchMalwareDatabaseVersion() { + const numberOfAttempts = 4; + return retry(async () => { const ecosystem = getEcoSystem(); const malwareDatabaseUrl = @@ -66,7 +70,7 @@ export async function fetchMalwareDatabaseVersion() { ); } return response.headers.get("etag") || undefined; - }, 3); + }, numberOfAttempts); } /** @@ -74,21 +78,27 @@ export async function fetchMalwareDatabaseVersion() { * * @template T * @param {() => Promise} func - The asynchronous function to retry - * @param {number} times - The number of retry attempts (will execute times + 1 total attempts) + * @param {number} attempts - The number of attempts * @returns {Promise} The return value of the function if successful * @throws {Error} The last error encountered if all retry attempts fail */ -async function retry(func, times) { +async function retry(func, attempts) { let lastError; - for (let i = 0; i <= times; i++) { + for (let i = 0; i < attempts; i++) { try { return await func(); } catch (error) { lastError = error; } - if (i < times) { + if (i < attempts - 1) { + // When this is not the last try, back-off expenentially: + // 1st attempt - 500ms delay + // 2nd attempt - 1000ms delay + // 3rd attempt - 2000ms delay + // 4th attempt - 4000ms delay + // ... await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 500)); } } diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 120c438..4aba43c 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -48,13 +48,13 @@ export async function openMalwareDatabase() { */ function getPackageStatus(name, version) { const normalizedName = normalizePackageName(name); - const packageData = malwareDatabase.find((pkg) => { - const normalizedPkgName = normalizePackageName(pkg.package_name); - return ( - normalizedPkgName === normalizedName && - (pkg.version === version || pkg.version === "*") - ); - }); + const packageData = malwareDatabase.find( + (pkg) => { + const normalizedPkgName = normalizePackageName(pkg.package_name); + return normalizedPkgName === normalizedName && + (pkg.version === version || pkg.version === "*"); + } + ); if (!packageData) { return MALWARE_STATUS_OK; From 0e6d002b4c4b81d3ce4e8246f54e62f7b1b5b2ed Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 14 Jan 2026 15:31:37 +0100 Subject: [PATCH 16/98] Don't swallow error on retry --- packages/safe-chain/src/api/aikido.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 88dffb2..be01518 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -4,6 +4,7 @@ import { ECOSYSTEM_JS, ECOSYSTEM_PY, } from "../config/settings.js"; +import { ui } from "../environment/userInteraction.js"; const malwareDatabaseUrls = { [ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json", @@ -89,6 +90,10 @@ async function retry(func, attempts) { try { return await func(); } catch (error) { + ui.writeVerbose( + "An error occurred while trying to download the Aikido Malware database", + error + ); lastError = error; } From 3210b68b43f43acaf37b1c35ad12831af5572bfd Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 14 Jan 2026 15:33:09 +0100 Subject: [PATCH 17/98] Update packages/safe-chain/src/api/aikido.js --- packages/safe-chain/src/api/aikido.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index be01518..abb2135 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -98,7 +98,7 @@ async function retry(func, attempts) { } if (i < attempts - 1) { - // When this is not the last try, back-off expenentially: + // When this is not the last try, back-off exponentially: // 1st attempt - 500ms delay // 2nd attempt - 1000ms delay // 3rd attempt - 2000ms delay From b7f793f1f93b381b7292db5cc726dc13cd5b7b6c Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 12 Jan 2026 14:53:23 -0800 Subject: [PATCH 18/98] Attempted fix for powershell swallowing '--' --- .../startup-scripts/init-pwsh.ps1 | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index 7fabcad..f02b900 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -6,27 +6,27 @@ $safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin' $env:PATH = "$env:PATH$pathSeparator$safeChainBin" function npx { - Invoke-WrappedCommand "npx" $args + Invoke-WrappedCommand "npx" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function yarn { - Invoke-WrappedCommand "yarn" $args + Invoke-WrappedCommand "yarn" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function pnpm { - Invoke-WrappedCommand "pnpm" $args + Invoke-WrappedCommand "pnpm" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function pnpx { - Invoke-WrappedCommand "pnpx" $args + Invoke-WrappedCommand "pnpx" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function bun { - Invoke-WrappedCommand "bun" $args + Invoke-WrappedCommand "bun" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function bunx { - Invoke-WrappedCommand "bunx" $args + Invoke-WrappedCommand "bunx" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function npm { @@ -37,37 +37,37 @@ function npm { return } - Invoke-WrappedCommand "npm" $args + Invoke-WrappedCommand "npm" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function pip { - Invoke-WrappedCommand "pip" $args + Invoke-WrappedCommand "pip" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function pip3 { - Invoke-WrappedCommand "pip3" $args + Invoke-WrappedCommand "pip3" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function uv { - Invoke-WrappedCommand "uv" $args + Invoke-WrappedCommand "uv" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function poetry { - Invoke-WrappedCommand "poetry" $args + Invoke-WrappedCommand "poetry" $args $MyInvocation.Line $MyInvocation.OffsetInLine } # `python -m pip`, `python -m pip3`. function python { - Invoke-WrappedCommand 'python' $args + Invoke-WrappedCommand 'python' $args $MyInvocation.Line $MyInvocation.OffsetInLine } # `python3 -m pip`, `python3 -m pip3'. function python3 { - Invoke-WrappedCommand 'python3' $args + Invoke-WrappedCommand 'python3' $args $MyInvocation.Line $MyInvocation.OffsetInLine } function pipx { - Invoke-WrappedCommand "pipx" $args + Invoke-WrappedCommand "pipx" $args $MyInvocation.Line $MyInvocation.OffsetInLine } function Write-SafeChainWarning { @@ -111,10 +111,44 @@ function Invoke-RealCommand { function Invoke-WrappedCommand { param( [string]$OriginalCmd, - [string[]]$Arguments + [string[]]$Arguments, + [string]$RawLine = $null, + [int]$RawOffset = 0 ) - if (Test-CommandAvailable "safe-chain") { + # Use raw line parsing to recover arguments like '--' that PowerShell consumes + if ($RawLine) { + $tokens = [System.Management.Automation.PSParser]::Tokenize($RawLine, [ref]$null) + $newArgs = @() + $foundCommand = $false + $canUseRaw = $true + + foreach ($t in $tokens) { + # Find the command token based on offset + if (-not $foundCommand) { + if ($t.Start -eq ($RawOffset - 1)) { $foundCommand = $true } + continue + } + # Stop at command separators + if ($t.Type -eq 'Operator' -and $t.Content -match '[|;&]') { break } + # Stop if complex variable expansion is used + if ($t.Type -eq 'Variable' -or $t.Type -eq 'Group' -or $t.Type -eq 'SubExpression') { + $canUseRaw = $false + break + } + $newArgs += $t.Content + } + + if ($foundCommand -and $canUseRaw) { + $Arguments = $newArgs + Write-Host "Safe-chain Powershell Wrapper: Reconstructed args: $($Arguments -join ' ')" + } + } + + if ($isWindowsPlatform -and (Test-CommandAvailable "safe-chain.cmd")) { + & safe-chain.cmd $OriginalCmd @Arguments + } + elseif (Test-CommandAvailable "safe-chain") { & safe-chain $OriginalCmd @Arguments } else { From 5c431291c74bddf201095f4f91a8d0b8a9408380 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 12 Jan 2026 15:12:19 -0800 Subject: [PATCH 19/98] Fix some logic --- .../startup-scripts/init-pwsh.ps1 | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index f02b900..e1ed660 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -108,6 +108,40 @@ function Invoke-RealCommand { } } +function Get-ReconstructedArguments { + param( + [string]$RawLine, + [int]$RawOffset + ) + + if (-not $RawLine) { return $null } + + $tokens = [System.Management.Automation.PSParser]::Tokenize($RawLine, [ref]$null) + $newArgs = @() + $foundCommand = $false + + foreach ($t in $tokens) { + if (-not $foundCommand) { + if ($t.Start -eq ($RawOffset - 1)) { $foundCommand = $true } + continue + } + + if ($t.Type -eq 'Operator' -and $t.Content -match '[|;&]') { break } + + # Stop if complex variable expansion is used + if ($t.Type -eq 'Variable' -or $t.Type -eq 'Group' -or $t.Type -eq 'SubExpression') { + return $null + } + + $newArgs += $t.Content + } + + if ($foundCommand) { + return ,$newArgs + } + return $null +} + function Invoke-WrappedCommand { param( [string]$OriginalCmd, @@ -118,29 +152,9 @@ function Invoke-WrappedCommand { # Use raw line parsing to recover arguments like '--' that PowerShell consumes if ($RawLine) { - $tokens = [System.Management.Automation.PSParser]::Tokenize($RawLine, [ref]$null) - $newArgs = @() - $foundCommand = $false - $canUseRaw = $true - - foreach ($t in $tokens) { - # Find the command token based on offset - if (-not $foundCommand) { - if ($t.Start -eq ($RawOffset - 1)) { $foundCommand = $true } - continue - } - # Stop at command separators - if ($t.Type -eq 'Operator' -and $t.Content -match '[|;&]') { break } - # Stop if complex variable expansion is used - if ($t.Type -eq 'Variable' -or $t.Type -eq 'Group' -or $t.Type -eq 'SubExpression') { - $canUseRaw = $false - break - } - $newArgs += $t.Content - } - - if ($foundCommand -and $canUseRaw) { - $Arguments = $newArgs + $reconstructedArgs = Get-ReconstructedArguments $RawLine $RawOffset + if ($null -ne $reconstructedArgs) { + $Arguments = $reconstructedArgs Write-Host "Safe-chain Powershell Wrapper: Reconstructed args: $($Arguments -join ' ')" } } From 4ef4218eb5ef6128f261624f8cd19c696e445077 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 12 Jan 2026 15:13:34 -0800 Subject: [PATCH 20/98] Remove comment --- .../src/shell-integration/startup-scripts/init-pwsh.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index e1ed660..f82d0fc 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -155,7 +155,6 @@ function Invoke-WrappedCommand { $reconstructedArgs = Get-ReconstructedArguments $RawLine $RawOffset if ($null -ne $reconstructedArgs) { $Arguments = $reconstructedArgs - Write-Host "Safe-chain Powershell Wrapper: Reconstructed args: $($Arguments -join ' ')" } } From d7a9884ff6d56ac8abd6ec7b537cb8da268411c6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 14 Jan 2026 17:41:23 +0100 Subject: [PATCH 21/98] Allow to exclude packages from the minimum package age --- README.md | 16 ++ packages/safe-chain/src/api/aikido.spec.js | 8 + packages/safe-chain/src/config/configFile.js | 22 +++ .../src/config/environmentVariables.js | 10 ++ packages/safe-chain/src/config/settings.js | 31 ++++ .../safe-chain/src/config/settings.spec.js | 135 ++++++++++++++++ .../interceptors/npm/modifyNpmInfo.js | 12 +- .../npm/npmInterceptor.minPackageAge.spec.js | 153 ++++++++++++++++++ .../npmInterceptor.packageDownload.spec.js | 1 + 9 files changed, 387 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17d2515..bc61787 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,22 @@ You can set the minimum package age through multiple sources (in order of priori } ``` +### Excluding Packages + +Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged): + +```shell +export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="react,@aikidosec/safe-chain" +``` + +```json +{ + "npm": { + "minimumPackageAgeExclusions": ["react", "@aikidosec/safe-chain"] + } +} +``` + ## Custom Registries Configure Safe Chain to scan packages from custom or private registries. diff --git a/packages/safe-chain/src/api/aikido.spec.js b/packages/safe-chain/src/api/aikido.spec.js index 2191d42..2e7cecb 100644 --- a/packages/safe-chain/src/api/aikido.spec.js +++ b/packages/safe-chain/src/api/aikido.spec.js @@ -8,6 +8,14 @@ describe("aikido API", async () => { defaultExport: mockFetch, }); + mock.module("../environment/userInteraction.js", { + namedExports: { + ui: { + writeVerbose: () => {}, + }, + }, + }); + mock.module("../config/settings.js", { namedExports: { getEcoSystem: () => "js", diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index a98304e..fd6ac26 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -16,6 +16,7 @@ import { getEcoSystem } from "./settings.js"; * @typedef {Object} SafeChainRegistryConfiguration * We cannot trust the input and should add the necessary validations. * @property {unknown | string[]} customRegistries + * @property {unknown | string[]} minimumPackageAgeExclusions */ /** @@ -127,6 +128,27 @@ export function getPipCustomRegistries() { return customRegistries.filter((item) => typeof item === "string"); } +/** + * Gets the minimum package age exclusions from the config file + * @returns {string[]} + */ +export function getNpmMinimumPackageAgeExclusions() { + const config = readConfigFile(); + + if (!config || !config.npm) { + return []; + } + + const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm); + const exclusions = npmConfig.minimumPackageAgeExclusions; + + if (!Array.isArray(exclusions)) { + return []; + } + + return exclusions.filter((item) => typeof item === "string"); +} + /** * @param {import("../api/aikido.js").MalwarePackage[]} data * @param {string | number} version diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index 1b85ed7..8a44841 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -34,3 +34,13 @@ export function getPipCustomRegistries() { export function getLoggingLevel() { return process.env.SAFE_CHAIN_LOGGING; } + +/** + * Gets the minimum package age exclusions from environment variable + * Expected format: comma-separated list of package names + * Example: "react,@aikidosec/safe-chain,lodash" + * @returns {string | undefined} + */ +export function getNpmMinimumPackageAgeExclusions() { + return process.env.SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS; +} diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 6910fe3..b9243b0 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -167,3 +167,34 @@ export function getPipCustomRegistries() { // Normalize each registry (remove protocol if any) return uniqueRegistries.map(normalizeRegistry); } + +/** + * Parses comma-separated exclusions from environment variable + * @param {string | undefined} envValue + * @returns {string[]} + */ +function parseExclusionsFromEnv(envValue) { + if (!envValue || typeof envValue !== "string") { + return []; + } + + return envValue + .split(",") + .map((exclusion) => exclusion.trim()) + .filter((exclusion) => exclusion.length > 0); +} + +/** + * Gets the minimum package age exclusions from both environment variable and config file (merged) + * @returns {string[]} + */ +export function getNpmMinimumPackageAgeExclusions() { + const envExclusions = parseExclusionsFromEnv( + environmentVariables.getNpmMinimumPackageAgeExclusions() + ); + const configExclusions = configFile.getNpmMinimumPackageAgeExclusions(); + + // Merge both sources and remove duplicates + const allExclusions = [...envExclusions, ...configExclusions]; + return [...new Set(allExclusions)]; +} diff --git a/packages/safe-chain/src/config/settings.spec.js b/packages/safe-chain/src/config/settings.spec.js index 314fac0..8db5b83 100644 --- a/packages/safe-chain/src/config/settings.spec.js +++ b/packages/safe-chain/src/config/settings.spec.js @@ -14,6 +14,7 @@ mock.module("fs", { const { getNpmCustomRegistries, getPipCustomRegistries, + getNpmMinimumPackageAgeExclusions, getLoggingLevel, LOGGING_SILENT, LOGGING_NORMAL, @@ -365,3 +366,137 @@ describe("getLoggingLevel", () => { assert.strictEqual(level, LOGGING_NORMAL); }); }); + +describe("getNpmMinimumPackageAgeExclusions", () => { + let originalEnv; + const envVarName = "SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS"; + + beforeEach(() => { + originalEnv = process.env[envVarName]; + delete process.env[envVarName]; + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env[envVarName] = originalEnv; + } else { + delete process.env[envVarName]; + } + configFileContent = undefined; + }); + + it("should return empty array when no exclusions configured", () => { + configFileContent = undefined; + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, []); + }); + + it("should return exclusions from config file", () => { + configFileContent = JSON.stringify({ + npm: { + minimumPackageAgeExclusions: ["react", "@aikidosec/safe-chain"], + }, + }); + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["react", "@aikidosec/safe-chain"]); + }); + + it("should parse comma-separated exclusions from environment variable", () => { + process.env[envVarName] = "lodash,express,@types/node"; + configFileContent = undefined; + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["lodash", "express", "@types/node"]); + }); + + it("should merge environment variable and config file exclusions", () => { + process.env[envVarName] = "lodash"; + configFileContent = JSON.stringify({ + npm: { + minimumPackageAgeExclusions: ["react"], + }, + }); + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["lodash", "react"]); + }); + + it("should remove duplicate exclusions when merging", () => { + process.env[envVarName] = "lodash,react"; + configFileContent = JSON.stringify({ + npm: { + minimumPackageAgeExclusions: ["react", "express"], + }, + }); + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["lodash", "react", "express"]); + }); + + it("should trim whitespace from environment variable exclusions", () => { + process.env[envVarName] = " lodash , react "; + configFileContent = undefined; + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["lodash", "react"]); + }); + + it("should handle scoped packages", () => { + configFileContent = JSON.stringify({ + npm: { + minimumPackageAgeExclusions: ["@babel/core", "@types/react"], + }, + }); + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["@babel/core", "@types/react"]); + }); + + it("should handle empty strings in comma-separated list", () => { + process.env[envVarName] = "lodash,,react,"; + configFileContent = undefined; + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["lodash", "react"]); + }); + + it("should return empty array for empty environment variable", () => { + process.env[envVarName] = ""; + configFileContent = undefined; + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, []); + }); + + it("should return empty array for whitespace-only environment variable", () => { + process.env[envVarName] = " , , "; + configFileContent = undefined; + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, []); + }); + + it("should filter non-string values from config file", () => { + configFileContent = JSON.stringify({ + npm: { + minimumPackageAgeExclusions: ["react", 123, null, "lodash", undefined], + }, + }); + + const exclusions = getNpmMinimumPackageAgeExclusions(); + + assert.deepStrictEqual(exclusions, ["react", "lodash"]); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 421666a..3407397 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -1,4 +1,4 @@ -import { getMinimumPackageAgeHours } from "../../../config/settings.js"; +import { getMinimumPackageAgeHours, getNpmMinimumPackageAgeExclusions } from "../../../config/settings.js"; import { ui } from "../../../environment/userInteraction.js"; import { getHeaderValueAsString } from "../../http-utils.js"; @@ -65,6 +65,16 @@ export function modifyNpmInfoResponse(body, headers) { return body; } + // Check if this package is excluded from minimum age filtering + const packageName = bodyJson.name; + const exclusions = getNpmMinimumPackageAgeExclusions(); + if (packageName && exclusions.includes(packageName)) { + ui.writeVerbose( + `Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).` + ); + return body; + } + const cutOff = new Date( new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 ); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index fb7ae56..ed00909 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -4,12 +4,14 @@ import assert from "node:assert"; describe("npmInterceptor minimum package age", async () => { let minimumPackageAgeSettings = 48; let skipMinimumPackageAgeSetting = false; + let minimumPackageAgeExclusionsSetting = []; mock.module("../../../config/settings.js", { namedExports: { getMinimumPackageAgeHours: () => minimumPackageAgeSettings, skipMinimumPackageAge: () => skipMinimumPackageAgeSetting, getNpmCustomRegistries: () => [], + getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting, }, }); @@ -357,6 +359,157 @@ describe("npmInterceptor minimum package age", async () => { assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0"); }); + it("Should not filter packages when package is in exclusion list", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["lodash"]; + + const packageUrl = "https://registry.npmjs.org/lodash"; + + const originalBody = JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "3.0.0", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + }, + time: { + created: getDate(-365 * 24), + modified: getDate(-3), + ["1.0.0"]: getDate(-7), + // cutoff-date here + ["2.0.0"]: getDate(-4), + ["3.0.0"]: getDate(-3), // Would normally be filtered + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedJson = JSON.parse(modifiedBody); + + // All versions should remain unchanged since lodash is excluded + assert.equal(Object.keys(modifiedJson.versions).length, 3); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("3.0.0")); + assert.equal(modifiedJson["dist-tags"]["latest"], "3.0.0"); + }); + + it("Should filter packages when package is NOT in exclusion list", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["react"]; // Different package + + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { latest: "3.0.0" }, + versions: { ["1.0.0"]: {}, ["3.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-3), + ["1.0.0"]: getDate(-7), + ["3.0.0"]: getDate(-3), + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + + // lodash should still be filtered since it's not in exclusions + assert.equal(Object.keys(modifiedJson.versions).length, 1); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); + }); + + it("Should handle scoped packages in exclusion list", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["@babel/core"]; + + const packageUrl = "https://registry.npmjs.org/@babel/core"; + + const originalBody = JSON.stringify({ + name: "@babel/core", + ["dist-tags"]: { latest: "7.0.0" }, + versions: { ["6.0.0"]: {}, ["7.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-1), + ["6.0.0"]: getDate(-100), + ["7.0.0"]: getDate(-1), // Would normally be filtered + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedJson = JSON.parse(modifiedBody); + + // All versions should remain for excluded scoped package + assert.equal(Object.keys(modifiedJson.versions).length, 2); + assert.ok(Object.keys(modifiedJson.versions).includes("6.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("7.0.0")); + }); + + it("Should handle multiple packages in exclusion list", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["react", "lodash", "@types/node"]; + + const packageUrl = "https://registry.npmjs.org/lodash"; + + const originalBody = JSON.stringify({ + name: "lodash", + ["dist-tags"]: { latest: "2.0.0" }, + versions: { ["1.0.0"]: {}, ["2.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-1), + ["1.0.0"]: getDate(-100), + ["2.0.0"]: getDate(-1), + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedJson = JSON.parse(modifiedBody); + + // All versions should remain since lodash is in the exclusion list + assert.equal(Object.keys(modifiedJson.versions).length, 2); + }); + + it("Should reset exclusions between tests", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = []; // Reset to empty + + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { latest: "2.0.0" }, + versions: { ["1.0.0"]: {}, ["2.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-1), + ["1.0.0"]: getDate(-100), + ["2.0.0"]: getDate(-1), + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + + // Version 2.0.0 should be filtered since exclusions are empty + assert.equal(Object.keys(modifiedJson.versions).length, 1); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + }); + function getDate(plusHours) { const date = new Date(); date.setHours(date.getHours() + plusHours); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js index 88fcbd0..e1b7c79 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js @@ -26,6 +26,7 @@ mock.module("../../../config/settings.js", { setEcoSystem: () => {}, getMinimumPackageAgeHours: () => 24, getNpmCustomRegistries: () => customRegistries, + getNpmMinimumPackageAgeExclusions: () => [], skipMinimumPackageAge: () => false, }, }); From 2d609066c837482ea20650b2e3f7f4f2ca6da982 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 14 Jan 2026 17:51:41 +0100 Subject: [PATCH 22/98] Allow trailing * for wildcard matching --- README.md | 6 +- .../interceptors/npm/modifyNpmInfo.js | 16 +++- .../npm/npmInterceptor.minPackageAge.spec.js | 81 +++++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bc61787..290304f 100644 --- a/README.md +++ b/README.md @@ -214,16 +214,16 @@ You can set the minimum package age through multiple sources (in order of priori ### Excluding Packages -Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged): +Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged). Supports wildcard patterns with trailing `*`: ```shell -export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="react,@aikidosec/safe-chain" +export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*,react-*,lodash" ``` ```json { "npm": { - "minimumPackageAgeExclusions": ["react", "@aikidosec/safe-chain"] + "minimumPackageAgeExclusions": ["@aikidosec/*", "react-*", "lodash"] } } ``` diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 3407397..9a36207 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -68,7 +68,7 @@ export function modifyNpmInfoResponse(body, headers) { // Check if this package is excluded from minimum age filtering const packageName = bodyJson.name; const exclusions = getNpmMinimumPackageAgeExclusions(); - if (packageName && exclusions.includes(packageName)) { + if (packageName && exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern))) { ui.writeVerbose( `Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).` ); @@ -187,3 +187,17 @@ function getMostRecentTag(tagList) { export function getHasSuppressedVersions() { return state.hasSuppressedVersions; } + +/** + * Checks if a package name matches an exclusion pattern. + * Supports trailing wildcard (*) for prefix matching. + * @param {string} packageName + * @param {string} pattern + * @returns {boolean} + */ +function matchesExclusionPattern(packageName, pattern) { + if (pattern.endsWith("*")) { + return packageName.startsWith(pattern.slice(0, -1)); + } + return packageName === pattern; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index ed00909..82fed71 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -481,6 +481,87 @@ describe("npmInterceptor minimum package age", async () => { assert.equal(Object.keys(modifiedJson.versions).length, 2); }); + it("Should exclude packages matching wildcard pattern @scope/*", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["@aikidosec/*"]; + + const packageUrl = "https://registry.npmjs.org/@aikidosec/safe-chain"; + + const originalBody = JSON.stringify({ + name: "@aikidosec/safe-chain", + ["dist-tags"]: { latest: "2.0.0" }, + versions: { ["1.0.0"]: {}, ["2.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-1), + ["1.0.0"]: getDate(-100), + ["2.0.0"]: getDate(-1), // Would normally be filtered + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedJson = JSON.parse(modifiedBody); + + // All versions should remain since @aikidosec/* matches @aikidosec/safe-chain + assert.equal(Object.keys(modifiedJson.versions).length, 2); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); + }); + + it("Should exclude packages matching wildcard pattern prefix-*", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["react-*"]; + + const packageUrl = "https://registry.npmjs.org/react-dom"; + + const originalBody = JSON.stringify({ + name: "react-dom", + ["dist-tags"]: { latest: "18.0.0" }, + versions: { ["17.0.0"]: {}, ["18.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-1), + ["17.0.0"]: getDate(-100), + ["18.0.0"]: getDate(-1), // Would normally be filtered + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedJson = JSON.parse(modifiedBody); + + // All versions should remain since react-* matches react-dom + assert.equal(Object.keys(modifiedJson.versions).length, 2); + }); + + it("Should NOT exclude packages that don't match wildcard pattern", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = false; + minimumPackageAgeExclusionsSetting = ["@aikidosec/*"]; + + const packageUrl = "https://registry.npmjs.org/@other/package"; + + const originalBody = JSON.stringify({ + name: "@other/package", + ["dist-tags"]: { latest: "2.0.0" }, + versions: { ["1.0.0"]: {}, ["2.0.0"]: {} }, + time: { + created: getDate(-365 * 24), + modified: getDate(-1), + ["1.0.0"]: getDate(-100), + ["2.0.0"]: getDate(-1), + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedJson = JSON.parse(modifiedBody); + + // Version 2.0.0 should be filtered since @other/package doesn't match @aikidosec/* + assert.equal(Object.keys(modifiedJson.versions).length, 1); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + }); + it("Should reset exclusions between tests", async () => { minimumPackageAgeSettings = 5; skipMinimumPackageAgeSetting = false; From 20cc62d6e188ae208f593387bd0bbfdf53f48caf Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 15 Jan 2026 15:13:00 +0100 Subject: [PATCH 23/98] Only allow wildcards for scoped packages (@scope/*) --- README.md | 6 ++--- .../interceptors/npm/modifyNpmInfo.js | 2 +- .../npm/npmInterceptor.minPackageAge.spec.js | 26 ------------------- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 290304f..128d662 100644 --- a/README.md +++ b/README.md @@ -214,16 +214,16 @@ You can set the minimum package age through multiple sources (in order of priori ### Excluding Packages -Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged). Supports wildcard patterns with trailing `*`: +Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged). Use `@scope/*` to trust all packages from an organization: ```shell -export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*,react-*,lodash" +export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*" ``` ```json { "npm": { - "minimumPackageAgeExclusions": ["@aikidosec/*", "react-*", "lodash"] + "minimumPackageAgeExclusions": ["@aikidosec/*"] } } ``` diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 9a36207..14e3ba7 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -196,7 +196,7 @@ export function getHasSuppressedVersions() { * @returns {boolean} */ function matchesExclusionPattern(packageName, pattern) { - if (pattern.endsWith("*")) { + if (pattern.endsWith("/*")) { return packageName.startsWith(pattern.slice(0, -1)); } return packageName === pattern; diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index 82fed71..834a2ad 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -509,32 +509,6 @@ describe("npmInterceptor minimum package age", async () => { assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); }); - it("Should exclude packages matching wildcard pattern prefix-*", async () => { - minimumPackageAgeSettings = 5; - skipMinimumPackageAgeSetting = false; - minimumPackageAgeExclusionsSetting = ["react-*"]; - - const packageUrl = "https://registry.npmjs.org/react-dom"; - - const originalBody = JSON.stringify({ - name: "react-dom", - ["dist-tags"]: { latest: "18.0.0" }, - versions: { ["17.0.0"]: {}, ["18.0.0"]: {} }, - time: { - created: getDate(-365 * 24), - modified: getDate(-1), - ["17.0.0"]: getDate(-100), - ["18.0.0"]: getDate(-1), // Would normally be filtered - }, - }); - - const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); - const modifiedJson = JSON.parse(modifiedBody); - - // All versions should remain since react-* matches react-dom - assert.equal(Object.keys(modifiedJson.versions).length, 2); - }); - it("Should NOT exclude packages that don't match wildcard pattern", async () => { minimumPackageAgeSettings = 5; skipMinimumPackageAgeSetting = false; From 607b4ee87d5b1e8a30346fbc7c810a88f6eea6f4 Mon Sep 17 00:00:00 2001 From: Uriel Corfa Date: Wed, 7 Jan 2026 17:18:48 +0100 Subject: [PATCH 24/98] Propagate command-not-found errors when invoking wrapped commands Before this change, if a package manager was not installed, safe-chain still sets the function and when invoked, the wrapper will invoke safe-chain, which will exit with error code 127 when it fails to invoke the wrapped command. As an example (with a shell prompt that shows $? when non-zero): ``` $ type -f pip bash: type: pip: not found 1$ pip 127$ ``` With this patch, the wrapper first checks for the existence of the wrapped command (ignoring functions), and if no such command exists, it instructs the shell to invoke it anyway. This results in the shell failing to find the command, and reporting an error as if the wrapper function wasn't there: ``` $ source init-posix.sh $ type -f pip bash: type: pip: not found 1$ pip Command 'pip' not found, but can be installed with: sudo apt install python3-pip 127$ ``` --- .../src/shell-integration/startup-scripts/init-posix.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index e649909..b9eebeb 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -76,6 +76,14 @@ function printSafeChainWarning() { function wrapSafeChainCommand() { local original_cmd="$1" + if ! type -f "${original_cmd}" > /dev/null 2>&1; then + # If the original command is not available, don't try to wrap it: invoke it + # transparently, so the shell can report errors as if this wrapper didn't + # exist. + command $@ + return $? + fi + if command -v safe-chain > /dev/null 2>&1; then # If the aikido command is available, just run it with the provided arguments safe-chain "$@" From 11d9e26a2d461e8e6a0287ea392bf7e4c46edd5b Mon Sep 17 00:00:00 2001 From: Uriel Corfa Date: Thu, 8 Jan 2026 09:56:59 +0100 Subject: [PATCH 25/98] init-posix: preserve arguments when exec'ing the original_cmd --- .../src/shell-integration/startup-scripts/init-posix.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index b9eebeb..ebaaf3c 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -80,7 +80,7 @@ function wrapSafeChainCommand() { # If the original command is not available, don't try to wrap it: invoke it # transparently, so the shell can report errors as if this wrapper didn't # exist. - command $@ + command "$@" return $? fi From b1fa9f5492d198a7d30ff15778f869860ca80d85 Mon Sep 17 00:00:00 2001 From: Uriel Corfa Date: Thu, 8 Jan 2026 10:01:13 +0100 Subject: [PATCH 26/98] Add the same handler for fish --- .../startup-scripts/init-fish.fish | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index ec58c8b..13463f6 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -71,13 +71,13 @@ end function printSafeChainWarning set original_cmd $argv[1] - + # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" set_color -b yellow black printf "Warning:" set_color normal printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd - + # Cyan text for the install command printf "Install safe-chain by using " set_color cyan @@ -90,6 +90,20 @@ function wrapSafeChainCommand set original_cmd $argv[1] set cmd_args $argv[2..-1] + if not type -fq $original_cmd + # If the original command is not available, don't try to wrap it: invoke + # it transparently, so the shell can report errors as if this wrapper + # didn't exist. fish always adds extra debug information when executing + # missing commands from within a function, so after the "command not + # found" handler, there will be information about how the + # wrapSafeChainCommand function errored out. To avoid users assuming this + # is a safe-chain bug, display an explicit error message afterwards. + command $original_cmd $cmd_args + set oldstatus $status + echo "safe-chain tried to run $original_cmd but it doesn't seem to be installed in your \$PATH." >&2 + return $oldstatus + end + if type -q safe-chain # If the safe-chain command is available, just run it with the provided arguments safe-chain $original_cmd $cmd_args From dba101daa700e3ed9de1120527c6be0c5b08d048 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 12:47:57 +0100 Subject: [PATCH 27/98] Add ultimate installer for Windows --- packages/safe-chain/bin/safe-chain.js | 32 +++--- .../src/installation/installUltimate.js | 101 ++++++++++++++++++ 2 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 packages/safe-chain/src/installation/installUltimate.js diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 841ccee..e33ad9f 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -16,6 +16,7 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import { knownAikidoTools } from "../src/shell-integration/helpers.js"; +import { installUltimate } from "../src/installation/installUltimate.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -62,6 +63,8 @@ if (tool) { process.exit(0); } else if (command === "setup") { setup(); +} else if (command === "--ultimate") { + installUltimate(); } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -82,36 +85,41 @@ if (tool) { function writeHelp() { ui.writeInformation( - chalk.bold("Usage: ") + chalk.cyan("safe-chain ") + chalk.bold("Usage: ") + chalk.cyan("safe-chain "), ); ui.emptyLine(); ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( - "teardown" + "teardown", )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( - "--version" - )}` + "--version", + )}`, ); ui.emptyLine(); ui.writeInformation( `- ${chalk.cyan( - "safe-chain setup" - )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` + "safe-chain setup", + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`, ); ui.writeInformation( `- ${chalk.cyan( - "safe-chain teardown" - )}: This will remove safe-chain aliases from your shell configuration.` + "safe-chain --ultimate", + )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, ); ui.writeInformation( `- ${chalk.cyan( - "safe-chain setup-ci" - )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` + "safe-chain teardown", + )}: This will remove safe-chain aliases from your shell configuration.`, + ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain setup-ci", + )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`, ); ui.writeInformation( `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan( - "-v" - )}): Display the current version of safe-chain.` + "-v", + )}): Display the current version of safe-chain.`, ); ui.emptyLine(); } diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js new file mode 100644 index 0000000..e1323db --- /dev/null +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -0,0 +1,101 @@ +import { platform, arch, tmpdir } from "os"; +import { createWriteStream, unlinkSync } from "fs"; +import { join } from "path"; +import { execSync } from "child_process"; +import { pipeline } from "stream/promises"; +import fetch from "make-fetch-happen"; +import { ui } from "../environment/userInteraction.js"; + +const ULTIMATE_VERSION = "v0.2.0"; + +export function installUltimate() { + const operatingSystem = platform(); + + if (operatingSystem === "win32") { + installOnWindows(); + } else { + ui.writeInformation( + `${operatingSystem} is not supported yet by safe-chain's ultimate version.`, + ); + } +} + +async function installOnWindows() { + if (!isRunningAsAdmin()) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator)." + ); + return; + } + + const architecture = getWindowsArchitecture(); + const downloadUrl = buildDownloadUrl(architecture); + const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + + ui.writeInformation(`Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`); + ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeVerbose(`Destination: ${msiPath}`); + await downloadFile(downloadUrl, msiPath); + + ui.writeInformation("Installing SafeChain Agent..."); + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + runMsiInstaller(msiPath); + + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + ui.writeInformation("SafeChain Agent installed successfully!"); +} + +function isRunningAsAdmin() { + try { + execSync("net session", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function getWindowsArchitecture() { + const nodeArch = arch(); + if (nodeArch === "x64") return "amd64"; + if (nodeArch === "arm64") return "arm64"; + throw new Error(`Unsupported architecture: ${nodeArch}`); +} + +/** + * @param {string} architecture + */ +function buildDownloadUrl(architecture) { + return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-${architecture}.msi`; +} + +/** + * @param {string} url + * @param {string} destPath + */ +async function downloadFile(url, destPath) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Download failed: ${response.statusText}`); + } + await pipeline(response.body, createWriteStream(destPath)); +} + +/** + * @param {string} msiPath + */ +function runMsiInstaller(msiPath) { + execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); +} + +/** + * @param {string} msiPath + */ +function cleanup(msiPath) { + try { + unlinkSync(msiPath); + } catch { + // Ignore cleanup errors + } +} From 2a649c5ef84d0bfc84d4459b044a121fdaeb8928 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:28:16 +0100 Subject: [PATCH 28/98] Start and stop safe-chain agent's Windows service. --- .../src/installation/installUltimate.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index e1323db..a638407 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -38,13 +38,18 @@ async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); + stopServiceIfRunning(); + ui.writeInformation("Installing SafeChain Agent..."); ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); runMsiInstaller(msiPath); + ui.writeInformation("Starting SafeChain Agent service..."); + startService(); + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); cleanup(msiPath); - ui.writeInformation("SafeChain Agent installed successfully!"); + ui.writeInformation("SafeChain Agent installed and started successfully!"); } function isRunningAsAdmin() { @@ -89,6 +94,22 @@ function runMsiInstaller(msiPath) { execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); } +function stopServiceIfRunning() { + try { + ui.writeInformation("Stopping existing SafeChain Agent service..."); + ui.writeVerbose('Running: net stop "SafeChainAgent"'); + execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); + } catch { + // Service is not running or doesn't exist, which is fine + ui.writeVerbose("SafeChain Agent service not running or not installed."); + } +} + +function startService() { + ui.writeVerbose('Running: net start "SafeChainAgent"'); + execSync('net start "SafeChainAgent"', { stdio: "inherit" }); +} + /** * @param {string} msiPath */ From 7f6ce79f4411d0d63f7392118ecb8be48a0f956b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:48:33 +0100 Subject: [PATCH 29/98] Overwrite the agent if it's already installed. --- packages/safe-chain/src/installation/installUltimate.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index a638407..5d38eb0 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -41,7 +41,7 @@ async function installOnWindows() { stopServiceIfRunning(); ui.writeInformation("Installing SafeChain Agent..."); - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`); runMsiInstaller(msiPath); ui.writeInformation("Starting SafeChain Agent service..."); @@ -91,7 +91,10 @@ async function downloadFile(url, destPath) { * @param {string} msiPath */ function runMsiInstaller(msiPath) { - execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); + // Use /i for install/upgrade with REINSTALL=ALL REINSTALLMODE=vomus + // This forces a reinstall of all features if the product is already installed + // /qn = quiet mode (no UI) + execSync(`msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`, { stdio: "inherit" }); } function stopServiceIfRunning() { From 8410b94b4ca5a2fc0132ccccb5d9bdc39f30366a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:54:32 +0100 Subject: [PATCH 30/98] Improve updating existing agent install --- .../src/installation/installUltimate.js | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 5d38eb0..1860b80 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -24,7 +24,7 @@ async function installOnWindows() { if (!isRunningAsAdmin()) { ui.writeError("Administrator privileges required."); ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator)." + "Please run this command in an elevated terminal (Run as Administrator).", ); return; } @@ -33,15 +33,20 @@ async function installOnWindows() { const downloadUrl = buildDownloadUrl(architecture); const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); - ui.writeInformation(`Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`); + ui.writeInformation( + `Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`, + ); ui.writeVerbose(`Download URL: ${downloadUrl}`); ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); stopServiceIfRunning(); + // Wait a moment for the service to fully stop before installing + await new Promise((resolve) => setTimeout(resolve, 10000)); + ui.writeInformation("Installing SafeChain Agent..."); - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`); + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); runMsiInstaller(msiPath); ui.writeInformation("Starting SafeChain Agent service..."); @@ -91,10 +96,23 @@ async function downloadFile(url, destPath) { * @param {string} msiPath */ function runMsiInstaller(msiPath) { - // Use /i for install/upgrade with REINSTALL=ALL REINSTALLMODE=vomus - // This forces a reinstall of all features if the product is already installed + // Try to install/upgrade + // /i = install (will upgrade if product code matches) // /qn = quiet mode (no UI) - execSync(`msiexec /i "${msiPath}" /qn REINSTALL=ALL REINSTALLMODE=vomus`, { stdio: "inherit" }); + // /norestart = suppress restarts + try { + execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); + } catch (error) { + // If installation fails, it might be because it's already installed + // Try to force a reinstall + ui.writeVerbose( + "Initial installation failed, attempting to force reinstall...", + ); + execSync( + `msiexec /i "${msiPath}" /qn /norestart REINSTALL=ALL REINSTALLMODE=vomus`, + { stdio: "inherit" }, + ); + } } function stopServiceIfRunning() { From 2bfce02e66f5f8911d68a9fcd3ab7c95e526b5e2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 13:55:41 +0100 Subject: [PATCH 31/98] Fix linting --- packages/safe-chain/src/installation/installUltimate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 1860b80..278aab4 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -102,7 +102,7 @@ function runMsiInstaller(msiPath) { // /norestart = suppress restarts try { execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); - } catch (error) { + } catch { // If installation fails, it might be because it's already installed // Try to force a reinstall ui.writeVerbose( From 14ff245924ae5cf47abb9138b932d09c943d03f2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:00:09 +0100 Subject: [PATCH 32/98] Uninstall safe-chain agent if it's there, before re-installing --- .../src/installation/installUltimate.js | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 278aab4..6cb3f46 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -41,9 +41,10 @@ async function installOnWindows() { await downloadFile(downloadUrl, msiPath); stopServiceIfRunning(); + uninstallIfInstalled(); - // Wait a moment for the service to fully stop before installing - await new Promise((resolve) => setTimeout(resolve, 10000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); ui.writeInformation("Installing SafeChain Agent..."); ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); @@ -92,27 +93,25 @@ async function downloadFile(url, destPath) { await pipeline(response.body, createWriteStream(destPath)); } +function uninstallIfInstalled() { + try { + ui.writeInformation("Uninstalling existing SafeChain Agent..."); + ui.writeVerbose('Running: wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive'); + execSync('wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive', { stdio: "inherit" }); + } catch { + // Not installed or uninstall failed, which is fine for a fresh install + ui.writeVerbose("No existing SafeChain Agent installation found."); + } +} + /** * @param {string} msiPath */ function runMsiInstaller(msiPath) { - // Try to install/upgrade - // /i = install (will upgrade if product code matches) + // /i = install // /qn = quiet mode (no UI) // /norestart = suppress restarts - try { - execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); - } catch { - // If installation fails, it might be because it's already installed - // Try to force a reinstall - ui.writeVerbose( - "Initial installation failed, attempting to force reinstall...", - ); - execSync( - `msiexec /i "${msiPath}" /qn /norestart REINSTALL=ALL REINSTALLMODE=vomus`, - { stdio: "inherit" }, - ); - } + execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); } function stopServiceIfRunning() { From 0be42c81326421ae1b577cbeb7bfeb2400bbeafe Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:06:43 +0100 Subject: [PATCH 33/98] Parse cli args in ultimate installation --- .../src/installation/installUltimate.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 6cb3f46..fc2b93f 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -5,10 +5,13 @@ import { execSync } from "child_process"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; import { ui } from "../environment/userInteraction.js"; +import { initializeCliArguments } from "../config/cliArguments.js"; const ULTIMATE_VERSION = "v0.2.0"; export function installUltimate() { + initializeCliArguments(process.argv); + const operatingSystem = platform(); if (operatingSystem === "win32") { @@ -96,8 +99,25 @@ async function downloadFile(url, destPath) { function uninstallIfInstalled() { try { ui.writeInformation("Uninstalling existing SafeChain Agent..."); - ui.writeVerbose('Running: wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive'); - execSync('wmic product where "name=\'SafeChain Agent\'" call uninstall /nointeractive', { stdio: "inherit" }); + + // Use PowerShell to find the product code, then use msiexec to uninstall + // This is the modern alternative to wmic which is deprecated + const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; + ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); + + const productCode = execSync(findProductCodeCmd, { + encoding: "utf8", + }).trim(); + + if (productCode) { + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + execSync(`msiexec /x ${productCode} /qn /norestart`, { + stdio: "inherit", + }); + } else { + ui.writeVerbose("No existing SafeChain Agent installation found."); + } } catch { // Not installed or uninstall failed, which is fine for a fresh install ui.writeVerbose("No existing SafeChain Agent installation found."); From bee196cc5589a3d0dd539cbe7de039f0de6036b1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:23:15 +0100 Subject: [PATCH 34/98] Check if the agents service is running before starting it --- .../safe-chain/src/installation/installUltimate.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index fc2b93f..dd8de84 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -146,6 +146,19 @@ function stopServiceIfRunning() { } function startService() { + try { + // Check if service is already running + ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); + const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); + + if (status.includes("RUNNING")) { + ui.writeVerbose("SafeChain Agent service is already running."); + return; + } + } catch { + // Service might not exist yet or query failed, proceed with start + } + ui.writeVerbose('Running: net start "SafeChainAgent"'); execSync('net start "SafeChainAgent"', { stdio: "inherit" }); } From d03a3a3a4bae115980d7a3e2db44de1470b10855 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:30:09 +0100 Subject: [PATCH 35/98] Improve output --- .../src/installation/installUltimate.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index dd8de84..d1ccf28 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -36,29 +36,35 @@ async function installOnWindows() { const downloadUrl = buildDownloadUrl(architecture); const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + ui.emptyLine(); ui.writeInformation( - `Downloading SafeChain Agent ${ULTIMATE_VERSION} for ${architecture}...`, + `📥 Downloading SafeChain Agent ${ULTIMATE_VERSION} (${architecture})...`, ); ui.writeVerbose(`Download URL: ${downloadUrl}`); ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); + ui.emptyLine(); stopServiceIfRunning(); uninstallIfInstalled(); // Wait a moment for uninstall to complete await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("Installing SafeChain Agent..."); + ui.writeInformation("⚙️ Installing SafeChain Agent..."); ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); runMsiInstaller(msiPath); - ui.writeInformation("Starting SafeChain Agent service..."); + ui.emptyLine(); + ui.writeInformation("🚀 Starting SafeChain Agent service..."); startService(); ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); cleanup(msiPath); - ui.writeInformation("SafeChain Agent installed and started successfully!"); + + ui.emptyLine(); + ui.writeInformation("✅ SafeChain Agent installed and started successfully!"); + ui.emptyLine(); } function isRunningAsAdmin() { @@ -98,8 +104,6 @@ async function downloadFile(url, destPath) { function uninstallIfInstalled() { try { - ui.writeInformation("Uninstalling existing SafeChain Agent..."); - // Use PowerShell to find the product code, then use msiexec to uninstall // This is the modern alternative to wmic which is deprecated const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; @@ -110,17 +114,18 @@ function uninstallIfInstalled() { }).trim(); if (productCode) { + ui.writeInformation("🗑️ Removing previous installation..."); ui.writeVerbose(`Found product code: ${productCode}`); ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); execSync(`msiexec /x ${productCode} /qn /norestart`, { stdio: "inherit", }); } else { - ui.writeVerbose("No existing SafeChain Agent installation found."); + ui.writeVerbose("No existing installation found (fresh install)."); } } catch { // Not installed or uninstall failed, which is fine for a fresh install - ui.writeVerbose("No existing SafeChain Agent installation found."); + ui.writeVerbose("No existing installation found (fresh install)."); } } @@ -136,12 +141,12 @@ function runMsiInstaller(msiPath) { function stopServiceIfRunning() { try { - ui.writeInformation("Stopping existing SafeChain Agent service..."); + ui.writeInformation("⏹️ Stopping running service..."); ui.writeVerbose('Running: net stop "SafeChainAgent"'); execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); } catch { // Service is not running or doesn't exist, which is fine - ui.writeVerbose("SafeChain Agent service not running or not installed."); + ui.writeVerbose("Service not running (will start after installation)."); } } From 27980aec8279a564cfbc3def0004211ba05746db Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:46:04 +0100 Subject: [PATCH 36/98] Restructure code into separate files --- .../src/installation/downloadAgent.js | 40 +++++ .../src/installation/installOnWindows.js | 146 +++++++++++++++ .../src/installation/installUltimate.js | 166 +----------------- packages/safe-chain/src/main.js | 10 +- 4 files changed, 193 insertions(+), 169 deletions(-) create mode 100644 packages/safe-chain/src/installation/downloadAgent.js create mode 100644 packages/safe-chain/src/installation/installOnWindows.js diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js new file mode 100644 index 0000000..2e45b79 --- /dev/null +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -0,0 +1,40 @@ +import { createWriteStream } from "fs"; +import { pipeline } from "stream/promises"; +import fetch from "make-fetch-happen"; + +const ULTIMATE_VERSION = "v0.2.0"; + +/** + * @typedef {"windows"} Platform + * @typedef {"amd64" | "arm64"} Architecture + */ + +/** + * Builds the download URL for the SafeChain Agent installer. + * @param {Platform} platform + * @param {Architecture} architecture + */ +export function getAgentDownloadUrl(platform, architecture) { + const extension = platform === "windows" ? "msi" : "pkg"; + return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-${platform}-${architecture}.${extension}`; +} + +/** + * Downloads a file from a URL to a local path. + * @param {string} url + * @param {string} destPath + */ +export async function downloadFile(url, destPath) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Download failed: ${response.statusText}`); + } + await pipeline(response.body, createWriteStream(destPath)); +} + +/** + * Returns the current agent version. + */ +export function getAgentVersion() { + return ULTIMATE_VERSION; +} diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js new file mode 100644 index 0000000..27db104 --- /dev/null +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -0,0 +1,146 @@ +import { arch, tmpdir } from "os"; +import { unlinkSync } from "fs"; +import { join } from "path"; +import { execSync } from "child_process"; +import { ui } from "../environment/userInteraction.js"; +import { + getAgentDownloadUrl, + getAgentVersion, + downloadFile, +} from "./downloadAgent.js"; + +export async function installOnWindows() { + if (!isRunningAsAdmin()) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return; + } + + const architecture = getWindowsArchitecture(); + const downloadUrl = getAgentDownloadUrl("windows", architecture); + const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + + ui.emptyLine(); + ui.writeInformation( + `📥 Downloading SafeChain Agent ${getAgentVersion()} (${architecture})...`, + ); + ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeVerbose(`Destination: ${msiPath}`); + await downloadFile(downloadUrl, msiPath); + + ui.emptyLine(); + stopServiceIfRunning(); + uninstallIfInstalled(); + + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); + + ui.writeInformation("⚙️ Installing SafeChain Agent..."); + runMsiInstaller(msiPath); + + ui.emptyLine(); + ui.writeInformation("🚀 Starting SafeChain Agent service..."); + startService(); + + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + + ui.emptyLine(); + ui.writeInformation("✅ SafeChain Agent installed and started successfully!"); + ui.emptyLine(); +} + +function isRunningAsAdmin() { + try { + execSync("net session", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function getWindowsArchitecture() { + const nodeArch = arch(); + if (nodeArch === "x64") return "amd64"; + if (nodeArch === "arm64") return "arm64"; + throw new Error(`Unsupported architecture: ${nodeArch}`); +} + +function uninstallIfInstalled() { + try { + // Use PowerShell to find the product code, then use msiexec to uninstall + // This is the modern alternative to wmic which is deprecated + const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; + ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); + + const productCode = execSync(findProductCodeCmd, { + encoding: "utf8", + }).trim(); + + if (productCode) { + ui.writeInformation("🗑️ Removing previous installation..."); + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + execSync(`msiexec /x ${productCode} /qn /norestart`, { + stdio: "inherit", + }); + } else { + ui.writeVerbose("No existing installation found (fresh install)."); + } + } catch { + // Not installed or uninstall failed, which is fine for a fresh install + ui.writeVerbose("No existing installation found (fresh install)."); + } +} + +/** + * @param {string} msiPath + */ +function runMsiInstaller(msiPath) { + // /i = install + // /qn = quiet mode (no UI) + ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); +} + +function stopServiceIfRunning() { + try { + ui.writeInformation("⏹️ Stopping running service..."); + ui.writeVerbose('Running: net stop "SafeChainAgent"'); + execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); + } catch { + // Service is not running or doesn't exist, which is fine + ui.writeVerbose("Service not running (will start after installation)."); + } +} + +function startService() { + try { + // Check if service is already running + ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); + const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); + + if (status.includes("RUNNING")) { + ui.writeVerbose("SafeChain Agent service is already running."); + return; + } + } catch { + // Service might not exist yet or query failed, proceed with start + } + + ui.writeVerbose('Running: net start "SafeChainAgent"'); + execSync('net start "SafeChainAgent"', { stdio: "inherit" }); +} + +/** + * @param {string} msiPath + */ +function cleanup(msiPath) { + try { + unlinkSync(msiPath); + } catch { + // Ignore cleanup errors + } +} diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index d1ccf28..7383d2c 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -1,13 +1,7 @@ -import { platform, arch, tmpdir } from "os"; -import { createWriteStream, unlinkSync } from "fs"; -import { join } from "path"; -import { execSync } from "child_process"; -import { pipeline } from "stream/promises"; -import fetch from "make-fetch-happen"; +import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; - -const ULTIMATE_VERSION = "v0.2.0"; +import { installOnWindows } from "./installOnWindows.js"; export function installUltimate() { initializeCliArguments(process.argv); @@ -22,159 +16,3 @@ export function installUltimate() { ); } } - -async function installOnWindows() { - if (!isRunningAsAdmin()) { - ui.writeError("Administrator privileges required."); - ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator).", - ); - return; - } - - const architecture = getWindowsArchitecture(); - const downloadUrl = buildDownloadUrl(architecture); - const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); - - ui.emptyLine(); - ui.writeInformation( - `📥 Downloading SafeChain Agent ${ULTIMATE_VERSION} (${architecture})...`, - ); - ui.writeVerbose(`Download URL: ${downloadUrl}`); - ui.writeVerbose(`Destination: ${msiPath}`); - await downloadFile(downloadUrl, msiPath); - - ui.emptyLine(); - stopServiceIfRunning(); - uninstallIfInstalled(); - - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - - ui.writeInformation("⚙️ Installing SafeChain Agent..."); - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn /norestart`); - runMsiInstaller(msiPath); - - ui.emptyLine(); - ui.writeInformation("🚀 Starting SafeChain Agent service..."); - startService(); - - ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - cleanup(msiPath); - - ui.emptyLine(); - ui.writeInformation("✅ SafeChain Agent installed and started successfully!"); - ui.emptyLine(); -} - -function isRunningAsAdmin() { - try { - execSync("net session", { stdio: "ignore" }); - return true; - } catch { - return false; - } -} - -function getWindowsArchitecture() { - const nodeArch = arch(); - if (nodeArch === "x64") return "amd64"; - if (nodeArch === "arm64") return "arm64"; - throw new Error(`Unsupported architecture: ${nodeArch}`); -} - -/** - * @param {string} architecture - */ -function buildDownloadUrl(architecture) { - return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-${architecture}.msi`; -} - -/** - * @param {string} url - * @param {string} destPath - */ -async function downloadFile(url, destPath) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Download failed: ${response.statusText}`); - } - await pipeline(response.body, createWriteStream(destPath)); -} - -function uninstallIfInstalled() { - try { - // Use PowerShell to find the product code, then use msiexec to uninstall - // This is the modern alternative to wmic which is deprecated - const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; - ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); - - const productCode = execSync(findProductCodeCmd, { - encoding: "utf8", - }).trim(); - - if (productCode) { - ui.writeInformation("🗑️ Removing previous installation..."); - ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - execSync(`msiexec /x ${productCode} /qn /norestart`, { - stdio: "inherit", - }); - } else { - ui.writeVerbose("No existing installation found (fresh install)."); - } - } catch { - // Not installed or uninstall failed, which is fine for a fresh install - ui.writeVerbose("No existing installation found (fresh install)."); - } -} - -/** - * @param {string} msiPath - */ -function runMsiInstaller(msiPath) { - // /i = install - // /qn = quiet mode (no UI) - // /norestart = suppress restarts - execSync(`msiexec /i "${msiPath}" /qn /norestart`, { stdio: "inherit" }); -} - -function stopServiceIfRunning() { - try { - ui.writeInformation("⏹️ Stopping running service..."); - ui.writeVerbose('Running: net stop "SafeChainAgent"'); - execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); - } catch { - // Service is not running or doesn't exist, which is fine - ui.writeVerbose("Service not running (will start after installation)."); - } -} - -function startService() { - try { - // Check if service is already running - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); - - if (status.includes("RUNNING")) { - ui.writeVerbose("SafeChain Agent service is already running."); - return; - } - } catch { - // Service might not exist yet or query failed, proceed with start - } - - ui.writeVerbose('Running: net start "SafeChainAgent"'); - execSync('net start "SafeChainAgent"', { stdio: "inherit" }); -} - -/** - * @param {string} msiPath - */ -function cleanup(msiPath) { - try { - unlinkSync(msiPath); - } catch { - // Ignore cleanup errors - } -} diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 9b7ba53..0b37eba 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -73,20 +73,20 @@ export async function main(args) { ui.writeVerbose( `${chalk.green("✔")} Safe-chain: Scanned ${ auditStats.totalPackages - } packages, no malware found.` + } packages, no malware found.`, ); } if (proxy.hasSuppressedVersions()) { ui.writeInformation( `${chalk.yellow( - "ℹ" - )} Safe-chain: Some package versions were suppressed due to minimum age requirement.` + "ℹ", + )} Safe-chain: Some package versions were suppressed due to minimum age requirement.`, ); ui.writeInformation( ` To disable this check, use: ${chalk.cyan( - "--safe-chain-skip-minimum-package-age" - )}` + "--safe-chain-skip-minimum-package-age", + )}`, ); } From fa94784130c064a712b0295213ca75f7f3ef7a0a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 14:53:33 +0100 Subject: [PATCH 37/98] Move download name construction to os installer function --- .../safe-chain/src/installation/downloadAgent.js | 13 +++---------- .../safe-chain/src/installation/installOnWindows.js | 3 ++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 2e45b79..d74cbf7 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -4,19 +4,12 @@ import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.0"; -/** - * @typedef {"windows"} Platform - * @typedef {"amd64" | "arm64"} Architecture - */ - /** * Builds the download URL for the SafeChain Agent installer. - * @param {Platform} platform - * @param {Architecture} architecture + * @param {string} fileName */ -export function getAgentDownloadUrl(platform, architecture) { - const extension = platform === "windows" ? "msi" : "pkg"; - return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-${platform}-${architecture}.${extension}`; +export function getAgentDownloadUrl(fileName) { + return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/${fileName}`; } /** diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 27db104..9f92893 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -19,7 +19,8 @@ export async function installOnWindows() { } const architecture = getWindowsArchitecture(); - const downloadUrl = getAgentDownloadUrl("windows", architecture); + const fileName = `SafeChainAgent-windows-${architecture}.msi`; + const downloadUrl = getAgentDownloadUrl(fileName); const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); ui.emptyLine(); From d86246a71d7e7f10d5fffed0f3bdc85d50cac995 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:22:24 +0100 Subject: [PATCH 38/98] Handle code quality comments --- packages/safe-chain/bin/safe-chain.js | 4 +++- packages/safe-chain/src/installation/installOnWindows.js | 4 ++-- packages/safe-chain/src/installation/installUltimate.js | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index e33ad9f..d048ce1 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -64,7 +64,9 @@ if (tool) { } else if (command === "setup") { setup(); } else if (command === "--ultimate") { - installUltimate(); + (async () => { + await installUltimate(); + })(); } else if (command === "teardown") { teardownDirectories(); teardown(); diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 9f92893..094ec58 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -103,7 +103,7 @@ function runMsiInstaller(msiPath) { // /i = install // /qn = quiet mode (no UI) ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); - execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); + execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); // noopengrep this is ok, we control the msiPath } function stopServiceIfRunning() { @@ -128,7 +128,7 @@ function startService() { return; } } catch { - // Service might not exist yet or query failed, proceed with start + ui.writeVerbose("Service not found or query failed, attempting to start."); } ui.writeVerbose('Running: net start "SafeChainAgent"'); diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 7383d2c..3b6846a 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -3,13 +3,13 @@ import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; import { installOnWindows } from "./installOnWindows.js"; -export function installUltimate() { +export async function installUltimate() { initializeCliArguments(process.argv); const operatingSystem = platform(); if (operatingSystem === "win32") { - installOnWindows(); + await installOnWindows(); } else { ui.writeInformation( `${operatingSystem} is not supported yet by safe-chain's ultimate version.`, From 67b4be83f9368497fc01a410bfd0ed33c7ac23f2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:24:49 +0100 Subject: [PATCH 39/98] Log when installer file cleanup failed --- packages/safe-chain/src/installation/installOnWindows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 094ec58..f3c9ee8 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -142,6 +142,6 @@ function cleanup(msiPath) { try { unlinkSync(msiPath); } catch { - // Ignore cleanup errors + ui.writeVerbose("Failed to clean up temporary installer file."); } } From 5fd3ce0b6edbbd41e128f1665bd61e1369bd68ea Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:31:41 +0100 Subject: [PATCH 40/98] Use safeSpawn instead of execSync --- .../src/installation/installOnWindows.js | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index f3c9ee8..54893bf 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,8 +1,8 @@ import { arch, tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; -import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; +import { safeSpawn } from "../utils/safeSpawn.js"; import { getAgentDownloadUrl, getAgentVersion, @@ -10,7 +10,7 @@ import { } from "./downloadAgent.js"; export async function installOnWindows() { - if (!isRunningAsAdmin()) { + if (!(await isRunningAsAdmin())) { ui.writeError("Administrator privileges required."); ui.writeInformation( "Please run this command in an elevated terminal (Run as Administrator).", @@ -32,18 +32,18 @@ export async function installOnWindows() { await downloadFile(downloadUrl, msiPath); ui.emptyLine(); - stopServiceIfRunning(); - uninstallIfInstalled(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); // Wait a moment for uninstall to complete await new Promise((resolve) => setTimeout(resolve, 2000)); ui.writeInformation("⚙️ Installing SafeChain Agent..."); - runMsiInstaller(msiPath); + await runMsiInstaller(msiPath); ui.emptyLine(); ui.writeInformation("🚀 Starting SafeChain Agent service..."); - startService(); + await startService(); ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); cleanup(msiPath); @@ -53,13 +53,9 @@ export async function installOnWindows() { ui.emptyLine(); } -function isRunningAsAdmin() { - try { - execSync("net session", { stdio: "ignore" }); - return true; - } catch { - return false; - } +async function isRunningAsAdmin() { + const result = await safeSpawn("net", ["session"], { stdio: "ignore" }); + return result.status === 0; } function getWindowsArchitecture() { @@ -69,29 +65,31 @@ function getWindowsArchitecture() { throw new Error(`Unsupported architecture: ${nodeArch}`); } -function uninstallIfInstalled() { - try { - // Use PowerShell to find the product code, then use msiexec to uninstall - // This is the modern alternative to wmic which is deprecated - const findProductCodeCmd = `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`; - ui.writeVerbose(`Finding product code: ${findProductCodeCmd}`); +async function uninstallIfInstalled() { + // Use PowerShell to find the product code, then use msiexec to uninstall + // This is the modern alternative to wmic which is deprecated + const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; + ui.writeVerbose(`Finding product code with PowerShell`); - const productCode = execSync(findProductCodeCmd, { - encoding: "utf8", - }).trim(); + const result = await safeSpawn("powershell", ["-Command", powershellScript], { + stdio: "pipe", + }); - if (productCode) { - ui.writeInformation("🗑️ Removing previous installation..."); - ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - execSync(`msiexec /x ${productCode} /qn /norestart`, { - stdio: "inherit", - }); - } else { - ui.writeVerbose("No existing installation found (fresh install)."); - } - } catch { - // Not installed or uninstall failed, which is fine for a fresh install + if (result.status !== 0) { + ui.writeVerbose("No existing installation found (fresh install)."); + return; + } + + const productCode = result.stdout.trim(); + + if (productCode) { + ui.writeInformation("🗑️ Removing previous installation..."); + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { + stdio: "inherit", + }); + } else { ui.writeVerbose("No existing installation found (fresh install)."); } } @@ -99,40 +97,43 @@ function uninstallIfInstalled() { /** * @param {string} msiPath */ -function runMsiInstaller(msiPath) { +async function runMsiInstaller(msiPath) { // /i = install // /qn = quiet mode (no UI) ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); - execSync(`msiexec /i "${msiPath}" /qn`, { stdio: "inherit" }); // noopengrep this is ok, we control the msiPath + await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { stdio: "inherit" }); } -function stopServiceIfRunning() { - try { - ui.writeInformation("⏹️ Stopping running service..."); - ui.writeVerbose('Running: net stop "SafeChainAgent"'); - execSync('net stop "SafeChainAgent"', { stdio: "inherit" }); - } catch { - // Service is not running or doesn't exist, which is fine +async function stopServiceIfRunning() { + ui.writeInformation("⏹️ Stopping running service..."); + ui.writeVerbose('Running: net stop "SafeChainAgent"'); + const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { + stdio: "inherit", + }); + + if (result.status !== 0) { ui.writeVerbose("Service not running (will start after installation)."); } } -function startService() { - try { - // Check if service is already running - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const status = execSync('sc query "SafeChainAgent"', { encoding: "utf8" }); +async function startService() { + // Check if service is already running + ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); + const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { + stdio: "pipe", + }); - if (status.includes("RUNNING")) { - ui.writeVerbose("SafeChain Agent service is already running."); - return; - } - } catch { + if (queryResult.status === 0 && queryResult.stdout.includes("RUNNING")) { + ui.writeVerbose("SafeChain Agent service is already running."); + return; + } + + if (queryResult.status !== 0) { ui.writeVerbose("Service not found or query failed, attempting to start."); } ui.writeVerbose('Running: net start "SafeChainAgent"'); - execSync('net start "SafeChainAgent"', { stdio: "inherit" }); + await safeSpawn("net", ["start", "SafeChainAgent"], { stdio: "inherit" }); } /** From e9b1c487b747dd952b7b2bf8ba3e0059e38e7a1b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:34:16 +0100 Subject: [PATCH 41/98] Code quality: use early return --- .../src/installation/installOnWindows.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 54893bf..a6e0938 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -82,16 +82,17 @@ async function uninstallIfInstalled() { const productCode = result.stdout.trim(); - if (productCode) { - ui.writeInformation("🗑️ Removing previous installation..."); - ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { - stdio: "inherit", - }); - } else { + if (!productCode) { ui.writeVerbose("No existing installation found (fresh install)."); + return; } + + ui.writeInformation("🗑️ Removing previous installation..."); + ui.writeVerbose(`Found product code: ${productCode}`); + ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); + await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { + stdio: "inherit", + }); } /** From 34832199254f8b32b00d22124600a80a4e12420b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:45:32 +0100 Subject: [PATCH 42/98] Improve error handling --- .../src/installation/installOnWindows.js | 93 ++++++++++++------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index a6e0938..6db1cb0 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -31,31 +31,43 @@ export async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); - ui.emptyLine(); - await stopServiceIfRunning(); - await uninstallIfInstalled(); + try { + ui.emptyLine(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("⚙️ Installing SafeChain Agent..."); - await runMsiInstaller(msiPath); + ui.writeInformation("⚙️ Installing SafeChain Agent..."); + await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("🚀 Starting SafeChain Agent service..."); - await startService(); + ui.emptyLine(); + ui.writeInformation("🚀 Starting SafeChain Agent service..."); + await startService(); - ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - cleanup(msiPath); - - ui.emptyLine(); - ui.writeInformation("✅ SafeChain Agent installed and started successfully!"); - ui.emptyLine(); + ui.emptyLine(); + ui.writeInformation( + "✅ SafeChain Agent installed and started successfully!", + ); + ui.emptyLine(); + } finally { + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + } } async function isRunningAsAdmin() { - const result = await safeSpawn("net", ["session"], { stdio: "ignore" }); - return result.status === 0; + const result = await safeSpawn( + "powershell", + [ + "-Command", + "([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)", + ], + { stdio: "pipe" }, + ); + + return result.status === 0 && result.stdout.trim() === "True"; } function getWindowsArchitecture() { @@ -66,8 +78,6 @@ function getWindowsArchitecture() { } async function uninstallIfInstalled() { - // Use PowerShell to find the product code, then use msiexec to uninstall - // This is the modern alternative to wmic which is deprecated const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; ui.writeVerbose(`Finding product code with PowerShell`); @@ -81,7 +91,6 @@ async function uninstallIfInstalled() { } const productCode = result.stdout.trim(); - if (!productCode) { ui.writeVerbose("No existing installation found (fresh install)."); return; @@ -89,27 +98,38 @@ async function uninstallIfInstalled() { ui.writeInformation("🗑️ Removing previous installation..."); ui.writeVerbose(`Found product code: ${productCode}`); - ui.writeVerbose(`Running: msiexec /x ${productCode} /qn /norestart`); - await safeSpawn("msiexec", ["/x", productCode, "/qn", "/norestart"], { - stdio: "inherit", - }); + + const uninstallResult = await safeSpawn( + "msiexec", + ["/x", productCode, "/qn", "/norestart"], + { stdio: "inherit" }, + ); + + if (uninstallResult.status !== 0) { + throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`); + } } /** * @param {string} msiPath */ async function runMsiInstaller(msiPath) { - // /i = install - // /qn = quiet mode (no UI) ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); - await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { stdio: "inherit" }); + + const result = await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { + stdio: "inherit", + }); + + if (result.status !== 0) { + throw new Error(`MSI installer failed (exit code: ${result.status})`); + } } async function stopServiceIfRunning() { ui.writeInformation("⏹️ Stopping running service..."); - ui.writeVerbose('Running: net stop "SafeChainAgent"'); + const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { - stdio: "inherit", + stdio: "pipe", }); if (result.status !== 0) { @@ -118,7 +138,6 @@ async function stopServiceIfRunning() { } async function startService() { - // Check if service is already running ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { stdio: "pipe", @@ -129,12 +148,14 @@ async function startService() { return; } - if (queryResult.status !== 0) { - ui.writeVerbose("Service not found or query failed, attempting to start."); - } - ui.writeVerbose('Running: net start "SafeChainAgent"'); - await safeSpawn("net", ["start", "SafeChainAgent"], { stdio: "inherit" }); + const startResult = await safeSpawn("net", ["start", "SafeChainAgent"], { + stdio: "pipe", + }); + + if (startResult.status !== 0) { + throw new Error(`Failed to start service (exit code: ${startResult.status})`); + } } /** From 3c40c60a3ef17821914cda3ea3123c700b59bcae Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:54:02 +0100 Subject: [PATCH 43/98] Write error output --- packages/safe-chain/src/installation/installOnWindows.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 6db1cb0..8610394 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -106,6 +106,8 @@ async function uninstallIfInstalled() { ); if (uninstallResult.status !== 0) { + ui.writeInformation(uninstallResult.stdout); + ui.writeInformation(uninstallResult.stderr); throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`); } } @@ -154,7 +156,9 @@ async function startService() { }); if (startResult.status !== 0) { - throw new Error(`Failed to start service (exit code: ${startResult.status})`); + throw new Error( + `Failed to start service (exit code: ${startResult.status})`, + ); } } From 38888813cf8b263728ef5db3c2f3b5dcbe62d982 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 15:58:11 +0100 Subject: [PATCH 44/98] Temporarily disable cleanup --- .../src/installation/installOnWindows.js | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 8610394..0257fd8 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -31,30 +31,29 @@ export async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); - try { - ui.emptyLine(); - await stopServiceIfRunning(); - await uninstallIfInstalled(); + // try { + ui.emptyLine(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("⚙️ Installing SafeChain Agent..."); - await runMsiInstaller(msiPath); + ui.writeInformation("⚙️ Installing SafeChain Agent..."); + await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("🚀 Starting SafeChain Agent service..."); - await startService(); + ui.emptyLine(); + ui.writeInformation("🚀 Starting SafeChain Agent service..."); + await startService(); - ui.emptyLine(); - ui.writeInformation( - "✅ SafeChain Agent installed and started successfully!", - ); - ui.emptyLine(); - } finally { - ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - cleanup(msiPath); - } + ui.emptyLine(); + ui.writeInformation("✅ SafeChain Agent installed and started successfully!"); + ui.emptyLine(); + // } + // finally { + // ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + // cleanup(msiPath); + // } } async function isRunningAsAdmin() { From a7315d29c4233aeaaa79809f0b0cb8c5eae685d0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:03:51 +0100 Subject: [PATCH 45/98] Write stdout stderr --- .../src/installation/installOnWindows.js | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 0257fd8..7511eef 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -31,29 +31,30 @@ export async function installOnWindows() { ui.writeVerbose(`Destination: ${msiPath}`); await downloadFile(downloadUrl, msiPath); - // try { - ui.emptyLine(); - await stopServiceIfRunning(); - await uninstallIfInstalled(); + try { + ui.emptyLine(); + await stopServiceIfRunning(); + await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Wait a moment for uninstall to complete + await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("⚙️ Installing SafeChain Agent..."); - await runMsiInstaller(msiPath); + ui.writeInformation("⚙️ Installing SafeChain Agent..."); + await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("🚀 Starting SafeChain Agent service..."); - await startService(); + ui.emptyLine(); + ui.writeInformation("🚀 Starting SafeChain Agent service..."); + await startService(); - ui.emptyLine(); - ui.writeInformation("✅ SafeChain Agent installed and started successfully!"); - ui.emptyLine(); - // } - // finally { - // ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); - // cleanup(msiPath); - // } + ui.emptyLine(); + ui.writeInformation( + "✅ SafeChain Agent installed and started successfully!", + ); + ui.emptyLine(); + } finally { + ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`); + cleanup(msiPath); + } } async function isRunningAsAdmin() { @@ -85,7 +86,9 @@ async function uninstallIfInstalled() { }); if (result.status !== 0) { - ui.writeVerbose("No existing installation found (fresh install)."); + ui.writeVerbose( + `No existing installation found (fresh install). Output: ${result.stdout} ${result.stderr}`, + ); return; } From 1de6a4ac4b457d47aada7572cae6f0204ea0e603 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:11:51 +0100 Subject: [PATCH 46/98] Use execSync to execute powershell command --- .../src/installation/installOnWindows.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 7511eef..84dcd9c 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,6 +1,7 @@ import { arch, tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; +import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { @@ -81,18 +82,15 @@ async function uninstallIfInstalled() { const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; ui.writeVerbose(`Finding product code with PowerShell`); - const result = await safeSpawn("powershell", ["-Command", powershellScript], { - stdio: "pipe", - }); - - if (result.status !== 0) { - ui.writeVerbose( - `No existing installation found (fresh install). Output: ${result.stdout} ${result.stderr}`, - ); + let productCode; + try { + productCode = execSync(`powershell -Command "${powershellScript}"`, { + encoding: "utf8", + }).trim(); + } catch { + ui.writeVerbose("No existing installation found (fresh install)."); return; } - - const productCode = result.stdout.trim(); if (!productCode) { ui.writeVerbose("No existing installation found (fresh install)."); return; From fc43d93828c969c0071e123be38e146ca5127bed Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:17:34 +0100 Subject: [PATCH 47/98] Fix uninstall --- packages/safe-chain/src/installation/installOnWindows.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 84dcd9c..d6e7207 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -79,14 +79,14 @@ function getWindowsArchitecture() { } async function uninstallIfInstalled() { - const powershellScript = `$app = Get-WmiObject -Class Win32_Product -Filter "Name='SafeChain Agent'"; if ($app) { Write-Output $app.IdentifyingNumber }`; ui.writeVerbose(`Finding product code with PowerShell`); let productCode; try { - productCode = execSync(`powershell -Command "${powershellScript}"`, { - encoding: "utf8", - }).trim(); + productCode = execSync( + `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, + { encoding: "utf8" }, + ).trim(); } catch { ui.writeVerbose("No existing installation found (fresh install)."); return; From 641bfe98352fa9acb9c999c735a3657c295ff268 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:23:59 +0100 Subject: [PATCH 48/98] Cleanup debug logging --- packages/safe-chain/src/installation/installOnWindows.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index d6e7207..60a339b 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -106,8 +106,6 @@ async function uninstallIfInstalled() { ); if (uninstallResult.status !== 0) { - ui.writeInformation(uninstallResult.stdout); - ui.writeInformation(uninstallResult.stderr); throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`); } } From 09130c3294a07353d63e236813cd008ec62ad0e3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 19 Jan 2026 16:25:50 +0100 Subject: [PATCH 49/98] Add explaining comments for powershell scritps --- packages/safe-chain/src/installation/installOnWindows.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 60a339b..41bb1ca 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -59,6 +59,8 @@ export async function installOnWindows() { } async function isRunningAsAdmin() { + // Uses Windows Security API to check if current process has admin privileges. + // Returns "True" or "False" as a string. const result = await safeSpawn( "powershell", [ @@ -79,6 +81,8 @@ function getWindowsArchitecture() { } async function uninstallIfInstalled() { + // Query Win32_Product via WMI to find the installed SafeChain Agent. + // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. ui.writeVerbose(`Finding product code with PowerShell`); let productCode; From 07aa10d869db8b1f5cf16f080fdf222e15bc9a85 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 19 Jan 2026 18:59:37 +0100 Subject: [PATCH 50/98] Fix naming of SafeChain Agent --- packages/safe-chain/src/installation/installOnWindows.js | 8 ++++---- packages/safe-chain/src/installation/installUltimate.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 41bb1ca..9837d18 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -40,16 +40,16 @@ export async function installOnWindows() { // Wait a moment for uninstall to complete await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("⚙️ Installing SafeChain Agent..."); + ui.writeInformation("⚙️ Installing SafeChain Ultimate..."); await runMsiInstaller(msiPath); ui.emptyLine(); - ui.writeInformation("🚀 Starting SafeChain Agent service..."); + ui.writeInformation("🚀 Starting SafeChain Ultimate service..."); await startService(); ui.emptyLine(); ui.writeInformation( - "✅ SafeChain Agent installed and started successfully!", + "✅ SafeChain Ultimate installed and started successfully!", ); ui.emptyLine(); } finally { @@ -148,7 +148,7 @@ async function startService() { }); if (queryResult.status === 0 && queryResult.stdout.includes("RUNNING")) { - ui.writeVerbose("SafeChain Agent service is already running."); + ui.writeVerbose("SafeChain Ultimate service is already running."); return; } diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 3b6846a..086b6a4 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -12,7 +12,7 @@ export async function installUltimate() { await installOnWindows(); } else { ui.writeInformation( - `${operatingSystem} is not supported yet by safe-chain's ultimate version.`, + `${operatingSystem} is not supported yet by SafeChain's ultimate version.`, ); } } From e7de25de5e09eaf544d2571015e7f05bc1aa5e49 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Mon, 19 Jan 2026 19:01:28 +0100 Subject: [PATCH 51/98] Update packages/safe-chain/src/installation/installOnWindows.js --- packages/safe-chain/src/installation/installOnWindows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 9837d18..3cd3428 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -26,7 +26,7 @@ export async function installOnWindows() { ui.emptyLine(); ui.writeInformation( - `📥 Downloading SafeChain Agent ${getAgentVersion()} (${architecture})...`, + `📥 Downloading SafeChain Ultimate ${getAgentVersion()} (${architecture})...`, ); ui.writeVerbose(`Download URL: ${downloadUrl}`); ui.writeVerbose(`Destination: ${msiPath}`); From a1d6f31d028dcccb499e589775ec509f4bd462ef Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 20 Jan 2026 09:12:00 +0100 Subject: [PATCH 52/98] Move os and arch detection to downloader, add checksum verification. --- .../src/installation/downloadAgent.js | 82 ++++++++++++++++++- .../src/installation/installOnWindows.js | 49 +++++------ 2 files changed, 102 insertions(+), 29 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index d74cbf7..2441f7d 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -1,9 +1,25 @@ -import { createWriteStream } from "fs"; +import { createWriteStream, createReadStream } from "fs"; +import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.0"; +const DOWNLOAD_URLS = { + win32: { + x64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-amd64.msi", + checksum: + "sha256:c699f74a3666d85b70b8ede076a2192a6a023f1b395e8e6c7556927ee698a020", + }, + arm64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-arm64.msi", + checksum: + "sha256:5b08dd4749c8befe5379bc01f7a8a5ac1d6a35b6bee37c6c72a4ba8744c3b052", + }, + }, +}; + /** * Builds the download URL for the SafeChain Agent installer. * @param {string} fileName @@ -31,3 +47,67 @@ export async function downloadFile(url, destPath) { export function getAgentVersion() { return ULTIMATE_VERSION; } + +/** + * Returns download info (url, checksum) for the current OS and architecture. + * @returns {{ url: string, checksum: string } | null} + */ +export function getDownloadInfoForCurrentPlatform() { + const platform = process.platform; + const arch = process.arch; + + if (!Object.hasOwn(DOWNLOAD_URLS, platform)) { + return null; + } + const platformUrls = + DOWNLOAD_URLS[/** @type {keyof typeof DOWNLOAD_URLS} */ (platform)]; + + if (!Object.hasOwn(platformUrls, arch)) { + return null; + } + + return platformUrls[/** @type {keyof typeof platformUrls} */ (arch)]; +} + +/** + * Verifies the checksum of a file. + * @param {string} filePath + * @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...") + * @returns {Promise} + */ +async function verifyChecksum(filePath, expectedChecksum) { + const [algorithm, expected] = expectedChecksum.split(":"); + + const hash = createHash(algorithm); + + if (filePath.includes("..")) throw new Error("Invalid file path"); + const stream = createReadStream(filePath); + + for await (const chunk of stream) { + hash.update(chunk); + } + + const actual = hash.digest("hex"); + return actual === expected; +} + +/** + * Downloads the SafeChain agent for the current OS/arch and verifies its checksum. + * @param {string} fileName - Destination file path + * @returns {Promise} The file path if successful, null if no download URL for current platform + */ +export async function downloadAgentToFile(fileName) { + const info = getDownloadInfoForCurrentPlatform(); + if (!info) { + return null; + } + + await downloadFile(info.url, fileName); + + const isValid = await verifyChecksum(fileName, info.checksum); + if (!isValid) { + throw new Error("Checksum verification failed"); + } + + return fileName; +} diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 3cd3428..33ae293 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -1,14 +1,13 @@ -import { arch, tmpdir } from "os"; +import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; -import { - getAgentDownloadUrl, - getAgentVersion, - downloadFile, -} from "./downloadAgent.js"; +import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; + +const WINDOWS_SERVICE_NAME = "SafeChainAgent"; +const WINDOWS_APP_NAME = "SafeChain Agent"; export async function installOnWindows() { if (!(await isRunningAsAdmin())) { @@ -19,18 +18,17 @@ export async function installOnWindows() { return; } - const architecture = getWindowsArchitecture(); - const fileName = `SafeChainAgent-windows-${architecture}.msi`; - const downloadUrl = getAgentDownloadUrl(fileName); - const msiPath = join(tmpdir(), `SafeChainAgent-${Date.now()}.msi`); + const msiPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.msi`); ui.emptyLine(); - ui.writeInformation( - `📥 Downloading SafeChain Ultimate ${getAgentVersion()} (${architecture})...`, - ); - ui.writeVerbose(`Download URL: ${downloadUrl}`); + ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`); ui.writeVerbose(`Destination: ${msiPath}`); - await downloadFile(downloadUrl, msiPath); + + const result = await downloadAgentToFile(msiPath); + if (!result) { + ui.writeError("No download available for this platform/architecture."); + return; + } try { ui.emptyLine(); @@ -73,13 +71,6 @@ async function isRunningAsAdmin() { return result.status === 0 && result.stdout.trim() === "True"; } -function getWindowsArchitecture() { - const nodeArch = arch(); - if (nodeArch === "x64") return "amd64"; - if (nodeArch === "arm64") return "arm64"; - throw new Error(`Unsupported architecture: ${nodeArch}`); -} - async function uninstallIfInstalled() { // Query Win32_Product via WMI to find the installed SafeChain Agent. // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. @@ -88,7 +79,7 @@ async function uninstallIfInstalled() { let productCode; try { productCode = execSync( - `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='SafeChain Agent'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, + `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='${WINDOWS_APP_NAME}'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`, { encoding: "utf8" }, ).trim(); } catch { @@ -132,7 +123,7 @@ async function runMsiInstaller(msiPath) { async function stopServiceIfRunning() { ui.writeInformation("⏹️ Stopping running service..."); - const result = await safeSpawn("net", ["stop", "SafeChainAgent"], { + const result = await safeSpawn("net", ["stop", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); @@ -142,8 +133,10 @@ async function stopServiceIfRunning() { } async function startService() { - ui.writeVerbose('Checking service status: sc query "SafeChainAgent"'); - const queryResult = await safeSpawn("sc", ["query", "SafeChainAgent"], { + ui.writeVerbose( + `Checking service status: sc query "${WINDOWS_SERVICE_NAME}"`, + ); + const queryResult = await safeSpawn("sc", ["query", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); @@ -152,8 +145,8 @@ async function startService() { return; } - ui.writeVerbose('Running: net start "SafeChainAgent"'); - const startResult = await safeSpawn("net", ["start", "SafeChainAgent"], { + ui.writeVerbose(`Running: net start "${WINDOWS_SERVICE_NAME}"`); + const startResult = await safeSpawn("net", ["start", WINDOWS_SERVICE_NAME], { stdio: "pipe", }); From 457b71a7d1a85d220b20abb867feacf8b45b0f9e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 20 Jan 2026 12:21:45 +0100 Subject: [PATCH 53/98] Don't start the windows service - the msi already does this --- .../src/installation/installOnWindows.js | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 33ae293..2380a7f 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -35,16 +35,9 @@ export async function installOnWindows() { await stopServiceIfRunning(); await uninstallIfInstalled(); - // Wait a moment for uninstall to complete - await new Promise((resolve) => setTimeout(resolve, 2000)); - ui.writeInformation("⚙️ Installing SafeChain Ultimate..."); await runMsiInstaller(msiPath); - ui.emptyLine(); - ui.writeInformation("🚀 Starting SafeChain Ultimate service..."); - await startService(); - ui.emptyLine(); ui.writeInformation( "✅ SafeChain Ultimate installed and started successfully!", @@ -132,31 +125,6 @@ async function stopServiceIfRunning() { } } -async function startService() { - ui.writeVerbose( - `Checking service status: sc query "${WINDOWS_SERVICE_NAME}"`, - ); - const queryResult = await safeSpawn("sc", ["query", WINDOWS_SERVICE_NAME], { - stdio: "pipe", - }); - - if (queryResult.status === 0 && queryResult.stdout.includes("RUNNING")) { - ui.writeVerbose("SafeChain Ultimate service is already running."); - return; - } - - ui.writeVerbose(`Running: net start "${WINDOWS_SERVICE_NAME}"`); - const startResult = await safeSpawn("net", ["start", WINDOWS_SERVICE_NAME], { - stdio: "pipe", - }); - - if (startResult.status !== 0) { - throw new Error( - `Failed to start service (exit code: ${startResult.status})`, - ); - } -} - /** * @param {string} msiPath */ From 6c1383a9d3ba3b5ae2097614383cd84370a4cf8a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 21 Jan 2026 07:58:06 +0100 Subject: [PATCH 54/98] Update download urls --- .../src/installation/downloadAgent.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 2441f7d..0ee994e 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -8,14 +8,26 @@ const ULTIMATE_VERSION = "v0.2.0"; const DOWNLOAD_URLS = { win32: { x64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-amd64.msi", + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-amd64.msi", checksum: - "sha256:c699f74a3666d85b70b8ede076a2192a6a023f1b395e8e6c7556927ee698a020", + "sha256:bba5deb250ebc6008f1cb33fa4209d2455a2f47fa99f0a40e3babef64939ac77", }, arm64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.2.0/SafeChainAgent-windows-arm64.msi", + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-arm64.msi", checksum: - "sha256:5b08dd4749c8befe5379bc01f7a8a5ac1d6a35b6bee37c6c72a4ba8744c3b052", + "sha256:9553ed15d5efed4185b990a1b86af0b11c23f11d96f8ce04e16b6b98aaf0506e", + }, + }, + darwin: { + x64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-amd64.pkg", + checksum: + "sha256:cbccf32e987a45bc8cc20b620f7b597ff7f9c2f966c2bc21132349612ddb619f", + }, + arm64: { + url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-arm64.pkg", + checksum: + "sha256:4d53a43a47bf7e8133eb61d306a1fb16348b9ec89c1c825e5f746f4fe847796e", }, }, }; From c8ee15dc576b99e7bbac7773f0ea6df27c7a653a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 21 Jan 2026 09:14:44 +0100 Subject: [PATCH 55/98] Add mac os installation --- .../src/installation/installOnMacOS.js | 78 +++++++++++++++++++ .../src/installation/installUltimate.js | 3 + 2 files changed, 81 insertions(+) create mode 100644 packages/safe-chain/src/installation/installOnMacOS.js diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js new file mode 100644 index 0000000..6963291 --- /dev/null +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -0,0 +1,78 @@ +import { tmpdir } from "os"; +import { unlinkSync } from "fs"; +import { join } from "path"; +import { ui } from "../environment/userInteraction.js"; +import { safeSpawn } from "../utils/safeSpawn.js"; +import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; + +const MACOS_SERVICE_LABEL = "com.aikido.SafeChainAgent"; + +export async function installOnMacOS() { + if (!isRunningAsRoot()) { + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(" sudo safe-chain --ultimate"); + return; + } + + const pkgPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.pkg`); + + ui.emptyLine(); + ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`); + ui.writeVerbose(`Destination: ${pkgPath}`); + + const result = await downloadAgentToFile(pkgPath); + if (!result) { + ui.writeError("No download available for this platform/architecture."); + return; + } + + try { + ui.writeInformation("⚙️ Installing SafeChain Ultimate..."); + await runPkgInstaller(pkgPath); + + ui.emptyLine(); + ui.writeInformation( + "✅ SafeChain Ultimate installed and started successfully!", + ); + ui.emptyLine(); + } finally { + ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`); + cleanup(pkgPath); + } +} + +function isRunningAsRoot() { + const rootUserUid = 0; + return process.getuid?.() === rootUserUid; +} + +/** + * @param {string} pkgPath + */ +async function runPkgInstaller(pkgPath) { + ui.writeVerbose(`Running: installer -pkg "${pkgPath}" -target /`); + + const result = await safeSpawn( + "installer", + ["-pkg", pkgPath, "-target", "/"], + { + stdio: "inherit", + }, + ); + + if (result.status !== 0) { + throw new Error(`PKG installer failed (exit code: ${result.status})`); + } +} + +/** + * @param {string} pkgPath + */ +function cleanup(pkgPath) { + try { + unlinkSync(pkgPath); + } catch { + ui.writeVerbose("Failed to clean up temporary installer file."); + } +} diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index 086b6a4..a79a2b1 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -2,6 +2,7 @@ import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; import { installOnWindows } from "./installOnWindows.js"; +import { installOnMacOS } from "./installOnMacOS.js"; export async function installUltimate() { initializeCliArguments(process.argv); @@ -10,6 +11,8 @@ export async function installUltimate() { if (operatingSystem === "win32") { await installOnWindows(); + } else if (operatingSystem === "darwin") { + await installOnMacOS(); } else { ui.writeInformation( `${operatingSystem} is not supported yet by SafeChain's ultimate version.`, From a4e903609a1a5085783542aef47b8fd0b0d194fd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 21 Jan 2026 09:18:26 +0100 Subject: [PATCH 56/98] Remove unused variable --- packages/safe-chain/src/installation/installOnMacOS.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 6963291..b2a953a 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -5,8 +5,6 @@ import { ui } from "../environment/userInteraction.js"; import { safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; -const MACOS_SERVICE_LABEL = "com.aikido.SafeChainAgent"; - export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); From 92ec4e47f9598c44967c1676335aaaf3f5511aaf Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 08:20:45 +0100 Subject: [PATCH 57/98] PR comments --- .../src/installation/downloadAgent.js | 18 +++++----- .../src/installation/installOnMacOS.js | 10 ++++-- .../src/installation/installOnWindows.js | 34 ++++++++++++++----- packages/safe-chain/src/utils/safeSpawn.js | 16 +++++++++ 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 0ee994e..2f2baac 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.0"; +const ULTIMATE_VERSION = "v0.2.1"; const DOWNLOAD_URLS = { win32: { x64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-amd64.msi", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-amd64.msi`, checksum: - "sha256:bba5deb250ebc6008f1cb33fa4209d2455a2f47fa99f0a40e3babef64939ac77", + "sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec", }, arm64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-windows-arm64.msi", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-arm64.msi`, checksum: - "sha256:9553ed15d5efed4185b990a1b86af0b11c23f11d96f8ce04e16b6b98aaf0506e", + "sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027", }, }, darwin: { x64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-amd64.pkg", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-amd64.pkg`, checksum: - "sha256:cbccf32e987a45bc8cc20b620f7b597ff7f9c2f966c2bc21132349612ddb619f", + "sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6", }, arm64: { - url: "https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.2-macos-release-artifact/SafeChainAgent-darwin-arm64.pkg", + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-arm64.pkg`, checksum: - "sha256:4d53a43a47bf7e8133eb61d306a1fb16348b9ec89c1c825e5f746f4fe847796e", + "sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73", }, }, }; diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index b2a953a..0d7081c 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -2,7 +2,7 @@ import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { ui } from "../environment/userInteraction.js"; -import { safeSpawn } from "../utils/safeSpawn.js"; +import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; export async function installOnMacOS() { @@ -49,9 +49,13 @@ function isRunningAsRoot() { * @param {string} pkgPath */ async function runPkgInstaller(pkgPath) { - ui.writeVerbose(`Running: installer -pkg "${pkgPath}" -target /`); + // Uses installer to install the package (https://ss64.com/mac/installer.html) + // Options: + // -pkg (required): The package to be installed. + // -target (required): The target volume is specified with the -target parameter. + // --> "-target /" installs to the current boot volume. - const result = await safeSpawn( + const result = await printVerboseAndSafeSpawn( "installer", ["-pkg", pkgPath, "-target", "/"], { diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 2380a7f..c6bc744 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -3,7 +3,7 @@ import { unlinkSync } from "fs"; import { join } from "path"; import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; -import { safeSpawn } from "../utils/safeSpawn.js"; +import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; const WINDOWS_SERVICE_NAME = "SafeChainAgent"; @@ -87,7 +87,12 @@ async function uninstallIfInstalled() { ui.writeInformation("🗑️ Removing previous installation..."); ui.writeVerbose(`Found product code: ${productCode}`); - const uninstallResult = await safeSpawn( + // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec) + // Options: + // - /x: Uninstalls the package. + // - /qn: Specifies there's no UI during the installation process. + // - /norestart: Stops the device from restarting after the installation completes. + const uninstallResult = await printVerboseAndSafeSpawn( "msiexec", ["/x", productCode, "/qn", "/norestart"], { stdio: "inherit" }, @@ -102,11 +107,18 @@ async function uninstallIfInstalled() { * @param {string} msiPath */ async function runMsiInstaller(msiPath) { - ui.writeVerbose(`Running: msiexec /i "${msiPath}" /qn`); + // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec) + // Options: + // - /i: Specifies normal installation + // - /qn: Specifies there's no UI during the installation process. - const result = await safeSpawn("msiexec", ["/i", msiPath, "/qn"], { - stdio: "inherit", - }); + const result = await printVerboseAndSafeSpawn( + "msiexec", + ["/i", msiPath, "/qn"], + { + stdio: "inherit", + }, + ); if (result.status !== 0) { throw new Error(`MSI installer failed (exit code: ${result.status})`); @@ -116,9 +128,13 @@ async function runMsiInstaller(msiPath) { async function stopServiceIfRunning() { ui.writeInformation("⏹️ Stopping running service..."); - const result = await safeSpawn("net", ["stop", WINDOWS_SERVICE_NAME], { - stdio: "pipe", - }); + const result = await printVerboseAndSafeSpawn( + "net", + ["stop", WINDOWS_SERVICE_NAME], + { + stdio: "pipe", + }, + ); if (result.status !== 0) { ui.writeVerbose("Service not running (will start after installation)."); diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index e17bdb5..69c827a 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,5 +1,6 @@ import { spawn, execSync } from "child_process"; import os from "os"; +import { ui } from "../environment/userInteraction.js"; /** * @param {string} arg @@ -135,3 +136,18 @@ export async function safeSpawn(command, args, options = {}) { }); }); } + +/** + * @param {string} command + * @param {string[]} args + * @param {import("child_process").SpawnOptions} options + * + * @returns {Promise<{status: number, stdout: string, stderr: string}>} + */ +export async function printVerboseAndSafeSpawn(command, args, options = {}) { + ui.writeVerbose(`Running: ${command} ${args.join(" ")}`); + + const result = await safeSpawn(command, args, options); + + return result; +} From b3a0ac802ebf98d562e5ff0df75b5e39fe1b45f4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 09:13:43 +0100 Subject: [PATCH 58/98] Fix linting --- packages/safe-chain/src/installation/installOnMacOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 0d7081c..074bbe6 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -2,7 +2,7 @@ import { tmpdir } from "os"; import { unlinkSync } from "fs"; import { join } from "path"; import { ui } from "../environment/userInteraction.js"; -import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; +import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; export async function installOnMacOS() { From b0048947b8c6df0ea356ad5a5612ac498727a0d4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 09:18:23 +0100 Subject: [PATCH 59/98] Fix download links --- packages/safe-chain/src/installation/downloadAgent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 2f2baac..df4a933 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -8,24 +8,24 @@ const ULTIMATE_VERSION = "v0.2.1"; const DOWNLOAD_URLS = { win32: { x64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-amd64.msi`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: "sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec", }, arm64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-windows-arm64.msi`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: "sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027", }, }, darwin: { x64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-amd64.pkg`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: "sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6", }, arm64: { - url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainAgent-darwin-arm64.pkg`, + url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: "sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73", }, From f283b72a11201d302bb4642f5a7e16a2709a4731 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 09:23:17 +0100 Subject: [PATCH 60/98] Update application names on Windows --- packages/safe-chain/src/installation/installOnWindows.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index c6bc744..0741fb7 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -6,8 +6,8 @@ import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; -const WINDOWS_SERVICE_NAME = "SafeChainAgent"; -const WINDOWS_APP_NAME = "SafeChain Agent"; +const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; +const WINDOWS_APP_NAME = "SafeChain Ultimate"; export async function installOnWindows() { if (!(await isRunningAsAdmin())) { From cf57a98b546b6443d919eabfb0f638ec6eae18d6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 20 Jan 2026 12:53:18 +0100 Subject: [PATCH 61/98] Support Windows in install-safe-shain.sh (git bash, cygwin, ...) --- install-scripts/install-safe-chain.sh | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 053e864..4c4718e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -42,8 +42,9 @@ detect_os() { echo "linuxstatic" fi ;; - Darwin*) echo "macos" ;; - *) error "Unsupported operating system: $(uname -s)" ;; + Darwin*) echo "macos" ;; + MINGW*|MSYS*|CYGWIN*) echo "win" ;; + *) error "Unsupported operating system: $(uname -s)" ;; esac } @@ -293,7 +294,11 @@ main() { # Detect platform OS=$(detect_os) ARCH=$(detect_arch) - BINARY_NAME="safe-chain-${OS}-${ARCH}" + if [ "$OS" = "win" ]; then + BINARY_NAME="safe-chain-${OS}-${ARCH}.exe" + else + BINARY_NAME="safe-chain-${OS}-${ARCH}" + fi info "Detected platform: ${OS}-${ARCH}" @@ -311,9 +316,15 @@ main() { download "$DOWNLOAD_URL" "$TEMP_FILE" # Rename and make executable - FINAL_FILE="${INSTALL_DIR}/safe-chain" + if [ "$OS" = "win" ]; then + FINAL_FILE="${INSTALL_DIR}/safe-chain.exe" + else + FINAL_FILE="${INSTALL_DIR}/safe-chain" + fi mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE" - chmod +x "$FINAL_FILE" || error "Failed to make binary executable" + if [ "$OS" != "win" ]; then + chmod +x "$FINAL_FILE" || error "Failed to make binary executable" + fi info "Binary installed to: $FINAL_FILE" From 16e6d0a8f29607e774da7c9f620199339f850a56 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 11:58:52 +0100 Subject: [PATCH 62/98] Fix tests for mitm registryproxy --- .../registryProxy/registryProxy.mitm.spec.js | 132 ++++++++++++++---- 1 file changed, 105 insertions(+), 27 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index df4332e..407aa3c 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -2,12 +2,17 @@ import { before, after, describe, it } from "node:test"; import assert from "node:assert"; import net from "net"; import tls from "tls"; +import { gunzipSync } from "zlib"; import { createSafeChainProxy, mergeSafeChainProxyEnvironmentVariables, } from "./registryProxy.js"; import { getCaCertPath } from "./certUtils.js"; -import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { + setEcoSystem, + ECOSYSTEM_JS, + ECOSYSTEM_PY, +} from "../config/settings.js"; import fs from "fs"; describe("registryProxy.mitm", () => { @@ -33,7 +38,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); assert.strictEqual(response.statusCode, 200); @@ -45,7 +50,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash/-/lodash-4.17.21.tgz" + "/lodash/-/lodash-4.17.21.tgz", ); // Should get a response (200 or redirect, but not 403 blocked) @@ -57,7 +62,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/this-package-definitely-does-not-exist-12345" + "/this-package-definitely-does-not-exist-12345", ); assert.strictEqual(response.statusCode, 404); @@ -68,7 +73,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash?write=true" + "/lodash?write=true", ); assert.strictEqual(response.statusCode, 200); @@ -79,7 +84,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.yarnpkg.com", - "/lodash" + "/lodash", ); assert.strictEqual(response.statusCode, 200); @@ -90,7 +95,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); // Check certificate common name matches the target hostname @@ -109,14 +114,14 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); const { cert: cert2 } = await makeRegistryRequestAndGetCert( proxyHost, proxyPort, "registry.yarnpkg.com", - "/lodash" + "/lodash", ); // Different hostnames should have different certificates @@ -130,14 +135,14 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "registry.npmjs.org", - "/lodash" + "/lodash", ); const { cert: cert2 } = await makeRegistryRequestAndGetCert( proxyHost, proxyPort, "registry.npmjs.org", - "/package/lodash" + "/package/lodash", ); // Same hostname should get the same certificate (fingerprint) @@ -159,7 +164,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "pypi.org", - "/packages/source/f/foo_bar/foo_bar-2.0.0.tar.gz" + "/packages/source/f/foo_bar/foo_bar-2.0.0.tar.gz", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -172,7 +177,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "files.pythonhosted.org", - "/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl" + "/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -185,7 +190,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "pypi.org", - "/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz" + "/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -198,7 +203,7 @@ describe("registryProxy.mitm", () => { proxyHost, proxyPort, "pypi.org", - "/packages/source/f/foo_bar/foo_bar-latest.tar.gz" + "/packages/source/f/foo_bar/foo_bar-latest.tar.gz", ); assert.notStrictEqual(response.statusCode, 403); assert.ok(typeof response.body === "string"); @@ -234,34 +239,73 @@ async function makeRegistryRequest(proxyHost, proxyPort, targetHost, path) { }); // Step 4: Send HTTP request over TLS - const httpRequest = `GET ${path} HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\n\r\n`; + const httpRequest = `GET ${path} HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\nAccept-encoding: gzip\r\n\r\n`; tlsSocket.write(httpRequest); - // Step 5: Read response + // Step 5: Read response as binary chunks return new Promise((resolve, reject) => { - let data = ""; + const chunks = []; tlsSocket.on("data", (chunk) => { - data += chunk.toString(); + chunks.push(chunk); }); tlsSocket.on("end", () => { - const lines = data.split("\r\n"); - const statusLine = lines[0]; + const buffer = Buffer.concat(chunks); + + // Find the header/body separator (\r\n\r\n) in binary + const separator = Buffer.from("\r\n\r\n"); + let separatorIndex = buffer.indexOf(separator); + if (separatorIndex === -1) { + return reject( + new Error("Invalid HTTP response: no header/body separator"), + ); + } + + // Extract headers as text + const headersText = buffer.subarray(0, separatorIndex).toString("utf8"); + const headerLines = headersText.split("\r\n"); + const statusLine = headerLines[0]; const statusCode = parseInt(statusLine.split(" ")[1]); - // Find body after empty line - const emptyLineIndex = lines.findIndex(line => line === ""); - const body = lines.slice(emptyLineIndex + 1).join("\r\n"); + // Parse headers into object + const headers = {}; + for (let i = 1; i < headerLines.length; i++) { + const colonIndex = headerLines[i].indexOf(":"); + if (colonIndex > 0) { + const key = headerLines[i].substring(0, colonIndex).toLowerCase(); + const value = headerLines[i].substring(colonIndex + 1).trim(); + headers[key] = value; + } + } - resolve({ statusCode, body }); + // Extract body as binary + let bodyBuffer = buffer.subarray(separatorIndex + separator.length); + + // Decode chunked transfer encoding if present + if (headers["transfer-encoding"] === "chunked") { + bodyBuffer = decodeChunked(bodyBuffer); + } + + // Decompress if gzip encoded + if (headers["content-encoding"] === "gzip" && bodyBuffer.length > 0) { + bodyBuffer = gunzipSync(bodyBuffer); + } + + const body = bodyBuffer.toString("utf8"); + resolve({ statusCode, body, headers }); }); tlsSocket.on("error", reject); }); } -async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, path) { +async function makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + targetHost, + path, +) { // Step 1: Connect to proxy const socket = await new Promise((resolve, reject) => { const sock = net.connect({ host: proxyHost, port: proxyPort }, () => { @@ -311,7 +355,7 @@ async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, p const statusCode = parseInt(statusLine.split(" ")[1]); // Find body after empty line - const emptyLineIndex = lines.findIndex(line => line === ""); + const emptyLineIndex = lines.findIndex((line) => line === ""); const body = lines.slice(emptyLineIndex + 1).join("\r\n"); resolve({ statusCode, body }); @@ -322,3 +366,37 @@ async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, p return { cert: peerCert, response }; } + +/** + * Decode HTTP chunked transfer encoding + * Format: \r\n\r\n ... 0\r\n\r\n + * @param {Buffer} buffer + * @returns {Buffer} + */ +function decodeChunked(buffer) { + const chunks = []; + let offset = 0; + + while (offset < buffer.length) { + // Find the end of the chunk size line + const lineEnd = buffer.indexOf(Buffer.from("\r\n"), offset); + if (lineEnd === -1) break; + + // Parse chunk size (hex) + const sizeHex = buffer.subarray(offset, lineEnd).toString("utf8"); + const chunkSize = parseInt(sizeHex, 16); + + // End of chunks + if (chunkSize === 0) break; + + // Extract chunk data + const dataStart = lineEnd + 2; + const dataEnd = dataStart + chunkSize; + chunks.push(buffer.subarray(dataStart, dataEnd)); + + // Move past chunk data and trailing \r\n + offset = dataEnd + 2; + } + + return Buffer.concat(chunks); +} From d8b703ebaf7b1eb09cad2e52ccb5d740257049c0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 22 Jan 2026 12:51:25 +0100 Subject: [PATCH 63/98] Add message about the certificate popup --- .../safe-chain/src/installation/installOnMacOS.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 074bbe6..0d475b1 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -4,6 +4,7 @@ import { join } from "path"; import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; +import chalk from "chalk"; export async function installOnMacOS() { if (!isRunningAsRoot()) { @@ -34,6 +35,17 @@ export async function installOnMacOS() { "✅ SafeChain Ultimate installed and started successfully!", ); ui.emptyLine(); + ui.writeInformation( + chalk.cyan("🔐 ") + + chalk.bold("ACTION REQUIRED: ") + + "macOS will show a popup to install our certificate.", + ); + ui.writeInformation( + " " + + chalk.bold("Please accept the certificate") + + " to complete the installation.", + ); + ui.emptyLine(); } finally { ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`); cleanup(pkgPath); From dcebe734df4a4702a638bc8f822f3a9137b8375a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 07:42:36 +0100 Subject: [PATCH 64/98] Don't insert empty line in rc file when it already ends with an empty line --- packages/safe-chain/src/shell-integration/helpers.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 064aca1..3e71d71 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -99,7 +99,7 @@ export const knownAikidoTools = [ aikidoCommand: "aikido-pipx", ecoSystem: ECOSYSTEM_PY, internalPackageManagerName: "pipx", - } + }, // When adding a new tool here, also update the documentation for the new tool in the README.md ]; @@ -216,7 +216,13 @@ export function addLineToFile(filePath, line, eol) { eol = eol || os.EOL; const fileContent = fs.readFileSync(filePath, "utf-8"); - const updatedContent = fileContent + eol + line + eol; + let updatedContent = fileContent; + + if (!fileContent.endsWith(eol)) { + updatedContent += eol; + } + + updatedContent += line + eol; fs.writeFileSync(filePath, updatedContent, "utf-8"); } From 53b7d9295cfd2741fbbfa5dc8c4cc8aceabe2726 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 11:29:19 +0100 Subject: [PATCH 65/98] Add uninstallation process for ultimate --- packages/safe-chain/bin/safe-chain.js | 14 ++- .../src/installation/installOnMacOS.js | 86 ++++++++++++++++++- .../src/installation/installOnWindows.js | 59 +++++++++++-- .../src/installation/installUltimate.js | 20 ++++- 4 files changed, 167 insertions(+), 12 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index d048ce1..06add7e 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -16,7 +16,10 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import { knownAikidoTools } from "../src/shell-integration/helpers.js"; -import { installUltimate } from "../src/installation/installUltimate.js"; +import { + installUltimate, + uninstallUltimate, +} from "../src/installation/installUltimate.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -67,6 +70,10 @@ if (tool) { (async () => { await installUltimate(); })(); +} else if (command === "--uninstall-ultimate") { + (async () => { + await uninstallUltimate(); + })(); } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -108,6 +115,11 @@ function writeHelp() { "safe-chain --ultimate", )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain --uninstall-ultimate", + )}: This uninstalls the ultimate version of safe-chain.`, + ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown", diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 0d475b1..b2d39ce 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -1,11 +1,14 @@ import { tmpdir } from "os"; -import { unlinkSync } from "fs"; +import { unlinkSync, rmSync } from "fs"; import { join } from "path"; +import { execSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; import chalk from "chalk"; +const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate"; + export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); @@ -52,6 +55,87 @@ export async function installOnMacOS() { } } +export async function uninstallOnMacOS() { + if (!isRunningAsRoot()) { + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(" sudo safe-chain --uninstall-ultimate"); + return; + } + + ui.emptyLine(); + + if (!isPackageInstalled()) { + ui.writeInformation("SafeChain Ultimate is not installed."); + return; + } + + ui.writeInformation("⏹️ Stopping service..."); + await stopService(); + + ui.writeInformation("🗑️ Removing installed files..."); + removeKnownFiles(); + + ui.writeInformation("🧹 Forgetting package receipt..."); + forgetPackage(); + + ui.emptyLine(); + ui.writeInformation("✅ SafeChain Ultimate has been uninstalled."); + ui.emptyLine(); +} + +function isPackageInstalled() { + try { + const output = execSync(`pkgutil --pkg-info ${MACOS_PKG_IDENTIFIER}`, { + encoding: "utf8", + stdio: "pipe", + }); + return output.includes(MACOS_PKG_IDENTIFIER); + } catch { + return false; + } +} + +async function stopService() { + const result = await printVerboseAndSafeSpawn( + "launchctl", + ["bootout", `system/${MACOS_PKG_IDENTIFIER}`], + { stdio: "pipe" }, + ); + + if (result.status !== 0) { + ui.writeVerbose("Service not running (will continue with uninstall)."); + } +} + +const MACOS_KNOWN_PATHS = [ + "/Library/Application Support/AikidoSecurity/SafeChainUltimate", + "/Library/Logs/AikidoSecurity/SafeChainUltimate", + `/Library/LaunchDaemons/${MACOS_PKG_IDENTIFIER}.plist`, +]; + +function removeKnownFiles() { + for (const filePath of MACOS_KNOWN_PATHS) { + try { + rmSync(filePath, { recursive: true, force: true }); + ui.writeVerbose(`Removed: ${filePath}`); + } catch { + ui.writeVerbose(`Failed to remove: ${filePath}`); + } + } +} + +function forgetPackage() { + try { + execSync(`pkgutil --forget ${MACOS_PKG_IDENTIFIER}`, { + encoding: "utf8", + stdio: "pipe", + }); + } catch { + ui.writeVerbose("Failed to forget package receipt."); + } +} + function isRunningAsRoot() { const rootUserUid = 0; return process.getuid?.() === rootUserUid; diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 0741fb7..16bf2b7 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -9,6 +9,34 @@ import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; const WINDOWS_APP_NAME = "SafeChain Ultimate"; +export async function uninstallOnWindows() { + if (!(await isRunningAsAdmin())) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return; + } + + ui.emptyLine(); + + const productCode = getInstalledProductCode(); + if (!productCode) { + ui.writeInformation("SafeChain Ultimate is not installed."); + return; + } + + ui.writeInformation("⏹️ Stopping running service..."); + await stopServiceIfRunning(); + + ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate..."); + await uninstallByProductCode(productCode); + + ui.emptyLine(); + ui.writeInformation("✅ SafeChain Ultimate has been uninstalled."); + ui.emptyLine(); +} + export async function installOnWindows() { if (!(await isRunningAsAdmin())) { ui.writeError("Administrator privileges required."); @@ -64,7 +92,11 @@ async function isRunningAsAdmin() { return result.status === 0 && result.stdout.trim() === "True"; } -async function uninstallIfInstalled() { +/** + * Returns the MSI product code for SafeChain Ultimate, or null if not installed. + * @returns {string | null} + */ +function getInstalledProductCode() { // Query Win32_Product via WMI to find the installed SafeChain Agent. // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. ui.writeVerbose(`Finding product code with PowerShell`); @@ -76,15 +108,15 @@ async function uninstallIfInstalled() { { encoding: "utf8" }, ).trim(); } catch { - ui.writeVerbose("No existing installation found (fresh install)."); - return; - } - if (!productCode) { - ui.writeVerbose("No existing installation found (fresh install)."); - return; + return null; } + return productCode || null; +} - ui.writeInformation("🗑️ Removing previous installation..."); +/** + * @param {string} productCode + */ +async function uninstallByProductCode(productCode) { ui.writeVerbose(`Found product code: ${productCode}`); // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec) @@ -103,6 +135,17 @@ async function uninstallIfInstalled() { } } +async function uninstallIfInstalled() { + const productCode = getInstalledProductCode(); + if (!productCode) { + ui.writeVerbose("No existing installation found (fresh install)."); + return; + } + + ui.writeInformation("🗑️ Removing previous installation..."); + await uninstallByProductCode(productCode); +} + /** * @param {string} msiPath */ diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index a79a2b1..cfcdcca 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -1,8 +1,24 @@ import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; -import { installOnWindows } from "./installOnWindows.js"; -import { installOnMacOS } from "./installOnMacOS.js"; +import { installOnWindows, uninstallOnWindows } from "./installOnWindows.js"; +import { installOnMacOS, uninstallOnMacOS } from "./installOnMacOS.js"; + +export async function uninstallUltimate() { + initializeCliArguments(process.argv); + + const operatingSystem = platform(); + + if (operatingSystem === "win32") { + await uninstallOnWindows(); + } else if (operatingSystem === "darwin") { + await uninstallOnMacOS(); + } else { + ui.writeInformation( + `Uninstall is not yet supported on ${operatingSystem}.`, + ); + } +} export async function installUltimate() { initializeCliArguments(process.argv); From ad18829d593de482c6dce3a403bf774e0916531a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 12:41:11 +0100 Subject: [PATCH 66/98] Bump safe-chain-internals version --- packages/safe-chain/src/installation/downloadAgent.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index df4a933..4d076ee 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.1"; +const ULTIMATE_VERSION = "v0.2.2"; const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: - "sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec", + "sha256:82d6939579c23c357d0f6d368001a5ac8dc66ce13d32ee1700467555ee97e10a", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: - "sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027", + "sha256:d626da40e3d0c4e02a36e6c7e309f18f0ffde64e97a4f2fefd4b25722842ac19", }, }, darwin: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: - "sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6", + "sha256:d7c31914deff8b332bf3d0e18ed00660e47ace87f06f22606c7866f7e0809507", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: - "sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73", + "sha256:73b092689e00c98e3c376afa50fc3477cedfd01445a113d42b36c5fcd956a6f4", }, }, }; From 8b8c2943dd3f21ae5e9f0e45a5ab37d88cc205c1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 12:44:47 +0100 Subject: [PATCH 67/98] Verify download links in a test --- .../src/installation/downloadAgent.js | 4 +- .../src/installation/downloadAgent.spec.js | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/safe-chain/src/installation/downloadAgent.spec.js diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index 4d076ee..cb2f84b 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -5,7 +5,7 @@ import fetch from "make-fetch-happen"; const ULTIMATE_VERSION = "v0.2.2"; -const DOWNLOAD_URLS = { +export const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, @@ -87,7 +87,7 @@ export function getDownloadInfoForCurrentPlatform() { * @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...") * @returns {Promise} */ -async function verifyChecksum(filePath, expectedChecksum) { +export async function verifyChecksum(filePath, expectedChecksum) { const [algorithm, expected] = expectedChecksum.split(":"); const hash = createHash(algorithm); diff --git a/packages/safe-chain/src/installation/downloadAgent.spec.js b/packages/safe-chain/src/installation/downloadAgent.spec.js new file mode 100644 index 0000000..17aecb9 --- /dev/null +++ b/packages/safe-chain/src/installation/downloadAgent.spec.js @@ -0,0 +1,45 @@ +import { describe, it, after } from "node:test"; +import assert from "node:assert"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { unlinkSync } from "node:fs"; +import { + DOWNLOAD_URLS, + downloadFile, + verifyChecksum, +} from "./downloadAgent.js"; + +describe("downloadAgent checksums", { timeout: 120_000 }, () => { + const downloadedFiles = []; + + after(() => { + for (const file of downloadedFiles) { + try { + unlinkSync(file); + } catch { + // ignore cleanup errors + } + } + }); + + for (const [platform, architectures] of Object.entries(DOWNLOAD_URLS)) { + for (const [arch, { url, checksum }] of Object.entries(architectures)) { + it(`${platform}/${arch} checksum matches`, async () => { + const destPath = join( + tmpdir(), + `safe-chain-test-${platform}-${arch}-${Date.now()}` + ); + downloadedFiles.push(destPath); + + await downloadFile(url, destPath); + + const isValid = await verifyChecksum(destPath, checksum); + assert.strictEqual( + isValid, + true, + `Checksum mismatch for ${platform}/${arch} (${url})` + ); + }); + } + } +}); From c3282e372b40198b8d11d87cdc0421f6f90ccef8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 12:57:40 +0100 Subject: [PATCH 68/98] Remove duplicate "Stopping running service" log --- packages/safe-chain/src/installation/installOnWindows.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 16bf2b7..f20bd9b 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -26,7 +26,6 @@ export async function uninstallOnWindows() { return; } - ui.writeInformation("⏹️ Stopping running service..."); await stopServiceIfRunning(); ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate..."); From 364061b186ed14ae0d3a4da9bfb3f4d738cdc979 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 27 Jan 2026 13:06:17 +0100 Subject: [PATCH 69/98] Update commands for ultimate --- packages/safe-chain/bin/safe-chain.js | 44 +++++++++++-------- .../src/installation/installOnMacOS.js | 4 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 06add7e..9a07657 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -66,14 +66,17 @@ if (tool) { process.exit(0); } else if (command === "setup") { setup(); -} else if (command === "--ultimate") { - (async () => { - await installUltimate(); - })(); -} else if (command === "--uninstall-ultimate") { - (async () => { - await uninstallUltimate(); - })(); +} else if (command === "ultimate") { + const subCommand = process.argv[3]; + if (subCommand === "uninstall") { + (async () => { + await uninstallUltimate(); + })(); + } else { + (async () => { + await installUltimate(); + })(); + } } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -100,7 +103,7 @@ function writeHelp() { ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( "teardown", - )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( + )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("ultimate")}, ${chalk.cyan("help")}, ${chalk.cyan( "--version", )}`, ); @@ -110,16 +113,6 @@ function writeHelp() { "safe-chain setup", )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`, ); - ui.writeInformation( - `- ${chalk.cyan( - "safe-chain --ultimate", - )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, - ); - ui.writeInformation( - `- ${chalk.cyan( - "safe-chain --uninstall-ultimate", - )}: This uninstalls the ultimate version of safe-chain.`, - ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown", @@ -136,6 +129,19 @@ function writeHelp() { )}): Display the current version of safe-chain.`, ); ui.emptyLine(); + ui.writeInformation(chalk.bold("Ultimate commands:")); + ui.emptyLine(); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain ultimate", + )}: Install the ultimate version of safe-chain, enabling protection for more eco-systems.`, + ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain ultimate uninstall", + )}: Uninstall the ultimate version of safe-chain.`, + ); + ui.emptyLine(); } async function getVersion() { diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index b2d39ce..018b911 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -13,7 +13,7 @@ export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain --ultimate"); + ui.writeInformation(" sudo safe-chain ultimate"); return; } @@ -59,7 +59,7 @@ export async function uninstallOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain --uninstall-ultimate"); + ui.writeInformation(" sudo safe-chain ultimate uninstall"); return; } From 8aad04e25956ef86d1a5cfae4477b6b743969df8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 07:53:39 +0100 Subject: [PATCH 70/98] PR comment: extract requireRootPrivileges / requireAdminPrivileges into separate function --- .../src/installation/installOnMacOS.js | 36 ++++++++++++------- .../src/installation/installOnWindows.js | 28 +++++++++------ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 018b911..21f8f1d 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -9,11 +9,29 @@ import chalk from "chalk"; const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate"; +/** + * Checks if root privileges are available and displays error message if not. + * @param {string} command - The sudo command to show in the error message + * @returns {boolean} True if running as root, false otherwise. + */ +function requireRootPrivileges(command) { + if (isRunningAsRoot()) { + return true; + } + + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(` ${command}`); + return false; +} + +function isRunningAsRoot() { + const rootUserUid = 0; + return process.getuid?.() === rootUserUid; +} + export async function installOnMacOS() { - if (!isRunningAsRoot()) { - ui.writeError("Root privileges required."); - ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain ultimate"); + if (!requireRootPrivileges("sudo safe-chain ultimate")) { return; } @@ -56,10 +74,7 @@ export async function installOnMacOS() { } export async function uninstallOnMacOS() { - if (!isRunningAsRoot()) { - ui.writeError("Root privileges required."); - ui.writeInformation("Please run this command with sudo:"); - ui.writeInformation(" sudo safe-chain ultimate uninstall"); + if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) { return; } @@ -136,11 +151,6 @@ function forgetPackage() { } } -function isRunningAsRoot() { - const rootUserUid = 0; - return process.getuid?.() === rootUserUid; -} - /** * @param {string} pkgPath */ diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index f20bd9b..4cee911 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -10,11 +10,7 @@ const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; const WINDOWS_APP_NAME = "SafeChain Ultimate"; export async function uninstallOnWindows() { - if (!(await isRunningAsAdmin())) { - ui.writeError("Administrator privileges required."); - ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator).", - ); + if (!(await requireAdminPrivileges())) { return; } @@ -37,11 +33,7 @@ export async function uninstallOnWindows() { } export async function installOnWindows() { - if (!(await isRunningAsAdmin())) { - ui.writeError("Administrator privileges required."); - ui.writeInformation( - "Please run this command in an elevated terminal (Run as Administrator).", - ); + if (!(await requireAdminPrivileges())) { return; } @@ -76,6 +68,22 @@ export async function installOnWindows() { } } +/** + * Checks if admin privileges are available and displays error message if not. + * @returns {Promise} True if running as admin, false otherwise. + */ +async function requireAdminPrivileges() { + if (await isRunningAsAdmin()) { + return true; + } + + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return false; +} + async function isRunningAsAdmin() { // Uses Windows Security API to check if current process has admin privileges. // Returns "True" or "False" as a string. From 81bf46d3c1c3b88e58b2f401cb3ee76c2136695f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 07:54:35 +0100 Subject: [PATCH 71/98] Rename output --- packages/safe-chain/src/installation/installOnMacOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 21f8f1d..ab20a8a 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -88,7 +88,7 @@ export async function uninstallOnMacOS() { ui.writeInformation("⏹️ Stopping service..."); await stopService(); - ui.writeInformation("🗑️ Removing installed files..."); + ui.writeInformation("🗑️ Removing files..."); removeKnownFiles(); ui.writeInformation("🧹 Forgetting package receipt..."); From 2073140d1ba8debd1533b99a25dc52e25cd6fe64 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 15:33:45 +0100 Subject: [PATCH 72/98] Mac: use uninstaller script --- .../src/installation/downloadAgent.js | 10 +-- .../src/installation/installOnMacOS.js | 65 +++++-------------- 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index cb2f84b..a5dcb0d 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.2"; +const ULTIMATE_VERSION = "v0.2.3"; export const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: - "sha256:82d6939579c23c357d0f6d368001a5ac8dc66ce13d32ee1700467555ee97e10a", + "sha256:bd196ae05b876588f828a57c4d19b3e7ad96ba40007cf2b36693dc6e792d28cc", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: - "sha256:d626da40e3d0c4e02a36e6c7e309f18f0ffde64e97a4f2fefd4b25722842ac19", + "sha256:79e046f24405e869494291e77c6d8640c8dc58d2ac1db87d3038e9eb8afbdc8b", }, }, darwin: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: - "sha256:d7c31914deff8b332bf3d0e18ed00660e47ace87f06f22606c7866f7e0809507", + "sha256:99868cb663eef44d063d995d2dcc063f55b10eb719ee945d05fe8cf5fef5e2a5", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: - "sha256:73b092689e00c98e3c376afa50fc3477cedfd01445a113d42b36c5fcd956a6f4", + "sha256:000b334c2eb85d8692be5d23af73f8b9fb686c9db726992223187b341ea79306", }, }, }; diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index ab20a8a..ae4fea3 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -1,7 +1,7 @@ import { tmpdir } from "os"; -import { unlinkSync, rmSync } from "fs"; +import { unlinkSync } from "fs"; import { join } from "path"; -import { execSync } from "child_process"; +import { execSync, spawnSync } from "child_process"; import { ui } from "../environment/userInteraction.js"; import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js"; import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; @@ -73,6 +73,9 @@ export async function installOnMacOS() { } } +const MACOS_UNINSTALL_SCRIPT = + "/Library/Application Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"; + export async function uninstallOnMacOS() { if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) { return; @@ -85,14 +88,20 @@ export async function uninstallOnMacOS() { return; } - ui.writeInformation("⏹️ Stopping service..."); - await stopService(); + ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate..."); + ui.writeVerbose(`Running: ${MACOS_UNINSTALL_SCRIPT}`); - ui.writeInformation("🗑️ Removing files..."); - removeKnownFiles(); + const result = spawnSync(MACOS_UNINSTALL_SCRIPT, { + stdio: "inherit", + shell: true, + }); - ui.writeInformation("🧹 Forgetting package receipt..."); - forgetPackage(); + if (result.status !== 0) { + ui.writeError( + `Uninstall script failed (exit code: ${result.status}). Please try again or remove manually.`, + ); + return; + } ui.emptyLine(); ui.writeInformation("✅ SafeChain Ultimate has been uninstalled."); @@ -111,46 +120,6 @@ function isPackageInstalled() { } } -async function stopService() { - const result = await printVerboseAndSafeSpawn( - "launchctl", - ["bootout", `system/${MACOS_PKG_IDENTIFIER}`], - { stdio: "pipe" }, - ); - - if (result.status !== 0) { - ui.writeVerbose("Service not running (will continue with uninstall)."); - } -} - -const MACOS_KNOWN_PATHS = [ - "/Library/Application Support/AikidoSecurity/SafeChainUltimate", - "/Library/Logs/AikidoSecurity/SafeChainUltimate", - `/Library/LaunchDaemons/${MACOS_PKG_IDENTIFIER}.plist`, -]; - -function removeKnownFiles() { - for (const filePath of MACOS_KNOWN_PATHS) { - try { - rmSync(filePath, { recursive: true, force: true }); - ui.writeVerbose(`Removed: ${filePath}`); - } catch { - ui.writeVerbose(`Failed to remove: ${filePath}`); - } - } -} - -function forgetPackage() { - try { - execSync(`pkgutil --forget ${MACOS_PKG_IDENTIFIER}`, { - encoding: "utf8", - stdio: "pipe", - }); - } catch { - ui.writeVerbose("Failed to forget package receipt."); - } -} - /** * @param {string} pkgPath */ From 8ca361e0e870bc28d643b2207b6c448834996131 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 28 Jan 2026 15:42:15 +0100 Subject: [PATCH 73/98] Fix uninstall script --- packages/safe-chain/src/installation/installOnMacOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index ae4fea3..22ce1a8 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -74,7 +74,7 @@ export async function installOnMacOS() { } const MACOS_UNINSTALL_SCRIPT = - "/Library/Application Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"; + "/Library/Application\\ Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"; export async function uninstallOnMacOS() { if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) { From 1c3037a1bfac17b0ce957b298810fd28e7e7d447 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 29 Jan 2026 17:06:39 +0100 Subject: [PATCH 74/98] Bump agent version to v1.0.0 --- packages/safe-chain/src/installation/downloadAgent.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/installation/downloadAgent.js b/packages/safe-chain/src/installation/downloadAgent.js index a5dcb0d..297908a 100644 --- a/packages/safe-chain/src/installation/downloadAgent.js +++ b/packages/safe-chain/src/installation/downloadAgent.js @@ -3,31 +3,31 @@ import { createHash } from "crypto"; import { pipeline } from "stream/promises"; import fetch from "make-fetch-happen"; -const ULTIMATE_VERSION = "v0.2.3"; +const ULTIMATE_VERSION = "v1.0.0"; export const DOWNLOAD_URLS = { win32: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`, checksum: - "sha256:bd196ae05b876588f828a57c4d19b3e7ad96ba40007cf2b36693dc6e792d28cc", + "sha256:c6a36f9b8e55ab6b7e8742cbabc4469d85809237c0f5e6c21af20b36c416ee1d", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`, checksum: - "sha256:79e046f24405e869494291e77c6d8640c8dc58d2ac1db87d3038e9eb8afbdc8b", + "sha256:46acd1af6a9938ea194c8ee8b34ca9b47c8de22e088a0791f3c0751dd6239c90", }, }, darwin: { x64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`, checksum: - "sha256:99868cb663eef44d063d995d2dcc063f55b10eb719ee945d05fe8cf5fef5e2a5", + "sha256:bb1829e8ca422e885baf37bef08dcbe7df7a30f248e2e89c4071564f7d4f3396", }, arm64: { url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`, checksum: - "sha256:000b334c2eb85d8692be5d23af73f8b9fb686c9db726992223187b341ea79306", + "sha256:7fe4a785709911cc366d8224b4c290677573b8c4833bd9054768299e55c5f0ed", }, }, }; From 162ac2caaea7e3eec72075556819f8e2eb1e5a3b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 30 Jan 2026 13:57:39 +0100 Subject: [PATCH 75/98] Add troubleshooting steps for powershell when executionpolicy doens't allow to run code --- docs/troubleshooting.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0cd6098..0b2845b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -149,6 +149,37 @@ Should include `~/.safe-chain/bin` **If persists:** Re-run the installation script +### PowerShell Execution Policy Blocks Scripts (Windows) + +**Symptom:** When opening PowerShell, you see an error like: + +``` +. : File C:\Users\\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because +running scripts is disabled on this system. +CategoryInfo : SecurityError: (:) [], PSSecurityException +FullyQualifiedErrorId : UnauthorizedAccess +``` + +**Cause:** Windows PowerShell's default execution policy (`Restricted`) blocks all script execution, including safe-chain's initialization script that's sourced from your PowerShell profile. + +**Resolution:** + +1. **Set the execution policy to allow local scripts:** + + Open PowerShell as Administrator and run: + + ```powershell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned + ``` + + This allows: + - Local scripts (like safe-chain's) to run without signing + - Downloaded scripts to run only if signed by a trusted publisher + +2. **Restart PowerShell** and verify the error is resolved. + +> **Note:** `RemoteSigned` is Microsoft's recommended execution policy for client computers. It provides a good balance between security and usability. + ### Shell Aliases Persist After Uninstallation **Symptom:** safe-chain commands still active after running uninstall script From 3042a9e095bd8b8ee6f5433ad3672f86582dc13a Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 14:15:00 +0100 Subject: [PATCH 76/98] add safe-chain ultimate logs --- packages/safe-chain/bin/safe-chain.js | 5 ++ .../src/ultimate/printUltimateLogs.js | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/safe-chain/src/ultimate/printUltimateLogs.js diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 9a07657..6ecdabd 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -20,6 +20,7 @@ import { installUltimate, uninstallUltimate, } from "../src/installation/installUltimate.js"; +import { printUltimateLogs } from "../src/ultimate/printUltimateLogs.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -72,6 +73,10 @@ if (tool) { (async () => { await uninstallUltimate(); })(); + } else if (subCommand === "logs") { + (async () => { + await printUltimateLogs(); + })(); } else { (async () => { await installUltimate(); diff --git a/packages/safe-chain/src/ultimate/printUltimateLogs.js b/packages/safe-chain/src/ultimate/printUltimateLogs.js new file mode 100644 index 0000000..65a978e --- /dev/null +++ b/packages/safe-chain/src/ultimate/printUltimateLogs.js @@ -0,0 +1,69 @@ +// @ts-nocheck +import { platform } from 'os'; +import { ui } from "../environment/userInteraction.js"; +import { readFileSync, existsSync } from "node:fs"; + +export async function printUltimateLogs() { + const { proxyLogPath, ultimateLogPath, proxyErrLogPath, ultimateErrLogPath } = getPathsPerPlatform(); + + await printLogs( + "SafeChain Proxy", + proxyLogPath, + proxyErrLogPath + ); + + await printLogs( + "SafeChain Ultimate", + ultimateLogPath, + ultimateErrLogPath + ); +} + +function getPathsPerPlatform() { + const os = platform(); + if (os === 'win32') { + const logDir = `C:\\ProgramData\\AikidoSecurity\\SafeChainUltimate\\logs`; + return { + proxyLogPath: `${logDir}\\SafeChainProxy.log`, + ultimateLogPath: `${logDir}\\SafeChainUltimate.log`, + proxyErrLogPath: `${logDir}\\SafeChainProxy.err`, + ultimateErrLogPath: `${logDir}\\SafeChainUltimate.err`, + }; + } else if (os === 'darwin') { + const logDir = `/Library/Logs/AikidoSecurity/SafeChainUltimate`; + return { + proxyLogPath: `${logDir}/safechain-proxy.log`, + ultimateLogPath: `${logDir}/safechain-ultimate.log`, + proxyErrLogPath: `${logDir}/safechain-proxy.error.log`, + ultimateErrLogPath: `${logDir}/safechain-ultimate.error.log`, + }; + } else { + throw new Error('Unsupported platform for log printing.'); + } +} + +async function printLogs(appName, logPath, errLogPath) { + ui.writeInformation(`=== ${appName} Logs ===`); + try { + if (existsSync(logPath)) { + const logs = readFileSync(logPath, "utf-8"); + ui.writeInformation(logs); + } else { + ui.writeWarning(`${appName} log file not found: ${logPath}`); + } + } catch (error) { + ui.writeError(`Failed to read ${appName} logs: ${error.message}`); + } + + ui.writeInformation(`=== ${appName} Error Logs ===`); + try { + if (existsSync(errLogPath)) { + const errLogs = readFileSync(errLogPath, "utf-8"); + ui.writeInformation(errLogs); + } else { + ui.writeInformation(`No error log file found for ${appName}.`); + } + } catch (error) { + ui.writeError(`Failed to read ${appName} error logs: ${error.message}`); + } +} From a904e9caa9b80cf4d19fc3dc801fb278ea3793f0 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 14:22:21 +0100 Subject: [PATCH 77/98] install archiver --- package-lock.json | 923 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 7 +- 2 files changed, 902 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index c852d4f..9ca91f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "packages/*", "test/e2e" ], + "dependencies": { + "archiver": "^7.0.1" + }, "devDependencies": { "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", @@ -555,6 +558,102 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -748,6 +847,16 @@ "win32" ] }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -919,6 +1028,18 @@ "node": ">= 6" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -932,7 +1053,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -942,7 +1062,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -954,6 +1073,243 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -964,7 +1320,6 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -975,11 +1330,16 @@ } } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/bare-events": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -1076,7 +1436,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -1127,6 +1486,15 @@ "dev": true, "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1152,6 +1520,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -1233,7 +1610,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1246,7 +1622,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1261,13 +1636,205 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1354,11 +1921,16 @@ "readable-stream": "^2.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encoding": { @@ -1484,11 +2056,28 @@ "node": ">=6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-events": "^2.7.0" @@ -1508,7 +2097,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, "license": "MIT" }, "node_modules/fdir": { @@ -1529,6 +2117,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -1686,7 +2290,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-symbols": { @@ -1789,7 +2392,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -1819,7 +2421,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -1877,19 +2478,50 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -1925,6 +2557,24 @@ ], "license": "MIT" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", @@ -2281,6 +2931,15 @@ "nan": "^2.17.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-package-arg": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", @@ -2381,6 +3040,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2512,11 +3186,19 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/progress": { @@ -2580,7 +3262,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -2592,6 +3273,27 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2636,7 +3338,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/safer-buffer": { @@ -2658,6 +3359,39 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2769,7 +3503,6 @@ "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -2781,7 +3514,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -2791,7 +3523,21 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2806,7 +3552,19 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2874,7 +3632,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -2896,7 +3653,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" @@ -3010,7 +3766,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-name": { @@ -3040,6 +3795,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3058,6 +3828,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3110,6 +3898,89 @@ "node": ">=10" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", diff --git a/package.json b/package.json index 2793f9c..864ac73 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,11 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { - "oxlint": "^1.22.0", + "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", - "@yao-pkg/pkg": "6.10.1" + "oxlint": "^1.22.0" + }, + "dependencies": { + "archiver": "^7.0.1" } } From 3bec78abf5d06946a2e1fcdade2682dba3e7bc7e Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 14:22:47 +0100 Subject: [PATCH 78/98] create an export async collectLogs --- .../src/ultimate/printUltimateLogs.js | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/ultimate/printUltimateLogs.js b/packages/safe-chain/src/ultimate/printUltimateLogs.js index 65a978e..c8e5403 100644 --- a/packages/safe-chain/src/ultimate/printUltimateLogs.js +++ b/packages/safe-chain/src/ultimate/printUltimateLogs.js @@ -1,7 +1,9 @@ -// @ts-nocheck import { platform } from 'os'; import { ui } from "../environment/userInteraction.js"; import { readFileSync, existsSync } from "node:fs"; +import {randomUUID} from "node:crypto"; +import {createWriteStream} from "fs"; +import archiver from 'archiver'; export async function printUltimateLogs() { const { proxyLogPath, ultimateLogPath, proxyErrLogPath, ultimateErrLogPath } = getPathsPerPlatform(); @@ -19,11 +21,44 @@ export async function printUltimateLogs() { ); } +export async function collectLogs() { + const { logDir } = getPathsPerPlatform(); + return new Promise((resolve, reject) => { + if (!existsSync(logDir)) { + ui.writeError(`Log directory not found: ${logDir}`); + reject(new Error(`Log directory not found: ${logDir}`)); + return; + } + + const date = new Date().toISOString().split('T')[0]; + const uuid = randomUUID(); + const zipFileName = `safechain-ultimate-${date}-${uuid}.zip`; + const output = createWriteStream(zipFileName); + const archive = archiver('zip', { zlib: { level: 9 } }); + + output.on('close', () => { + ui.writeInformation(`Logs collected and zipped as: ${zipFileName}`); + resolve(zipFileName); + }); + + archive.on('error', (err) => { + ui.writeError(`Failed to zip logs: ${err.message}`); + reject(err); + }); + + archive.pipe(output); + archive.directory(logDir, false); + archive.finalize(); + }); +} + + function getPathsPerPlatform() { const os = platform(); if (os === 'win32') { const logDir = `C:\\ProgramData\\AikidoSecurity\\SafeChainUltimate\\logs`; return { + logDir, proxyLogPath: `${logDir}\\SafeChainProxy.log`, ultimateLogPath: `${logDir}\\SafeChainUltimate.log`, proxyErrLogPath: `${logDir}\\SafeChainProxy.err`, @@ -32,6 +67,7 @@ function getPathsPerPlatform() { } else if (os === 'darwin') { const logDir = `/Library/Logs/AikidoSecurity/SafeChainUltimate`; return { + logDir, proxyLogPath: `${logDir}/safechain-proxy.log`, ultimateLogPath: `${logDir}/safechain-ultimate.log`, proxyErrLogPath: `${logDir}/safechain-proxy.error.log`, From dbfff46d3d817cb5e9d177ae58e340eb0a3ef4a0 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 14:25:20 +0100 Subject: [PATCH 79/98] add docs & collect-logs to safe-chain bin --- packages/safe-chain/bin/safe-chain.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 6ecdabd..770362b 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -20,7 +20,10 @@ import { installUltimate, uninstallUltimate, } from "../src/installation/installUltimate.js"; -import { printUltimateLogs } from "../src/ultimate/printUltimateLogs.js"; +import { + collectLogs, + printUltimateLogs +} from "../src/ultimate/printUltimateLogs.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -77,6 +80,10 @@ if (tool) { (async () => { await printUltimateLogs(); })(); + } else if (subCommand === "collect-logs") { + (async () => { + await collectLogs(); + })(); } else { (async () => { await installUltimate(); @@ -141,6 +148,16 @@ function writeHelp() { "safe-chain ultimate", )}: Install the ultimate version of safe-chain, enabling protection for more eco-systems.`, ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain ultimate logs", + )}: Prints standard and error logs for safe-chain ultimate and it's proxy.`, + ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain ultimate collect-logs", + )}: Creates a zip archive of safe-chain ultimate logs that can be shared with support.`, + ); ui.writeInformation( `- ${chalk.cyan( "safe-chain ultimate uninstall", From ff924c0f01bd785a7097d5e973f5b9080b69c410 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 14:26:26 +0100 Subject: [PATCH 80/98] use path.resolve to print full file --- packages/safe-chain/src/ultimate/printUltimateLogs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/ultimate/printUltimateLogs.js b/packages/safe-chain/src/ultimate/printUltimateLogs.js index c8e5403..a11c9f7 100644 --- a/packages/safe-chain/src/ultimate/printUltimateLogs.js +++ b/packages/safe-chain/src/ultimate/printUltimateLogs.js @@ -4,6 +4,7 @@ import { readFileSync, existsSync } from "node:fs"; import {randomUUID} from "node:crypto"; import {createWriteStream} from "fs"; import archiver from 'archiver'; +import path from "node:path"; export async function printUltimateLogs() { const { proxyLogPath, ultimateLogPath, proxyErrLogPath, ultimateErrLogPath } = getPathsPerPlatform(); @@ -37,7 +38,7 @@ export async function collectLogs() { const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { - ui.writeInformation(`Logs collected and zipped as: ${zipFileName}`); + ui.writeInformation(`Logs collected and zipped as: ${path.resolve(zipFileName)}`); resolve(zipFileName); }); From 0752139c4ec8daa4223609e7242f7dbf9933a154 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Fri, 30 Jan 2026 14:42:43 +0100 Subject: [PATCH 81/98] add 'archiver' types --- package-lock.json | 21 +++++++++++++++++++++ package.json | 1 + 2 files changed, 22 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9ca91f2..4b20556 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "archiver": "^7.0.1" }, "devDependencies": { + "@types/archiver": "^7.0.0", "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", "oxlint": "^1.22.0" @@ -857,6 +858,16 @@ "node": ">=14" } }, + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -931,6 +942,16 @@ "@types/node": "*" } }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", diff --git a/package.json b/package.json index 864ac73..818539e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { + "@types/archiver": "^7.0.0", "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", "oxlint": "^1.22.0" From 1adb2b34ca06a84b23e052d141d3e7e27b4f90b9 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Fri, 30 Jan 2026 14:44:26 +0100 Subject: [PATCH 82/98] Cleanup linting errors --- packages/safe-chain/src/ultimate/printUltimateLogs.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/ultimate/printUltimateLogs.js b/packages/safe-chain/src/ultimate/printUltimateLogs.js index a11c9f7..2fe432b 100644 --- a/packages/safe-chain/src/ultimate/printUltimateLogs.js +++ b/packages/safe-chain/src/ultimate/printUltimateLogs.js @@ -79,6 +79,11 @@ function getPathsPerPlatform() { } } +/** + * @param {string} appName + * @param {string} logPath + * @param {string} errLogPath + */ async function printLogs(appName, logPath, errLogPath) { ui.writeInformation(`=== ${appName} Logs ===`); try { @@ -89,7 +94,7 @@ async function printLogs(appName, logPath, errLogPath) { ui.writeWarning(`${appName} log file not found: ${logPath}`); } } catch (error) { - ui.writeError(`Failed to read ${appName} logs: ${error.message}`); + ui.writeError(`Failed to read ${appName} logs: ${error}`); } ui.writeInformation(`=== ${appName} Error Logs ===`); @@ -101,6 +106,6 @@ async function printLogs(appName, logPath, errLogPath) { ui.writeInformation(`No error log file found for ${appName}.`); } } catch (error) { - ui.writeError(`Failed to read ${appName} error logs: ${error.message}`); + ui.writeError(`Failed to read ${appName} error logs: ${error}`); } } From 4079bff705021ede9ea078ab682cc5995c9151c9 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Fri, 30 Jan 2026 15:16:39 +0100 Subject: [PATCH 83/98] rename to troubleshooting-* --- packages/safe-chain/bin/safe-chain.js | 15 ++++++--------- ...UltimateLogs.js => ultimateTroubleshooting.js} | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) rename packages/safe-chain/src/ultimate/{printUltimateLogs.js => ultimateTroubleshooting.js} (98%) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 770362b..b1d66b1 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -20,10 +20,7 @@ import { installUltimate, uninstallUltimate, } from "../src/installation/installUltimate.js"; -import { - collectLogs, - printUltimateLogs -} from "../src/ultimate/printUltimateLogs.js"; +import {printUltimateLogs, troubleshootingExport } from "../src/ultimate/ultimateTroubleshooting.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -76,13 +73,13 @@ if (tool) { (async () => { await uninstallUltimate(); })(); - } else if (subCommand === "logs") { + } else if (subCommand === "troubleshooting-logs") { (async () => { await printUltimateLogs(); })(); - } else if (subCommand === "collect-logs") { + } else if (subCommand === "troubleshooting-export") { (async () => { - await collectLogs(); + await troubleshootingExport(); })(); } else { (async () => { @@ -150,12 +147,12 @@ function writeHelp() { ); ui.writeInformation( `- ${chalk.cyan( - "safe-chain ultimate logs", + "safe-chain ultimate troubleshooting-logs", )}: Prints standard and error logs for safe-chain ultimate and it's proxy.`, ); ui.writeInformation( `- ${chalk.cyan( - "safe-chain ultimate collect-logs", + "safe-chain ultimate troubleshooting-export", )}: Creates a zip archive of safe-chain ultimate logs that can be shared with support.`, ); ui.writeInformation( diff --git a/packages/safe-chain/src/ultimate/printUltimateLogs.js b/packages/safe-chain/src/ultimate/ultimateTroubleshooting.js similarity index 98% rename from packages/safe-chain/src/ultimate/printUltimateLogs.js rename to packages/safe-chain/src/ultimate/ultimateTroubleshooting.js index 2fe432b..e333615 100644 --- a/packages/safe-chain/src/ultimate/printUltimateLogs.js +++ b/packages/safe-chain/src/ultimate/ultimateTroubleshooting.js @@ -22,7 +22,7 @@ export async function printUltimateLogs() { ); } -export async function collectLogs() { +export async function troubleshootingExport() { const { logDir } = getPathsPerPlatform(); return new Promise((resolve, reject) => { if (!existsSync(logDir)) { From 8cd3c8a97ac4c0630fd51546b89a36806648324c Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Fri, 30 Jan 2026 15:19:56 +0100 Subject: [PATCH 84/98] troubleshooting-export: update description --- packages/safe-chain/bin/safe-chain.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index b1d66b1..e438e12 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -153,7 +153,7 @@ function writeHelp() { ui.writeInformation( `- ${chalk.cyan( "safe-chain ultimate troubleshooting-export", - )}: Creates a zip archive of safe-chain ultimate logs that can be shared with support.`, + )}: Creates a zip archive of useful data for troubleshooting safe-chain ultimate, that can be shared with our support team.`, ); ui.writeInformation( `- ${chalk.cyan( From 4ab463306fe31752de0827043402fa29940bb33b Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 15:47:41 +0100 Subject: [PATCH 85/98] Revert "add 'archiver' types" This reverts commit ef057626359dd19140978c6582111b3f5c456c57. --- package-lock.json | 21 --------------------- package.json | 1 - 2 files changed, 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b20556..9ca91f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "archiver": "^7.0.1" }, "devDependencies": { - "@types/archiver": "^7.0.0", "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", "oxlint": "^1.22.0" @@ -858,16 +857,6 @@ "node": ">=14" } }, - "node_modules/@types/archiver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", - "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/readdir-glob": "*" - } - }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -942,16 +931,6 @@ "@types/node": "*" } }, - "node_modules/@types/readdir-glob": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", - "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/retry": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", diff --git a/package.json b/package.json index 818539e..864ac73 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { - "@types/archiver": "^7.0.0", "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", "oxlint": "^1.22.0" From ccef402dc6ffe07441905968f58c6793c561ad1d Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 15:47:49 +0100 Subject: [PATCH 86/98] Revert "install archiver" This reverts commit 4c29eb3549905ce066c4a5c1eeadaa9b027a5fd1. --- package-lock.json | 923 ++-------------------------------------------- package.json | 7 +- 2 files changed, 28 insertions(+), 902 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ca91f2..c852d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "packages/*", "test/e2e" ], - "dependencies": { - "archiver": "^7.0.1" - }, "devDependencies": { "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", @@ -558,102 +555,6 @@ "node": "20 || >=22" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -847,16 +748,6 @@ "win32" ] }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -1028,18 +919,6 @@ "node": ">= 6" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -1053,6 +932,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1062,6 +942,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1073,243 +954,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", - "license": "MIT", - "dependencies": { - "archiver-utils": "^5.0.2", - "async": "^3.2.4", - "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", - "license": "MIT", - "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/archiver/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/archiver/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/archiver/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/archiver/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1320,6 +964,7 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -1330,16 +975,11 @@ } } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, "node_modules/bare-events": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -1436,6 +1076,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -1486,15 +1127,6 @@ "dev": true, "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1520,15 +1152,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -1610,6 +1233,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1622,6 +1246,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1636,205 +1261,13 @@ "node": ">= 0.8" } }, - "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/compress-commons/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/compress-commons/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/compress-commons/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/compress-commons/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/crc32-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/crc32-stream/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/crc32-stream/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/crc32-stream/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1921,16 +1354,11 @@ "readable-stream": "^2.0.2" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/encoding": { @@ -2056,28 +1484,11 @@ "node": ">=6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "bare-events": "^2.7.0" @@ -2097,6 +1508,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, "license": "MIT" }, "node_modules/fdir": { @@ -2117,22 +1529,6 @@ } } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -2290,6 +1686,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/has-symbols": { @@ -2392,6 +1789,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -2421,6 +1819,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -2478,50 +1877,19 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2557,24 +1925,6 @@ ], "license": "MIT" }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, "node_modules/lru-cache": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", @@ -2931,15 +2281,6 @@ "nan": "^2.17.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-package-arg": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", @@ -3040,21 +2381,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -3186,19 +2512,11 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, "license": "MIT" }, "node_modules/progress": { @@ -3262,6 +2580,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -3273,27 +2592,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3338,6 +2636,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, "license": "MIT" }, "node_modules/safer-buffer": { @@ -3359,39 +2658,6 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3503,6 +2769,7 @@ "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -3514,6 +2781,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -3523,21 +2791,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3552,19 +2806,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3632,6 +2874,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -3653,6 +2896,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" @@ -3766,6 +3010,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-name": { @@ -3795,21 +3040,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3828,24 +3058,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3898,89 +3110,6 @@ "node": ">=10" } }, - "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", - "license": "MIT", - "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zip-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/zip-stream/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/zip-stream/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/zip-stream/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", diff --git a/package.json b/package.json index 864ac73..2793f9c 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,8 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { - "@yao-pkg/pkg": "6.10.1", + "oxlint": "^1.22.0", "esbuild": "^0.27.0", - "oxlint": "^1.22.0" - }, - "dependencies": { - "archiver": "^7.0.1" + "@yao-pkg/pkg": "6.10.1" } } From 78e4a439164095817d4eb1ff9858629aed2077b0 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 30 Jan 2026 15:48:39 +0100 Subject: [PATCH 87/98] install deps in safe-chain/package.json --- package-lock.json | 942 ++++++++++++++++++++++++++++++- packages/safe-chain/package.json | 2 + 2 files changed, 918 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index c852d4f..ea8c410 100644 --- a/package-lock.json +++ b/package-lock.json @@ -555,6 +555,102 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -748,6 +844,26 @@ "win32" ] }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -822,6 +938,16 @@ "@types/node": "*" } }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", @@ -919,6 +1045,18 @@ "node": ">= 6" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -932,7 +1070,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -942,7 +1079,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -954,6 +1090,243 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -964,7 +1337,6 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -975,11 +1347,16 @@ } } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/bare-events": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -1076,7 +1453,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -1127,6 +1503,15 @@ "dev": true, "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1152,6 +1537,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -1233,7 +1627,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1246,7 +1639,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1261,13 +1653,205 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1354,11 +1938,16 @@ "readable-stream": "^2.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encoding": { @@ -1484,11 +2073,28 @@ "node": ">=6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-events": "^2.7.0" @@ -1508,7 +2114,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, "license": "MIT" }, "node_modules/fdir": { @@ -1529,6 +2134,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -1686,7 +2307,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-symbols": { @@ -1789,7 +2409,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -1819,7 +2438,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -1877,19 +2495,50 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -1925,6 +2574,24 @@ ], "license": "MIT" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", @@ -2281,6 +2948,15 @@ "nan": "^2.17.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-package-arg": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", @@ -2381,6 +3057,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2512,11 +3203,19 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/progress": { @@ -2580,7 +3279,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -2592,6 +3290,27 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2636,7 +3355,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/safer-buffer": { @@ -2658,6 +3376,39 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2769,7 +3520,6 @@ "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -2781,7 +3531,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -2791,7 +3540,21 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2806,7 +3569,19 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2874,7 +3649,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -2896,7 +3670,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" @@ -3010,7 +3783,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-name": { @@ -3040,6 +3812,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3058,6 +3845,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3110,11 +3915,95 @@ "node": ">=10" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { + "archiver": "^7.0.1", "certifi": "14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", @@ -3142,6 +4031,7 @@ "safe-chain": "bin/safe-chain.js" }, "devDependencies": { + "@types/archiver": "^7.0.0", "@types/ini": "^4.1.1", "@types/make-fetch-happen": "^10.0.4", "@types/node": "^18.19.130", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 3d527cb..d4f3501 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -38,6 +38,7 @@ "license": "AGPL-3.0-or-later", "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.", "dependencies": { + "archiver": "^7.0.1", "certifi": "14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", @@ -48,6 +49,7 @@ "semver": "7.7.2" }, "devDependencies": { + "@types/archiver": "^7.0.0", "@types/ini": "^4.1.1", "@types/make-fetch-happen": "^10.0.4", "@types/node": "^18.19.130", From e01b06b238c6fd6d719aabec8438a259995b4bd3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 2 Feb 2026 15:28:44 +0100 Subject: [PATCH 88/98] Verify the number of arguments for ultimate commands --- packages/safe-chain/bin/safe-chain.js | 37 ++++++++++++++++++- .../src/installation/installUltimate.js | 2 - 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index e438e12..dbefa10 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -20,7 +20,10 @@ import { installUltimate, uninstallUltimate, } from "../src/installation/installUltimate.js"; -import {printUltimateLogs, troubleshootingExport } from "../src/ultimate/ultimateTroubleshooting.js"; +import { + printUltimateLogs, + troubleshootingExport, +} from "../src/ultimate/ultimateTroubleshooting.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -68,20 +71,34 @@ if (tool) { } else if (command === "setup") { setup(); } else if (command === "ultimate") { - const subCommand = process.argv[3]; + const cliArgs = initializeCliArguments(process.argv.slice(2)); + const subCommand = cliArgs[1]; if (subCommand === "uninstall") { + guardCliArgsMaxLenght(2, cliArgs, "safe-chain ultimate uninstall"); (async () => { await uninstallUltimate(); })(); } else if (subCommand === "troubleshooting-logs") { + guardCliArgsMaxLenght( + 2, + cliArgs, + "safe-chain ultimate troubleshooting-logs", + ); (async () => { await printUltimateLogs(); })(); } else if (subCommand === "troubleshooting-export") { + guardCliArgsMaxLenght( + 2, + cliArgs, + "safe-chain ultimate troubleshooting-export", + ); (async () => { await troubleshootingExport(); })(); } else { + guardCliArgsMaxLenght(1, cliArgs, "safe-chain ultimate"); + // Install command = when no subcommand is provided (safe-chain ultimate) (async () => { await installUltimate(); })(); @@ -104,6 +121,22 @@ if (tool) { process.exit(1); } +/** + * @param {Number} maxLength + * @param {String[]} args + * @param {String} command + */ +function guardCliArgsMaxLenght(maxLength, args, command) { + if (args.length > maxLength) { + ui.writeError(`Unexpected number of arguments for command ${command}.`); + ui.emptyLine(); + + writeHelp(); + + process.exit(1); + } +} + function writeHelp() { ui.writeInformation( chalk.bold("Usage: ") + chalk.cyan("safe-chain "), diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index cfcdcca..257c953 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -21,8 +21,6 @@ export async function uninstallUltimate() { } export async function installUltimate() { - initializeCliArguments(process.argv); - const operatingSystem = platform(); if (operatingSystem === "win32") { From aa667d48e4e1c5d12ec66ccb05cc554017eea56a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 11 Feb 2026 14:00:20 +0100 Subject: [PATCH 89/98] Add rama proxy feature flag --- install-scripts/install-safe-chain.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 4c4718e..960691e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -237,6 +237,9 @@ parse_arguments() { --include-python) warn "--include-python is deprecated and ignored. Python ecosystem is now included by default." ;; + --experimental-rama-proxy) + USE_RAMA_PROXY=true + ;; *) error "Unknown argument: $arg" ;; @@ -248,6 +251,7 @@ parse_arguments() { main() { # Initialize argument flags USE_CI_SETUP=false + USE_RAMA_PROXY=false # Parse command-line arguments parse_arguments "$@" @@ -329,7 +333,7 @@ main() { info "Binary installed to: $FINAL_FILE" # Download safechain-proxy - if [ "$OS" = "macos" ] || [ "$OS" = "linux" ] || [ "$OS" = "linuxstatic" ]; then + if [ "$USE_RAMA_PROXY" = "true" ] && { [ "$OS" = "macos" ] || [ "$OS" = "linux" ] || [ "$OS" = "linuxstatic" ]; }; then info "Downloading safechain-proxy..." if [ "$OS" = "macos" ]; then From 6cd11158416046627ab469337e4d6133cc1473a6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 11 Feb 2026 14:23:43 +0100 Subject: [PATCH 90/98] Update proxy version to 1.0.0 --- install-scripts/install-safe-chain.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 960691e..d282a9e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -338,16 +338,16 @@ main() { if [ "$OS" = "macos" ]; then if [ "$ARCH" = "arm64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-darwin-arm64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-darwin-arm64" else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-darwin-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-darwin-amd64" fi else # Linux (both linux and linuxstatic) if [ "$ARCH" = "x64" ]; then - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-linux-amd64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-linux-amd64" else - PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v0.0.7-linux-proxy-bins/safechain-proxy-linux-arm64" + PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-linux-arm64" fi fi From ca071729be804cee77cd22103ed2aff97d5a0ab2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 11 Feb 2026 16:04:03 +0100 Subject: [PATCH 91/98] Move existing proxy files to builtInProxy folder --- .../src/packagemanager/pip/runPipCommand.js | 79 +++-- .../packagemanager/pip/runPipCommand.spec.js | 273 +++++++++++++----- .../src/packagemanager/pipx/runPipXCommand.js | 23 +- .../pipx/runPipXCommand.spec.js | 8 +- .../poetry/createPoetryPackageManager.js | 22 +- .../src/packagemanager/uv/runUvCommand.js | 22 +- .../{ => builtInProxy}/certBundle.js | 19 +- .../{ => builtInProxy}/certBundle.spec.js | 0 .../{ => builtInProxy}/certUtils.js | 0 .../builtInProxy/createBuiltInProxyServer.js | 150 ++++++++++ .../{ => builtInProxy}/getConnectTimeout.js | 0 .../{ => builtInProxy}/http-utils.js | 0 .../createInterceptorForEcoSystem.js | 2 +- .../interceptors/interceptorBuilder.js | 0 .../interceptors/npm/modifyNpmInfo.js | 30 +- .../interceptors/npm/npmInterceptor.js | 8 +- .../npm/npmInterceptor.minPackageAge.spec.js | 60 ++-- .../npmInterceptor.packageDownload.spec.js | 16 +- .../interceptors/npm/parseNpmPackageUrl.js | 0 .../interceptors/pipInterceptor.js | 17 +- ...pipInterceptor.pipCustomRegistries.spec.js | 30 +- .../interceptors/pipInterceptor.spec.js | 10 +- .../{ => builtInProxy}/isImdsEndpoint.js | 0 .../{ => builtInProxy}/mitmRequestHandler.js | 16 +- .../{ => builtInProxy}/plainHttpProxy.js | 4 +- .../tunnelRequestHandler.js | 21 +- .../registryProxy/createBuiltInProxyServer.js | 150 ++++++++++ .../{ => ramaProxy}/createRamaProxy.js | 14 +- .../registryProxy.connect-tunnel.spec.js | 36 +-- .../src/registryProxy/registryProxy.js | 151 +--------- .../registryProxy/registryProxy.mitm.spec.js | 2 +- 31 files changed, 766 insertions(+), 397 deletions(-) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/certBundle.js (91%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/certBundle.spec.js (100%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/certUtils.js (100%) create mode 100644 packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/getConnectTimeout.js (100%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/http-utils.js (100%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/createInterceptorForEcoSystem.js (93%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/interceptorBuilder.js (100%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/npm/modifyNpmInfo.js (91%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/npm/npmInterceptor.js (89%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/npm/npmInterceptor.minPackageAge.spec.js (94%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/npm/npmInterceptor.packageDownload.spec.js (95%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/npm/parseNpmPackageUrl.js (100%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/pipInterceptor.js (90%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/pipInterceptor.pipCustomRegistries.spec.js (88%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/interceptors/pipInterceptor.spec.js (94%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/isImdsEndpoint.js (100%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/mitmRequestHandler.js (96%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/plainHttpProxy.js (97%) rename packages/safe-chain/src/registryProxy/{ => builtInProxy}/tunnelRequestHandler.js (95%) create mode 100644 packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js rename packages/safe-chain/src/registryProxy/{ => ramaProxy}/createRamaProxy.js (90%) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 83bc03e..425eca4 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,8 +1,13 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; -import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js"; +import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; +import { + PIP_COMMAND, + PIP3_COMMAND, + PYTHON_COMMAND, + PYTHON3_COMMAND, +} from "./pipSettings.js"; import fs from "node:fs/promises"; import fsSync from "node:fs"; import os from "node:os"; @@ -20,7 +25,11 @@ import { spawn } from "child_process"; export function shouldBypassSafeChain(command, args) { if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) { // Check if args start with -m pip - if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) { + if ( + args.length >= 2 && + args[0] === "-m" && + (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND) + ) { return false; } return true; @@ -32,43 +41,49 @@ export function shouldBypassSafeChain(command, args) { * Sets fallback CA bundle environment variables used by Python libraries. * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python * network libraries respect the combined CA bundle, even if they don't read pip's config. - * + * * @param {NodeJS.ProcessEnv} env - Environment object to modify * @param {string} combinedCaPath - Path to the combined CA bundle */ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { // REQUESTS_CA_BUNDLE: Used by the popular 'requests' library if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", + ); } env.REQUESTS_CA_BUNDLE = combinedCaPath; // SSL_CERT_FILE: Used by some Python SSL libraries and urllib if (env.SSL_CERT_FILE) { - ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", + ); } env.SSL_CERT_FILE = combinedCaPath; // PIP_CERT: Pip's own environment variable for certificate verification if (env.PIP_CERT) { - ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", + ); } env.PIP_CERT = combinedCaPath; } /** * Runs a pip command with safe-chain's certificate bundle and proxy configuration. - * + * * Creates a temporary pip config file to configure: * - Cert bundle for HTTPS verification * - Proxy settings - * + * * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges * their settings with safe-chain's, leaving the original file unchanged. - * + * * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow * users to read/write persistent config. Only CA environment variables are set for these commands. - * + * * @param {string} command - The pip command executable (e.g., 'pip3' or 'python3') * @param {string[]} args - Command line arguments to pass to pip * @returns {Promise<{status: number}>} Exit status of the pip command @@ -76,12 +91,16 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { export async function runPip(command, args) { // Check if we should bypass safe-chain (python/python3 without -m pip) if (shouldBypassSafeChain(command, args)) { - ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`); + ui.writeVerbose( + `Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`, + ); // Spawn the ORIGINAL command with ORIGINAL args return new Promise((_resolve) => { const proc = spawn(command, args, { stdio: "inherit" }); proc.on("exit", (/** @type {number | null} */ code) => { - ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`); + ui.writeVerbose( + `${command} ${args.join(" ")} exited with status ${code}`, + ); ui.writeBufferedLogsAndStopBuffering(); process.exit(code ?? 0); }); @@ -104,28 +123,32 @@ export async function runPip(command, args) { // Commands that need access to persistent config/cache/state files // These should not have PIP_CONFIG_FILE overridden as it would prevent them from // reading/writing to the user's actual pip configuration and cache directories - const configRelatedCommands = ['config', 'cache', 'debug', 'completion']; - const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]); + const configRelatedCommands = ["config", "cache", "debug", "completion"]; + const isConfigRelatedCommand = + args.length > 0 && configRelatedCommands.includes(args[0]); // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file) // will tell pip to use the provided CA bundle for HTTPS verification. // Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active), // otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables - const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; + const proxy = + env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ""; const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); let cleanupConfigPath = null; // Track temp file for cleanup if (isConfigRelatedCommand) { - ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`); - - // Still set the fallback CA bundle environment variables to avoid edge cases where a + ui.writeVerbose( + `Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`, + ); + + // Still set the fallback CA bundle environment variables to avoid edge cases where a // plugin or extension triggers a network call during config introspection // This can do no harm setFallbackCaBundleEnvironmentVariables(env, combinedCaPath); - + const result = await safeSpawn(command, args, { stdio: "inherit", env, @@ -145,9 +168,10 @@ export async function runPip(command, args) { await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; cleanupConfigPath = pipConfigPath; - } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { - ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); + ui.writeVerbose( + "Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.", + ); const userConfig = env.PIP_CONFIG_FILE; // Read the existing config without modifying it @@ -159,25 +183,28 @@ export async function runPip(command, args) { // Cert if (typeof parsed.global.cert !== "undefined") { - ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config."); + ui.writeWarning( + "Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.", + ); } parsed.global.cert = combinedCaPath; // Proxy if (typeof parsed.global.proxy !== "undefined") { - ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config."); + ui.writeWarning( + "Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.", + ); } if (proxy) { parsed.global.proxy = proxy; } - + const updated = ini.stringify(parsed); // Save to a new temp file to avoid overwriting user's original config await fs.writeFile(pipConfigPath, updated, "utf-8"); env.PIP_CONFIG_FILE = pipConfigPath; cleanupConfigPath = pipConfigPath; - } else { // The user provided PIP_CONFIG_FILE does not exist on disk // PIP will handle this as an error and inform the user diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 0707333..b256725 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -24,7 +24,10 @@ describe("runPipCommand environment variable handling", () => { // Capture the config file content before the function cleans it up if (options.env.PIP_CONFIG_FILE) { try { - capturedConfigContent = await fs.readFile(options.env.PIP_CONFIG_FILE, "utf-8"); + capturedConfigContent = await fs.readFile( + options.env.PIP_CONFIG_FILE, + "utf-8", + ); } catch { // Ignore if file doesn't exist or can't be read } @@ -49,7 +52,7 @@ describe("runPipCommand environment variable handling", () => { }); // Mock certBundle to return a test combined bundle path - mock.module("../../registryProxy/certBundle.js", { + mock.module("../../registryProxy/builtInProxy/certBundle.js", { namedExports: { getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem", }, @@ -65,7 +68,12 @@ describe("runPipCommand environment variable handling", () => { }); it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => { - const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]); + const res = await runPip("pip3", [ + "config", + "set", + "global.index-url", + "https://test.pypi.org/simple", + ]); assert.strictEqual(res.status, 0); assert.ok(capturedArgs, "safeSpawn should have been called"); @@ -73,24 +81,24 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip config commands" + "PIP_CONFIG_FILE should NOT be set for pip config commands", ); // But CA environment variables should still be set assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem", - "REQUESTS_CA_BUNDLE should still be set" + "REQUESTS_CA_BUNDLE should still be set", ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", - "SSL_CERT_FILE should still be set" + "SSL_CERT_FILE should still be set", ); assert.strictEqual( capturedArgs.options.env.PIP_CERT, "/tmp/test-combined-ca.pem", - "PIP_CERT should still be set" + "PIP_CERT should still be set", ); }); @@ -102,7 +110,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip config get" + "PIP_CONFIG_FILE should NOT be set for pip config get", ); }); @@ -114,7 +122,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip config list" + "PIP_CONFIG_FILE should NOT be set for pip config list", ); }); @@ -126,14 +134,14 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip cache commands" + "PIP_CONFIG_FILE should NOT be set for pip cache commands", ); // CA env vars should still be set assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", - "SSL_CERT_FILE should still be set" + "SSL_CERT_FILE should still be set", ); }); @@ -145,7 +153,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip debug" + "PIP_CONFIG_FILE should NOT be set for pip debug", ); }); @@ -157,7 +165,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip completion" + "PIP_CONFIG_FILE should NOT be set for pip completion", ); }); @@ -169,13 +177,20 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CERT, "/tmp/test-combined-ca.pem", - "PIP_CERT should be set to combined bundle path" + "PIP_CERT should be set to combined bundle path", ); // Check PIP_CONFIG_FILE env var exists and is a non-empty string const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; assert.ok(configPath, "PIP_CONFIG_FILE should be set"); - assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string"); - assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path"); + assert.strictEqual( + typeof configPath, + "string", + "PIP_CONFIG_FILE should be a string", + ); + assert.ok( + configPath.length > 0, + "PIP_CONFIG_FILE should be a non-empty path", + ); }); it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => { @@ -188,12 +203,12 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem", - "REQUESTS_CA_BUNDLE should be set to combined bundle path" + "REQUESTS_CA_BUNDLE should be set to combined bundle path", ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", - "SSL_CERT_FILE should be set to combined bundle path" + "SSL_CERT_FILE should be set to combined bundle path", ); }); @@ -208,11 +223,11 @@ describe("runPipCommand environment variable handling", () => { // Env vars should be set unconditionally assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, - "/tmp/test-combined-ca.pem" + "/tmp/test-combined-ca.pem", ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, - "/tmp/test-combined-ca.pem" + "/tmp/test-combined-ca.pem", ); }); @@ -224,11 +239,11 @@ describe("runPipCommand environment variable handling", () => { // Environment variables still set (pip CLI --cert takes precedence) assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, - "/tmp/test-combined-ca.pem" + "/tmp/test-combined-ca.pem", ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, - "/tmp/test-combined-ca.pem" + "/tmp/test-combined-ca.pem", ); }); @@ -239,13 +254,16 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.HTTPS_PROXY, "http://localhost:8080", - "HTTPS_PROXY should be set by proxy merge" + "HTTPS_PROXY should be set by proxy merge", ); }); it("should create a new temp config when existing config exists (original file untouched)", async () => { const tmpDir = os.tmpdir(); - const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const userCfgPath = path.join( + tmpDir, + `safe-chain-test-pip-${Date.now()}.ini`, + ); const initial = "[global]\nindex-url = https://example.com/simple\n"; await fs.writeFile(userCfgPath, initial, "utf-8"); @@ -253,19 +271,42 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual(newCfgPath, userCfgPath, "should point to a new temp config file"); + assert.notStrictEqual( + newCfgPath, + userCfgPath, + "should point to a new temp config file", + ); // Original file unchanged const originalContent = await fs.readFile(userCfgPath, "utf-8"); const originalParsed = ini.parse(originalContent); - assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); + assert.strictEqual( + originalParsed.global.cert, + undefined, + "original file should not gain cert", + ); // New file has merged settings (read from captured content before cleanup) - assert.ok(capturedConfigContent, "config content should have been captured"); + assert.ok( + capturedConfigContent, + "config content should have been captured", + ); const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert"); - assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env"); - assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved"); + assert.strictEqual( + newParsed.global.cert, + "/tmp/test-combined-ca.pem", + "new config should include cert", + ); + assert.strictEqual( + newParsed.global.proxy, + "http://localhost:8080", + "new config should include proxy from env", + ); + assert.strictEqual( + newParsed.global["index-url"], + "https://example.com/simple", + "index-url should be preserved", + ); customEnv = null; }); @@ -274,24 +315,30 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - assert.ok(capturedConfigContent, "config content should have been captured"); + assert.ok( + capturedConfigContent, + "config content should have been captured", + ); const parsed = ini.parse(capturedConfigContent); assert.ok(parsed.global, "[global] should exist after creation"); assert.strictEqual( parsed.global.proxy, "http://localhost:8080", - "proxy should be set from merged env" + "proxy should be set from merged env", ); assert.strictEqual( parsed.global.cert, "/tmp/test-combined-ca.pem", - "cert should be set during creation" + "cert should be set during creation", ); }); it("should create new temp config adding cert but preserving existing proxy (original file unchanged)", async () => { const tmpDir = os.tmpdir(); - const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const userCfgPath = path.join( + tmpDir, + `safe-chain-test-pip-${Date.now()}.ini`, + ); const initial = "[global]\nproxy = http://original:9999\n"; await fs.writeFile(userCfgPath, initial, "utf-8"); @@ -299,18 +346,41 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual(newCfgPath, userCfgPath, "should use a new temp config file"); + assert.notStrictEqual( + newCfgPath, + userCfgPath, + "should use a new temp config file", + ); // Original file unchanged const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); - assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); - assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains"); + assert.strictEqual( + originalParsed.global.cert, + undefined, + "original file should not gain cert", + ); + assert.strictEqual( + originalParsed.global.proxy, + "http://original:9999", + "original proxy remains", + ); // New file: cert and proxy always overwritten (read from captured content) - assert.ok(capturedConfigContent, "config content should have been captured"); + assert.ok( + capturedConfigContent, + "config content should have been captured", + ); const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); - assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); + assert.strictEqual( + newParsed.global.cert, + "/tmp/test-combined-ca.pem", + "cert always overwritten in temp config", + ); + assert.strictEqual( + newParsed.global.proxy, + "http://localhost:8080", + "proxy always overwritten in temp config", + ); customEnv = null; }); @@ -321,7 +391,7 @@ describe("runPipCommand environment variable handling", () => { "[global]", "cert = /path/to/existing.pem", "proxy = http://original:9999", - "" + "", ].join("\n"); await fs.writeFile(cfgPath, initialIni, "utf-8"); @@ -329,25 +399,51 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0, "execution should succeed"); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual(newCfgPath, cfgPath, "should use a newly generated temp config file"); + assert.notStrictEqual( + newCfgPath, + cfgPath, + "should use a newly generated temp config file", + ); // Original file stays untouched const originalContent = await fs.readFile(cfgPath, "utf-8"); const originalParsed = ini.parse(originalContent); - assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved"); - assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved"); + assert.strictEqual( + originalParsed.global.cert, + "/path/to/existing.pem", + "original cert preserved", + ); + assert.strictEqual( + originalParsed.global.proxy, + "http://original:9999", + "original proxy preserved", + ); - // New temp config: cert and proxy always overwritten (read from captured content) - assert.ok(capturedConfigContent, "config content should have been captured"); - const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); - assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); + // New temp config: cert and proxy always overwritten (read from captured content) + assert.ok( + capturedConfigContent, + "config content should have been captured", + ); + const newParsed = ini.parse(capturedConfigContent); + assert.strictEqual( + newParsed.global.cert, + "/tmp/test-combined-ca.pem", + "cert always overwritten in temp config", + ); + assert.strictEqual( + newParsed.global.proxy, + "http://localhost:8080", + "proxy always overwritten in temp config", + ); customEnv = null; }); it("should create new temp config preserving existing cert and adding missing proxy", async () => { const tmpDir = os.tmpdir(); - const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const userCfgPath = path.join( + tmpDir, + `safe-chain-test-pip-${Date.now()}.ini`, + ); const initial = "[global]\ncert = /path/to/existing.pem\n"; await fs.writeFile(userCfgPath, initial, "utf-8"); @@ -355,29 +451,55 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual(newCfgPath, userCfgPath, "should produce a new temp config file"); + assert.notStrictEqual( + newCfgPath, + userCfgPath, + "should produce a new temp config file", + ); // Original remains unchanged const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); - assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged"); - assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing"); + assert.strictEqual( + originalParsed.global.cert, + "/path/to/existing.pem", + "original cert unchanged", + ); + assert.strictEqual( + originalParsed.global.proxy, + undefined, + "original proxy still missing", + ); - // New file: cert and proxy always overwritten (read from captured content) - assert.ok(capturedConfigContent, "config content should have been captured"); - const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); - assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); + // New file: cert and proxy always overwritten (read from captured content) + assert.ok( + capturedConfigContent, + "config content should have been captured", + ); + const newParsed = ini.parse(capturedConfigContent); + assert.strictEqual( + newParsed.global.cert, + "/tmp/test-combined-ca.pem", + "cert always overwritten in temp config", + ); + assert.strictEqual( + newParsed.global.proxy, + "http://localhost:8080", + "proxy always overwritten in temp config", + ); customEnv = null; }); it("should log warnings when cert and proxy are already set in user config file", async () => { const tmpDir = os.tmpdir(); - const cfgPath = path.join(tmpDir, `safe-chain-test-pip-warn-${Date.now()}.ini`); + const cfgPath = path.join( + tmpDir, + `safe-chain-test-pip-warn-${Date.now()}.ini`, + ); const initialIni = [ "[global]", "cert = /user/cert.pem", "proxy = http://user-proxy:9999", - "" + "", ].join("\n"); await fs.writeFile(cfgPath, initialIni, "utf-8"); @@ -387,16 +509,28 @@ describe("runPipCommand environment variable handling", () => { let output = ""; const originalWrite = process.stdout.write; const originalError = process.stderr.write; - process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); }; - process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); }; + process.stdout.write = (chunk, ...args) => { + output += chunk; + return originalWrite.apply(process.stdout, [chunk, ...args]); + }; + process.stderr.write = (chunk, ...args) => { + output += chunk; + return originalError.apply(process.stderr, [chunk, ...args]); + }; await runPip("pip3", ["install", "requests"]); process.stdout.write = originalWrite; process.stderr.write = originalError; - assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output"); - assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output"); + assert.ok( + output.includes("cert found in PIP_CONFIG_FILE"), + "Should warn about cert overwrite in output", + ); + assert.ok( + output.includes("proxy found in PIP_CONFIG_FILE"), + "Should warn about proxy overwrite in output", + ); customEnv = null; }); @@ -407,13 +541,18 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true); assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true); - assert.strictEqual(shouldBypassSafeChain("python", ["-m", "http.server"]), true); - assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "http.server"]), true); + assert.strictEqual( + shouldBypassSafeChain("python", ["-m", "http.server"]), + true, + ); + assert.strictEqual( + shouldBypassSafeChain("python3", ["-m", "http.server"]), + true, + ); assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false); assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false); }); - }); diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js index 2f70cfa..73a6384 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js @@ -1,11 +1,11 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; /** * Sets CA bundle environment variables used by Python libraries and pipx. - * + * * @param {NodeJS.ProcessEnv} env - Env object * @param {string} combinedCaPath - Path to the combined CA bundle * @return {NodeJS.ProcessEnv} Modified environment object @@ -14,17 +14,23 @@ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) { let retVal = { ...env }; if (env.SSL_CERT_FILE) { - ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", + ); } retVal.SSL_CERT_FILE = combinedCaPath; if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", + ); } retVal.REQUESTS_CA_BUNDLE = combinedCaPath; if (env.PIP_CERT) { - ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", + ); } retVal.PIP_CERT = combinedCaPath; return retVal; @@ -32,7 +38,7 @@ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) { /** * Runs a pipx command with safe-chain's certificate bundle and proxy configuration. - * + * * @param {string} command - The command to execute * @param {string[]} args - Command line arguments * @returns {Promise<{status: number}>} Exit status of the command @@ -42,7 +48,10 @@ export async function runPipX(command, args) { const env = mergeSafeChainProxyEnvironmentVariables(process.env); const combinedCaPath = getCombinedCaBundlePath(); - const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath); + const modifiedEnv = getPipXCaBundleEnvironmentVariables( + env, + combinedCaPath, + ); // Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration // These are already set by mergeSafeChainProxyEnvironmentVariables diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js index dd04dc2..4b92bd1 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js @@ -41,7 +41,7 @@ describe("runPipXCommand", () => { }, }); - mock.module("../../registryProxy/certBundle.js", { + mock.module("../../registryProxy/builtInProxy/certBundle.js", { namedExports: { getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem", }, @@ -65,7 +65,11 @@ describe("runPipXCommand", () => { const res = await runPipX("pipx", ["install", "ruff"]); assert.strictEqual(res.status, 0); - assert.strictEqual(safeSpawnMock.mock.calls.length, 1, "safeSpawn should be called once"); + assert.strictEqual( + safeSpawnMock.mock.calls.length, + 1, + "safeSpawn should be called once", + ); const [, , options] = safeSpawnMock.mock.calls[0].arguments; const env = options.env; diff --git a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js index c8094e5..72741d3 100644 --- a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js +++ b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js @@ -1,7 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; /** * @returns {import("../currentPackageManager.js").PackageManager} @@ -19,36 +19,42 @@ export function createPoetryPackageManager() { /** * Sets CA bundle environment variables used by Poetry and Python libraries. * Poetry uses the Python requests library which respects these environment variables. - * + * * @param {NodeJS.ProcessEnv} env - Environment object to modify * @param {string} combinedCaPath - Path to the combined CA bundle */ function setPoetryCaBundleEnvironmentVariables(env, combinedCaPath) { // SSL_CERT_FILE: Used by Python SSL libraries and requests if (env.SSL_CERT_FILE) { - ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", + ); } env.SSL_CERT_FILE = combinedCaPath; // REQUESTS_CA_BUNDLE: Used by the requests library (which Poetry uses) if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", + ); } env.REQUESTS_CA_BUNDLE = combinedCaPath; // PIP_CERT: Poetry may use pip internally if (env.PIP_CERT) { - ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", + ); } env.PIP_CERT = combinedCaPath; } /** * Runs a poetry command with safe-chain's certificate bundle and proxy configuration. - * + * * Poetry respects standard HTTP_PROXY/HTTPS_PROXY environment variables through * the Python requests library. - * + * * @param {string[]} args - Command line arguments to pass to poetry * @returns {Promise<{status: number}>} Exit status of the poetry command */ @@ -63,7 +69,7 @@ async function runPoetryCommand(args) { stdio: "inherit", env, }); - + return { status: result.status }; } catch (/** @type any */ error) { if (error.status) { diff --git a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js index ed02fe3..5d6bd4f 100644 --- a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js +++ b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js @@ -1,44 +1,50 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; /** * Sets CA bundle environment variables used by Python libraries and uv. - * + * * @param {NodeJS.ProcessEnv} env - Env object * @param {string} combinedCaPath - Path to the combined CA bundle */ function setUvCaBundleEnvironmentVariables(env, combinedCaPath) { // SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients if (env.SSL_CERT_FILE) { - ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", + ); } env.SSL_CERT_FILE = combinedCaPath; // REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally) if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", + ); } env.REQUESTS_CA_BUNDLE = combinedCaPath; // PIP_CERT: Some underlying pip operations may respect this if (env.PIP_CERT) { - ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); + ui.writeWarning( + "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", + ); } env.PIP_CERT = combinedCaPath; } /** * Runs a uv command with safe-chain's certificate bundle and proxy configuration. - * + * * uv respects standard environment variables for proxy and TLS configuration: * - HTTP_PROXY / HTTPS_PROXY: Proxy settings * - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification - * + * * Unlike pip (which requires a temporary config file for cert configuration), uv directly * honors environment variables, so no config/ini file is needed. - * + * * @param {string} command - The uv command to execute (typically 'uv') * @param {string[]} args - Command line arguments to pass to uv * @returns {Promise<{status: number}>} Exit status of the uv command diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.js similarity index 91% rename from packages/safe-chain/src/registryProxy/certBundle.js rename to packages/safe-chain/src/registryProxy/builtInProxy/certBundle.js index 42549b9..06aaf23 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.js @@ -6,7 +6,7 @@ import certifi from "certifi"; import tls from "node:tls"; import { X509Certificate } from "node:crypto"; import { getCaCertPath } from "./certUtils.js"; -import { ui } from "../environment/userInteraction.js"; +import { ui } from "../../environment/userInteraction.js"; /** * Check if a PEM string contains only parsable cert blocks. @@ -50,7 +50,7 @@ function isParsable(pem) { * - Mozilla roots via certifi (for public HTTPS) * - Node's built-in root certificates (fallback) * - User's custom certificates (if NODE_EXTRA_CA_CERTS environment variable is set) - * + * * @returns {string} Path to the combined CA bundle PEM file */ export function getCombinedCaBundlePath() { @@ -92,14 +92,21 @@ export function getCombinedCaBundlePath() { const userPem = readUserCertificateFile(userCertPath); if (userPem) { parts.push(userPem.trim()); - ui.writeVerbose(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`); + ui.writeVerbose( + `Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`, + ); } else { - ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`); + ui.writeWarning( + `Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`, + ); } } const combined = parts.filter(Boolean).join("\n"); - const target = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`); + const target = path.join( + os.tmpdir(), + `safe-chain-ca-bundle-${Date.now()}.pem`, + ); fs.writeFileSync(target, combined, { encoding: "utf8" }); return target; } @@ -177,5 +184,3 @@ function readUserCertificateFile(certPath) { return null; } } - - diff --git a/packages/safe-chain/src/registryProxy/certBundle.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.spec.js similarity index 100% rename from packages/safe-chain/src/registryProxy/certBundle.spec.js rename to packages/safe-chain/src/registryProxy/builtInProxy/certBundle.spec.js diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/builtInProxy/certUtils.js similarity index 100% rename from packages/safe-chain/src/registryProxy/certUtils.js rename to packages/safe-chain/src/registryProxy/builtInProxy/certUtils.js diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js b/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js new file mode 100644 index 0000000..f6b5d62 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js @@ -0,0 +1,150 @@ +import * as http from "http"; +import { tunnelRequest } from "./tunnelRequestHandler.js"; +import { mitmConnect } from "./mitmRequestHandler.js"; +import { handleHttpProxyRequest } from "./plainHttpProxy.js"; +import { getCombinedCaBundlePath } from "./certBundle.js"; +import { ui } from "../../environment/userInteraction.js"; +import chalk from "chalk"; +import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; +import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; + +/** * + * @returns {import("../registryProxy.js").SafeChainProxy} */ +export function createBuiltInProxyServer() { + const SERVER_STOP_TIMEOUT_MS = 1000; + /** + * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} + */ + const state = { + port: null, + blockedRequests: [], + }; + + const server = http.createServer( + // This handles direct HTTP requests (non-CONNECT requests) + // This is normally http-only traffic, but we also handle + // https for clients that don't properly use CONNECT + handleHttpProxyRequest, + ); + + // This handles HTTPS requests via the CONNECT method + server.on("connect", handleConnect); + + return { + startServer: () => startServer(server), + stopServer: () => stopServer(server), + verifyNoMaliciousPackages, + hasSuppressedVersions: getHasSuppressedVersions, + getServerPort: () => state.port, + getCombinedCaBundlePath, + }; + + /** + * @param {import("http").Server} server + * + * @returns {Promise} + */ + function startServer(server) { + return new Promise((resolve, reject) => { + // Passing port 0 makes the OS assign an available port + server.listen(0, () => { + const address = server.address(); + if (address && typeof address === "object") { + state.port = address.port; + resolve(); + } else { + reject(new Error("Failed to start proxy server")); + } + }); + + server.on("error", (err) => { + reject(err); + }); + }); + } + + /** + * @param {import("http").Server} server + * + * @returns {Promise} + */ + function stopServer(server) { + return new Promise((resolve) => { + try { + server.close(() => { + resolve(); + }); + } catch { + resolve(); + } + setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); + }); + } + + /** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ + 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 interceptor = createInterceptorForUrl(req.url || ""); + + if (interceptor) { + // Subscribe to malware blocked events + interceptor.on( + "malwareBlocked", + ( + /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event, + ) => { + onMalwareBlocked(event.packageName, event.version, event.targetUrl); + }, + ); + + mitmConnect(req, clientSocket, interceptor); + } else { + // For other hosts, just tunnel the request to the destination tcp socket + ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); + tunnelRequest(req, clientSocket, head); + } + } + + /** + * + * @param {string} packageName + * @param {string} version + * @param {string} url + */ + function onMalwareBlocked(packageName, version, url) { + state.blockedRequests.push({ packageName, version, url }); + } + + function verifyNoMaliciousPackages() { + if (state.blockedRequests.length === 0) { + // No malicious packages were blocked, so nothing to block + return true; + } + + ui.emptyLine(); + + ui.writeInformation( + `Safe-chain: ${chalk.bold( + `blocked ${state.blockedRequests.length} malicious package downloads`, + )}:`, + ); + + for (const req of state.blockedRequests) { + ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); + } + + ui.emptyLine(); + ui.writeExitWithoutInstallingMaliciousPackages(); + ui.emptyLine(); + + return false; + } +} diff --git a/packages/safe-chain/src/registryProxy/getConnectTimeout.js b/packages/safe-chain/src/registryProxy/builtInProxy/getConnectTimeout.js similarity index 100% rename from packages/safe-chain/src/registryProxy/getConnectTimeout.js rename to packages/safe-chain/src/registryProxy/builtInProxy/getConnectTimeout.js diff --git a/packages/safe-chain/src/registryProxy/http-utils.js b/packages/safe-chain/src/registryProxy/builtInProxy/http-utils.js similarity index 100% rename from packages/safe-chain/src/registryProxy/http-utils.js rename to packages/safe-chain/src/registryProxy/builtInProxy/http-utils.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/createInterceptorForEcoSystem.js similarity index 93% rename from packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/createInterceptorForEcoSystem.js index 79b5200..680be59 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/createInterceptorForEcoSystem.js @@ -2,7 +2,7 @@ import { ECOSYSTEM_JS, ECOSYSTEM_PY, getEcoSystem, -} from "../../config/settings.js"; +} from "../../../config/settings.js"; import { npmInterceptorForUrl } from "./npm/npmInterceptor.js"; import { pipInterceptorForUrl } from "./pipInterceptor.js"; diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/interceptorBuilder.js similarity index 100% rename from packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/interceptorBuilder.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js similarity index 91% rename from packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js index 14e3ba7..cfa708c 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js @@ -1,5 +1,8 @@ -import { getMinimumPackageAgeHours, getNpmMinimumPackageAgeExclusions } from "../../../config/settings.js"; -import { ui } from "../../../environment/userInteraction.js"; +import { + getMinimumPackageAgeHours, + getNpmMinimumPackageAgeExclusions, +} from "../../../../config/settings.js"; +import { ui } from "../../../../environment/userInteraction.js"; import { getHeaderValueAsString } from "../../http-utils.js"; const state = { @@ -68,15 +71,20 @@ export function modifyNpmInfoResponse(body, headers) { // Check if this package is excluded from minimum age filtering const packageName = bodyJson.name; const exclusions = getNpmMinimumPackageAgeExclusions(); - if (packageName && exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern))) { + if ( + packageName && + exclusions.some((pattern) => + matchesExclusionPattern(packageName, pattern), + ) + ) { ui.writeVerbose( - `Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).` + `Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).`, ); return body; } const cutOff = new Date( - new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 + new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000, ); const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; @@ -113,7 +121,7 @@ export function modifyNpmInfoResponse(body, headers) { return Buffer.from(JSON.stringify(bodyJson)); } catch (/** @type {any} */ err) { ui.writeVerbose( - `Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}` + `Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}`, ); return body; } @@ -129,7 +137,7 @@ function deleteVersionFromJson(json, version) { const packageName = typeof json?.name === "string" ? json.name : "(unknown)"; ui.writeVerbose( - `Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` + `Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`, ); delete json.time[version]; @@ -148,18 +156,20 @@ function deleteVersionFromJson(json, version) { */ function calculateLatestTag(tagList) { const entries = Object.entries(tagList).filter( - ([version, _]) => version !== "created" && version !== "modified" + ([version, _]) => version !== "created" && version !== "modified", ); const latestFullRelease = getMostRecentTag( - Object.fromEntries(entries.filter(([version, _]) => !version.includes("-"))) + Object.fromEntries( + entries.filter(([version, _]) => !version.includes("-")), + ), ); if (latestFullRelease) { return latestFullRelease; } const latestPrerelease = getMostRecentTag( - Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))) + Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))), ); return latestPrerelease; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js similarity index 89% rename from packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js index 3d3b8b4..a8c1a61 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js @@ -1,8 +1,8 @@ import { getNpmCustomRegistries, skipMinimumPackageAge, -} from "../../../config/settings.js"; -import { isMalwarePackage } from "../../../scanning/audit/index.js"; +} from "../../../../config/settings.js"; +import { isMalwarePackage } from "../../../../scanning/audit/index.js"; import { interceptRequests } from "../interceptorBuilder.js"; import { isPackageInfoUrl, @@ -23,7 +23,7 @@ const knownJsRegistries = [ */ export function npmInterceptorForUrl(url) { const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find( - (reg) => url.includes(reg) + (reg) => url.includes(reg), ); if (registry) { @@ -41,7 +41,7 @@ function buildNpmInterceptor(registry) { return interceptRequests(async (reqContext) => { const { packageName, version } = parseNpmPackageUrl( reqContext.targetUrl, - registry + registry, ); if (await isMalwarePackage(packageName, version)) { diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js similarity index 94% rename from packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index 834a2ad..302c5b8 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -6,23 +6,24 @@ describe("npmInterceptor minimum package age", async () => { let skipMinimumPackageAgeSetting = false; let minimumPackageAgeExclusionsSetting = []; - mock.module("../../../config/settings.js", { + mock.module("../../../../config/settings.js", { namedExports: { getMinimumPackageAgeHours: () => minimumPackageAgeSettings, skipMinimumPackageAge: () => skipMinimumPackageAgeSetting, getNpmCustomRegistries: () => [], - getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting, + getNpmMinimumPackageAgeExclusions: () => + minimumPackageAgeExclusionsSetting, }, }); - mock.module("../../../scanning/audit/index.js", { + mock.module("../../../../scanning/audit/index.js", { namedExports: { isMalwarePackage: async () => { return false; }, }, }); - mock.module("../../../environment/userInteraction.js", { + mock.module("../../../../environment/userInteraction.js", { namedExports: { ui: { startProcess: () => {}, @@ -64,9 +65,8 @@ describe("npmInterceptor minimum package age", async () => { ]) { it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => { const interceptor = npmInterceptorForUrl(packageInfoUrl); - const requestInterceptor = await interceptor.handleRequest( - packageInfoUrl - ); + const requestInterceptor = + await interceptor.handleRequest(packageInfoUrl); assert.equal(requestInterceptor.modifiesResponse(), true); }); @@ -120,9 +120,8 @@ describe("npmInterceptor minimum package age", async () => { ]) { it(`modifyResponse should be false for special endpoints: ${specialEndpoint}`, async () => { const interceptor = npmInterceptorForUrl(specialEndpoint); - const requestInterceptor = await interceptor.handleRequest( - specialEndpoint - ); + const requestInterceptor = + await interceptor.handleRequest(specialEndpoint); assert.equal(requestInterceptor.modifiesResponse(), false); }); @@ -152,7 +151,7 @@ describe("npmInterceptor minimum package age", async () => { ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); @@ -193,7 +192,7 @@ describe("npmInterceptor minimum package age", async () => { ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); @@ -225,7 +224,7 @@ describe("npmInterceptor minimum package age", async () => { // cutoff-date here ["2.0.0-alpha"]: getDate(-4), }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); @@ -261,7 +260,7 @@ describe("npmInterceptor minimum package age", async () => { const modifiedBody = await runModifyNpmInfoRequest( packageUrl, - originalBody + originalBody, ); const modifiedJson = JSON.parse(modifiedBody); @@ -303,7 +302,7 @@ describe("npmInterceptor minimum package age", async () => { ["3.0.0"]: getDate(-40), // ~1.7 days old - should be removed ["4.0.0"]: getDate(-24), // 1 day old - should be removed }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); @@ -347,7 +346,7 @@ describe("npmInterceptor minimum package age", async () => { // 1-hour cutoff here ["3.0.0"]: getDate(0), // just published - should be removed }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); @@ -386,7 +385,10 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + originalBody, + ); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain unchanged since lodash is excluded @@ -416,7 +418,7 @@ describe("npmInterceptor minimum package age", async () => { ["1.0.0"]: getDate(-7), ["3.0.0"]: getDate(-3), }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); @@ -446,7 +448,10 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + originalBody, + ); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain for excluded scoped package @@ -474,7 +479,10 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + originalBody, + ); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain since lodash is in the exclusion list @@ -500,7 +508,10 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + originalBody, + ); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain since @aikidosec/* matches @aikidosec/safe-chain @@ -528,7 +539,10 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + originalBody, + ); const modifiedJson = JSON.parse(modifiedBody); // Version 2.0.0 should be filtered since @other/package doesn't match @aikidosec/* @@ -555,7 +569,7 @@ describe("npmInterceptor minimum package age", async () => { ["1.0.0"]: getDate(-100), ["2.0.0"]: getDate(-1), }, - }) + }), ); const modifiedJson = JSON.parse(modifiedBody); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js similarity index 95% rename from packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js index e1b7c79..5cba422 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js @@ -5,7 +5,7 @@ let lastPackage; let malwareResponse = false; let customRegistries = []; -mock.module("../../../scanning/audit/index.js", { +mock.module("../../../../scanning/audit/index.js", { namedExports: { isMalwarePackage: async (packageName, version) => { lastPackage = { packageName, version }; @@ -14,7 +14,7 @@ mock.module("../../../scanning/audit/index.js", { }, }); -mock.module("../../../config/settings.js", { +mock.module("../../../../config/settings.js", { namedExports: { LOGGING_SILENT: "silent", LOGGING_NORMAL: "normal", @@ -136,7 +136,7 @@ describe("npmInterceptor", async () => { const interceptor = npmInterceptorForUrl(url); assert.ok( interceptor, - "Interceptor should be created for known npm registry" + "Interceptor should be created for known npm registry", ); await interceptor.handleRequest(url); @@ -153,7 +153,7 @@ describe("npmInterceptor", async () => { assert.equal( interceptor, undefined, - "Interceptor should be undefined for unknown registry" + "Interceptor should be undefined for unknown registry", ); }); @@ -170,12 +170,12 @@ describe("npmInterceptor", async () => { assert.equal( result.blockResponse.statusCode, 403, - "Block response should have status code 403" + "Block response should have status code 403", ); assert.equal( result.blockResponse.message, "Forbidden - blocked by safe-chain", - "Block response should have correct status message" + "Block response should have correct status message", ); }); }); @@ -212,7 +212,7 @@ describe("npmInterceptor with custom registries", async () => { assert.ok( interceptor, - "Interceptor should be created for custom registry with scoped package" + "Interceptor should be created for custom registry with scoped package", ); await interceptor.handleRequest(url); @@ -262,7 +262,7 @@ describe("npmInterceptor with custom registries", async () => { assert.equal( interceptor, undefined, - "Should not create interceptor for unknown registry" + "Should not create interceptor for unknown registry", ); }); }); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/parseNpmPackageUrl.js similarity index 100% rename from packages/safe-chain/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/parseNpmPackageUrl.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js similarity index 90% rename from packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js index e781e30..47cdee8 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js @@ -1,5 +1,5 @@ -import { getPipCustomRegistries } from "../../config/settings.js"; -import { isMalwarePackage } from "../../scanning/audit/index.js"; +import { getPipCustomRegistries } from "../../../config/settings.js"; +import { isMalwarePackage } from "../../../scanning/audit/index.js"; import { interceptRequests } from "./interceptorBuilder.js"; const knownPipRegistries = [ @@ -33,16 +33,18 @@ function buildPipInterceptor(registry) { return interceptRequests(async (reqContext) => { const { packageName, version } = parsePipPackageFromUrl( reqContext.targetUrl, - registry + registry, ); // Normalize underscores to hyphens for DB matching, as PyPI allows underscores in distribution names. // Per python, packages that differ only by hyphen vs underscore are considered the same. - const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName; + const hyphenName = packageName?.includes("_") + ? packageName.replace(/_/g, "-") + : packageName; const isMalicious = - await isMalwarePackage(packageName, version) - || await isMalwarePackage(hyphenName, version); + (await isMalwarePackage(packageName, version)) || + (await isMalwarePackage(hyphenName, version)); if (isMalicious) { reqContext.blockMalware(packageName, version); @@ -110,7 +112,8 @@ function parsePipPackageFromUrl(url, registry) { } // Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata) - const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i; + const sdistExtWithMetadataRe = + /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i; const sdistExtMatch = filename.match(sdistExtWithMetadataRe); if (sdistExtMatch) { const base = filename.replace(sdistExtWithMetadataRe, ""); diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js similarity index 88% rename from packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js index fc9c91e..b57218e 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js @@ -6,13 +6,13 @@ describe("pipInterceptor custom registries", async () => { let malwareResponse = false; let customRegistries = []; - mock.module("../../config/settings.js", { + mock.module("../../../config/settings.js", { namedExports: { getPipCustomRegistries: () => customRegistries, }, }); - mock.module("../../scanning/audit/index.js", { + mock.module("../../../scanning/audit/index.js", { namedExports: { isMalwarePackage: async (packageName, version) => { lastPackage = { packageName, version }; @@ -30,10 +30,7 @@ describe("pipInterceptor custom registries", async () => { const interceptor = pipInterceptorForUrl(url); - assert.ok( - interceptor, - "Interceptor should be created for custom registry" - ); + assert.ok(interceptor, "Interceptor should be created for custom registry"); }); it("should parse package from custom registry URL", async () => { @@ -69,10 +66,7 @@ describe("pipInterceptor custom registries", async () => { }); it("should handle multiple custom registries", async () => { - customRegistries = [ - "registry-one.example.com", - "registry-two.example.com", - ]; + customRegistries = ["registry-one.example.com", "registry-two.example.com"]; const url1 = "https://registry-one.example.com/packages/package1-1.0.0.tar.gz"; @@ -85,7 +79,7 @@ describe("pipInterceptor custom registries", async () => { assert.ok(interceptor1, "Interceptor should be created for first registry"); assert.ok( interceptor2, - "Interceptor should be created for second registry" + "Interceptor should be created for second registry", ); }); @@ -105,12 +99,12 @@ describe("pipInterceptor custom registries", async () => { assert.equal( result.blockResponse.statusCode, 403, - "Block response should have status code 403" + "Block response should have status code 403", ); assert.equal( result.blockResponse.message, "Forbidden - blocked by safe-chain", - "Block response should have correct status message" + "Block response should have correct status message", ); malwareResponse = false; @@ -126,7 +120,7 @@ describe("pipInterceptor custom registries", async () => { assert.ok( interceptor, - "Interceptor should be created for known registry even with custom registries set" + "Interceptor should be created for known registry even with custom registries set", ); await interceptor.handleRequest(url); @@ -139,14 +133,15 @@ describe("pipInterceptor custom registries", async () => { it("should not create interceptor for unknown registry when custom registries are set", () => { customRegistries = ["my-custom-registry.example.com"]; - const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz"; + const url = + "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz"; const interceptor = pipInterceptorForUrl(url); assert.equal( interceptor, undefined, - "Interceptor should be undefined for unknown registry" + "Interceptor should be undefined for unknown registry", ); }); @@ -160,7 +155,7 @@ describe("pipInterceptor custom registries", async () => { assert.equal( interceptor, undefined, - "Interceptor should be undefined when no custom registries are configured" + "Interceptor should be undefined when no custom registries are configured", ); }); @@ -196,4 +191,3 @@ describe("pipInterceptor custom registries", async () => { }); }); }); - diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js similarity index 94% rename from packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js rename to packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js index 482a800..dd812f1 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js @@ -5,7 +5,7 @@ describe("pipInterceptor", async () => { let lastPackage; let malwareResponse = false; - mock.module("../../scanning/audit/index.js", { + mock.module("../../../scanning/audit/index.js", { namedExports: { isMalwarePackage: async (packageName, version) => { lastPackage = { packageName, version }; @@ -100,7 +100,7 @@ describe("pipInterceptor", async () => { const interceptor = pipInterceptorForUrl(url); assert.ok( interceptor, - "Interceptor should be created for known npm registry" + "Interceptor should be created for known npm registry", ); await interceptor.handleRequest(url); @@ -117,7 +117,7 @@ describe("pipInterceptor", async () => { assert.equal( interceptor, undefined, - "Interceptor should be undefined for unknown registry" + "Interceptor should be undefined for unknown registry", ); }); @@ -134,12 +134,12 @@ describe("pipInterceptor", async () => { assert.equal( result.blockResponse.statusCode, 403, - "Block response should have status code 403" + "Block response should have status code 403", ); assert.equal( result.blockResponse.message, "Forbidden - blocked by safe-chain", - "Block response should have correct status message" + "Block response should have correct status message", ); }); }); diff --git a/packages/safe-chain/src/registryProxy/isImdsEndpoint.js b/packages/safe-chain/src/registryProxy/builtInProxy/isImdsEndpoint.js similarity index 100% rename from packages/safe-chain/src/registryProxy/isImdsEndpoint.js rename to packages/safe-chain/src/registryProxy/builtInProxy/isImdsEndpoint.js diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js similarity index 96% rename from packages/safe-chain/src/registryProxy/mitmRequestHandler.js rename to packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js index 8268559..9a45270 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js @@ -1,7 +1,7 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; -import { ui } from "../environment/userInteraction.js"; +import { ui } from "../../environment/userInteraction.js"; import { gunzipSync, gzipSync } from "zlib"; /** @@ -19,7 +19,7 @@ export function mitmConnect(req, clientSocket, interceptor) { clientSocket.on("error", (err) => { ui.writeVerbose( - `Safe-chain: Client socket error for ${req.url}: ${err.message}` + `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. @@ -89,7 +89,7 @@ function createHttpsServer(hostname, port, interceptor) { key: cert.privateKey, cert: cert.certificate, }, - handleRequest + handleRequest, ); return server; @@ -119,7 +119,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) { proxyReq.on("error", (err) => { ui.writeVerbose( - `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}` + `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`, ); res.writeHead(502); res.end("Bad Gateway"); @@ -127,7 +127,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) { req.on("error", (err) => { ui.writeError( - `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}` + `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`, ); proxyReq.destroy(); }); @@ -138,7 +138,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) { req.on("end", () => { ui.writeVerbose( - `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` + `Safe-chain: Finished proxying request to ${req.url} for ${hostname}`, ); proxyReq.end(); }); @@ -180,7 +180,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) { const proxyReq = https.request(options, (proxyRes) => { proxyRes.on("error", (err) => { ui.writeError( - `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}` + `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`, ); if (!res.headersSent) { res.writeHead(502); @@ -190,7 +190,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) { if (!proxyRes.statusCode) { ui.writeError( - `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}` + `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`, ); res.writeHead(500); res.end("Internal Server Error"); diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js similarity index 97% rename from packages/safe-chain/src/registryProxy/plainHttpProxy.js rename to packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js index 75b9d77..6d74588 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js @@ -1,6 +1,6 @@ import * as http from "http"; import * as https from "https"; -import { ui } from "../environment/userInteraction.js"; +import { ui } from "../../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req @@ -61,7 +61,7 @@ export function handleHttpProxyRequest(req, res) { res.end(); } }); - } + }, ) .on("error", (err) => { if (!res.headersSent) { diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js similarity index 95% rename from packages/safe-chain/src/registryProxy/tunnelRequestHandler.js rename to packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js index 5eac381..f7a3b9d 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js @@ -1,5 +1,5 @@ import * as net from "net"; -import { ui } from "../environment/userInteraction.js"; +import { ui } from "../../environment/userInteraction.js"; import { isImdsEndpoint } from "./isImdsEndpoint.js"; import { getConnectTimeout } from "./getConnectTimeout.js"; @@ -49,11 +49,11 @@ function tunnelRequestToDestination(req, clientSocket, head) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); if (isImds) { ui.writeVerbose( - `Safe-chain: Closing connection because previously timedout connect to ${hostname}` + `Safe-chain: Closing connection because previously timedout connect to ${hostname}`, ); } else { ui.writeError( - `Safe-chain: Closing connection because previously timedout connect to ${hostname}` + `Safe-chain: Closing connection because previously timedout connect to ${hostname}`, ); } return; @@ -67,11 +67,11 @@ function tunnelRequestToDestination(req, clientSocket, head) { if (isImds) { timedoutImdsEndpoints.push(hostname); ui.writeVerbose( - `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms` + `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`, ); } else { ui.writeError( - `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms` + `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`, ); } serverSocket.destroy(); @@ -111,11 +111,11 @@ function tunnelRequestToDestination(req, clientSocket, head) { clearTimeout(connectTimer); if (isImds) { ui.writeVerbose( - `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}` + `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`, ); } else { ui.writeError( - `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}` + `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`, ); } if (clientSocket.writable) { @@ -173,7 +173,7 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { clientSocket.pipe(proxySocket); } else { ui.writeError( - `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}` + `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`, ); if (clientSocket.writable) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); @@ -189,14 +189,14 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { ui.writeError( `Safe-chain: error connecting to proxy ${proxy.hostname}:${ proxy.port || 8080 - } - ${err.message}` + } - ${err.message}`, ); if (clientSocket.writable) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } } else { ui.writeError( - `Safe-chain: proxy socket error after connection - ${err.message}` + `Safe-chain: proxy socket error after connection - ${err.message}`, ); if (clientSocket.writable) { clientSocket.end(); @@ -210,4 +210,3 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { } }); } - diff --git a/packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js b/packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js new file mode 100644 index 0000000..7703978 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js @@ -0,0 +1,150 @@ +import * as http from "http"; +import { tunnelRequest } from "./tunnelRequestHandler.js"; +import { mitmConnect } from "./mitmRequestHandler.js"; +import { handleHttpProxyRequest } from "./plainHttpProxy.js"; +import { getCombinedCaBundlePath } from "./builtInProxy/certBundle.js"; +import { ui } from "../environment/userInteraction.js"; +import chalk from "chalk"; +import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; +import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; + +/** * + * @returns {import("./registryProxy.js").SafeChainProxy} */ +export function createBuiltInProxyServer() { + const SERVER_STOP_TIMEOUT_MS = 1000; + /** + * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} + */ + const state = { + port: null, + blockedRequests: [], + }; + + const server = http.createServer( + // This handles direct HTTP requests (non-CONNECT requests) + // This is normally http-only traffic, but we also handle + // https for clients that don't properly use CONNECT + handleHttpProxyRequest, + ); + + // This handles HTTPS requests via the CONNECT method + server.on("connect", handleConnect); + + return { + startServer: () => startServer(server), + stopServer: () => stopServer(server), + verifyNoMaliciousPackages, + hasSuppressedVersions: getHasSuppressedVersions, + getServerPort: () => state.port, + getCombinedCaBundlePath, + }; + + /** + * @param {import("http").Server} server + * + * @returns {Promise} + */ + function startServer(server) { + return new Promise((resolve, reject) => { + // Passing port 0 makes the OS assign an available port + server.listen(0, () => { + const address = server.address(); + if (address && typeof address === "object") { + state.port = address.port; + resolve(); + } else { + reject(new Error("Failed to start proxy server")); + } + }); + + server.on("error", (err) => { + reject(err); + }); + }); + } + + /** + * @param {import("http").Server} server + * + * @returns {Promise} + */ + function stopServer(server) { + return new Promise((resolve) => { + try { + server.close(() => { + resolve(); + }); + } catch { + resolve(); + } + setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); + }); + } + + /** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ + 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 interceptor = createInterceptorForUrl(req.url || ""); + + if (interceptor) { + // Subscribe to malware blocked events + interceptor.on( + "malwareBlocked", + ( + /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event, + ) => { + onMalwareBlocked(event.packageName, event.version, event.targetUrl); + }, + ); + + mitmConnect(req, clientSocket, interceptor); + } else { + // For other hosts, just tunnel the request to the destination tcp socket + ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); + tunnelRequest(req, clientSocket, head); + } + } + + /** + * + * @param {string} packageName + * @param {string} version + * @param {string} url + */ + function onMalwareBlocked(packageName, version, url) { + state.blockedRequests.push({ packageName, version, url }); + } + + function verifyNoMaliciousPackages() { + if (state.blockedRequests.length === 0) { + // No malicious packages were blocked, so nothing to block + return true; + } + + ui.emptyLine(); + + ui.writeInformation( + `Safe-chain: ${chalk.bold( + `blocked ${state.blockedRequests.length} malicious package downloads`, + )}:`, + ); + + for (const req of state.blockedRequests) { + ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); + } + + ui.emptyLine(); + ui.writeExitWithoutInstallingMaliciousPackages(); + ui.emptyLine(); + + return false; + } +} diff --git a/packages/safe-chain/src/registryProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js similarity index 90% rename from packages/safe-chain/src/registryProxy/createRamaProxy.js rename to packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js index 553e201..42b6549 100644 --- a/packages/safe-chain/src/registryProxy/createRamaProxy.js +++ b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js @@ -1,18 +1,18 @@ -import { ChildProcess, spawn } from "node:child_process"; +import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; import { mkdtempSync, readFile, writeFile } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; -import { ui } from "../environment/userInteraction.js"; -import { getLoggingLevel, LOGGING_VERBOSE } from "../config/settings.js"; +import { ui } from "../../environment/userInteraction.js"; +import { getLoggingLevel, LOGGING_VERBOSE } from "../../config/settings.js"; const readFilePromise = promisify(readFile); const writeFilePromise = promisify(writeFile); /** * @typedef {Object} RamaProxyInstance - * @property {ChildProcess} process + * @property {import("node:child_process").ChildProcess} process * @property {string} proxyAddress * @property {string} metaAddress * @property {string} certPath @@ -35,7 +35,7 @@ export function getRamaPath() { /** * @param {string} ramaPath * - * @returns {import("./registryProxy.js").SafeChainProxy} */ + * @returns {import("../registryProxy.js").SafeChainProxy} */ export function createRamaProxy(ramaPath) { const tempDir = mkdtempSync(join(tmpdir(), "safe-chain-proxy-")); /** @type {RamaProxyInstance | null} */ @@ -45,7 +45,7 @@ export function createRamaProxy(ramaPath) { startServer: async () => { ramaInstance = await startRama(ramaPath, tempDir); ui.writeVerbose( - `Proxy started at address "${ramaInstance.proxyAddress}"` + `Proxy started at address "${ramaInstance.proxyAddress}"`, ); }, stopServer: async () => { @@ -98,7 +98,7 @@ async function startRama(ramaPath, dataFolder) { const proxyAddress = await readFilePromise(proxyAddrPath, "utf-8"); const metaAddress = await readFilePromise( join(dataFolder, "meta.addr.txt"), - "utf-8" + "utf-8", ); const certResponse = await fetch(`http://${metaAddress}/ca`); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js index ace84ee..a4288f0 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js @@ -14,14 +14,14 @@ const mockIsImdsEndpoint = (host) => { ].includes(host); }; -mock.module("./isImdsEndpoint.js", { +mock.module("./builtInProxy/isImdsEndpoint.js", { namedExports: { isImdsEndpoint: mockIsImdsEndpoint, }, }); // Mock getConnectTimeout to speed up tests -mock.module("./getConnectTimeout.js", { +mock.module("./builtInProxy/getConnectTimeout.js", { namedExports: { getConnectTimeout: (host) => { // IMDS endpoints: 100ms (real: 3s) @@ -56,7 +56,7 @@ describe("registryProxy.connectTunnel", () => { const tunnelResponse = await establishHttpsTunnel( socket, "postman-echo.com", - 443 + 443, ); assert.ok(tunnelResponse.includes("HTTP/1.1 200 Connection Established")); @@ -69,7 +69,7 @@ describe("registryProxy.connectTunnel", () => { const httpsResponse = await sendHttpsRequestThroughTunnel( socket, "GET", - new URL("https://postman-echo.com/status/200") + new URL("https://postman-echo.com/status/200"), ); assert.ok(httpsResponse.includes("HTTP/1.1 200 OK")); @@ -85,25 +85,25 @@ describe("registryProxy.connectTunnel", () => { // without interception by the safe-chain MITM proxy. const certInfo = await getTlsCertificateInfo( socket, - new URL("https://postman-echo.com") + new URL("https://postman-echo.com"), ); // Verify the certificate is NOT issued by our safe-chain CA // Our self-signed CA would have issuer: "Safe-Chain Proxy CA" assert.ok( certInfo.issuer !== undefined, - "Certificate should have an issuer" + "Certificate should have an issuer", ); assert.ok( !certInfo.issuer.includes("Safe-Chain"), - `Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}` + `Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}`, ); // Verify it's a real certificate with proper hostname assert.strictEqual( certInfo.subject.includes("postman-echo.com"), true, - `Certificate subject should include postman-echo.com, got: ${certInfo.subject}` + `Certificate subject should include postman-echo.com, got: ${certInfo.subject}`, ); socket.destroy(); @@ -185,13 +185,13 @@ describe("registryProxy.connectTunnel", () => { // Should return 504 Gateway Timeout (not 502 - 504 is for actual timeouts) assert.ok( responseData.includes("HTTP/1.1 504 Gateway Timeout"), - "Should return 504 for timeout" + "Should return 504 for timeout", ); // Should timeout around 100ms for IMDS endpoints (allow some margin) assert.ok( duration >= 80 && duration < 200, - `IMDS timeout should be ~80-200ms, got ${duration}ms` + `IMDS timeout should be ~80-200ms, got ${duration}ms`, ); socket.destroy(); @@ -232,13 +232,13 @@ describe("registryProxy.connectTunnel", () => { // Should return 502 immediately (cached timeout) assert.ok( responseData.includes("HTTP/1.1 502 Bad Gateway"), - "Should return 502 for cached timeout" + "Should return 502 for cached timeout", ); // Should be nearly instant (< 50ms) since it's cached assert.ok( duration < 50, - `Cached IMDS timeout should be instant, got ${duration}ms` + `Cached IMDS timeout should be instant, got ${duration}ms`, ); socket2.destroy(); @@ -283,14 +283,14 @@ describe("registryProxy.connectTunnel", () => { // Should return 504 Gateway Timeout (not 502 - 504 is for actual timeouts) assert.ok( responseData.includes("HTTP/1.1 504 Gateway Timeout"), - "Should return 504 for timeout" + "Should return 504 for timeout", ); // Should NOT be instant - it should retry the connection (taking ~500ms due to mock timeout) // If it was cached, it would return in < 50ms assert.ok( duration >= 400, - `Non-IMDS timeout should NOT be cached, but got instant response in ${duration}ms` + `Non-IMDS timeout should NOT be cached, but got instant response in ${duration}ms`, ); socket2.destroy(); @@ -343,7 +343,7 @@ function sendHttpsRequestThroughTunnel( socket, verb, url, - rejectUnauthorized = false + rejectUnauthorized = false, ) { return new Promise((resolve, reject) => { const tlsSocket = tls.connect( @@ -356,9 +356,9 @@ function sendHttpsRequestThroughTunnel( }, () => { tlsSocket.write( - `${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n` + `${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n`, ); - } + }, ); let tlsData = ""; @@ -404,7 +404,7 @@ function getTlsCertificateInfo(socket, url) { tlsSocket.end(); resolve({ issuer, subject }); - } + }, ); tlsSocket.on("error", (err) => { diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3f67e6f..fb08398 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -1,13 +1,6 @@ -import * as http from "http"; -import { tunnelRequest } from "./tunnelRequestHandler.js"; -import { mitmConnect } from "./mitmRequestHandler.js"; -import { handleHttpProxyRequest } from "./plainHttpProxy.js"; -import { getCombinedCaBundlePath } from "./certBundle.js"; import { ui } from "../environment/userInteraction.js"; -import chalk from "chalk"; -import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; -import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; -import { createRamaProxy, getRamaPath } from "./createRamaProxy.js"; +import { createRamaProxy, getRamaPath } from "./ramaProxy/createRamaProxy.js"; +import { createBuiltInProxyServer } from "./builtInProxy/createBuiltInProxyServer.js"; /** * @typedef {Object} SafeChainProxy @@ -77,143 +70,3 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { return proxyEnv; } - -/** @returns {SafeChainProxy} */ -function createBuiltInProxyServer() { - const SERVER_STOP_TIMEOUT_MS = 1000; - /** - * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} - */ - const state = { - port: null, - blockedRequests: [], - }; - - const server = http.createServer( - // This handles direct HTTP requests (non-CONNECT requests) - // This is normally http-only traffic, but we also handle - // https for clients that don't properly use CONNECT - handleHttpProxyRequest - ); - - // This handles HTTPS requests via the CONNECT method - server.on("connect", handleConnect); - - return { - startServer: () => startServer(server), - stopServer: () => stopServer(server), - verifyNoMaliciousPackages, - hasSuppressedVersions: getHasSuppressedVersions, - getServerPort: () => state.port, - getCombinedCaBundlePath, - }; - - /** - * @param {import("http").Server} server - * - * @returns {Promise} - */ - function startServer(server) { - return new Promise((resolve, reject) => { - // Passing port 0 makes the OS assign an available port - server.listen(0, () => { - const address = server.address(); - if (address && typeof address === "object") { - state.port = address.port; - resolve(); - } else { - reject(new Error("Failed to start proxy server")); - } - }); - - server.on("error", (err) => { - reject(err); - }); - }); - } - - /** - * @param {import("http").Server} server - * - * @returns {Promise} - */ - function stopServer(server) { - return new Promise((resolve) => { - try { - server.close(() => { - resolve(); - }); - } catch { - resolve(); - } - setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); - }); - } - - /** - * @param {import("http").IncomingMessage} req - * @param {import("http").ServerResponse} clientSocket - * @param {Buffer} head - * - * @returns {void} - */ - 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 interceptor = createInterceptorForUrl(req.url || ""); - - if (interceptor) { - // Subscribe to malware blocked events - interceptor.on( - "malwareBlocked", - ( - /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event - ) => { - onMalwareBlocked(event.packageName, event.version, event.targetUrl); - } - ); - - mitmConnect(req, clientSocket, interceptor); - } else { - // For other hosts, just tunnel the request to the destination tcp socket - ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); - tunnelRequest(req, clientSocket, head); - } - } - - /** - * - * @param {string} packageName - * @param {string} version - * @param {string} url - */ - function onMalwareBlocked(packageName, version, url) { - state.blockedRequests.push({ packageName, version, url }); - } - - function verifyNoMaliciousPackages() { - if (state.blockedRequests.length === 0) { - // No malicious packages were blocked, so nothing to block - return true; - } - - ui.emptyLine(); - - ui.writeInformation( - `Safe-chain: ${chalk.bold( - `blocked ${state.blockedRequests.length} malicious package downloads` - )}:` - ); - - for (const req of state.blockedRequests) { - ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); - } - - ui.emptyLine(); - ui.writeExitWithoutInstallingMaliciousPackages(); - ui.emptyLine(); - - return false; - } -} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index 407aa3c..4ac9fed 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -7,7 +7,7 @@ import { createSafeChainProxy, mergeSafeChainProxyEnvironmentVariables, } from "./registryProxy.js"; -import { getCaCertPath } from "./certUtils.js"; +import { getCaCertPath } from "./builtInProxy/certUtils.js"; import { setEcoSystem, ECOSYSTEM_JS, From 7170f9af61ec32a2d886f102029e2569c9f3f884 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 11 Feb 2026 16:19:02 +0100 Subject: [PATCH 92/98] Fix typecheck --- .../registryProxy/createBuiltInProxyServer.js | 150 ------------------ 1 file changed, 150 deletions(-) delete mode 100644 packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js diff --git a/packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js b/packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js deleted file mode 100644 index 7703978..0000000 --- a/packages/safe-chain/src/registryProxy/createBuiltInProxyServer.js +++ /dev/null @@ -1,150 +0,0 @@ -import * as http from "http"; -import { tunnelRequest } from "./tunnelRequestHandler.js"; -import { mitmConnect } from "./mitmRequestHandler.js"; -import { handleHttpProxyRequest } from "./plainHttpProxy.js"; -import { getCombinedCaBundlePath } from "./builtInProxy/certBundle.js"; -import { ui } from "../environment/userInteraction.js"; -import chalk from "chalk"; -import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; -import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; - -/** * - * @returns {import("./registryProxy.js").SafeChainProxy} */ -export function createBuiltInProxyServer() { - const SERVER_STOP_TIMEOUT_MS = 1000; - /** - * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} - */ - const state = { - port: null, - blockedRequests: [], - }; - - const server = http.createServer( - // This handles direct HTTP requests (non-CONNECT requests) - // This is normally http-only traffic, but we also handle - // https for clients that don't properly use CONNECT - handleHttpProxyRequest, - ); - - // This handles HTTPS requests via the CONNECT method - server.on("connect", handleConnect); - - return { - startServer: () => startServer(server), - stopServer: () => stopServer(server), - verifyNoMaliciousPackages, - hasSuppressedVersions: getHasSuppressedVersions, - getServerPort: () => state.port, - getCombinedCaBundlePath, - }; - - /** - * @param {import("http").Server} server - * - * @returns {Promise} - */ - function startServer(server) { - return new Promise((resolve, reject) => { - // Passing port 0 makes the OS assign an available port - server.listen(0, () => { - const address = server.address(); - if (address && typeof address === "object") { - state.port = address.port; - resolve(); - } else { - reject(new Error("Failed to start proxy server")); - } - }); - - server.on("error", (err) => { - reject(err); - }); - }); - } - - /** - * @param {import("http").Server} server - * - * @returns {Promise} - */ - function stopServer(server) { - return new Promise((resolve) => { - try { - server.close(() => { - resolve(); - }); - } catch { - resolve(); - } - setTimeout(() => resolve(), SERVER_STOP_TIMEOUT_MS); - }); - } - - /** - * @param {import("http").IncomingMessage} req - * @param {import("http").ServerResponse} clientSocket - * @param {Buffer} head - * - * @returns {void} - */ - 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 interceptor = createInterceptorForUrl(req.url || ""); - - if (interceptor) { - // Subscribe to malware blocked events - interceptor.on( - "malwareBlocked", - ( - /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event, - ) => { - onMalwareBlocked(event.packageName, event.version, event.targetUrl); - }, - ); - - mitmConnect(req, clientSocket, interceptor); - } else { - // For other hosts, just tunnel the request to the destination tcp socket - ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); - tunnelRequest(req, clientSocket, head); - } - } - - /** - * - * @param {string} packageName - * @param {string} version - * @param {string} url - */ - function onMalwareBlocked(packageName, version, url) { - state.blockedRequests.push({ packageName, version, url }); - } - - function verifyNoMaliciousPackages() { - if (state.blockedRequests.length === 0) { - // No malicious packages were blocked, so nothing to block - return true; - } - - ui.emptyLine(); - - ui.writeInformation( - `Safe-chain: ${chalk.bold( - `blocked ${state.blockedRequests.length} malicious package downloads`, - )}:`, - ); - - for (const req of state.blockedRequests) { - ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`); - } - - ui.emptyLine(); - ui.writeExitWithoutInstallingMaliciousPackages(); - ui.emptyLine(); - - return false; - } -} From ba604eaeaa0cffc4d7805b630f22b83f1ef02a78 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 12 Feb 2026 10:18:37 +0100 Subject: [PATCH 93/98] Use CA bundle when using rama proxy --- .../src/packagemanager/pip/runPipCommand.js | 8 +- .../packagemanager/pip/runPipCommand.spec.js | 8 +- .../src/packagemanager/pipx/runPipXCommand.js | 8 +- .../pipx/runPipXCommand.spec.js | 8 +- .../poetry/createPoetryPackageManager.js | 8 +- .../src/packagemanager/uv/runUvCommand.js | 8 +- .../builtInProxy/certBundle.spec.js | 379 ------------------ .../builtInProxy/createBuiltInProxyServer.js | 14 +- .../{builtInProxy => }/certBundle.js | 17 +- .../src/registryProxy/certBundle.spec.js | 180 +++++++++ .../ramaProxy/createRamaProxy.js | 13 +- .../src/registryProxy/registryProxy.js | 37 +- 12 files changed, 267 insertions(+), 421 deletions(-) delete mode 100644 packages/safe-chain/src/registryProxy/builtInProxy/certBundle.spec.js rename packages/safe-chain/src/registryProxy/{builtInProxy => }/certBundle.js (91%) create mode 100644 packages/safe-chain/src/registryProxy/certBundle.spec.js diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 425eca4..226e814 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,7 +1,9 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; -import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; +import { + getProxySettings, + mergeSafeChainProxyEnvironmentVariables, +} from "../../registryProxy/registryProxy.js"; import { PIP_COMMAND, PIP3_COMMAND, @@ -118,7 +120,7 @@ export async function runPip(command, args) { // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs) // so that any network request made by pip, including those outside explicit CLI args, // validates correctly under both MITM'd and tunneled HTTPS. - const combinedCaPath = getCombinedCaBundlePath(); + const combinedCaPath = getProxySettings().caCertBundlePath; // Commands that need access to persistent config/cache/state files // These should not have PIP_CONFIG_FILE overridden as it would prevent them from diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index b256725..24d5ca6 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -48,11 +48,17 @@ describe("runPipCommand environment variable handling", () => { HTTPS_PROXY: "http://localhost:8080", HTTP_PROXY: "", }), + getProxySettings: () => { + return { + proxyUrl: "http://localhost:8080", + caCertBundlePath: "/tmp/test-combined-ca.pem", + }; + }, }, }); // Mock certBundle to return a test combined bundle path - mock.module("../../registryProxy/builtInProxy/certBundle.js", { + mock.module("../../registryProxy/certBundle.js", { namedExports: { getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem", }, diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js index 73a6384..e1554cb 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js @@ -1,7 +1,9 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; -import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; +import { + getProxySettings, + mergeSafeChainProxyEnvironmentVariables, +} from "../../registryProxy/registryProxy.js"; /** * Sets CA bundle environment variables used by Python libraries and pipx. @@ -47,7 +49,7 @@ export async function runPipX(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - const combinedCaPath = getCombinedCaBundlePath(); + const combinedCaPath = getProxySettings().caCertBundlePath; const modifiedEnv = getPipXCaBundleEnvironmentVariables( env, combinedCaPath, diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js index 4b92bd1..56d75f9 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js @@ -38,10 +38,16 @@ describe("runPipXCommand", () => { mergeCalls.push(env); return { ...env, ...mergedEnvReturn }; }, + getProxySettings: () => { + return { + proxyUrl: "", + caCertBundlePath: "/tmp/test-combined-ca.pem", + }; + }, }, }); - mock.module("../../registryProxy/builtInProxy/certBundle.js", { + mock.module("../../registryProxy/certBundle.js", { namedExports: { getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem", }, diff --git a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js index 72741d3..956fb05 100644 --- a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js +++ b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js @@ -1,7 +1,9 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; -import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; +import { + getProxySettings, + mergeSafeChainProxyEnvironmentVariables, +} from "../../registryProxy/registryProxy.js"; /** * @returns {import("../currentPackageManager.js").PackageManager} @@ -62,7 +64,7 @@ async function runPoetryCommand(args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - const combinedCaPath = getCombinedCaBundlePath(); + const combinedCaPath = getProxySettings().caCertBundlePath; setPoetryCaBundleEnvironmentVariables(env, combinedCaPath); const result = await safeSpawn("poetry", args, { diff --git a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js index 5d6bd4f..e44922f 100644 --- a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js +++ b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js @@ -1,7 +1,9 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; -import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "../../registryProxy/builtInProxy/certBundle.js"; +import { + getProxySettings, + mergeSafeChainProxyEnvironmentVariables, +} from "../../registryProxy/registryProxy.js"; /** * Sets CA bundle environment variables used by Python libraries and uv. @@ -53,7 +55,7 @@ export async function runUv(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - const combinedCaPath = getCombinedCaBundlePath(); + const combinedCaPath = getProxySettings().caCertBundlePath; setUvCaBundleEnvironmentVariables(env, combinedCaPath); // Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.spec.js deleted file mode 100644 index e3b58fb..0000000 --- a/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.spec.js +++ /dev/null @@ -1,379 +0,0 @@ -import { describe, it, beforeEach, mock } from "node:test"; -import assert from "node:assert"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import tls from "node:tls"; - -// Utility to remove the generated bundle so the module rebuilds it on demand -function removeBundleIfExists() { - const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem"); - try { - if (fs.existsSync(target)) fs.unlinkSync(target); - } catch { - // ignore - } -} - -// Utility to get a valid PEM certificate for testing -function getValidCert() { - const cert = typeof tls.rootCertificates?.[0] === "string" ? tls.rootCertificates[0] : ""; - assert.ok(cert.includes("BEGIN CERTIFICATE"), "Environment lacks Node root certificates for test"); - return cert; -} - -describe("certBundle.getCombinedCaBundlePath", () => { - beforeEach(() => { - mock.restoreAll(); - removeBundleIfExists(); - }); - - it("includes Safe Chain CA when parsable and produces a PEM bundle", async () => { - // Prepare a temporary Safe Chain CA file with a recognizable marker and a valid cert block - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-")); - const safeChainPath = path.join(tmpDir, "safechain-ca.pem"); - const marker = "# SAFE_CHAIN_TEST_MARKER"; - const rootPem = typeof tls.rootCertificates?.[0] === "string" ? tls.rootCertificates[0] : ""; - assert.ok(rootPem.includes("BEGIN CERTIFICATE"), "Environment lacks Node root certificates for test"); - fs.writeFileSync(safeChainPath, `${marker}\n${rootPem}`, "utf8"); - - // Mock the certUtils.getCaCertPath to return our temp file - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - assert.match(contents, /-----BEGIN CERTIFICATE-----/); - assert.ok(contents.includes(marker), "Bundle should include Safe Chain CA content when parsable"); - }); - - it("ignores invalid Safe Chain CA but still builds from other sources", async () => { - // Write an invalid file (no cert blocks) - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-")); - const safeChainPath = path.join(tmpDir, "safechain-invalid.pem"); - const invalidMarker = "INVALID_SAFE_CHAIN_CONTENT"; - fs.writeFileSync(safeChainPath, invalidMarker, "utf8"); - - // Mock the certUtils.getCaCertPath to return our invalid file - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - // Ensure fresh build - removeBundleIfExists(); - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Bundle should contain certificate blocks from certifi/Node roots"); - assert.ok(!contents.includes(invalidMarker), "Bundle should not include invalid Safe Chain content"); - }); -}); - -describe("certBundle.getCombinedCaBundlePath with user certs", () => { - beforeEach(() => { - mock.restoreAll(); - delete process.env.NODE_EXTRA_CA_CERTS; - }); - - it("returns a path with full CA bundle (Safe Chain + Mozilla + Node roots) when no user cert in env", async () => { - // Mock getCaCertPath to return valid cert - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain certificate blocks"); - // Should include base bundle (Safe Chain + Mozilla/Node roots) - assert.ok(contents.length > 1000, "Bundle should be substantial with Mozilla/Node roots included"); - }); - - it("merges user cert with full base bundle (Safe Chain CA + Mozilla + Node roots)", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - - // Create Safe Chain CA - const safeChainPath = path.join(tmpDir, "safechain.pem"); - const safeChainCert = getValidCert(); - fs.writeFileSync(safeChainPath, safeChainCert, "utf8"); - - // Create user cert file - const userCertPath = path.join(tmpDir, "user-cert.pem"); - const userCert = getValidCert(); - fs.writeFileSync(userCertPath, userCert, "utf8"); - process.env.NODE_EXTRA_CA_CERTS = userCertPath; - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - - // Both certs should be in the bundle - const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length; - assert.ok(certCount >= 2, "Bundle should contain both Safe Chain and user certificates"); - }); - - it("ignores non-existent user cert path", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - process.env.NODE_EXTRA_CA_CERTS = "/nonexistent/path.pem"; - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - // Should still have Safe Chain CA - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - }); - - it("ignores invalid PEM user cert", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - const userCertPath = path.join(tmpDir, "invalid.pem"); - fs.writeFileSync(userCertPath, "NOT A VALID PEM", "utf8"); - process.env.NODE_EXTRA_CA_CERTS = userCertPath; - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - // Should still have Safe Chain CA only - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - assert.ok(!contents.includes("NOT A VALID"), "Should not include invalid cert"); - }); - - it("rejects user cert with path traversal attempts", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - process.env.NODE_EXTRA_CA_CERTS = "../../../etc/passwd"; - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - // Should only have Safe Chain CA, rejected the traversal path - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - }); - - it("rejects user cert with symlink", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - // Create a target file and a symlink to it - const targetCert = path.join(tmpDir, "target.pem"); - fs.writeFileSync(targetCert, getValidCert(), "utf8"); - - const symlinkPath = path.join(tmpDir, "symlink.pem"); - try { - fs.symlinkSync(targetCert, symlinkPath); - } catch { - // Skip test if symlinks are not supported (e.g., on Windows without admin) - return; - } - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - process.env.NODE_EXTRA_CA_CERTS = symlinkPath; - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - // Should only have Safe Chain CA, symlinks are rejected - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - }); - - it("rejects user cert that is a directory", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - const certDir = path.join(tmpDir, "certs"); - fs.mkdirSync(certDir); - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - process.env.NODE_EXTRA_CA_CERTS = certDir; - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - // Should only have Safe Chain CA - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - }); - - it("handles empty string user cert path", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - process.env.NODE_EXTRA_CA_CERTS = " "; - const bundlePath = getCombinedCaBundlePath(); - - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - }); - - it("accepts files with CRLF line endings (Windows-style)", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - // Create a real file with CRLF content to test Windows line ending support - const userCertPath = path.join(tmpDir, "user-cert-crlf.pem"); - const userCert = getValidCert(); - const certWithCRLF = userCert.replace(/\n/g, "\r\n"); - fs.writeFileSync(userCertPath, certWithCRLF, "utf8"); - process.env.NODE_EXTRA_CA_CERTS = userCertPath; - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - const bundlePath = getCombinedCaBundlePath(); - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length; - assert.ok(certCount >= 2, "Bundle should contain Safe Chain and user certificates with CRLF"); - }); - - it("detects and handles Windows-style path syntax (drive letters and UNC)", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - - // Test that Windows path syntax is recognized (even if files don't exist on macOS/Linux) - // These should gracefully fail (return Safe Chain CA only) rather than crash - const winPaths = [ - "C:\\temp\\cert.pem", - "D:\\Users\\name\\certs\\ca.pem", - "\\\\server\\share\\cert.pem" - ]; - - for (const winPath of winPaths) { - process.env.NODE_EXTRA_CA_CERTS = winPath; - const bundlePath = getCombinedCaBundlePath(); - assert.ok(fs.existsSync(bundlePath), `Bundle should exist for ${winPath}`); - const contents = fs.readFileSync(bundlePath, "utf8"); - assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Should contain Safe Chain CA"); - } - }); - - it("rejects path traversal with Windows-style paths (C:\\temp\\..\\etc\\passwd)", async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); - const safeChainPath = path.join(tmpDir, "safechain.pem"); - fs.writeFileSync(safeChainPath, getValidCert(), "utf8"); - - mock.module("./certUtils.js", { - namedExports: { - getCaCertPath: () => safeChainPath, - }, - }); - - const { getCombinedCaBundlePath } = await import("./certBundle.js"); - - // Test various Windows-style traversal attempts - const traversalPaths = [ - "C:\\temp\\..\\etc\\passwd", - "D:\\Users\\..\\..\\Windows\\System32", - "\\\\server\\share\\..\\admin", - "../../../etc/passwd", // Unix-style for comparison - ]; - - // First, get baseline bundle without user certs to know expected cert count - delete process.env.NODE_EXTRA_CA_CERTS; - const baselineBundlePath = getCombinedCaBundlePath(); - const baselineContents = fs.readFileSync(baselineBundlePath, "utf8"); - const baselineCertCount = (baselineContents.match(/-----BEGIN CERTIFICATE-----/g) || []).length; - - for (const badPath of traversalPaths) { - process.env.NODE_EXTRA_CA_CERTS = badPath; - const bundlePath = getCombinedCaBundlePath(); - assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); - const contents = fs.readFileSync(bundlePath, "utf8"); - // Should contain base bundle (Safe Chain + Mozilla + Node roots) but NOT user cert - const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []).length; - assert.strictEqual(certCount, baselineCertCount, `Traversal path ${badPath} should be rejected; base bundle only (no user cert added)`); - } - }); -}); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js b/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js index f6b5d62..22cd394 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/createBuiltInProxyServer.js @@ -2,11 +2,12 @@ import * as http from "http"; import { tunnelRequest } from "./tunnelRequestHandler.js"; import { mitmConnect } from "./mitmRequestHandler.js"; import { handleHttpProxyRequest } from "./plainHttpProxy.js"; -import { getCombinedCaBundlePath } from "./certBundle.js"; import { ui } from "../../environment/userInteraction.js"; import chalk from "chalk"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; +import { getCaCertPath } from "./certUtils.js"; +import { readFileSync } from "fs"; /** * * @returns {import("../registryProxy.js").SafeChainProxy} */ @@ -36,7 +37,7 @@ export function createBuiltInProxyServer() { verifyNoMaliciousPackages, hasSuppressedVersions: getHasSuppressedVersions, getServerPort: () => state.port, - getCombinedCaBundlePath, + getCaCert, }; /** @@ -147,4 +148,13 @@ export function createBuiltInProxyServer() { return false; } + + function getCaCert() { + try { + const safeChainPath = getCaCertPath(); + return readFileSync(safeChainPath, "utf8"); + } catch { + return null; + } + } } diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js similarity index 91% rename from packages/safe-chain/src/registryProxy/builtInProxy/certBundle.js rename to packages/safe-chain/src/registryProxy/certBundle.js index 06aaf23..b42042a 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -5,8 +5,7 @@ import path from "node:path"; import certifi from "certifi"; import tls from "node:tls"; import { X509Certificate } from "node:crypto"; -import { getCaCertPath } from "./certUtils.js"; -import { ui } from "../../environment/userInteraction.js"; +import { ui } from "../environment/userInteraction.js"; /** * Check if a PEM string contains only parsable cert blocks. @@ -50,20 +49,14 @@ function isParsable(pem) { * - Mozilla roots via certifi (for public HTTPS) * - Node's built-in root certificates (fallback) * - User's custom certificates (if NODE_EXTRA_CA_CERTS environment variable is set) + * @param {string | null} proxyCaCert * * @returns {string} Path to the combined CA bundle PEM file */ -export function getCombinedCaBundlePath() { - const parts = []; - +export function getCombinedCaBundlePath(proxyCaCert) { // 1) Safe Chain CA (for MITM'd registries) - const safeChainPath = getCaCertPath(); - try { - const safeChainPem = fs.readFileSync(safeChainPath, "utf8"); - if (isParsable(safeChainPem)) parts.push(safeChainPem.trim()); - } catch { - // Ignore if Safe Chain CA is not available - } + const parts = []; + if (proxyCaCert && isParsable(proxyCaCert)) parts.push(proxyCaCert.trim()); // 2) certifi (Mozilla CA bundle for all public HTTPS) try { diff --git a/packages/safe-chain/src/registryProxy/certBundle.spec.js b/packages/safe-chain/src/registryProxy/certBundle.spec.js new file mode 100644 index 0000000..3287554 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/certBundle.spec.js @@ -0,0 +1,180 @@ +import { describe, it, beforeEach, mock } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import tls from "node:tls"; + +// Utility to remove the generated bundle so the module rebuilds it on demand +function removeBundleIfExists() { + const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem"); + try { + if (fs.existsSync(target)) fs.unlinkSync(target); + } catch { + // ignore + } +} + +// Utility to get a valid PEM certificate for testing +function getValidCert() { + const cert = + typeof tls.rootCertificates?.[0] === "string" + ? tls.rootCertificates[0] + : ""; + assert.ok( + cert.includes("BEGIN CERTIFICATE"), + "Environment lacks Node root certificates for test", + ); + return cert; +} + +describe("certBundle.getCombinedCaBundlePath", () => { + beforeEach(() => { + mock.restoreAll(); + removeBundleIfExists(); + }); + + it("includes Safe Chain CA when parsable and produces a PEM bundle", async () => { + // Prepare a temporary Safe Chain CA file with a recognizable marker and a valid cert block + const marker = "# SAFE_CHAIN_TEST_MARKER"; + const rootPem = + typeof tls.rootCertificates?.[0] === "string" + ? tls.rootCertificates[0] + : ""; + assert.ok( + rootPem.includes("BEGIN CERTIFICATE"), + "Environment lacks Node root certificates for test", + ); + + const { getCombinedCaBundlePath } = await import("./certBundle.js"); + const bundlePath = getCombinedCaBundlePath(`${marker}\n${rootPem}`); + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + assert.match(contents, /-----BEGIN CERTIFICATE-----/); + assert.ok( + contents.includes(marker), + "Bundle should include Safe Chain CA content when parsable", + ); + }); + + it("ignores invalid Safe Chain CA but still builds from other sources", async () => { + // Write an invalid file (no cert blocks) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-")); + const safeChainPath = path.join(tmpDir, "safechain-invalid.pem"); + const invalidMarker = "INVALID_SAFE_CHAIN_CONTENT"; + fs.writeFileSync(safeChainPath, invalidMarker, "utf8"); + + // Ensure fresh build + removeBundleIfExists(); + const { getCombinedCaBundlePath } = await import("./certBundle.js"); + const bundlePath = getCombinedCaBundlePath(invalidMarker); + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + assert.match( + contents, + /-----BEGIN CERTIFICATE-----/, + "Bundle should contain certificate blocks from certifi/Node roots", + ); + assert.ok( + !contents.includes(invalidMarker), + "Bundle should not include invalid Safe Chain content", + ); + }); +}); + +describe("certBundle.getCombinedCaBundlePath with user certs", () => { + beforeEach(() => { + mock.restoreAll(); + delete process.env.NODE_EXTRA_CA_CERTS; + }); + + it("returns a path with full CA bundle (Safe Chain + Mozilla + Node roots) when no user cert in env", async () => { + const { getCombinedCaBundlePath } = await import("./certBundle.js"); + const bundlePath = getCombinedCaBundlePath(getValidCert()); + + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + assert.match( + contents, + /-----BEGIN CERTIFICATE-----/, + "Should contain certificate blocks", + ); + // Should include base bundle (Safe Chain + Mozilla/Node roots) + assert.ok( + contents.length > 1000, + "Bundle should be substantial with Mozilla/Node roots included", + ); + }); + + it("merges user cert with full base bundle (Safe Chain CA + Mozilla + Node roots)", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); + + // Create Safe Chain CA + const safeChainCert = getValidCert(); + + // Create user cert file + const userCertPath = path.join(tmpDir, "user-cert.pem"); + const userCert = getValidCert(); + fs.writeFileSync(userCertPath, userCert, "utf8"); + process.env.NODE_EXTRA_CA_CERTS = userCertPath; + + const { getCombinedCaBundlePath } = await import("./certBundle.js"); + const bundlePath = getCombinedCaBundlePath(safeChainCert); + + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + + // Both certs should be in the bundle + const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []) + .length; + assert.ok( + certCount >= 2, + "Bundle should contain both Safe Chain and user certificates", + ); + }); + + it("ignores invalid PEM user cert", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); + + const userCertPath = path.join(tmpDir, "invalid.pem"); + fs.writeFileSync(userCertPath, "NOT A VALID PEM", "utf8"); + process.env.NODE_EXTRA_CA_CERTS = userCertPath; + + const { getCombinedCaBundlePath } = await import("./certBundle.js"); + const bundlePath = getCombinedCaBundlePath(getValidCert()); + + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + // Should still have Safe Chain CA only + assert.match( + contents, + /-----BEGIN CERTIFICATE-----/, + "Should contain Safe Chain CA", + ); + assert.ok( + !contents.includes("NOT A VALID"), + "Should not include invalid cert", + ); + }); + + it("accepts files with CRLF line endings (Windows-style)", async () => { + // Create a real file with CRLF content to test Windows line ending support + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "certtest-")); + const userCertPath = path.join(tmpDir, "user-cert-crlf.pem"); + const userCert = getValidCert(); + const certWithCRLF = userCert.replace(/\n/g, "\r\n"); + fs.writeFileSync(userCertPath, certWithCRLF, "utf8"); + process.env.NODE_EXTRA_CA_CERTS = userCertPath; + + const { getCombinedCaBundlePath } = await import("./certBundle.js"); + const bundlePath = getCombinedCaBundlePath(getValidCert()); + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + const certCount = (contents.match(/-----BEGIN CERTIFICATE-----/g) || []) + .length; + assert.ok( + certCount >= 2, + "Bundle should contain Safe Chain and user certificates with CRLF", + ); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js index 42b6549..3c7b50a 100644 --- a/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js +++ b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; -import { mkdtempSync, readFile, writeFile } from "node:fs"; +import { mkdtempSync, readFile } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; @@ -8,14 +8,13 @@ import { ui } from "../../environment/userInteraction.js"; import { getLoggingLevel, LOGGING_VERBOSE } from "../../config/settings.js"; const readFilePromise = promisify(readFile); -const writeFilePromise = promisify(writeFile); /** * @typedef {Object} RamaProxyInstance * @property {import("node:child_process").ChildProcess} process * @property {string} proxyAddress * @property {string} metaAddress - * @property {string} certPath + * @property {string} caCert */ /** @@ -61,7 +60,7 @@ export function createRamaProxy(ramaPath) { const url = new URL(`http://${ramaInstance.proxyAddress}`); return url.port ? parseInt(url.port, 10) : null; }, - getCombinedCaBundlePath: () => ramaInstance?.certPath ?? "", + getCaCert: () => ramaInstance?.caCert ?? null, }; } @@ -102,14 +101,12 @@ async function startRama(ramaPath, dataFolder) { ); const certResponse = await fetch(`http://${metaAddress}/ca`); - const cert = await certResponse.text(); - const certPath = join(dataFolder, "cert.ca"); - await writeFilePromise(certPath, cert); + const caCert = await certResponse.text(); return { process, proxyAddress, metaAddress, - certPath, + caCert, }; } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index fb08398..4b7ef82 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -1,6 +1,7 @@ import { ui } from "../environment/userInteraction.js"; import { createRamaProxy, getRamaPath } from "./ramaProxy/createRamaProxy.js"; import { createBuiltInProxyServer } from "./builtInProxy/createBuiltInProxyServer.js"; +import { getCombinedCaBundlePath } from "./certBundle.js"; /** * @typedef {Object} SafeChainProxy @@ -9,7 +10,11 @@ import { createBuiltInProxyServer } from "./builtInProxy/createBuiltInProxyServe * @prop {() => boolean} verifyNoMaliciousPackages * @prop {() => boolean} hasSuppressedVersions * @prop {() => Number | null} getServerPort - * @prop {() => string} getCombinedCaBundlePath + * @prop {() => string | null} getCaCert + * + * @typedef {Object} ProxySettings + * @prop {string | null} proxyUrl + * @prop {string} caCertBundlePath */ /** @type {SafeChainProxy} */ @@ -31,6 +36,27 @@ export function createSafeChainProxy() { return server; } +/** + * @returns {ProxySettings} + */ +export function getProxySettings() { + if (!server || !server.getServerPort()) { + return { + proxyUrl: null, + caCertBundlePath: getCombinedCaBundlePath(null), + }; + } + + const proxyUrl = `http://localhost:${server.getServerPort()}`; + const caCert = server.getCaCert(); + const caCertBundlePath = getCombinedCaBundlePath(caCert); + + return { + proxyUrl, + caCertBundlePath, + }; +} + /** * @returns {Record} */ @@ -39,13 +65,12 @@ function getSafeChainProxyEnvironmentVariables() { return {}; } - const proxyUrl = `http://localhost:${server.getServerPort()}`; - const caCertPath = server.getCombinedCaBundlePath(); + const proxySettings = getProxySettings(); return { - HTTPS_PROXY: proxyUrl, - GLOBAL_AGENT_HTTP_PROXY: proxyUrl, - NODE_EXTRA_CA_CERTS: caCertPath, + HTTPS_PROXY: proxySettings.proxyUrl ?? "", + GLOBAL_AGENT_HTTP_PROXY: proxySettings.proxyUrl ?? "", + NODE_EXTRA_CA_CERTS: proxySettings.caCertBundlePath, }; } From 0fdfbf6ba995e16e1710f174b60ebc8b16d975c9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 27 Feb 2026 15:38:56 +0100 Subject: [PATCH 94/98] Fix readme duplication --- README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.md b/README.md index 697bab1..f23a0ae 100644 --- a/README.md +++ b/README.md @@ -226,22 +226,6 @@ export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*" } ``` -### Excluding Packages - -Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged). Use `@scope/*` to trust all packages from an organization: - -```shell -export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*" -``` - -```json -{ - "npm": { - "minimumPackageAgeExclusions": ["@aikidosec/*"] - } -} -``` - ## Custom Registries Configure Safe Chain to scan packages from custom or private registries. From e8a4fbcd762c0ab308d63e9be516f76db0a50fa4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 2 Mar 2026 15:54:41 +0100 Subject: [PATCH 95/98] Cleanup formatting changes from PR --- README.md | 3 +- .../src/packagemanager/pip/runPipCommand.js | 64 ++--- .../packagemanager/pip/runPipCommand.spec.js | 268 +++++------------- .../src/packagemanager/pipx/runPipXCommand.js | 21 +- .../pipx/runPipXCommand.spec.js | 6 +- .../poetry/createPoetryPackageManager.js | 20 +- .../src/packagemanager/uv/runUvCommand.js | 20 +- .../interceptors/npm/modifyNpmInfo.js | 23 +- .../interceptors/npm/npmInterceptor.js | 4 +- .../npm/npmInterceptor.minPackageAge.spec.js | 44 ++- .../npmInterceptor.packageDownload.spec.js | 12 +- .../interceptors/pipInterceptor.js | 13 +- ...pipInterceptor.pipCustomRegistries.spec.js | 25 +- .../interceptors/pipInterceptor.spec.js | 8 +- .../builtInProxy/mitmRequestHandler.js | 14 +- .../builtInProxy/plainHttpProxy.js | 2 +- .../builtInProxy/tunnelRequestHandler.js | 18 +- .../src/registryProxy/certBundle.js | 14 +- .../registryProxy.connect-tunnel.spec.js | 28 +- 19 files changed, 200 insertions(+), 407 deletions(-) diff --git a/README.md b/README.md index f23a0ae..d5270e5 100644 --- a/README.md +++ b/README.md @@ -385,8 +385,7 @@ steps: - step: name: Install script: - - npm install -g @aikidosec/safe-chain - - safe-chain setup-ci + - curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - export PATH=~/.safe-chain/shims:$PATH - npm ci ``` diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 226e814..52a8691 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -4,12 +4,7 @@ import { getProxySettings, mergeSafeChainProxyEnvironmentVariables, } from "../../registryProxy/registryProxy.js"; -import { - PIP_COMMAND, - PIP3_COMMAND, - PYTHON_COMMAND, - PYTHON3_COMMAND, -} from "./pipSettings.js"; +import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js"; import fs from "node:fs/promises"; import fsSync from "node:fs"; import os from "node:os"; @@ -27,11 +22,7 @@ import { spawn } from "child_process"; export function shouldBypassSafeChain(command, args) { if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) { // Check if args start with -m pip - if ( - args.length >= 2 && - args[0] === "-m" && - (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND) - ) { + if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) { return false; } return true; @@ -43,49 +34,43 @@ export function shouldBypassSafeChain(command, args) { * Sets fallback CA bundle environment variables used by Python libraries. * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python * network libraries respect the combined CA bundle, even if they don't read pip's config. - * + * * @param {NodeJS.ProcessEnv} env - Environment object to modify * @param {string} combinedCaPath - Path to the combined CA bundle */ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { // REQUESTS_CA_BUNDLE: Used by the popular 'requests' library if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning( - "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); } env.REQUESTS_CA_BUNDLE = combinedCaPath; // SSL_CERT_FILE: Used by some Python SSL libraries and urllib if (env.SSL_CERT_FILE) { - ui.writeWarning( - "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); } env.SSL_CERT_FILE = combinedCaPath; // PIP_CERT: Pip's own environment variable for certificate verification if (env.PIP_CERT) { - ui.writeWarning( - "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); } env.PIP_CERT = combinedCaPath; } /** * Runs a pip command with safe-chain's certificate bundle and proxy configuration. - * + * * Creates a temporary pip config file to configure: * - Cert bundle for HTTPS verification * - Proxy settings - * + * * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges * their settings with safe-chain's, leaving the original file unchanged. - * + * * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow * users to read/write persistent config. Only CA environment variables are set for these commands. - * + * * @param {string} command - The pip command executable (e.g., 'pip3' or 'python3') * @param {string[]} args - Command line arguments to pass to pip * @returns {Promise<{status: number}>} Exit status of the pip command @@ -93,16 +78,12 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { export async function runPip(command, args) { // Check if we should bypass safe-chain (python/python3 without -m pip) if (shouldBypassSafeChain(command, args)) { - ui.writeVerbose( - `Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`, - ); + ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`); // Spawn the ORIGINAL command with ORIGINAL args return new Promise((_resolve) => { const proc = spawn(command, args, { stdio: "inherit" }); proc.on("exit", (/** @type {number | null} */ code) => { - ui.writeVerbose( - `${command} ${args.join(" ")} exited with status ${code}`, - ); + ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`); ui.writeBufferedLogsAndStopBuffering(); process.exit(code ?? 0); }); @@ -125,26 +106,22 @@ export async function runPip(command, args) { // Commands that need access to persistent config/cache/state files // These should not have PIP_CONFIG_FILE overridden as it would prevent them from // reading/writing to the user's actual pip configuration and cache directories - const configRelatedCommands = ["config", "cache", "debug", "completion"]; - const isConfigRelatedCommand = - args.length > 0 && configRelatedCommands.includes(args[0]); + const configRelatedCommands = ['config', 'cache', 'debug', 'completion']; + const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]); // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file) // will tell pip to use the provided CA bundle for HTTPS verification. // Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active), // otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables - const proxy = - env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ""; + const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); let cleanupConfigPath = null; // Track temp file for cleanup if (isConfigRelatedCommand) { - ui.writeVerbose( - `Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`, - ); + ui.writeVerbose( `Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`); // Still set the fallback CA bundle environment variables to avoid edge cases where a // plugin or extension triggers a network call during config introspection @@ -171,9 +148,7 @@ export async function runPip(command, args) { env.PIP_CONFIG_FILE = pipConfigPath; cleanupConfigPath = pipConfigPath; } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { - ui.writeVerbose( - "Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.", - ); + ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); const userConfig = env.PIP_CONFIG_FILE; // Read the existing config without modifying it @@ -185,9 +160,7 @@ export async function runPip(command, args) { // Cert if (typeof parsed.global.cert !== "undefined") { - ui.writeWarning( - "Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.", - ); + ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config."); } parsed.global.cert = combinedCaPath; @@ -207,6 +180,7 @@ export async function runPip(command, args) { await fs.writeFile(pipConfigPath, updated, "utf-8"); env.PIP_CONFIG_FILE = pipConfigPath; cleanupConfigPath = pipConfigPath; + } else { // The user provided PIP_CONFIG_FILE does not exist on disk // PIP will handle this as an error and inform the user diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 24d5ca6..235b472 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -74,12 +74,7 @@ describe("runPipCommand environment variable handling", () => { }); it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => { - const res = await runPip("pip3", [ - "config", - "set", - "global.index-url", - "https://test.pypi.org/simple", - ]); + const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]); assert.strictEqual(res.status, 0); assert.ok(capturedArgs, "safeSpawn should have been called"); @@ -87,24 +82,24 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip config commands", + "PIP_CONFIG_FILE should NOT be set for pip config commands" ); // But CA environment variables should still be set assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem", - "REQUESTS_CA_BUNDLE should still be set", + "REQUESTS_CA_BUNDLE should still be set" ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", - "SSL_CERT_FILE should still be set", + "SSL_CERT_FILE should still be set" ); assert.strictEqual( capturedArgs.options.env.PIP_CERT, "/tmp/test-combined-ca.pem", - "PIP_CERT should still be set", + "PIP_CERT should still be set" ); }); @@ -116,7 +111,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip config get", + "PIP_CONFIG_FILE should NOT be set for pip config get" ); }); @@ -128,7 +123,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip config list", + "PIP_CONFIG_FILE should NOT be set for pip config list" ); }); @@ -140,14 +135,14 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip cache commands", + "PIP_CONFIG_FILE should NOT be set for pip cache commands" ); // CA env vars should still be set assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", - "SSL_CERT_FILE should still be set", + "SSL_CERT_FILE should still be set" ); }); @@ -159,7 +154,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip debug", + "PIP_CONFIG_FILE should NOT be set for pip debug" ); }); @@ -171,7 +166,7 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CONFIG_FILE, undefined, - "PIP_CONFIG_FILE should NOT be set for pip completion", + "PIP_CONFIG_FILE should NOT be set for pip completion" ); }); @@ -183,20 +178,13 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.PIP_CERT, "/tmp/test-combined-ca.pem", - "PIP_CERT should be set to combined bundle path", + "PIP_CERT should be set to combined bundle path" ); // Check PIP_CONFIG_FILE env var exists and is a non-empty string const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; assert.ok(configPath, "PIP_CONFIG_FILE should be set"); - assert.strictEqual( - typeof configPath, - "string", - "PIP_CONFIG_FILE should be a string", - ); - assert.ok( - configPath.length > 0, - "PIP_CONFIG_FILE should be a non-empty path", - ); + assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string"); + assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path"); }); it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => { @@ -209,12 +197,12 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem", - "REQUESTS_CA_BUNDLE should be set to combined bundle path", + "REQUESTS_CA_BUNDLE should be set to combined bundle path" ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", - "SSL_CERT_FILE should be set to combined bundle path", + "SSL_CERT_FILE should be set to combined bundle path" ); }); @@ -223,17 +211,17 @@ describe("runPipCommand environment variable handling", () => { "install", "certifi", "--index-url", - "https://test.pypi.org/simple", + "https://test.pypi.org/simple" ]); assert.strictEqual(res.status, 0); // Env vars should be set unconditionally assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, - "/tmp/test-combined-ca.pem", + "/tmp/test-combined-ca.pem" ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, - "/tmp/test-combined-ca.pem", + "/tmp/test-combined-ca.pem" ); }); @@ -245,11 +233,11 @@ describe("runPipCommand environment variable handling", () => { // Environment variables still set (pip CLI --cert takes precedence) assert.strictEqual( capturedArgs.options.env.REQUESTS_CA_BUNDLE, - "/tmp/test-combined-ca.pem", + "/tmp/test-combined-ca.pem" ); assert.strictEqual( capturedArgs.options.env.SSL_CERT_FILE, - "/tmp/test-combined-ca.pem", + "/tmp/test-combined-ca.pem" ); }); @@ -260,16 +248,13 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual( capturedArgs.options.env.HTTPS_PROXY, "http://localhost:8080", - "HTTPS_PROXY should be set by proxy merge", + "HTTPS_PROXY should be set by proxy merge" ); }); it("should create a new temp config when existing config exists (original file untouched)", async () => { const tmpDir = os.tmpdir(); - const userCfgPath = path.join( - tmpDir, - `safe-chain-test-pip-${Date.now()}.ini`, - ); + const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); const initial = "[global]\nindex-url = https://example.com/simple\n"; await fs.writeFile(userCfgPath, initial, "utf-8"); @@ -277,42 +262,19 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual( - newCfgPath, - userCfgPath, - "should point to a new temp config file", - ); + assert.notStrictEqual(newCfgPath, userCfgPath, "should point to a new temp config file"); // Original file unchanged const originalContent = await fs.readFile(userCfgPath, "utf-8"); const originalParsed = ini.parse(originalContent); - assert.strictEqual( - originalParsed.global.cert, - undefined, - "original file should not gain cert", - ); + assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); // New file has merged settings (read from captured content before cleanup) - assert.ok( - capturedConfigContent, - "config content should have been captured", - ); + assert.ok(capturedConfigContent, "config content should have been captured"); const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual( - newParsed.global.cert, - "/tmp/test-combined-ca.pem", - "new config should include cert", - ); - assert.strictEqual( - newParsed.global.proxy, - "http://localhost:8080", - "new config should include proxy from env", - ); - assert.strictEqual( - newParsed.global["index-url"], - "https://example.com/simple", - "index-url should be preserved", - ); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env"); + assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved"); customEnv = null; }); @@ -321,30 +283,24 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - assert.ok( - capturedConfigContent, - "config content should have been captured", - ); + assert.ok(capturedConfigContent, "config content should have been captured"); const parsed = ini.parse(capturedConfigContent); assert.ok(parsed.global, "[global] should exist after creation"); assert.strictEqual( parsed.global.proxy, "http://localhost:8080", - "proxy should be set from merged env", + "proxy should be set from merged env" ); assert.strictEqual( parsed.global.cert, "/tmp/test-combined-ca.pem", - "cert should be set during creation", + "cert should be set during creation" ); }); it("should create new temp config adding cert but preserving existing proxy (original file unchanged)", async () => { const tmpDir = os.tmpdir(); - const userCfgPath = path.join( - tmpDir, - `safe-chain-test-pip-${Date.now()}.ini`, - ); + const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); const initial = "[global]\nproxy = http://original:9999\n"; await fs.writeFile(userCfgPath, initial, "utf-8"); @@ -352,41 +308,18 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual( - newCfgPath, - userCfgPath, - "should use a new temp config file", - ); + assert.notStrictEqual(newCfgPath, userCfgPath, "should use a new temp config file"); // Original file unchanged const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); - assert.strictEqual( - originalParsed.global.cert, - undefined, - "original file should not gain cert", - ); - assert.strictEqual( - originalParsed.global.proxy, - "http://original:9999", - "original proxy remains", - ); + assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); + assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains"); // New file: cert and proxy always overwritten (read from captured content) - assert.ok( - capturedConfigContent, - "config content should have been captured", - ); + assert.ok(capturedConfigContent, "config content should have been captured"); const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual( - newParsed.global.cert, - "/tmp/test-combined-ca.pem", - "cert always overwritten in temp config", - ); - assert.strictEqual( - newParsed.global.proxy, - "http://localhost:8080", - "proxy always overwritten in temp config", - ); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); @@ -397,7 +330,7 @@ describe("runPipCommand environment variable handling", () => { "[global]", "cert = /path/to/existing.pem", "proxy = http://original:9999", - "", + "" ].join("\n"); await fs.writeFile(cfgPath, initialIni, "utf-8"); @@ -405,51 +338,25 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0, "execution should succeed"); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual( - newCfgPath, - cfgPath, - "should use a newly generated temp config file", - ); + assert.notStrictEqual(newCfgPath, cfgPath, "should use a newly generated temp config file"); // Original file stays untouched const originalContent = await fs.readFile(cfgPath, "utf-8"); const originalParsed = ini.parse(originalContent); - assert.strictEqual( - originalParsed.global.cert, - "/path/to/existing.pem", - "original cert preserved", - ); - assert.strictEqual( - originalParsed.global.proxy, - "http://original:9999", - "original proxy preserved", - ); + assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved"); + assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved"); - // New temp config: cert and proxy always overwritten (read from captured content) - assert.ok( - capturedConfigContent, - "config content should have been captured", - ); - const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual( - newParsed.global.cert, - "/tmp/test-combined-ca.pem", - "cert always overwritten in temp config", - ); - assert.strictEqual( - newParsed.global.proxy, - "http://localhost:8080", - "proxy always overwritten in temp config", - ); + // New temp config: cert and proxy always overwritten (read from captured content) + assert.ok(capturedConfigContent, "config content should have been captured"); + const newParsed = ini.parse(capturedConfigContent); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); it("should create new temp config preserving existing cert and adding missing proxy", async () => { const tmpDir = os.tmpdir(); - const userCfgPath = path.join( - tmpDir, - `safe-chain-test-pip-${Date.now()}.ini`, - ); + const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); const initial = "[global]\ncert = /path/to/existing.pem\n"; await fs.writeFile(userCfgPath, initial, "utf-8"); @@ -457,55 +364,29 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; - assert.notStrictEqual( - newCfgPath, - userCfgPath, - "should produce a new temp config file", - ); + assert.notStrictEqual(newCfgPath, userCfgPath, "should produce a new temp config file"); // Original remains unchanged const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); - assert.strictEqual( - originalParsed.global.cert, - "/path/to/existing.pem", - "original cert unchanged", - ); - assert.strictEqual( - originalParsed.global.proxy, - undefined, - "original proxy still missing", - ); + assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged"); + assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing"); - // New file: cert and proxy always overwritten (read from captured content) - assert.ok( - capturedConfigContent, - "config content should have been captured", - ); - const newParsed = ini.parse(capturedConfigContent); - assert.strictEqual( - newParsed.global.cert, - "/tmp/test-combined-ca.pem", - "cert always overwritten in temp config", - ); - assert.strictEqual( - newParsed.global.proxy, - "http://localhost:8080", - "proxy always overwritten in temp config", - ); + // New file: cert and proxy always overwritten (read from captured content) + assert.ok(capturedConfigContent, "config content should have been captured"); + const newParsed = ini.parse(capturedConfigContent); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); it("should log warnings when cert and proxy are already set in user config file", async () => { const tmpDir = os.tmpdir(); - const cfgPath = path.join( - tmpDir, - `safe-chain-test-pip-warn-${Date.now()}.ini`, - ); + const cfgPath = path.join(tmpDir, `safe-chain-test-pip-warn-${Date.now()}.ini`); const initialIni = [ "[global]", "cert = /user/cert.pem", "proxy = http://user-proxy:9999", - "", + "" ].join("\n"); await fs.writeFile(cfgPath, initialIni, "utf-8"); @@ -515,28 +396,16 @@ describe("runPipCommand environment variable handling", () => { let output = ""; const originalWrite = process.stdout.write; const originalError = process.stderr.write; - process.stdout.write = (chunk, ...args) => { - output += chunk; - return originalWrite.apply(process.stdout, [chunk, ...args]); - }; - process.stderr.write = (chunk, ...args) => { - output += chunk; - return originalError.apply(process.stderr, [chunk, ...args]); - }; + process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); }; + process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); }; await runPip("pip3", ["install", "requests"]); process.stdout.write = originalWrite; process.stderr.write = originalError; - assert.ok( - output.includes("cert found in PIP_CONFIG_FILE"), - "Should warn about cert overwrite in output", - ); - assert.ok( - output.includes("proxy found in PIP_CONFIG_FILE"), - "Should warn about proxy overwrite in output", - ); + assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output"); + assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output"); customEnv = null; }); @@ -547,18 +416,13 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true); assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true); - assert.strictEqual( - shouldBypassSafeChain("python", ["-m", "http.server"]), - true, - ); - assert.strictEqual( - shouldBypassSafeChain("python3", ["-m", "http.server"]), - true, - ); + assert.strictEqual(shouldBypassSafeChain("python", ["-m", "http.server"]), true); + assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "http.server"]), true); assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false); assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false); }); + }); diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js index e1554cb..5dc8df1 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js @@ -7,7 +7,7 @@ import { /** * Sets CA bundle environment variables used by Python libraries and pipx. - * + * * @param {NodeJS.ProcessEnv} env - Env object * @param {string} combinedCaPath - Path to the combined CA bundle * @return {NodeJS.ProcessEnv} Modified environment object @@ -16,23 +16,17 @@ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) { let retVal = { ...env }; if (env.SSL_CERT_FILE) { - ui.writeWarning( - "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); } retVal.SSL_CERT_FILE = combinedCaPath; if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning( - "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); } retVal.REQUESTS_CA_BUNDLE = combinedCaPath; if (env.PIP_CERT) { - ui.writeWarning( - "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); } retVal.PIP_CERT = combinedCaPath; return retVal; @@ -40,7 +34,7 @@ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) { /** * Runs a pipx command with safe-chain's certificate bundle and proxy configuration. - * + * * @param {string} command - The command to execute * @param {string[]} args - Command line arguments * @returns {Promise<{status: number}>} Exit status of the command @@ -50,10 +44,7 @@ export async function runPipX(command, args) { const env = mergeSafeChainProxyEnvironmentVariables(process.env); const combinedCaPath = getProxySettings().caCertBundlePath; - const modifiedEnv = getPipXCaBundleEnvironmentVariables( - env, - combinedCaPath, - ); + const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath); // Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration // These are already set by mergeSafeChainProxyEnvironmentVariables diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js index 56d75f9..a6c328d 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.spec.js @@ -71,11 +71,7 @@ describe("runPipXCommand", () => { const res = await runPipX("pipx", ["install", "ruff"]); assert.strictEqual(res.status, 0); - assert.strictEqual( - safeSpawnMock.mock.calls.length, - 1, - "safeSpawn should be called once", - ); + assert.strictEqual(safeSpawnMock.mock.calls.length, 1, "safeSpawn should be called once"); const [, , options] = safeSpawnMock.mock.calls[0].arguments; const env = options.env; diff --git a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js index 956fb05..631ff4e 100644 --- a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js +++ b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js @@ -21,42 +21,36 @@ export function createPoetryPackageManager() { /** * Sets CA bundle environment variables used by Poetry and Python libraries. * Poetry uses the Python requests library which respects these environment variables. - * + * * @param {NodeJS.ProcessEnv} env - Environment object to modify * @param {string} combinedCaPath - Path to the combined CA bundle */ function setPoetryCaBundleEnvironmentVariables(env, combinedCaPath) { // SSL_CERT_FILE: Used by Python SSL libraries and requests if (env.SSL_CERT_FILE) { - ui.writeWarning( - "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); } env.SSL_CERT_FILE = combinedCaPath; // REQUESTS_CA_BUNDLE: Used by the requests library (which Poetry uses) if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning( - "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); } env.REQUESTS_CA_BUNDLE = combinedCaPath; // PIP_CERT: Poetry may use pip internally if (env.PIP_CERT) { - ui.writeWarning( - "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); } env.PIP_CERT = combinedCaPath; } /** * Runs a poetry command with safe-chain's certificate bundle and proxy configuration. - * + * * Poetry respects standard HTTP_PROXY/HTTPS_PROXY environment variables through * the Python requests library. - * + * * @param {string[]} args - Command line arguments to pass to poetry * @returns {Promise<{status: number}>} Exit status of the poetry command */ @@ -71,7 +65,7 @@ async function runPoetryCommand(args) { stdio: "inherit", env, }); - + return { status: result.status }; } catch (/** @type any */ error) { if (error.status) { diff --git a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js index e44922f..2e5cc6f 100644 --- a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js +++ b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js @@ -7,46 +7,40 @@ import { /** * Sets CA bundle environment variables used by Python libraries and uv. - * + * * @param {NodeJS.ProcessEnv} env - Env object * @param {string} combinedCaPath - Path to the combined CA bundle */ function setUvCaBundleEnvironmentVariables(env, combinedCaPath) { // SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients if (env.SSL_CERT_FILE) { - ui.writeWarning( - "Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); } env.SSL_CERT_FILE = combinedCaPath; // REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally) if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning( - "Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); } env.REQUESTS_CA_BUNDLE = combinedCaPath; // PIP_CERT: Some underlying pip operations may respect this if (env.PIP_CERT) { - ui.writeWarning( - "Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.", - ); + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); } env.PIP_CERT = combinedCaPath; } /** * Runs a uv command with safe-chain's certificate bundle and proxy configuration. - * + * * uv respects standard environment variables for proxy and TLS configuration: * - HTTP_PROXY / HTTPS_PROXY: Proxy settings * - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification - * + * * Unlike pip (which requires a temporary config file for cert configuration), uv directly * honors environment variables, so no config/ini file is needed. - * + * * @param {string} command - The uv command to execute (typically 'uv') * @param {string[]} args - Command line arguments to pass to uv * @returns {Promise<{status: number}>} Exit status of the uv command diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js index cfa708c..ae9a72c 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/modifyNpmInfo.js @@ -71,20 +71,15 @@ export function modifyNpmInfoResponse(body, headers) { // Check if this package is excluded from minimum age filtering const packageName = bodyJson.name; const exclusions = getNpmMinimumPackageAgeExclusions(); - if ( - packageName && - exclusions.some((pattern) => - matchesExclusionPattern(packageName, pattern), - ) - ) { + if (packageName && exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern))) { ui.writeVerbose( - `Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).`, + `Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).` ); return body; } const cutOff = new Date( - new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000, + new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 ); const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; @@ -121,7 +116,7 @@ export function modifyNpmInfoResponse(body, headers) { return Buffer.from(JSON.stringify(bodyJson)); } catch (/** @type {any} */ err) { ui.writeVerbose( - `Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}`, + `Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}` ); return body; } @@ -137,7 +132,7 @@ function deleteVersionFromJson(json, version) { const packageName = typeof json?.name === "string" ? json.name : "(unknown)"; ui.writeVerbose( - `Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`, + `Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` ); delete json.time[version]; @@ -156,20 +151,18 @@ function deleteVersionFromJson(json, version) { */ function calculateLatestTag(tagList) { const entries = Object.entries(tagList).filter( - ([version, _]) => version !== "created" && version !== "modified", + ([version, _]) => version !== "created" && version !== "modified" ); const latestFullRelease = getMostRecentTag( - Object.fromEntries( - entries.filter(([version, _]) => !version.includes("-")), - ), + Object.fromEntries(entries.filter(([version, _]) => !version.includes("-"))) ); if (latestFullRelease) { return latestFullRelease; } const latestPrerelease = getMostRecentTag( - Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))), + Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))) ); return latestPrerelease; } diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js index a8c1a61..5a70c85 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.js @@ -23,7 +23,7 @@ const knownJsRegistries = [ */ export function npmInterceptorForUrl(url) { const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find( - (reg) => url.includes(reg), + (reg) => url.includes(reg) ); if (registry) { @@ -41,7 +41,7 @@ function buildNpmInterceptor(registry) { return interceptRequests(async (reqContext) => { const { packageName, version } = parseNpmPackageUrl( reqContext.targetUrl, - registry, + registry ); if (await isMalwarePackage(packageName, version)) { diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index 302c5b8..9bc8e58 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -11,8 +11,7 @@ describe("npmInterceptor minimum package age", async () => { getMinimumPackageAgeHours: () => minimumPackageAgeSettings, skipMinimumPackageAge: () => skipMinimumPackageAgeSetting, getNpmCustomRegistries: () => [], - getNpmMinimumPackageAgeExclusions: () => - minimumPackageAgeExclusionsSetting, + getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting, }, }); @@ -65,8 +64,9 @@ describe("npmInterceptor minimum package age", async () => { ]) { it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => { const interceptor = npmInterceptorForUrl(packageInfoUrl); - const requestInterceptor = - await interceptor.handleRequest(packageInfoUrl); + const requestInterceptor = await interceptor.handleRequest( + packageInfoUrl + ); assert.equal(requestInterceptor.modifiesResponse(), true); }); @@ -120,8 +120,9 @@ describe("npmInterceptor minimum package age", async () => { ]) { it(`modifyResponse should be false for special endpoints: ${specialEndpoint}`, async () => { const interceptor = npmInterceptorForUrl(specialEndpoint); - const requestInterceptor = - await interceptor.handleRequest(specialEndpoint); + const requestInterceptor = await interceptor.handleRequest( + specialEndpoint + ); assert.equal(requestInterceptor.modifiesResponse(), false); }); @@ -151,7 +152,7 @@ describe("npmInterceptor minimum package age", async () => { ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); @@ -192,7 +193,7 @@ describe("npmInterceptor minimum package age", async () => { ["2.0.0"]: getDate(-4), ["3.0.0"]: getDate(-3), }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); @@ -224,7 +225,7 @@ describe("npmInterceptor minimum package age", async () => { // cutoff-date here ["2.0.0-alpha"]: getDate(-4), }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); @@ -260,7 +261,7 @@ describe("npmInterceptor minimum package age", async () => { const modifiedBody = await runModifyNpmInfoRequest( packageUrl, - originalBody, + originalBody ); const modifiedJson = JSON.parse(modifiedBody); @@ -302,7 +303,7 @@ describe("npmInterceptor minimum package age", async () => { ["3.0.0"]: getDate(-40), // ~1.7 days old - should be removed ["4.0.0"]: getDate(-24), // 1 day old - should be removed }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); @@ -346,7 +347,7 @@ describe("npmInterceptor minimum package age", async () => { // 1-hour cutoff here ["3.0.0"]: getDate(0), // just published - should be removed }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); @@ -418,7 +419,7 @@ describe("npmInterceptor minimum package age", async () => { ["1.0.0"]: getDate(-7), ["3.0.0"]: getDate(-3), }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); @@ -479,10 +480,7 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest( - packageUrl, - originalBody, - ); + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain since lodash is in the exclusion list @@ -508,10 +506,7 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest( - packageUrl, - originalBody, - ); + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain since @aikidosec/* matches @aikidosec/safe-chain @@ -539,10 +534,7 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest( - packageUrl, - originalBody, - ); + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); const modifiedJson = JSON.parse(modifiedBody); // Version 2.0.0 should be filtered since @other/package doesn't match @aikidosec/* @@ -569,7 +561,7 @@ describe("npmInterceptor minimum package age", async () => { ["1.0.0"]: getDate(-100), ["2.0.0"]: getDate(-1), }, - }), + }) ); const modifiedJson = JSON.parse(modifiedBody); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js index 5cba422..2c76f62 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js @@ -136,7 +136,7 @@ describe("npmInterceptor", async () => { const interceptor = npmInterceptorForUrl(url); assert.ok( interceptor, - "Interceptor should be created for known npm registry", + "Interceptor should be created for known npm registry" ); await interceptor.handleRequest(url); @@ -153,7 +153,7 @@ describe("npmInterceptor", async () => { assert.equal( interceptor, undefined, - "Interceptor should be undefined for unknown registry", + "Interceptor should be undefined for unknown registry" ); }); @@ -170,12 +170,12 @@ describe("npmInterceptor", async () => { assert.equal( result.blockResponse.statusCode, 403, - "Block response should have status code 403", + "Block response should have status code 403" ); assert.equal( result.blockResponse.message, "Forbidden - blocked by safe-chain", - "Block response should have correct status message", + "Block response should have correct status message" ); }); }); @@ -212,7 +212,7 @@ describe("npmInterceptor with custom registries", async () => { assert.ok( interceptor, - "Interceptor should be created for custom registry with scoped package", + "Interceptor should be created for custom registry with scoped package" ); await interceptor.handleRequest(url); @@ -262,7 +262,7 @@ describe("npmInterceptor with custom registries", async () => { assert.equal( interceptor, undefined, - "Should not create interceptor for unknown registry", + "Should not create interceptor for unknown registry" ); }); }); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js index 47cdee8..9d876e8 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.js @@ -33,18 +33,16 @@ function buildPipInterceptor(registry) { return interceptRequests(async (reqContext) => { const { packageName, version } = parsePipPackageFromUrl( reqContext.targetUrl, - registry, + registry ); // Normalize underscores to hyphens for DB matching, as PyPI allows underscores in distribution names. // Per python, packages that differ only by hyphen vs underscore are considered the same. - const hyphenName = packageName?.includes("_") - ? packageName.replace(/_/g, "-") - : packageName; + const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName; const isMalicious = - (await isMalwarePackage(packageName, version)) || - (await isMalwarePackage(hyphenName, version)); + await isMalwarePackage(packageName, version) + || await isMalwarePackage(hyphenName, version); if (isMalicious) { reqContext.blockMalware(packageName, version); @@ -112,8 +110,7 @@ function parsePipPackageFromUrl(url, registry) { } // Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata) - const sdistExtWithMetadataRe = - /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i; + const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i; const sdistExtMatch = filename.match(sdistExtWithMetadataRe); if (sdistExtMatch) { const base = filename.replace(sdistExtWithMetadataRe, ""); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js index b57218e..0175176 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js @@ -30,7 +30,10 @@ describe("pipInterceptor custom registries", async () => { const interceptor = pipInterceptorForUrl(url); - assert.ok(interceptor, "Interceptor should be created for custom registry"); + assert.ok( + interceptor, + "Interceptor should be created for custom registry" + ); }); it("should parse package from custom registry URL", async () => { @@ -66,7 +69,10 @@ describe("pipInterceptor custom registries", async () => { }); it("should handle multiple custom registries", async () => { - customRegistries = ["registry-one.example.com", "registry-two.example.com"]; + customRegistries = [ + "registry-one.example.com", + "registry-two.example.com", + ]; const url1 = "https://registry-one.example.com/packages/package1-1.0.0.tar.gz"; @@ -79,7 +85,7 @@ describe("pipInterceptor custom registries", async () => { assert.ok(interceptor1, "Interceptor should be created for first registry"); assert.ok( interceptor2, - "Interceptor should be created for second registry", + "Interceptor should be created for second registry" ); }); @@ -99,12 +105,12 @@ describe("pipInterceptor custom registries", async () => { assert.equal( result.blockResponse.statusCode, 403, - "Block response should have status code 403", + "Block response should have status code 403" ); assert.equal( result.blockResponse.message, "Forbidden - blocked by safe-chain", - "Block response should have correct status message", + "Block response should have correct status message" ); malwareResponse = false; @@ -120,7 +126,7 @@ describe("pipInterceptor custom registries", async () => { assert.ok( interceptor, - "Interceptor should be created for known registry even with custom registries set", + "Interceptor should be created for known registry even with custom registries set" ); await interceptor.handleRequest(url); @@ -133,15 +139,14 @@ describe("pipInterceptor custom registries", async () => { it("should not create interceptor for unknown registry when custom registries are set", () => { customRegistries = ["my-custom-registry.example.com"]; - const url = - "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz"; + const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz"; const interceptor = pipInterceptorForUrl(url); assert.equal( interceptor, undefined, - "Interceptor should be undefined for unknown registry", + "Interceptor should be undefined for unknown registry" ); }); @@ -155,7 +160,7 @@ describe("pipInterceptor custom registries", async () => { assert.equal( interceptor, undefined, - "Interceptor should be undefined when no custom registries are configured", + "Interceptor should be undefined when no custom registries are configured" ); }); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js index dd812f1..c324edd 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/pipInterceptor.spec.js @@ -100,7 +100,7 @@ describe("pipInterceptor", async () => { const interceptor = pipInterceptorForUrl(url); assert.ok( interceptor, - "Interceptor should be created for known npm registry", + "Interceptor should be created for known npm registry" ); await interceptor.handleRequest(url); @@ -117,7 +117,7 @@ describe("pipInterceptor", async () => { assert.equal( interceptor, undefined, - "Interceptor should be undefined for unknown registry", + "Interceptor should be undefined for unknown registry" ); }); @@ -134,12 +134,12 @@ describe("pipInterceptor", async () => { assert.equal( result.blockResponse.statusCode, 403, - "Block response should have status code 403", + "Block response should have status code 403" ); assert.equal( result.blockResponse.message, "Forbidden - blocked by safe-chain", - "Block response should have correct status message", + "Block response should have correct status message" ); }); }); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js index 9a45270..b31d631 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/mitmRequestHandler.js @@ -19,7 +19,7 @@ export function mitmConnect(req, clientSocket, interceptor) { clientSocket.on("error", (err) => { ui.writeVerbose( - `Safe-chain: Client socket error for ${req.url}: ${err.message}`, + `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. @@ -89,7 +89,7 @@ function createHttpsServer(hostname, port, interceptor) { key: cert.privateKey, cert: cert.certificate, }, - handleRequest, + handleRequest ); return server; @@ -119,7 +119,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) { proxyReq.on("error", (err) => { ui.writeVerbose( - `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`, + `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}` ); res.writeHead(502); res.end("Bad Gateway"); @@ -127,7 +127,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) { req.on("error", (err) => { ui.writeError( - `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`, + `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}` ); proxyReq.destroy(); }); @@ -138,7 +138,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) { req.on("end", () => { ui.writeVerbose( - `Safe-chain: Finished proxying request to ${req.url} for ${hostname}`, + `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` ); proxyReq.end(); }); @@ -180,7 +180,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) { const proxyReq = https.request(options, (proxyRes) => { proxyRes.on("error", (err) => { ui.writeError( - `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`, + `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}` ); if (!res.headersSent) { res.writeHead(502); @@ -190,7 +190,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) { if (!proxyRes.statusCode) { ui.writeError( - `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`, + `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}` ); res.writeHead(500); res.end("Internal Server Error"); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js index 6d74588..9854774 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/plainHttpProxy.js @@ -61,7 +61,7 @@ export function handleHttpProxyRequest(req, res) { res.end(); } }); - }, + } ) .on("error", (err) => { if (!res.headersSent) { diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js index f7a3b9d..861be8a 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/tunnelRequestHandler.js @@ -49,11 +49,11 @@ function tunnelRequestToDestination(req, clientSocket, head) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); if (isImds) { ui.writeVerbose( - `Safe-chain: Closing connection because previously timedout connect to ${hostname}`, + `Safe-chain: Closing connection because previously timedout connect to ${hostname}` ); } else { ui.writeError( - `Safe-chain: Closing connection because previously timedout connect to ${hostname}`, + `Safe-chain: Closing connection because previously timedout connect to ${hostname}` ); } return; @@ -67,11 +67,11 @@ function tunnelRequestToDestination(req, clientSocket, head) { if (isImds) { timedoutImdsEndpoints.push(hostname); ui.writeVerbose( - `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`, + `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms` ); } else { ui.writeError( - `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`, + `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms` ); } serverSocket.destroy(); @@ -111,11 +111,11 @@ function tunnelRequestToDestination(req, clientSocket, head) { clearTimeout(connectTimer); if (isImds) { ui.writeVerbose( - `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`, + `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}` ); } else { ui.writeError( - `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`, + `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}` ); } if (clientSocket.writable) { @@ -173,7 +173,7 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { clientSocket.pipe(proxySocket); } else { ui.writeError( - `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`, + `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}` ); if (clientSocket.writable) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); @@ -189,14 +189,14 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { ui.writeError( `Safe-chain: error connecting to proxy ${proxy.hostname}:${ proxy.port || 8080 - } - ${err.message}`, + } - ${err.message}` ); if (clientSocket.writable) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } } else { ui.writeError( - `Safe-chain: proxy socket error after connection - ${err.message}`, + `Safe-chain: proxy socket error after connection - ${err.message}` ); if (clientSocket.writable) { clientSocket.end(); diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js index b42042a..d6b0416 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -85,21 +85,14 @@ export function getCombinedCaBundlePath(proxyCaCert) { const userPem = readUserCertificateFile(userCertPath); if (userPem) { parts.push(userPem.trim()); - ui.writeVerbose( - `Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`, - ); + ui.writeVerbose(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`); } else { - ui.writeWarning( - `Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`, - ); + ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`); } } const combined = parts.filter(Boolean).join("\n"); - const target = path.join( - os.tmpdir(), - `safe-chain-ca-bundle-${Date.now()}.pem`, - ); + const target = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`); fs.writeFileSync(target, combined, { encoding: "utf8" }); return target; } @@ -177,3 +170,4 @@ function readUserCertificateFile(certPath) { return null; } } + diff --git a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js index a4288f0..014c737 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js @@ -56,7 +56,7 @@ describe("registryProxy.connectTunnel", () => { const tunnelResponse = await establishHttpsTunnel( socket, "postman-echo.com", - 443, + 443 ); assert.ok(tunnelResponse.includes("HTTP/1.1 200 Connection Established")); @@ -69,7 +69,7 @@ describe("registryProxy.connectTunnel", () => { const httpsResponse = await sendHttpsRequestThroughTunnel( socket, "GET", - new URL("https://postman-echo.com/status/200"), + new URL("https://postman-echo.com/status/200") ); assert.ok(httpsResponse.includes("HTTP/1.1 200 OK")); @@ -85,25 +85,25 @@ describe("registryProxy.connectTunnel", () => { // without interception by the safe-chain MITM proxy. const certInfo = await getTlsCertificateInfo( socket, - new URL("https://postman-echo.com"), + new URL("https://postman-echo.com") ); // Verify the certificate is NOT issued by our safe-chain CA // Our self-signed CA would have issuer: "Safe-Chain Proxy CA" assert.ok( certInfo.issuer !== undefined, - "Certificate should have an issuer", + "Certificate should have an issuer" ); assert.ok( !certInfo.issuer.includes("Safe-Chain"), - `Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}`, + `Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}` ); // Verify it's a real certificate with proper hostname assert.strictEqual( certInfo.subject.includes("postman-echo.com"), true, - `Certificate subject should include postman-echo.com, got: ${certInfo.subject}`, + `Certificate subject should include postman-echo.com, got: ${certInfo.subject}` ); socket.destroy(); @@ -232,13 +232,13 @@ describe("registryProxy.connectTunnel", () => { // Should return 502 immediately (cached timeout) assert.ok( responseData.includes("HTTP/1.1 502 Bad Gateway"), - "Should return 502 for cached timeout", + "Should return 502 for cached timeout" ); // Should be nearly instant (< 50ms) since it's cached assert.ok( duration < 50, - `Cached IMDS timeout should be instant, got ${duration}ms`, + `Cached IMDS timeout should be instant, got ${duration}ms` ); socket2.destroy(); @@ -283,14 +283,14 @@ describe("registryProxy.connectTunnel", () => { // Should return 504 Gateway Timeout (not 502 - 504 is for actual timeouts) assert.ok( responseData.includes("HTTP/1.1 504 Gateway Timeout"), - "Should return 504 for timeout", + "Should return 504 for timeout" ); // Should NOT be instant - it should retry the connection (taking ~500ms due to mock timeout) // If it was cached, it would return in < 50ms assert.ok( duration >= 400, - `Non-IMDS timeout should NOT be cached, but got instant response in ${duration}ms`, + `Non-IMDS timeout should NOT be cached, but got instant response in ${duration}ms` ); socket2.destroy(); @@ -343,7 +343,7 @@ function sendHttpsRequestThroughTunnel( socket, verb, url, - rejectUnauthorized = false, + rejectUnauthorized = false ) { return new Promise((resolve, reject) => { const tlsSocket = tls.connect( @@ -356,9 +356,9 @@ function sendHttpsRequestThroughTunnel( }, () => { tlsSocket.write( - `${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n`, + `${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n` ); - }, + } ); let tlsData = ""; @@ -404,7 +404,7 @@ function getTlsCertificateInfo(socket, url) { tlsSocket.end(); resolve({ issuer, subject }); - }, + } ); tlsSocket.on("error", (err) => { From b03c1f681712d8838bcbd32c7501674584f07293 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 2 Mar 2026 16:06:10 +0100 Subject: [PATCH 96/98] Cleanup pt2 --- .../src/packagemanager/pip/runPipCommand.js | 17 ++++++++--------- .../packagemanager/pip/runPipCommand.spec.js | 9 +++------ .../npm/npmInterceptor.minPackageAge.spec.js | 10 ++-------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 52a8691..cbf6c63 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -121,13 +121,13 @@ export async function runPip(command, args) { let cleanupConfigPath = null; // Track temp file for cleanup if (isConfigRelatedCommand) { - ui.writeVerbose( `Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`); - - // Still set the fallback CA bundle environment variables to avoid edge cases where a + ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`); + + // Still set the fallback CA bundle environment variables to avoid edge cases where a // plugin or extension triggers a network call during config introspection // This can do no harm setFallbackCaBundleEnvironmentVariables(env, combinedCaPath); - + const result = await safeSpawn(command, args, { stdio: "inherit", env, @@ -147,6 +147,7 @@ export async function runPip(command, args) { await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; cleanupConfigPath = pipConfigPath; + } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); const userConfig = env.PIP_CONFIG_FILE; @@ -166,21 +167,19 @@ export async function runPip(command, args) { // Proxy if (typeof parsed.global.proxy !== "undefined") { - ui.writeWarning( - "Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.", - ); + ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config."); } if (proxy) { parsed.global.proxy = proxy; } - + const updated = ini.stringify(parsed); // Save to a new temp file to avoid overwriting user's original config await fs.writeFile(pipConfigPath, updated, "utf-8"); env.PIP_CONFIG_FILE = pipConfigPath; cleanupConfigPath = pipConfigPath; - + } else { // The user provided PIP_CONFIG_FILE does not exist on disk // PIP will handle this as an error and inform the user diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 235b472..c469978 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -24,10 +24,7 @@ describe("runPipCommand environment variable handling", () => { // Capture the config file content before the function cleans it up if (options.env.PIP_CONFIG_FILE) { try { - capturedConfigContent = await fs.readFile( - options.env.PIP_CONFIG_FILE, - "utf-8", - ); + capturedConfigContent = await fs.readFile(options.env.PIP_CONFIG_FILE, "utf-8"); } catch { // Ignore if file doesn't exist or can't be read } @@ -211,7 +208,7 @@ describe("runPipCommand environment variable handling", () => { "install", "certifi", "--index-url", - "https://test.pypi.org/simple" + "https://test.pypi.org/simple", ]); assert.strictEqual(res.status, 0); // Env vars should be set unconditionally @@ -424,5 +421,5 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false); }); - + }); diff --git a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index 9bc8e58..a31cce2 100644 --- a/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/builtInProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -386,10 +386,7 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest( - packageUrl, - originalBody, - ); + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain unchanged since lodash is excluded @@ -449,10 +446,7 @@ describe("npmInterceptor minimum package age", async () => { }, }); - const modifiedBody = await runModifyNpmInfoRequest( - packageUrl, - originalBody, - ); + const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody); const modifiedJson = JSON.parse(modifiedBody); // All versions should remain for excluded scoped package From 261aca9701ce8d9339f6d9a9c5a4cc544d5a3f6f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 9 Mar 2026 09:22:54 +0100 Subject: [PATCH 97/98] Clean up rama proxy process start --- .../src/registryProxy/ramaProxy/createRamaProxy.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js index 3c7b50a..b6a4988 100644 --- a/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js +++ b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js @@ -72,12 +72,8 @@ export function createRamaProxy(ramaPath) { async function startRama(ramaPath, dataFolder) { const startTime = Date.now(); const args = ["--secrets", "memory", "--data", dataFolder]; - const process = - getLoggingLevel() === LOGGING_VERBOSE - ? spawn(ramaPath, args, { - stdio: "inherit", - }) - : spawn(ramaPath, args); + const stdio = LOGGING_VERBOSE ? "inherit" : "pipe"; + const process = spawn(ramaPath, args, { stdio: stdio }); // wait for the proxy process to start (poll for proxy.addr.txt file) const proxyAddrPath = join(dataFolder, "proxy.addr.txt"); From 8086f6e7d749a1a3d9be70c5719afdbc1076b500 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 9 Mar 2026 09:24:55 +0100 Subject: [PATCH 98/98] Fix verbose logging --- .../safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js index b6a4988..43be0b6 100644 --- a/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js +++ b/packages/safe-chain/src/registryProxy/ramaProxy/createRamaProxy.js @@ -72,7 +72,7 @@ export function createRamaProxy(ramaPath) { async function startRama(ramaPath, dataFolder) { const startTime = Date.now(); const args = ["--secrets", "memory", "--data", dataFolder]; - const stdio = LOGGING_VERBOSE ? "inherit" : "pipe"; + const stdio = getLoggingLevel() === LOGGING_VERBOSE ? "inherit" : "pipe"; const process = spawn(ramaPath, args, { stdio: stdio }); // wait for the proxy process to start (poll for proxy.addr.txt file)