@aikidosec/safe-chain 1.2.0 → 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/README.md +4 -6
- package/bin/aikido-pip.js +2 -5
- package/bin/aikido-pip3.js +2 -6
- package/bin/aikido-python.js +5 -16
- package/bin/aikido-python3.js +5 -16
- package/bin/safe-chain.js +5 -59
- package/docs/npm-to-binary-migration.md +3 -3
- package/package.json +1 -1
- package/src/packagemanager/currentPackageManager.js +3 -2
- package/src/packagemanager/pip/createPackageManager.js +9 -5
- package/src/packagemanager/pip/pipSettings.js +4 -28
- package/src/packagemanager/pip/runPipCommand.js +37 -1
- package/src/registryProxy/isImdsEndpoint.js +13 -0
- package/src/registryProxy/tunnelRequestHandler.js +65 -3
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +1 -1
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-

|
|
2
2
|
|
|
3
3
|
# Aikido Safe Chain
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- ✅ **Blocks packages newer than 24 hours** without breaking your build
|
|
11
11
|
- ✅ **Tokenless, free, no build data shared**
|
|
12
12
|
|
|
13
|
-
Aikido Safe Chain
|
|
13
|
+
Aikido Safe Chain supports the following package managers:
|
|
14
14
|
|
|
15
15
|
- 📦 **npm**
|
|
16
16
|
- 📦 **npx**
|
|
@@ -29,7 +29,7 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi
|
|
|
29
29
|
|
|
30
30
|
Installing the Aikido Safe Chain is easy with our one-line installer.
|
|
31
31
|
|
|
32
|
-
> ⚠️ **Already installed via npm?** See the [migration guide](docs/npm-to-binary-migration.md) to switch to the binary version.
|
|
32
|
+
> ⚠️ **Already installed via npm?** See the [migration guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/npm-to-binary-migration.md) to switch to the binary version.
|
|
33
33
|
|
|
34
34
|
### Unix/Linux/macOS
|
|
35
35
|
|
|
@@ -111,7 +111,7 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc
|
|
|
111
111
|
- ✅ **PowerShell**
|
|
112
112
|
- ✅ **PowerShell Core**
|
|
113
113
|
|
|
114
|
-
More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md).
|
|
114
|
+
More information about the shell integration can be found in the [shell integration documentation](https://github.com/AikidoSec/safe-chain/blob/main/docs/shell-integration.md).
|
|
115
115
|
|
|
116
116
|
## Uninstallation
|
|
117
117
|
|
|
@@ -182,8 +182,6 @@ You can set the minimum package age through multiple sources (in order of priori
|
|
|
182
182
|
|
|
183
183
|
You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
|
|
184
184
|
|
|
185
|
-
For optimal protection in CI/CD environments, we recommend using **npm >= 10.4.0** as it provides full dependency tree scanning. Other package managers currently offer limited scanning of install command arguments only.
|
|
186
|
-
|
|
187
185
|
## Installation for CI/CD
|
|
188
186
|
|
|
189
187
|
Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases.
|
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 {
|
|
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
|
-
|
|
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
|
package/bin/aikido-pip3.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
package/bin/aikido-python.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
4
|
-
import {
|
|
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
|
-
(
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
})();
|
package/bin/aikido-python3.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
4
|
-
import {
|
|
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
|
-
(
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
@@ -20,7 +20,7 @@ Depending on the version manager you're using, the uninstall process differs:
|
|
|
20
20
|
npm uninstall -g @aikidosec/safe-chain
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
4. **Install the binary version** (see [Installation](
|
|
23
|
+
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
|
|
24
24
|
|
|
25
25
|
### nvm (Node Version Manager)
|
|
26
26
|
|
|
@@ -51,7 +51,7 @@ Depending on the version manager you're using, the uninstall process differs:
|
|
|
51
51
|
|
|
52
52
|
Repeat for each Node version where safe-chain was installed.
|
|
53
53
|
|
|
54
|
-
4. **Install the binary version** (see [Installation](
|
|
54
|
+
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
|
|
55
55
|
|
|
56
56
|
### Volta
|
|
57
57
|
|
|
@@ -69,7 +69,7 @@ Depending on the version manager you're using, the uninstall process differs:
|
|
|
69
69
|
volta uninstall @aikidosec/safe-chain
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
4. **Install the binary version** (see [Installation](
|
|
72
|
+
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
|
|
73
73
|
|
|
74
74
|
## Troubleshooting
|
|
75
75
|
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
return runPip(
|
|
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
|
-
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
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 {
|