@aikidosec/safe-chain 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/aikido-pip.js CHANGED
@@ -3,15 +3,12 @@
3
3
  import { main } from "../src/main.js";
4
4
  import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
5
  import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
6
- import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
6
+ import { PIP_PACKAGE_MANAGER, PIP_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
7
7
 
8
8
  // Set eco system
9
9
  setEcoSystem(ECOSYSTEM_PY);
10
10
 
11
- // Set current invocation
12
- setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
13
-
14
- initializePackageManager(PIP_PACKAGE_MANAGER);
11
+ initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP_COMMAND, args: process.argv.slice(2) });
15
12
 
16
13
  (async () => {
17
14
  // Pass through only user-supplied pip args
@@ -3,16 +3,12 @@
3
3
  import { main } from "../src/main.js";
4
4
  import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
5
  import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
6
- import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
6
+ import { PIP_PACKAGE_MANAGER, PIP3_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
7
7
 
8
8
  // Set eco system
9
9
  setEcoSystem(ECOSYSTEM_PY);
10
10
 
11
- // Set current invocation
12
- setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
13
-
14
- // Create package manager
15
- initializePackageManager(PIP_PACKAGE_MANAGER);
11
+ initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP3_COMMAND, args: process.argv.slice(2) });
16
12
 
17
13
  (async () => {
18
14
  // Pass through only user-supplied pip args
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
4
- import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
4
+ import { PIP_PACKAGE_MANAGER, PYTHON_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
5
5
  import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
6
6
  import { main } from "../src/main.js";
7
7
 
@@ -11,20 +11,9 @@ setEcoSystem(ECOSYSTEM_PY);
11
11
  // Strip nodejs and wrapper script from args
12
12
  let argv = process.argv.slice(2);
13
13
 
14
- (async () => {
15
- if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
16
- setEcoSystem(ECOSYSTEM_PY);
17
- setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
18
- initializePackageManager(PIP_PACKAGE_MANAGER);
19
-
20
- // Strip off the '-m pip' or '-m pip3' from the args
21
- argv = argv.slice(2);
14
+ initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON_COMMAND, args: argv });
22
15
 
23
- var exitCode = await main(argv);
24
- process.exit(exitCode);
25
- } else {
26
- // Forward to real python binary for non-pip flows
27
- const { spawn } = await import('child_process');
28
- spawn('python', argv, { stdio: 'inherit' });
29
- }
16
+ (async () => {
17
+ var exitCode = await main(argv);
18
+ process.exit(exitCode);
30
19
  })();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
4
- import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
4
+ import { PIP_PACKAGE_MANAGER, PYTHON3_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
5
5
  import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
6
6
  import { main } from "../src/main.js";
7
7
 
@@ -11,20 +11,9 @@ setEcoSystem(ECOSYSTEM_PY);
11
11
  // Strip nodejs and wrapper script from args
12
12
  let argv = process.argv.slice(2);
13
13
 
14
- (async () => {
15
- if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
16
- setEcoSystem(ECOSYSTEM_PY);
17
- setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
18
- initializePackageManager(PIP_PACKAGE_MANAGER);
19
-
20
- // Strip off the '-m pip' or '-m pip3' from the args
21
- argv = argv.slice(2);
14
+ initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON3_COMMAND, args: argv });
22
15
 
23
- var exitCode = await main(argv);
24
- process.exit(exitCode);
25
- } else {
26
- // Forward to real python3 binary for non-pip flows
27
- const { spawn } = await import('child_process');
28
- spawn('python3', argv, { stdio: 'inherit' });
29
- }
16
+ (async () => {
17
+ var exitCode = await main(argv);
18
+ process.exit(exitCode);
30
19
  })();
package/bin/safe-chain.js CHANGED
@@ -13,11 +13,6 @@ import path from "path";
13
13
  import { fileURLToPath } from "url";
14
14
  import fs from "fs";
15
15
  import { knownAikidoTools } from "../src/shell-integration/helpers.js";
16
- import {
17
- PIP_INVOCATIONS,
18
- PIP_PACKAGE_MANAGER,
19
- setCurrentPipInvocation,
20
- } from "../src/packagemanager/pip/pipSettings.js";
21
16
 
22
17
  /** @type {string} */
23
18
  // This checks the current file's dirname in a way that's compatible with:
@@ -46,15 +41,14 @@ const command = process.argv[2];
46
41
 
47
42
  const tool = knownAikidoTools.find((tool) => tool.tool === command);
48
43
 
49
- if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) {
50
- (async function () {
51
- await executePip(tool);
52
- })();
53
- } else if (tool) {
44
+ if (tool) {
54
45
  const args = process.argv.slice(3);
55
46
 
56
47
  setEcoSystem(tool.ecoSystem);
57
- initializePackageManager(tool.internalPackageManagerName);
48
+
49
+ // Provide tool context to PM (pip uses this; others ignore)
50
+ const toolContext = { tool: tool.tool, args };
51
+ initializePackageManager(tool.internalPackageManagerName, toolContext);
58
52
 
59
53
  (async () => {
60
54
  var exitCode = await main(args);
@@ -140,51 +134,3 @@ async function getVersion() {
140
134
 
141
135
  return "0.0.0";
142
136
  }
143
-
144
- /**
145
- * @param {import("../src/shell-integration/helpers.js").AikidoTool} tool
146
- */
147
- async function executePip(tool) {
148
- // Scanners for pip / pip3 / python / python3 use a slightly different approach:
149
- // - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so
150
- // - It needs to set which tool to run (pip / pip3 / python / python3)
151
- // - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager
152
- // - Python / python3 skips safe-chain if not being run with -m pip or -m pip3
153
-
154
- let args = process.argv.slice(3);
155
- setEcoSystem(tool.ecoSystem);
156
- initializePackageManager(PIP_PACKAGE_MANAGER);
157
-
158
- let shouldSkip = false;
159
- if (tool.tool === "pip") {
160
- setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
161
- } else if (tool.tool === "pip3") {
162
- setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
163
- } else if (tool.tool === "python") {
164
- if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
165
- setCurrentPipInvocation(
166
- args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP
167
- );
168
- args = args.slice(2);
169
- } else {
170
- shouldSkip = true;
171
- }
172
- } else if (tool.tool === "python3") {
173
- if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
174
- setCurrentPipInvocation(
175
- args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP
176
- );
177
- args = args.slice(2);
178
- } else {
179
- shouldSkip = true;
180
- }
181
- }
182
-
183
- if (shouldSkip) {
184
- const { spawn } = await import("child_process");
185
- spawn(tool.tool, args, { stdio: "inherit" });
186
- } else {
187
- var exitCode = await main(args);
188
- process.exit(exitCode);
189
- }
190
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -35,10 +35,11 @@ const state = {
35
35
 
36
36
  /**
37
37
  * @param {string} packageManagerName
38
+ * @param {{ tool: string, args: string[] }} [context] - Optional tool context for package managers like pip
38
39
  *
39
40
  * @return {PackageManager}
40
41
  */
41
- export function initializePackageManager(packageManagerName) {
42
+ export function initializePackageManager(packageManagerName, context) {
42
43
  if (packageManagerName === "npm") {
43
44
  state.packageManagerName = createNpmPackageManager();
44
45
  } else if (packageManagerName === "npx") {
@@ -54,7 +55,7 @@ export function initializePackageManager(packageManagerName) {
54
55
  } else if (packageManagerName === "bunx") {
55
56
  state.packageManagerName = createBunxPackageManager();
56
57
  } else if (packageManagerName === "pip") {
57
- state.packageManagerName = createPipPackageManager();
58
+ state.packageManagerName = createPipPackageManager(context);
58
59
  } else if (packageManagerName === "uv") {
59
60
  state.packageManagerName = createUvPackageManager();
60
61
  } else {
@@ -1,17 +1,21 @@
1
1
  import { runPip } from "./runPipCommand.js";
2
- import { getCurrentPipInvocation } from "./pipSettings.js";
2
+ import { PIP_COMMAND } from "./pipSettings.js";
3
+
3
4
  /**
5
+ * @param {{ tool: string, args: string[] }} [context] - Optional context with tool name and args
4
6
  * @returns {import("../currentPackageManager.js").PackageManager}
5
7
  */
6
- export function createPipPackageManager() {
8
+ export function createPipPackageManager(context) {
9
+ const tool = context?.tool || PIP_COMMAND;
10
+
7
11
  return {
8
12
  /**
9
13
  * @param {string[]} args
10
14
  */
11
15
  runCommand: (args) => {
12
- const invocation = getCurrentPipInvocation();
13
- const fullArgs = [...invocation.args, ...args];
14
- return runPip(invocation.command, fullArgs);
16
+ // Args from main.js are already stripped of --safe-chain-* flags
17
+ // We just pass the tool (e.g. "python3") and the args (e.g. ["-m", "pip", "install", ...])
18
+ return runPip(tool, args);
15
19
  },
16
20
  // For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
17
21
  isSupportedCommand: () => false,
@@ -1,30 +1,6 @@
1
1
  export const PIP_PACKAGE_MANAGER = "pip";
2
2
 
3
- // All supported python/pip invocations for Safe Chain interception
4
- export const PIP_INVOCATIONS = {
5
- PIP: { command: "pip", args: [] },
6
- PIP3: { command: "pip3", args: [] },
7
- PY_PIP: { command: "python", args: ["-m", "pip"] },
8
- PY3_PIP: { command: "python3", args: ["-m", "pip"] },
9
- PY_PIP3: { command: "python", args: ["-m", "pip3"] },
10
- PY3_PIP3: { command: "python3", args: ["-m", "pip3"] }
11
- };
12
-
13
- /**
14
- * @type {{ command: string, args: string[] }}
15
- */
16
- let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip
17
-
18
- /**
19
- * @param {{ command: string, args: string[] }} invocation
20
- */
21
- export function setCurrentPipInvocation(invocation) {
22
- currentInvocation = invocation;
23
- }
24
-
25
- /**
26
- * @returns {{ command: string, args: string[] }}
27
- */
28
- export function getCurrentPipInvocation() {
29
- return currentInvocation;
30
- }
3
+ export const PIP_COMMAND = "pip";
4
+ export const PIP3_COMMAND = "pip3";
5
+ export const PYTHON_COMMAND = "python";
6
+ export const PYTHON3_COMMAND = "python3";
@@ -2,12 +2,31 @@ import { ui } from "../../environment/userInteraction.js";
2
2
  import { safeSpawn } from "../../utils/safeSpawn.js";
3
3
  import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
4
  import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
5
+ import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
5
6
  import fs from "node:fs/promises";
6
7
  import fsSync from "node:fs";
7
8
  import os from "node:os";
8
9
  import path from "node:path";
9
10
  import ini from "ini";
10
11
 
12
+ /**
13
+ * Checks if this pip invocation should bypass safe-chain and spawn directly.
14
+ * Returns true if the tool is python/python3 but NOT being run with -m pip/pip3.
15
+ * @param {string} command - The command executable
16
+ * @param {string[]} args - The arguments
17
+ * @returns {boolean}
18
+ */
19
+ function shouldBypassSafeChain(command, args) {
20
+ if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
21
+ // Check if args start with -m pip
22
+ if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+
11
30
  /**
12
31
  * Sets fallback CA bundle environment variables used by Python libraries.
13
32
  * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
@@ -49,11 +68,28 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
49
68
  * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
50
69
  * users to read/write persistent config. Only CA environment variables are set for these commands.
51
70
  *
52
- * @param {string} command - The pip command to execute (e.g., 'pip3')
71
+ * @param {string} command - The pip command executable (e.g., 'pip3' or 'python3')
53
72
  * @param {string[]} args - Command line arguments to pass to pip
54
73
  * @returns {Promise<{status: number}>} Exit status of the pip command
55
74
  */
56
75
  export async function runPip(command, args) {
76
+ // Check if we should bypass safe-chain (python/python3 without -m pip)
77
+ if (shouldBypassSafeChain(command, args)) {
78
+ ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
79
+ // Spawn the ORIGINAL command with ORIGINAL args
80
+ const { spawn } = await import("child_process");
81
+ return new Promise((_resolve) => {
82
+ const proc = spawn(command, args, { stdio: "inherit" });
83
+ proc.on("exit", (/** @type {number | null} */ code) => {
84
+ process.exit(code ?? 0);
85
+ });
86
+ proc.on("error", (/** @type {Error} */ err) => {
87
+ ui.writeError(`Error executing command: ${err.message}`);
88
+ process.exit(1);
89
+ });
90
+ });
91
+ }
92
+
57
93
  try {
58
94
  const env = mergeSafeChainProxyEnvironmentVariables(process.env);
59
95
 
@@ -0,0 +1,13 @@
1
+ // Instance Metadata Service (IMDS) endpoints used by cloud providers.
2
+ // Cloud SDK tools probe these to detect environment and retrieve credentials.
3
+ // When outside cloud environments, connections timeout - we reduce timeout (3s vs 30s)
4
+ // and suppress error logging since this is expected behavior.
5
+ const imdsEndpoints = [
6
+ "metadata.google.internal",
7
+ "metadata.goog",
8
+ "169.254.169.254", // AWS, Azure, Oracle Cloud, GCP
9
+ ];
10
+
11
+ export function isImdsEndpoint(/** @type {string} */ host) {
12
+ return imdsEndpoints.includes(host);
13
+ }
@@ -1,5 +1,9 @@
1
1
  import * as net from "net";
2
2
  import { ui } from "../environment/userInteraction.js";
3
+ import { isImdsEndpoint } from "./isImdsEndpoint.js";
4
+
5
+ /** @type {string[]} */
6
+ let timedoutEndpoints = [];
3
7
 
4
8
  /**
5
9
  * @param {import("http").IncomingMessage} req
@@ -37,6 +41,21 @@ export function tunnelRequest(req, clientSocket, head) {
37
41
  */
38
42
  function tunnelRequestToDestination(req, clientSocket, head) {
39
43
  const { port, hostname } = new URL(`http://${req.url}`);
44
+ const isImds = isImdsEndpoint(hostname);
45
+
46
+ if (timedoutEndpoints.includes(hostname)) {
47
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
48
+ if (isImds) {
49
+ ui.writeVerbose(
50
+ `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
51
+ );
52
+ } else {
53
+ ui.writeError(
54
+ `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
55
+ );
56
+ }
57
+ return;
58
+ }
40
59
 
41
60
  const serverSocket = net.connect(
42
61
  Number.parseInt(port) || 443,
@@ -49,6 +68,31 @@ function tunnelRequestToDestination(req, clientSocket, head) {
49
68
  }
50
69
  );
51
70
 
71
+ // Set explicit connection timeout to avoid waiting for OS default (~2 minutes).
72
+ // IMDS endpoints get shorter timeout (3s) since they're commonly unreachable outside cloud environments.
73
+ const connectTimeout = getConnectTimeout(hostname);
74
+ serverSocket.setTimeout(connectTimeout);
75
+
76
+ serverSocket.on("timeout", () => {
77
+ timedoutEndpoints.push(hostname);
78
+ // Suppress error logging for IMDS endpoints - timeouts are expected when not in cloud
79
+ if (isImds) {
80
+ ui.writeVerbose(
81
+ `Safe-chain: connect to ${hostname}:${
82
+ port || 443
83
+ } timed out after ${connectTimeout}ms`
84
+ );
85
+ } else {
86
+ ui.writeError(
87
+ `Safe-chain: connect to ${hostname}:${
88
+ port || 443
89
+ } timed out after ${connectTimeout}ms`
90
+ );
91
+ }
92
+ serverSocket.destroy(); // Clean up socket to prevent event loop hanging
93
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
94
+ });
95
+
52
96
  clientSocket.on("error", () => {
53
97
  // This can happen if the client TCP socket sends RST instead of FIN.
54
98
  // Not subscribing to 'error' event will cause node to throw and crash.
@@ -58,9 +102,15 @@ function tunnelRequestToDestination(req, clientSocket, head) {
58
102
  });
59
103
 
60
104
  serverSocket.on("error", (err) => {
61
- ui.writeError(
62
- `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
63
- );
105
+ if (isImds) {
106
+ ui.writeVerbose(
107
+ `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
108
+ );
109
+ } else {
110
+ ui.writeError(
111
+ `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
112
+ );
113
+ }
64
114
  if (clientSocket.writable) {
65
115
  clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
66
116
  }
@@ -145,3 +195,15 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
145
195
  }
146
196
  });
147
197
  }
198
+
199
+ /**
200
+ * Returns appropriate connection timeout for a host.
201
+ * - IMDS endpoints: 3s (fail fast when outside cloud, reduce 5min delay to ~20s)
202
+ * - Other endpoints: 30s (allow for slow networks while preventing indefinite hangs)
203
+ */
204
+ function getConnectTimeout(/** @type {string} */ host) {
205
+ if (isImdsEndpoint(host)) {
206
+ return 3000;
207
+ }
208
+ return 30000;
209
+ }
@@ -1,6 +1,6 @@
1
1
  # Use cross-platform path separator (: on Unix, ; on Windows)
2
2
  $pathSeparator = if ($IsWindows) { ';' } else { ':' }
3
- $safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
3
+ $safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
4
4
  $env:PATH = "$env:PATH$pathSeparator$safeChainBin"
5
5
 
6
6
  function npx {
@@ -1,6 +1,6 @@
1
1
  # Use cross-platform path separator (: on Unix, ; on Windows)
2
2
  $pathSeparator = if ($IsWindows) { ';' } else { ':' }
3
- $safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
3
+ $safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
4
4
  $env:PATH = "$env:PATH$pathSeparator$safeChainBin"
5
5
 
6
6
  function npx {