From e8a4fbcd762c0ab308d63e9be516f76db0a50fa4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 2 Mar 2026 15:54:41 +0100 Subject: [PATCH] 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) => {