Cleanup formatting changes from PR

This commit is contained in:
Sander Declerck 2026-03-02 15:54:41 +01:00
parent 912f62f3b9
commit e8a4fbcd76
No known key found for this signature in database
19 changed files with 200 additions and 407 deletions

View file

@ -385,8 +385,7 @@ steps:
- step: - step:
name: Install name: Install
script: script:
- npm install -g @aikidosec/safe-chain - curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
- safe-chain setup-ci
- export PATH=~/.safe-chain/shims:$PATH - export PATH=~/.safe-chain/shims:$PATH
- npm ci - npm ci
``` ```

View file

@ -4,12 +4,7 @@ import {
getProxySettings, getProxySettings,
mergeSafeChainProxyEnvironmentVariables, mergeSafeChainProxyEnvironmentVariables,
} from "../../registryProxy/registryProxy.js"; } from "../../registryProxy/registryProxy.js";
import { import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
PIP_COMMAND,
PIP3_COMMAND,
PYTHON_COMMAND,
PYTHON3_COMMAND,
} from "./pipSettings.js";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import fsSync from "node:fs"; import fsSync from "node:fs";
import os from "node:os"; import os from "node:os";
@ -27,11 +22,7 @@ import { spawn } from "child_process";
export function shouldBypassSafeChain(command, args) { export function shouldBypassSafeChain(command, args) {
if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) { if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
// Check if args start with -m pip // Check if args start with -m pip
if ( if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
args.length >= 2 &&
args[0] === "-m" &&
(args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)
) {
return false; return false;
} }
return true; return true;
@ -50,25 +41,19 @@ export function shouldBypassSafeChain(command, args) {
function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
// REQUESTS_CA_BUNDLE: Used by the popular 'requests' library // REQUESTS_CA_BUNDLE: Used by the popular 'requests' library
if (env.REQUESTS_CA_BUNDLE) { if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
"Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.",
);
} }
env.REQUESTS_CA_BUNDLE = combinedCaPath; env.REQUESTS_CA_BUNDLE = combinedCaPath;
// SSL_CERT_FILE: Used by some Python SSL libraries and urllib // SSL_CERT_FILE: Used by some Python SSL libraries and urllib
if (env.SSL_CERT_FILE) { if (env.SSL_CERT_FILE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
"Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.",
);
} }
env.SSL_CERT_FILE = combinedCaPath; env.SSL_CERT_FILE = combinedCaPath;
// PIP_CERT: Pip's own environment variable for certificate verification // PIP_CERT: Pip's own environment variable for certificate verification
if (env.PIP_CERT) { if (env.PIP_CERT) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
"Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.",
);
} }
env.PIP_CERT = combinedCaPath; env.PIP_CERT = combinedCaPath;
} }
@ -93,16 +78,12 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
export async function runPip(command, args) { export async function runPip(command, args) {
// Check if we should bypass safe-chain (python/python3 without -m pip) // Check if we should bypass safe-chain (python/python3 without -m pip)
if (shouldBypassSafeChain(command, args)) { if (shouldBypassSafeChain(command, args)) {
ui.writeVerbose( ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`,
);
// Spawn the ORIGINAL command with ORIGINAL args // Spawn the ORIGINAL command with ORIGINAL args
return new Promise((_resolve) => { return new Promise((_resolve) => {
const proc = spawn(command, args, { stdio: "inherit" }); const proc = spawn(command, args, { stdio: "inherit" });
proc.on("exit", (/** @type {number | null} */ code) => { proc.on("exit", (/** @type {number | null} */ code) => {
ui.writeVerbose( ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`);
`${command} ${args.join(" ")} exited with status ${code}`,
);
ui.writeBufferedLogsAndStopBuffering(); ui.writeBufferedLogsAndStopBuffering();
process.exit(code ?? 0); process.exit(code ?? 0);
}); });
@ -125,26 +106,22 @@ export async function runPip(command, args) {
// Commands that need access to persistent config/cache/state files // Commands that need access to persistent config/cache/state files
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from // 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 // reading/writing to the user's actual pip configuration and cache directories
const configRelatedCommands = ["config", "cache", "debug", "completion"]; const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
const isConfigRelatedCommand = const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
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) // 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. // 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), // 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 // otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables
const proxy = const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || "";
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
let cleanupConfigPath = null; // Track temp file for cleanup let cleanupConfigPath = null; // Track temp file for cleanup
if (isConfigRelatedCommand) { if (isConfigRelatedCommand) {
ui.writeVerbose( ui.writeVerbose( `Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
`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 // Still set the fallback CA bundle environment variables to avoid edge cases where a
// plugin or extension triggers a network call during config introspection // 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; env.PIP_CONFIG_FILE = pipConfigPath;
cleanupConfigPath = pipConfigPath; cleanupConfigPath = pipConfigPath;
} else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
ui.writeVerbose( ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
"Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.",
);
const userConfig = env.PIP_CONFIG_FILE; const userConfig = env.PIP_CONFIG_FILE;
// Read the existing config without modifying it // Read the existing config without modifying it
@ -185,9 +160,7 @@ export async function runPip(command, args) {
// Cert // Cert
if (typeof parsed.global.cert !== "undefined") { if (typeof parsed.global.cert !== "undefined") {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
"Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.",
);
} }
parsed.global.cert = combinedCaPath; parsed.global.cert = combinedCaPath;
@ -207,6 +180,7 @@ export async function runPip(command, args) {
await fs.writeFile(pipConfigPath, updated, "utf-8"); await fs.writeFile(pipConfigPath, updated, "utf-8");
env.PIP_CONFIG_FILE = pipConfigPath; env.PIP_CONFIG_FILE = pipConfigPath;
cleanupConfigPath = pipConfigPath; cleanupConfigPath = pipConfigPath;
} else { } else {
// The user provided PIP_CONFIG_FILE does not exist on disk // The user provided PIP_CONFIG_FILE does not exist on disk
// PIP will handle this as an error and inform the user // PIP will handle this as an error and inform the user

View file

@ -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 () => { it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => {
const res = await runPip("pip3", [ const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]);
"config",
"set",
"global.index-url",
"https://test.pypi.org/simple",
]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called"); assert.ok(capturedArgs, "safeSpawn should have been called");
@ -87,24 +82,24 @@ describe("runPipCommand environment variable handling", () => {
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE, capturedArgs.options.env.PIP_CONFIG_FILE,
undefined, 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 // But CA environment variables should still be set
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.REQUESTS_CA_BUNDLE, capturedArgs.options.env.REQUESTS_CA_BUNDLE,
"/tmp/test-combined-ca.pem", "/tmp/test-combined-ca.pem",
"REQUESTS_CA_BUNDLE should still be set", "REQUESTS_CA_BUNDLE should still be set"
); );
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE, capturedArgs.options.env.SSL_CERT_FILE,
"/tmp/test-combined-ca.pem", "/tmp/test-combined-ca.pem",
"SSL_CERT_FILE should still be set", "SSL_CERT_FILE should still be set"
); );
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.PIP_CERT, capturedArgs.options.env.PIP_CERT,
"/tmp/test-combined-ca.pem", "/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( assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE, capturedArgs.options.env.PIP_CONFIG_FILE,
undefined, 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( assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE, capturedArgs.options.env.PIP_CONFIG_FILE,
undefined, 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( assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE, capturedArgs.options.env.PIP_CONFIG_FILE,
undefined, 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 // CA env vars should still be set
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE, capturedArgs.options.env.SSL_CERT_FILE,
"/tmp/test-combined-ca.pem", "/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( assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE, capturedArgs.options.env.PIP_CONFIG_FILE,
undefined, 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( assert.strictEqual(
capturedArgs.options.env.PIP_CONFIG_FILE, capturedArgs.options.env.PIP_CONFIG_FILE,
undefined, 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( assert.strictEqual(
capturedArgs.options.env.PIP_CERT, capturedArgs.options.env.PIP_CERT,
"/tmp/test-combined-ca.pem", "/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 // Check PIP_CONFIG_FILE env var exists and is a non-empty string
const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; const configPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.ok(configPath, "PIP_CONFIG_FILE should be set"); assert.ok(configPath, "PIP_CONFIG_FILE should be set");
assert.strictEqual( assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string");
typeof configPath, assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path");
"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 () => { 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( assert.strictEqual(
capturedArgs.options.env.REQUESTS_CA_BUNDLE, capturedArgs.options.env.REQUESTS_CA_BUNDLE,
"/tmp/test-combined-ca.pem", "/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( assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE, capturedArgs.options.env.SSL_CERT_FILE,
"/tmp/test-combined-ca.pem", "/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", "install",
"certifi", "certifi",
"--index-url", "--index-url",
"https://test.pypi.org/simple", "https://test.pypi.org/simple"
]); ]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
// Env vars should be set unconditionally // Env vars should be set unconditionally
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.REQUESTS_CA_BUNDLE, capturedArgs.options.env.REQUESTS_CA_BUNDLE,
"/tmp/test-combined-ca.pem", "/tmp/test-combined-ca.pem"
); );
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE, 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) // Environment variables still set (pip CLI --cert takes precedence)
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.REQUESTS_CA_BUNDLE, capturedArgs.options.env.REQUESTS_CA_BUNDLE,
"/tmp/test-combined-ca.pem", "/tmp/test-combined-ca.pem"
); );
assert.strictEqual( assert.strictEqual(
capturedArgs.options.env.SSL_CERT_FILE, 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( assert.strictEqual(
capturedArgs.options.env.HTTPS_PROXY, capturedArgs.options.env.HTTPS_PROXY,
"http://localhost:8080", "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 () => { it("should create a new temp config when existing config exists (original file untouched)", async () => {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const userCfgPath = path.join( const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
tmpDir,
`safe-chain-test-pip-${Date.now()}.ini`,
);
const initial = "[global]\nindex-url = https://example.com/simple\n"; const initial = "[global]\nindex-url = https://example.com/simple\n";
await fs.writeFile(userCfgPath, initial, "utf-8"); await fs.writeFile(userCfgPath, initial, "utf-8");
@ -277,42 +262,19 @@ describe("runPipCommand environment variable handling", () => {
const res = await runPip("pip3", ["install", "requests"]); const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual( assert.notStrictEqual(newCfgPath, userCfgPath, "should point to a new temp config file");
newCfgPath,
userCfgPath,
"should point to a new temp config file",
);
// Original file unchanged // Original file unchanged
const originalContent = await fs.readFile(userCfgPath, "utf-8"); const originalContent = await fs.readFile(userCfgPath, "utf-8");
const originalParsed = ini.parse(originalContent); const originalParsed = ini.parse(originalContent);
assert.strictEqual( assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert");
originalParsed.global.cert,
undefined,
"original file should not gain cert",
);
// New file has merged settings (read from captured content before cleanup) // New file has merged settings (read from captured content before cleanup)
assert.ok( assert.ok(capturedConfigContent, "config content should have been captured");
capturedConfigContent,
"config content should have been captured",
);
const newParsed = ini.parse(capturedConfigContent); const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual( assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert");
newParsed.global.cert, assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env");
"/tmp/test-combined-ca.pem", assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved");
"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; customEnv = null;
}); });
@ -321,30 +283,24 @@ describe("runPipCommand environment variable handling", () => {
const res = await runPip("pip3", ["install", "requests"]); const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
assert.ok( assert.ok(capturedConfigContent, "config content should have been captured");
capturedConfigContent,
"config content should have been captured",
);
const parsed = ini.parse(capturedConfigContent); const parsed = ini.parse(capturedConfigContent);
assert.ok(parsed.global, "[global] should exist after creation"); assert.ok(parsed.global, "[global] should exist after creation");
assert.strictEqual( assert.strictEqual(
parsed.global.proxy, parsed.global.proxy,
"http://localhost:8080", "http://localhost:8080",
"proxy should be set from merged env", "proxy should be set from merged env"
); );
assert.strictEqual( assert.strictEqual(
parsed.global.cert, parsed.global.cert,
"/tmp/test-combined-ca.pem", "/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 () => { it("should create new temp config adding cert but preserving existing proxy (original file unchanged)", async () => {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const userCfgPath = path.join( const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
tmpDir,
`safe-chain-test-pip-${Date.now()}.ini`,
);
const initial = "[global]\nproxy = http://original:9999\n"; const initial = "[global]\nproxy = http://original:9999\n";
await fs.writeFile(userCfgPath, initial, "utf-8"); await fs.writeFile(userCfgPath, initial, "utf-8");
@ -352,41 +308,18 @@ describe("runPipCommand environment variable handling", () => {
const res = await runPip("pip3", ["install", "requests"]); const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual( assert.notStrictEqual(newCfgPath, userCfgPath, "should use a new temp config file");
newCfgPath,
userCfgPath,
"should use a new temp config file",
);
// Original file unchanged // Original file unchanged
const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8"));
assert.strictEqual( assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert");
originalParsed.global.cert, assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains");
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) // New file: cert and proxy always overwritten (read from captured content)
assert.ok( assert.ok(capturedConfigContent, "config content should have been captured");
capturedConfigContent,
"config content should have been captured",
);
const newParsed = ini.parse(capturedConfigContent); const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual( assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
newParsed.global.cert, assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
"/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; customEnv = null;
}); });
@ -397,7 +330,7 @@ describe("runPipCommand environment variable handling", () => {
"[global]", "[global]",
"cert = /path/to/existing.pem", "cert = /path/to/existing.pem",
"proxy = http://original:9999", "proxy = http://original:9999",
"", ""
].join("\n"); ].join("\n");
await fs.writeFile(cfgPath, initialIni, "utf-8"); await fs.writeFile(cfgPath, initialIni, "utf-8");
@ -405,51 +338,25 @@ describe("runPipCommand environment variable handling", () => {
const res = await runPip("pip3", ["install", "requests"]); const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0, "execution should succeed"); assert.strictEqual(res.status, 0, "execution should succeed");
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual( assert.notStrictEqual(newCfgPath, cfgPath, "should use a newly generated temp config file");
newCfgPath,
cfgPath,
"should use a newly generated temp config file",
);
// Original file stays untouched // Original file stays untouched
const originalContent = await fs.readFile(cfgPath, "utf-8"); const originalContent = await fs.readFile(cfgPath, "utf-8");
const originalParsed = ini.parse(originalContent); const originalParsed = ini.parse(originalContent);
assert.strictEqual( assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved");
originalParsed.global.cert, assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved");
"/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) // New temp config: cert and proxy always overwritten (read from captured content)
assert.ok( assert.ok(capturedConfigContent, "config content should have been captured");
capturedConfigContent,
"config content should have been captured",
);
const newParsed = ini.parse(capturedConfigContent); const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual( assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
newParsed.global.cert, assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
"/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; customEnv = null;
}); });
it("should create new temp config preserving existing cert and adding missing proxy", async () => { it("should create new temp config preserving existing cert and adding missing proxy", async () => {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const userCfgPath = path.join( const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
tmpDir,
`safe-chain-test-pip-${Date.now()}.ini`,
);
const initial = "[global]\ncert = /path/to/existing.pem\n"; const initial = "[global]\ncert = /path/to/existing.pem\n";
await fs.writeFile(userCfgPath, initial, "utf-8"); await fs.writeFile(userCfgPath, initial, "utf-8");
@ -457,55 +364,29 @@ describe("runPipCommand environment variable handling", () => {
const res = await runPip("pip3", ["install", "requests"]); const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual( assert.notStrictEqual(newCfgPath, userCfgPath, "should produce a new temp config file");
newCfgPath,
userCfgPath,
"should produce a new temp config file",
);
// Original remains unchanged // Original remains unchanged
const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8"));
assert.strictEqual( assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged");
originalParsed.global.cert, assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing");
"/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) // New file: cert and proxy always overwritten (read from captured content)
assert.ok( assert.ok(capturedConfigContent, "config content should have been captured");
capturedConfigContent,
"config content should have been captured",
);
const newParsed = ini.parse(capturedConfigContent); const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual( assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
newParsed.global.cert, assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
"/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; customEnv = null;
}); });
it("should log warnings when cert and proxy are already set in user config file", async () => { it("should log warnings when cert and proxy are already set in user config file", async () => {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const cfgPath = path.join( const cfgPath = path.join(tmpDir, `safe-chain-test-pip-warn-${Date.now()}.ini`);
tmpDir,
`safe-chain-test-pip-warn-${Date.now()}.ini`,
);
const initialIni = [ const initialIni = [
"[global]", "[global]",
"cert = /user/cert.pem", "cert = /user/cert.pem",
"proxy = http://user-proxy:9999", "proxy = http://user-proxy:9999",
"", ""
].join("\n"); ].join("\n");
await fs.writeFile(cfgPath, initialIni, "utf-8"); await fs.writeFile(cfgPath, initialIni, "utf-8");
@ -515,28 +396,16 @@ describe("runPipCommand environment variable handling", () => {
let output = ""; let output = "";
const originalWrite = process.stdout.write; const originalWrite = process.stdout.write;
const originalError = process.stderr.write; const originalError = process.stderr.write;
process.stdout.write = (chunk, ...args) => { process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); };
output += chunk; process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); };
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"]); await runPip("pip3", ["install", "requests"]);
process.stdout.write = originalWrite; process.stdout.write = originalWrite;
process.stderr.write = originalError; process.stderr.write = originalError;
assert.ok( assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output");
output.includes("cert found in PIP_CONFIG_FILE"), assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output");
"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; customEnv = null;
}); });
@ -547,18 +416,13 @@ describe("runPipCommand environment variable handling", () => {
assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true); assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true);
assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true); assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true);
assert.strictEqual( assert.strictEqual(shouldBypassSafeChain("python", ["-m", "http.server"]), true);
shouldBypassSafeChain("python", ["-m", "http.server"]), assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "http.server"]), true);
true,
);
assert.strictEqual(
shouldBypassSafeChain("python3", ["-m", "http.server"]),
true,
);
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false); assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false);
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false);
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false); assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false);
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false); assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false);
}); });
}); });

View file

@ -16,23 +16,17 @@ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
let retVal = { ...env }; let retVal = { ...env };
if (env.SSL_CERT_FILE) { if (env.SSL_CERT_FILE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
"Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.",
);
} }
retVal.SSL_CERT_FILE = combinedCaPath; retVal.SSL_CERT_FILE = combinedCaPath;
if (env.REQUESTS_CA_BUNDLE) { if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
"Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.",
);
} }
retVal.REQUESTS_CA_BUNDLE = combinedCaPath; retVal.REQUESTS_CA_BUNDLE = combinedCaPath;
if (env.PIP_CERT) { if (env.PIP_CERT) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
"Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.",
);
} }
retVal.PIP_CERT = combinedCaPath; retVal.PIP_CERT = combinedCaPath;
return retVal; return retVal;
@ -50,10 +44,7 @@ export async function runPipX(command, args) {
const env = mergeSafeChainProxyEnvironmentVariables(process.env); const env = mergeSafeChainProxyEnvironmentVariables(process.env);
const combinedCaPath = getProxySettings().caCertBundlePath; const combinedCaPath = getProxySettings().caCertBundlePath;
const modifiedEnv = getPipXCaBundleEnvironmentVariables( const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath);
env,
combinedCaPath,
);
// Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration // Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
// These are already set by mergeSafeChainProxyEnvironmentVariables // These are already set by mergeSafeChainProxyEnvironmentVariables

View file

@ -71,11 +71,7 @@ describe("runPipXCommand", () => {
const res = await runPipX("pipx", ["install", "ruff"]); const res = await runPipX("pipx", ["install", "ruff"]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
assert.strictEqual( assert.strictEqual(safeSpawnMock.mock.calls.length, 1, "safeSpawn should be called once");
safeSpawnMock.mock.calls.length,
1,
"safeSpawn should be called once",
);
const [, , options] = safeSpawnMock.mock.calls[0].arguments; const [, , options] = safeSpawnMock.mock.calls[0].arguments;
const env = options.env; const env = options.env;

View file

@ -28,25 +28,19 @@ export function createPoetryPackageManager() {
function setPoetryCaBundleEnvironmentVariables(env, combinedCaPath) { function setPoetryCaBundleEnvironmentVariables(env, combinedCaPath) {
// SSL_CERT_FILE: Used by Python SSL libraries and requests // SSL_CERT_FILE: Used by Python SSL libraries and requests
if (env.SSL_CERT_FILE) { if (env.SSL_CERT_FILE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
"Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.",
);
} }
env.SSL_CERT_FILE = combinedCaPath; env.SSL_CERT_FILE = combinedCaPath;
// REQUESTS_CA_BUNDLE: Used by the requests library (which Poetry uses) // REQUESTS_CA_BUNDLE: Used by the requests library (which Poetry uses)
if (env.REQUESTS_CA_BUNDLE) { if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
"Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.",
);
} }
env.REQUESTS_CA_BUNDLE = combinedCaPath; env.REQUESTS_CA_BUNDLE = combinedCaPath;
// PIP_CERT: Poetry may use pip internally // PIP_CERT: Poetry may use pip internally
if (env.PIP_CERT) { if (env.PIP_CERT) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
"Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.",
);
} }
env.PIP_CERT = combinedCaPath; env.PIP_CERT = combinedCaPath;
} }

View file

@ -14,25 +14,19 @@ import {
function setUvCaBundleEnvironmentVariables(env, combinedCaPath) { function setUvCaBundleEnvironmentVariables(env, combinedCaPath) {
// SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients // SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients
if (env.SSL_CERT_FILE) { if (env.SSL_CERT_FILE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
"Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.",
);
} }
env.SSL_CERT_FILE = combinedCaPath; env.SSL_CERT_FILE = combinedCaPath;
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally) // REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
if (env.REQUESTS_CA_BUNDLE) { if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
"Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.",
);
} }
env.REQUESTS_CA_BUNDLE = combinedCaPath; env.REQUESTS_CA_BUNDLE = combinedCaPath;
// PIP_CERT: Some underlying pip operations may respect this // PIP_CERT: Some underlying pip operations may respect this
if (env.PIP_CERT) { if (env.PIP_CERT) {
ui.writeWarning( ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
"Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.",
);
} }
env.PIP_CERT = combinedCaPath; env.PIP_CERT = combinedCaPath;
} }

View file

@ -71,20 +71,15 @@ export function modifyNpmInfoResponse(body, headers) {
// Check if this package is excluded from minimum age filtering // Check if this package is excluded from minimum age filtering
const packageName = bodyJson.name; const packageName = bodyJson.name;
const exclusions = getNpmMinimumPackageAgeExclusions(); const exclusions = getNpmMinimumPackageAgeExclusions();
if ( if (packageName && exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern))) {
packageName &&
exclusions.some((pattern) =>
matchesExclusionPattern(packageName, pattern),
)
) {
ui.writeVerbose( 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; return body;
} }
const cutOff = new Date( const cutOff = new Date(
new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000, new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000
); );
const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; const hasLatestTag = !!bodyJson["dist-tags"]["latest"];
@ -121,7 +116,7 @@ export function modifyNpmInfoResponse(body, headers) {
return Buffer.from(JSON.stringify(bodyJson)); return Buffer.from(JSON.stringify(bodyJson));
} catch (/** @type {any} */ err) { } catch (/** @type {any} */ err) {
ui.writeVerbose( 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; return body;
} }
@ -137,7 +132,7 @@ function deleteVersionFromJson(json, version) {
const packageName = typeof json?.name === "string" ? json.name : "(unknown)"; const packageName = typeof json?.name === "string" ? json.name : "(unknown)";
ui.writeVerbose( 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]; delete json.time[version];
@ -156,20 +151,18 @@ function deleteVersionFromJson(json, version) {
*/ */
function calculateLatestTag(tagList) { function calculateLatestTag(tagList) {
const entries = Object.entries(tagList).filter( const entries = Object.entries(tagList).filter(
([version, _]) => version !== "created" && version !== "modified", ([version, _]) => version !== "created" && version !== "modified"
); );
const latestFullRelease = getMostRecentTag( const latestFullRelease = getMostRecentTag(
Object.fromEntries( Object.fromEntries(entries.filter(([version, _]) => !version.includes("-")))
entries.filter(([version, _]) => !version.includes("-")),
),
); );
if (latestFullRelease) { if (latestFullRelease) {
return latestFullRelease; return latestFullRelease;
} }
const latestPrerelease = getMostRecentTag( const latestPrerelease = getMostRecentTag(
Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))), Object.fromEntries(entries.filter(([version, _]) => version.includes("-")))
); );
return latestPrerelease; return latestPrerelease;
} }

View file

@ -23,7 +23,7 @@ const knownJsRegistries = [
*/ */
export function npmInterceptorForUrl(url) { export function npmInterceptorForUrl(url) {
const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find( const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find(
(reg) => url.includes(reg), (reg) => url.includes(reg)
); );
if (registry) { if (registry) {
@ -41,7 +41,7 @@ function buildNpmInterceptor(registry) {
return interceptRequests(async (reqContext) => { return interceptRequests(async (reqContext) => {
const { packageName, version } = parseNpmPackageUrl( const { packageName, version } = parseNpmPackageUrl(
reqContext.targetUrl, reqContext.targetUrl,
registry, registry
); );
if (await isMalwarePackage(packageName, version)) { if (await isMalwarePackage(packageName, version)) {

View file

@ -11,8 +11,7 @@ describe("npmInterceptor minimum package age", async () => {
getMinimumPackageAgeHours: () => minimumPackageAgeSettings, getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting, skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
getNpmCustomRegistries: () => [], getNpmCustomRegistries: () => [],
getNpmMinimumPackageAgeExclusions: () => getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting,
minimumPackageAgeExclusionsSetting,
}, },
}); });
@ -65,8 +64,9 @@ describe("npmInterceptor minimum package age", async () => {
]) { ]) {
it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => { it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => {
const interceptor = npmInterceptorForUrl(packageInfoUrl); const interceptor = npmInterceptorForUrl(packageInfoUrl);
const requestInterceptor = const requestInterceptor = await interceptor.handleRequest(
await interceptor.handleRequest(packageInfoUrl); packageInfoUrl
);
assert.equal(requestInterceptor.modifiesResponse(), true); 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 () => { it(`modifyResponse should be false for special endpoints: ${specialEndpoint}`, async () => {
const interceptor = npmInterceptorForUrl(specialEndpoint); const interceptor = npmInterceptorForUrl(specialEndpoint);
const requestInterceptor = const requestInterceptor = await interceptor.handleRequest(
await interceptor.handleRequest(specialEndpoint); specialEndpoint
);
assert.equal(requestInterceptor.modifiesResponse(), false); assert.equal(requestInterceptor.modifiesResponse(), false);
}); });
@ -151,7 +152,7 @@ describe("npmInterceptor minimum package age", async () => {
["2.0.0"]: getDate(-4), ["2.0.0"]: getDate(-4),
["3.0.0"]: getDate(-3), ["3.0.0"]: getDate(-3),
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
@ -192,7 +193,7 @@ describe("npmInterceptor minimum package age", async () => {
["2.0.0"]: getDate(-4), ["2.0.0"]: getDate(-4),
["3.0.0"]: getDate(-3), ["3.0.0"]: getDate(-3),
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
@ -224,7 +225,7 @@ describe("npmInterceptor minimum package age", async () => {
// cutoff-date here // cutoff-date here
["2.0.0-alpha"]: getDate(-4), ["2.0.0-alpha"]: getDate(-4),
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
@ -260,7 +261,7 @@ describe("npmInterceptor minimum package age", async () => {
const modifiedBody = await runModifyNpmInfoRequest( const modifiedBody = await runModifyNpmInfoRequest(
packageUrl, packageUrl,
originalBody, originalBody
); );
const modifiedJson = JSON.parse(modifiedBody); 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 ["3.0.0"]: getDate(-40), // ~1.7 days old - should be removed
["4.0.0"]: getDate(-24), // 1 day old - should be removed ["4.0.0"]: getDate(-24), // 1 day old - should be removed
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
@ -346,7 +347,7 @@ describe("npmInterceptor minimum package age", async () => {
// 1-hour cutoff here // 1-hour cutoff here
["3.0.0"]: getDate(0), // just published - should be removed ["3.0.0"]: getDate(0), // just published - should be removed
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
@ -418,7 +419,7 @@ describe("npmInterceptor minimum package age", async () => {
["1.0.0"]: getDate(-7), ["1.0.0"]: getDate(-7),
["3.0.0"]: getDate(-3), ["3.0.0"]: getDate(-3),
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
@ -479,10 +480,7 @@ describe("npmInterceptor minimum package age", async () => {
}, },
}); });
const modifiedBody = await runModifyNpmInfoRequest( const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
packageUrl,
originalBody,
);
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
// All versions should remain since lodash is in the exclusion list // 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( const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
packageUrl,
originalBody,
);
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
// All versions should remain since @aikidosec/* matches @aikidosec/safe-chain // All versions should remain since @aikidosec/* matches @aikidosec/safe-chain
@ -539,10 +534,7 @@ describe("npmInterceptor minimum package age", async () => {
}, },
}); });
const modifiedBody = await runModifyNpmInfoRequest( const modifiedBody = await runModifyNpmInfoRequest(packageUrl, originalBody);
packageUrl,
originalBody,
);
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);
// Version 2.0.0 should be filtered since @other/package doesn't match @aikidosec/* // 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), ["1.0.0"]: getDate(-100),
["2.0.0"]: getDate(-1), ["2.0.0"]: getDate(-1),
}, },
}), })
); );
const modifiedJson = JSON.parse(modifiedBody); const modifiedJson = JSON.parse(modifiedBody);

View file

@ -136,7 +136,7 @@ describe("npmInterceptor", async () => {
const interceptor = npmInterceptorForUrl(url); const interceptor = npmInterceptorForUrl(url);
assert.ok( assert.ok(
interceptor, interceptor,
"Interceptor should be created for known npm registry", "Interceptor should be created for known npm registry"
); );
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
@ -153,7 +153,7 @@ describe("npmInterceptor", async () => {
assert.equal( assert.equal(
interceptor, interceptor,
undefined, undefined,
"Interceptor should be undefined for unknown registry", "Interceptor should be undefined for unknown registry"
); );
}); });
@ -170,12 +170,12 @@ describe("npmInterceptor", async () => {
assert.equal( assert.equal(
result.blockResponse.statusCode, result.blockResponse.statusCode,
403, 403,
"Block response should have status code 403", "Block response should have status code 403"
); );
assert.equal( assert.equal(
result.blockResponse.message, result.blockResponse.message,
"Forbidden - blocked by safe-chain", "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( assert.ok(
interceptor, 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); await interceptor.handleRequest(url);
@ -262,7 +262,7 @@ describe("npmInterceptor with custom registries", async () => {
assert.equal( assert.equal(
interceptor, interceptor,
undefined, undefined,
"Should not create interceptor for unknown registry", "Should not create interceptor for unknown registry"
); );
}); });
}); });

View file

@ -33,18 +33,16 @@ function buildPipInterceptor(registry) {
return interceptRequests(async (reqContext) => { return interceptRequests(async (reqContext) => {
const { packageName, version } = parsePipPackageFromUrl( const { packageName, version } = parsePipPackageFromUrl(
reqContext.targetUrl, reqContext.targetUrl,
registry, registry
); );
// Normalize underscores to hyphens for DB matching, as PyPI allows underscores in distribution names. // 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. // Per python, packages that differ only by hyphen vs underscore are considered the same.
const hyphenName = packageName?.includes("_") const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName;
? packageName.replace(/_/g, "-")
: packageName;
const isMalicious = const isMalicious =
(await isMalwarePackage(packageName, version)) || await isMalwarePackage(packageName, version)
(await isMalwarePackage(hyphenName, version)); || await isMalwarePackage(hyphenName, version);
if (isMalicious) { if (isMalicious) {
reqContext.blockMalware(packageName, version); reqContext.blockMalware(packageName, version);
@ -112,8 +110,7 @@ function parsePipPackageFromUrl(url, registry) {
} }
// Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata) // Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata)
const sdistExtWithMetadataRe = const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
/\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
const sdistExtMatch = filename.match(sdistExtWithMetadataRe); const sdistExtMatch = filename.match(sdistExtWithMetadataRe);
if (sdistExtMatch) { if (sdistExtMatch) {
const base = filename.replace(sdistExtWithMetadataRe, ""); const base = filename.replace(sdistExtWithMetadataRe, "");

View file

@ -30,7 +30,10 @@ describe("pipInterceptor custom registries", async () => {
const interceptor = pipInterceptorForUrl(url); 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 () => { 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 () => { 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 = const url1 =
"https://registry-one.example.com/packages/package1-1.0.0.tar.gz"; "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(interceptor1, "Interceptor should be created for first registry");
assert.ok( assert.ok(
interceptor2, 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( assert.equal(
result.blockResponse.statusCode, result.blockResponse.statusCode,
403, 403,
"Block response should have status code 403", "Block response should have status code 403"
); );
assert.equal( assert.equal(
result.blockResponse.message, result.blockResponse.message,
"Forbidden - blocked by safe-chain", "Forbidden - blocked by safe-chain",
"Block response should have correct status message", "Block response should have correct status message"
); );
malwareResponse = false; malwareResponse = false;
@ -120,7 +126,7 @@ describe("pipInterceptor custom registries", async () => {
assert.ok( assert.ok(
interceptor, 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); 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", () => { it("should not create interceptor for unknown registry when custom registries are set", () => {
customRegistries = ["my-custom-registry.example.com"]; customRegistries = ["my-custom-registry.example.com"];
const url = const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz";
"https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz";
const interceptor = pipInterceptorForUrl(url); const interceptor = pipInterceptorForUrl(url);
assert.equal( assert.equal(
interceptor, interceptor,
undefined, 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( assert.equal(
interceptor, interceptor,
undefined, undefined,
"Interceptor should be undefined when no custom registries are configured", "Interceptor should be undefined when no custom registries are configured"
); );
}); });

View file

@ -100,7 +100,7 @@ describe("pipInterceptor", async () => {
const interceptor = pipInterceptorForUrl(url); const interceptor = pipInterceptorForUrl(url);
assert.ok( assert.ok(
interceptor, interceptor,
"Interceptor should be created for known npm registry", "Interceptor should be created for known npm registry"
); );
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
@ -117,7 +117,7 @@ describe("pipInterceptor", async () => {
assert.equal( assert.equal(
interceptor, interceptor,
undefined, undefined,
"Interceptor should be undefined for unknown registry", "Interceptor should be undefined for unknown registry"
); );
}); });
@ -134,12 +134,12 @@ describe("pipInterceptor", async () => {
assert.equal( assert.equal(
result.blockResponse.statusCode, result.blockResponse.statusCode,
403, 403,
"Block response should have status code 403", "Block response should have status code 403"
); );
assert.equal( assert.equal(
result.blockResponse.message, result.blockResponse.message,
"Forbidden - blocked by safe-chain", "Forbidden - blocked by safe-chain",
"Block response should have correct status message", "Block response should have correct status message"
); );
}); });
}); });

View file

@ -19,7 +19,7 @@ export function mitmConnect(req, clientSocket, interceptor) {
clientSocket.on("error", (err) => { clientSocket.on("error", (err) => {
ui.writeVerbose( ui.writeVerbose(
`Safe-chain: Client socket error for ${req.url}: ${err.message}`, `Safe-chain: Client socket error for ${req.url}: ${err.message}`
); );
// NO-OP // NO-OP
// This can happen if the client TCP socket sends RST instead of FIN. // 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, key: cert.privateKey,
cert: cert.certificate, cert: cert.certificate,
}, },
handleRequest, handleRequest
); );
return server; return server;
@ -119,7 +119,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) {
proxyReq.on("error", (err) => { proxyReq.on("error", (err) => {
ui.writeVerbose( 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.writeHead(502);
res.end("Bad Gateway"); res.end("Bad Gateway");
@ -127,7 +127,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) {
req.on("error", (err) => { req.on("error", (err) => {
ui.writeError( 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(); proxyReq.destroy();
}); });
@ -138,7 +138,7 @@ function forwardRequest(req, hostname, port, res, requestHandler) {
req.on("end", () => { req.on("end", () => {
ui.writeVerbose( ui.writeVerbose(
`Safe-chain: Finished proxying request to ${req.url} for ${hostname}`, `Safe-chain: Finished proxying request to ${req.url} for ${hostname}`
); );
proxyReq.end(); proxyReq.end();
}); });
@ -180,7 +180,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) {
const proxyReq = https.request(options, (proxyRes) => { const proxyReq = https.request(options, (proxyRes) => {
proxyRes.on("error", (err) => { proxyRes.on("error", (err) => {
ui.writeError( 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) { if (!res.headersSent) {
res.writeHead(502); res.writeHead(502);
@ -190,7 +190,7 @@ function createProxyRequest(hostname, port, req, res, requestHandler) {
if (!proxyRes.statusCode) { if (!proxyRes.statusCode) {
ui.writeError( 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.writeHead(500);
res.end("Internal Server Error"); res.end("Internal Server Error");

View file

@ -61,7 +61,7 @@ export function handleHttpProxyRequest(req, res) {
res.end(); res.end();
} }
}); });
}, }
) )
.on("error", (err) => { .on("error", (err) => {
if (!res.headersSent) { if (!res.headersSent) {

View file

@ -49,11 +49,11 @@ function tunnelRequestToDestination(req, clientSocket, head) {
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
if (isImds) { if (isImds) {
ui.writeVerbose( ui.writeVerbose(
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`, `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
); );
} else { } else {
ui.writeError( ui.writeError(
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`, `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
); );
} }
return; return;
@ -67,11 +67,11 @@ function tunnelRequestToDestination(req, clientSocket, head) {
if (isImds) { if (isImds) {
timedoutImdsEndpoints.push(hostname); timedoutImdsEndpoints.push(hostname);
ui.writeVerbose( 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 { } else {
ui.writeError( 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(); serverSocket.destroy();
@ -111,11 +111,11 @@ function tunnelRequestToDestination(req, clientSocket, head) {
clearTimeout(connectTimer); clearTimeout(connectTimer);
if (isImds) { if (isImds) {
ui.writeVerbose( ui.writeVerbose(
`Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`, `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
); );
} else { } else {
ui.writeError( ui.writeError(
`Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`, `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
); );
} }
if (clientSocket.writable) { if (clientSocket.writable) {
@ -173,7 +173,7 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
clientSocket.pipe(proxySocket); clientSocket.pipe(proxySocket);
} else { } else {
ui.writeError( 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) { if (clientSocket.writable) {
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
@ -189,14 +189,14 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
ui.writeError( ui.writeError(
`Safe-chain: error connecting to proxy ${proxy.hostname}:${ `Safe-chain: error connecting to proxy ${proxy.hostname}:${
proxy.port || 8080 proxy.port || 8080
} - ${err.message}`, } - ${err.message}`
); );
if (clientSocket.writable) { if (clientSocket.writable) {
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
} }
} else { } else {
ui.writeError( ui.writeError(
`Safe-chain: proxy socket error after connection - ${err.message}`, `Safe-chain: proxy socket error after connection - ${err.message}`
); );
if (clientSocket.writable) { if (clientSocket.writable) {
clientSocket.end(); clientSocket.end();

View file

@ -85,21 +85,14 @@ export function getCombinedCaBundlePath(proxyCaCert) {
const userPem = readUserCertificateFile(userCertPath); const userPem = readUserCertificateFile(userCertPath);
if (userPem) { if (userPem) {
parts.push(userPem.trim()); parts.push(userPem.trim());
ui.writeVerbose( ui.writeVerbose(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`,
);
} else { } else {
ui.writeWarning( ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`,
);
} }
} }
const combined = parts.filter(Boolean).join("\n"); const combined = parts.filter(Boolean).join("\n");
const target = path.join( const target = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`);
os.tmpdir(),
`safe-chain-ca-bundle-${Date.now()}.pem`,
);
fs.writeFileSync(target, combined, { encoding: "utf8" }); fs.writeFileSync(target, combined, { encoding: "utf8" });
return target; return target;
} }
@ -177,3 +170,4 @@ function readUserCertificateFile(certPath) {
return null; return null;
} }
} }

View file

@ -56,7 +56,7 @@ describe("registryProxy.connectTunnel", () => {
const tunnelResponse = await establishHttpsTunnel( const tunnelResponse = await establishHttpsTunnel(
socket, socket,
"postman-echo.com", "postman-echo.com",
443, 443
); );
assert.ok(tunnelResponse.includes("HTTP/1.1 200 Connection Established")); assert.ok(tunnelResponse.includes("HTTP/1.1 200 Connection Established"));
@ -69,7 +69,7 @@ describe("registryProxy.connectTunnel", () => {
const httpsResponse = await sendHttpsRequestThroughTunnel( const httpsResponse = await sendHttpsRequestThroughTunnel(
socket, socket,
"GET", "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")); assert.ok(httpsResponse.includes("HTTP/1.1 200 OK"));
@ -85,25 +85,25 @@ describe("registryProxy.connectTunnel", () => {
// without interception by the safe-chain MITM proxy. // without interception by the safe-chain MITM proxy.
const certInfo = await getTlsCertificateInfo( const certInfo = await getTlsCertificateInfo(
socket, socket,
new URL("https://postman-echo.com"), new URL("https://postman-echo.com")
); );
// Verify the certificate is NOT issued by our safe-chain CA // Verify the certificate is NOT issued by our safe-chain CA
// Our self-signed CA would have issuer: "Safe-Chain Proxy CA" // Our self-signed CA would have issuer: "Safe-Chain Proxy CA"
assert.ok( assert.ok(
certInfo.issuer !== undefined, certInfo.issuer !== undefined,
"Certificate should have an issuer", "Certificate should have an issuer"
); );
assert.ok( assert.ok(
!certInfo.issuer.includes("Safe-Chain"), !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 // Verify it's a real certificate with proper hostname
assert.strictEqual( assert.strictEqual(
certInfo.subject.includes("postman-echo.com"), certInfo.subject.includes("postman-echo.com"),
true, true,
`Certificate subject should include postman-echo.com, got: ${certInfo.subject}`, `Certificate subject should include postman-echo.com, got: ${certInfo.subject}`
); );
socket.destroy(); socket.destroy();
@ -232,13 +232,13 @@ describe("registryProxy.connectTunnel", () => {
// Should return 502 immediately (cached timeout) // Should return 502 immediately (cached timeout)
assert.ok( assert.ok(
responseData.includes("HTTP/1.1 502 Bad Gateway"), 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 // Should be nearly instant (< 50ms) since it's cached
assert.ok( assert.ok(
duration < 50, duration < 50,
`Cached IMDS timeout should be instant, got ${duration}ms`, `Cached IMDS timeout should be instant, got ${duration}ms`
); );
socket2.destroy(); socket2.destroy();
@ -283,14 +283,14 @@ describe("registryProxy.connectTunnel", () => {
// Should return 504 Gateway Timeout (not 502 - 504 is for actual timeouts) // Should return 504 Gateway Timeout (not 502 - 504 is for actual timeouts)
assert.ok( assert.ok(
responseData.includes("HTTP/1.1 504 Gateway Timeout"), 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) // Should NOT be instant - it should retry the connection (taking ~500ms due to mock timeout)
// If it was cached, it would return in < 50ms // If it was cached, it would return in < 50ms
assert.ok( assert.ok(
duration >= 400, 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(); socket2.destroy();
@ -343,7 +343,7 @@ function sendHttpsRequestThroughTunnel(
socket, socket,
verb, verb,
url, url,
rejectUnauthorized = false, rejectUnauthorized = false
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tlsSocket = tls.connect( const tlsSocket = tls.connect(
@ -356,9 +356,9 @@ function sendHttpsRequestThroughTunnel(
}, },
() => { () => {
tlsSocket.write( 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 = ""; let tlsData = "";
@ -404,7 +404,7 @@ function getTlsCertificateInfo(socket, url) {
tlsSocket.end(); tlsSocket.end();
resolve({ issuer, subject }); resolve({ issuer, subject });
}, }
); );
tlsSocket.on("error", (err) => { tlsSocket.on("error", (err) => {