From 28d24bb6eaabd66253d159a111a75da3ec0d3b3b Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 10:26:26 -0800 Subject: [PATCH] Another iteration --- packages/safe-chain/bin/aikido-pip.js | 16 ++--- packages/safe-chain/bin/aikido-pip3.js | 18 +++--- packages/safe-chain/bin/aikido-python.js | 25 ++++---- packages/safe-chain/bin/aikido-python3.js | 27 ++++---- .../packagemanager/currentPackageManager.js | 4 +- .../pip/createPackageManager.js | 15 +++-- .../src/packagemanager/pip/pipSettings.js | 31 ++++++++++ .../src/packagemanager/pip/runPipCommand.js | 2 +- .../src/registryProxy/registryProxy.js | 4 +- test/e2e/Dockerfile | 1 + test/e2e/pip-ci.e2e.spec.js | 62 ++++++++++++------- test/e2e/pip.e2e.spec.js | 36 ----------- 12 files changed, 134 insertions(+), 107 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/pip/pipSettings.js diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 92ba4e3..59951ed 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -1,19 +1,19 @@ #!/usr/bin/env node + import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; - -// Defaults -let packageManagerName = "pip"; -// Pass through user args as-is -const argv = process.argv.slice(2); +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; // Set eco system -// This can be used in other parts of the code to determine which eco system we are working with setEcoSystem(ECOSYSTEM_PY); -initializePackageManager(packageManagerName); -var exitCode = await main(argv); +// Set current invocation +setCurrentPipInvocation(PIP_INVOCATIONS.PIP); +initializePackageManager(PIP_PACKAGE_MANAGER); + +// Pass through only user-supplied pip args +var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index e24fda4..e388383 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -3,17 +3,17 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; -// Explicit pip3 entrypoint -const packageManagerName = "pip3"; - -// Copy argv as-is -const argv = process.argv.slice(2); - -// Set ecosystem to Python +// Set eco system setEcoSystem(ECOSYSTEM_PY); -initializePackageManager(packageManagerName); -var exitCode = await main(argv); +// Set current invocation +setCurrentPipInvocation(PIP_INVOCATIONS.PIP3); +// Create package manager +initializePackageManager(PIP_PACKAGE_MANAGER); + +// Pass through only user-supplied pip args +var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index c22c601..fba6b70 100644 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -1,22 +1,25 @@ #!/usr/bin/env node - import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; import { main } from "../src/main.js"; -const argv = process.argv.slice(2); +// Set eco system +setEcoSystem(ECOSYSTEM_PY); -const supportedArgs = ["pip", "pip3"]; -if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) { +// Strip '-m pip' or '-m pip3' from args if present +let argv = process.argv.slice(2); +if (argv[0] === '-m' && argv[1] === 'pip') { setEcoSystem(ECOSYSTEM_PY); - - initializePackageManager(argv[1]); - var exitCode = await main(argv.slice(2)); - process.exit(exitCode); + setCurrentPipInvocation(PIP_INVOCATIONS.PY_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); + argv = argv.slice(2); + var exitCode = await main(argv); + process.exit(exitCode); } else { - // Fallback: run the real python - const { spawn } = await import("child_process"); - spawn("python", argv, { stdio: "inherit" }); + // Forward to real python binary for non-pip flows + const { spawn } = await import('child_process'); + spawn('python', argv, { stdio: 'inherit' }); } diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index 48659e5..c74a3f3 100644 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -1,22 +1,25 @@ #!/usr/bin/env node - import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; import { main } from "../src/main.js"; -const argv = process.argv.slice(2); +// Set eco system +setEcoSystem(ECOSYSTEM_PY); -const supportedArgs = ["pip", "pip3"]; - -if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) { +// Strip nodejs and wrapper script from args +let argv = process.argv.slice(2); +if (argv[0] === '-m' && argv[1] === 'pip') { setEcoSystem(ECOSYSTEM_PY); - // python3 -m pip or python3 -m pip3: always use pip3 package manager - initializePackageManager("pip3"); - var exitCode = await main(argv.slice(2)); - process.exit(exitCode); + setCurrentPipInvocation(PIP_INVOCATIONS.PY3_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); + // Strip '-m pip' or '-m pip3' from args if present + argv = argv.slice(2); + var exitCode = await main(argv); + process.exit(exitCode); } else { - // Fallback: run the real python3 - const { spawn } = await import("child_process"); - spawn("python3", argv, { stdio: "inherit" }); + // Forward to real python3 binary for non-pip flows + const { spawn } = await import('child_process'); + spawn('python3', argv, { stdio: 'inherit' }); } diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 42cb93e..2db4167 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -52,8 +52,8 @@ export function initializePackageManager(packageManagerName) { state.packageManagerName = createBunPackageManager(); } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); - } else if (packageManagerName === "pip" || packageManagerName === "pip3") { - state.packageManagerName = createPipPackageManager(packageManagerName); + } else if (packageManagerName === "pip") { + state.packageManagerName = createPipPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index cb5484d..6415dcc 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -1,12 +1,19 @@ import { runPip } from "./runPipCommand.js"; - +import { getCurrentPipInvocation } from "./pipSettings.js"; /** - * @param {string} [command] * @returns {import("../currentPackageManager.js").PackageManager} */ -export function createPipPackageManager(command = "pip") { +export function createPipPackageManager() { return { - runCommand: /** @param {string[]} args */ (args) => runPip(command, args), + /** + * @param {string[]} args + */ + runCommand: (args) => { + const invocation = getCurrentPipInvocation(); + const fullArgs = [...invocation.args, ...args]; + console.debug('[safe-chain debug] runCommand:', invocation.command, fullArgs); + return runPip(invocation.command, fullArgs); + }, // For pip, rely solely on MITM proxy to detect/deny downloads from known registries. isSupportedCommand: () => false, getDependencyUpdatesForCommand: () => [], diff --git a/packages/safe-chain/src/packagemanager/pip/pipSettings.js b/packages/safe-chain/src/packagemanager/pip/pipSettings.js new file mode 100644 index 0000000..2dd7929 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -0,0 +1,31 @@ +// Constant for pip package manager name +export const PIP_PACKAGE_MANAGER = "pip"; + +// Enum of possible Python/pip invocations for Safe Chain interception +export const PIP_INVOCATIONS = { + PIP: { command: "pip", args: [] }, + PIP3: { command: "pip3", args: [] }, + PY_PIP: { command: "python", args: ["-m", "pip"] }, + PY3_PIP: { command: "python3", args: ["-m", "pip"] } +}; + +/** + * @type {{ command: string, args: string[] }} + */ +let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip + +/** + * @param {{ command: string, args: string[] }} invocation + */ +export function setCurrentPipInvocation(invocation) { + console.debug('[safe-chain debug] setCurrentPipInvocation:', invocation); + currentInvocation = invocation; +} + +/** + * @returns {{ command: string, args: string[] }} + */ +export function getCurrentPipInvocation() { + console.debug('[safe-chain debug] getCurrentPipInvocation:', currentInvocation); + return currentInvocation; +} diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 6fae388..e3252f9 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -26,10 +26,10 @@ export async function runPip(command, args) { }); return { status: result.status }; } catch (/** @type any */ error) { + ui.writeError("Error executing command:", error.message); if (error.status) { return { status: error.status }; } else { - ui.writeError("Error executing command:", error.message); return { status: 1 }; } } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index c5e272b..57027fc 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -142,10 +142,12 @@ function handleConnect(req, clientSocket, head) { isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg)); } + // Debug: log CONNECT request URL and MITM/tunnel decision + ui.writeVerbose(`[Safe-chain debug] CONNECT request: url=${url}, ecosystem=${ecosystem}, isKnownRegistry=${isKnownRegistry}`); + if (isKnownRegistry) { mitmConnect(req, clientSocket, isAllowedUrl); } else { - // For other hosts, just tunnel the request to the destination tcp socket ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index e590d19..6c9743e 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -53,6 +53,7 @@ RUN curl -fsSL https://bun.sh/install | bash # Install Python and pip (pip3) RUN apt-get update && apt-get install -y python${PYTHON_VERSION} python3-pip && \ ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 && \ + ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python && \ ln -sf /usr/bin/pip3 /usr/local/bin/pip3 # Copy and install Safe chain diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index bcca90a..fe013bb 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -35,6 +35,22 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { assert.ok(result.output.includes("hello"), `Output was: ${result.output}`); assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command"); }); + + it("does not intercept python3 test.py", async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); + const result = await shell.runCommand("python3 test.py"); + assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 script execution"); + }); + + it("does not intercept python test.py", async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); + const result = await shell.runCommand("python test.py"); + assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python script execution"); + }); }); for (let shell of ["bash", "zsh"]) { @@ -89,27 +105,6 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); }); - it(`setup-ci routes python -m pip3 through safe-chain for ${shell}`, async () => { - const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); - await installationShell.runCommand( - "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" - ); - await installationShell.runCommand( - "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" - ); - - const projectShell = await container.openShell(shell); - const result = await projectShell.runCommand( - "python -m pip3 install --break-system-packages certifi" - ); - - assert.ok( - result.output.includes("no malware found."), - `Output did not contain scan message. Output was:\n${result.output}` - ); - }); - it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand("safe-chain setup-ci"); @@ -131,7 +126,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); }); - it(`setup-ci routes python3 -m pip3 through safe-chain for ${shell}`, async () => { + it(`setup-ci routes pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand("safe-chain setup-ci"); await installationShell.runCommand( @@ -143,7 +138,28 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { const projectShell = await container.openShell(shell); const result = await projectShell.runCommand( - "python3 -m pip3 install --break-system-packages certifi" + "pip install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + + it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => { + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + const result = await projectShell.runCommand( + "pip3 install --break-system-packages certifi" ); assert.ok( diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 5d046a7..0cb6c2b 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -161,24 +161,6 @@ describe("E2E: pip coverage", () => { ); }); - it(`python -m pip3 routes to aikido-pip3 (uses pip3 command)`, async () => { - const shell = await container.openShell("zsh"); - const result = await shell.runCommand( - "python -m pip3 install --break-system-packages requests" - ); - - assert.ok( - result.output.includes("no malware found."), - `Output did not include expected text. Output was:\n${result.output}` - ); - // Verify it completed successfully (would fail if routing was incorrect) - assert.ok( - result.output.includes("Successfully installed") || - result.output.includes("Requirement already satisfied"), - `Installation did not succeed. Output was:\n${result.output}` - ); - }); - it(`python3 -m pip routes to aikido-pip3 (uses pip3 command)`, async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand( @@ -197,24 +179,6 @@ describe("E2E: pip coverage", () => { ); }); - it(`python3 -m pip3 routes to aikido-pip3 (uses pip3 command)`, async () => { - const shell = await container.openShell("zsh"); - const result = await shell.runCommand( - "python3 -m pip3 install --break-system-packages requests" - ); - - assert.ok( - result.output.includes("no malware found."), - `Output did not include expected text. Output was:\n${result.output}` - ); - // Verify it completed successfully (would fail if routing was incorrect) - assert.ok( - result.output.includes("Successfully installed") || - result.output.includes("Requirement already satisfied"), - `Installation did not succeed. Output was:\n${result.output}` - ); - }); - it(`pip3 can install from GitHub URL using the CA bundle`, async () => { const shell = await container.openShell("zsh"); // Install a simple package from GitHub - this should use TCP tunnel, not MITM