diff --git a/package-lock.json b/package-lock.json index ee38fa8..a9c32df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2096,6 +2096,8 @@ "aikido-npx": "bin/aikido-npx.js", "aikido-pip": "bin/aikido-pip.js", "aikido-pip3": "bin/aikido-pip3.js", + "aikido-python": "bin/aikido-python.js", + "aikido-python3": "bin/aikido-python3.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 92ba4e3..39184f0 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -3,17 +3,16 @@ 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 new file mode 100755 index 0000000..1ef4e34 --- /dev/null +++ b/packages/safe-chain/bin/aikido-python.js @@ -0,0 +1,28 @@ +#!/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"; + +// Set eco system +setEcoSystem(ECOSYSTEM_PY); + +// Strip nodejs and wrapper script from args +let argv = process.argv.slice(2); + +if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { + setEcoSystem(ECOSYSTEM_PY); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); + + // Strip off the '-m pip' or '-m pip3' from the args + argv = argv.slice(2); + + var exitCode = await main(argv); + process.exit(exitCode); +} else { + // 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 new file mode 100755 index 0000000..f53e5d2 --- /dev/null +++ b/packages/safe-chain/bin/aikido-python3.js @@ -0,0 +1,28 @@ +#!/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"; + +// Set eco system +setEcoSystem(ECOSYSTEM_PY); + +// Strip nodejs and wrapper script from args +let argv = process.argv.slice(2); + +if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { + setEcoSystem(ECOSYSTEM_PY); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); + + // Strip off the '-m pip' or '-m pip3' from the args + argv = argv.slice(2); + + var exitCode = await main(argv); + process.exit(exitCode); +} else { + // 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/package.json b/packages/safe-chain/package.json index d93a058..f21a372 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -17,6 +17,8 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-pip": "bin/aikido-pip.js", "aikido-pip3": "bin/aikido-pip3.js", + "aikido-python": "bin/aikido-python.js", + "aikido-python3": "bin/aikido-python3.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", 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..6ec5d1a 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -1,12 +1,18 @@ 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]; + 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..0316b77 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -0,0 +1,30 @@ +export const PIP_PACKAGE_MANAGER = "pip"; + +// All supported 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"] }, + PY_PIP3: { command: "python", args: ["-m", "pip3"] }, + PY3_PIP3: { command: "python3", args: ["-m", "pip3"] } +}; + +/** + * @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) { + currentInvocation = invocation; +} + +/** + * @returns {{ command: string, args: string[] }} + */ +export function getCurrentPipInvocation() { + return currentInvocation; +} diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 6fae388..793302d 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -29,7 +29,8 @@ export async function runPip(command, args) { if (error.status) { return { status: error.status }; } else { - ui.writeError("Error executing command:", error.message); + ui.writeError(`Error executing command: ${error.message}`); + ui.writeError(`Is '${command}' installed and available on your system?`); return { status: 1 }; } } diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 803051a..4e292b3 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -57,8 +57,6 @@ export async function auditChanges(changes) { ); for (const change of changes) { - //Uncomment next line during manual testing - //console.log(" Safe-chain: auditing package:", change); const malwarePackage = malwarePackages.find( (pkg) => pkg.name === change.name && pkg.version === change.version ); diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 4ba7c24..c405c54 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -22,6 +22,8 @@ export const knownAikidoTools = [ { tool: "bunx", aikidoCommand: "aikido-bunx" }, { tool: "pip", aikidoCommand: "aikido-pip" }, { tool: "pip3", aikidoCommand: "aikido-pip3" }, + { tool: "python", aikidoCommand: "aikido-python" }, + { tool: "python3", aikidoCommand: "aikido-python3" }, // When adding a new tool here, also update the documentation for the new tool in the README.md ]; diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh index 6e6d826..e914e5b 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -19,4 +19,4 @@ else echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2 exit 1 fi -fi \ No newline at end of file +fi diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 64fff16..8793832 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -51,13 +51,9 @@ function createUnixShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool except pip (CI support not yet implemented) + // Create a shim for each tool let created = 0; for (const toolInfo of knownAikidoTools) { - if (toolInfo.tool === "pip") { - continue; // Skip pip shims in CI for now - } - const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); @@ -98,18 +94,14 @@ function createWindowsShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool except pip (CI support not yet implemented) + // Create a shim for each tool let created = 0; for (const toolInfo of knownAikidoTools) { - if (toolInfo.tool === "pip") { - continue; // Skip pip shims in CI for now - } - const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); - const shimPath = path.join(shimsDir, `${toolInfo.tool}.cmd`); + const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`; fs.writeFileSync(shimPath, shimContent, "utf-8"); created++; } diff --git a/packages/safe-chain/src/shell-integration/setup-ci.spec.js b/packages/safe-chain/src/shell-integration/setup-ci.spec.js index 0a26124..92ef82e 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.spec.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.spec.js @@ -147,4 +147,4 @@ describe("Setup CI shell integration", () => { assert.ok(!fs.existsSync(unixNpmShim), "Unix npm shim should not exist on Windows"); }); }); -}); \ No newline at end of file +}); diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index 13494d1..ebf89ff 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -79,26 +79,10 @@ end # `python -m pip`, `python -m pip3`. function python - if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] - set mod $argv[2] - set args $argv[3..-1] - if test $mod = "pip3" - wrapSafeChainCommand "pip3" "aikido-pip3" $args - else - wrapSafeChainCommand "pip" "aikido-pip" $args - end - else - command python $argv - end + wrapSafeChainCommand "python" "aikido-python" $argv end # `python3 -m pip`, `python3 -m pip3'. function python3 - if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] - set args $argv[3..-1] - # python3 always uses pip3, regardless of whether user types `pip` or `pip3` - wrapSafeChainCommand "pip3" "aikido-pip3" $args - else - command python3 $argv - end + wrapSafeChainCommand "python3" "aikido-python3" $argv end diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index 05b8b81..278b31a 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -71,26 +71,10 @@ function pip3() { # `python -m pip`, `python -m pip3`. function python() { - if [[ "$1" == "-m" && "$2" == pip* ]]; then - local mod="$2" - shift 2 - if [[ "$mod" == "pip3" ]]; then - wrapSafeChainCommand "pip3" "aikido-pip3" "$@" - else - wrapSafeChainCommand "pip" "aikido-pip" "$@" - fi - else - command python "$@" - fi + wrapSafeChainCommand "python" "aikido-python" "$@" } # `python3 -m pip`, `python3 -m pip3'. function python3() { - if [[ "$1" == "-m" && "$2" == pip* ]]; then - shift 2 - # python3 always uses pip3, regardless of whether user types `pip` or `pip3` - wrapSafeChainCommand "pip3" "aikido-pip3" "$@" - else - command python3 "$@" - fi + wrapSafeChainCommand "python3" "aikido-python3" "$@" } diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index 6425f2f..b692107 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -97,27 +97,11 @@ function pip3 { # `python -m pip`, `python -m pip3`. function python { - param([Parameter(ValueFromRemainingArguments=$true)]$Args) - if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { - $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } - if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } - else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs } - } - else { - Invoke-RealCommand 'python' $Args - } + Invoke-WrappedCommand 'python' 'aikido-python' $args } # `python3 -m pip`, `python3 -m pip3'. function python3 { - param([Parameter(ValueFromRemainingArguments=$true)]$Args) - if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { - # python3 always uses pip3, regardless of whether user types `pip` or `pip3` - $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } - Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs - } - else { - Invoke-RealCommand 'python3' $Args - } + Invoke-WrappedCommand 'python3' 'aikido-python3' $args } diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index e590d19..cf5f39b 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -53,7 +53,19 @@ 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/pip3 /usr/local/bin/pip3 + ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python && \ + ln -sf /usr/bin/pip3 /usr/local/bin/pip3 && \ + cat <<'EOF' > /usr/lib/python3/dist-packages/pip3.py +""" +Shim module so 'python[3] -m pip3 …' resolves to pip's CLI entry point. +""" +try: + import pip._internal + pip._internal.main() +except Exception as exc: + print("pip3 module shim failed:", exc) + raise +EOF # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js new file mode 100644 index 0000000..fe013bb --- /dev/null +++ b/test/e2e/pip-ci.e2e.spec.js @@ -0,0 +1,171 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: safe-chain setup-ci command for pip/pip3", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + container = new DockerTestContainer(); + await container.start(); + }); + + afterEach(async () => { + if (container) { + await container.stop(); + container = null; + } + }); + + describe("E2E: pip CI support", () => { + it("does not intercept python3 --version", async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python3 --version"); + assert.ok(result.output.match(/Python \d+\.\d+\.\d+/), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 command"); + }); + + it("does not intercept python3 -c 'print(\"hello\")'", async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python3 -c 'print(\"hello\")'"); + 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"]) { + it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => { + // Setup safe-chain CI shims + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + + // Add $HOME/.safe-chain/shims to PATH for subsequent shells + 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); + // Use --break-system-packages to avoid Debian/Ubuntu external management restrictions + const result = await projectShell.runCommand( + "pip3 install --break-system-packages certifi" + ); + + const hasExpectedOutput = result.output.includes( + "no malware found." + ); + assert.ok( + hasExpectedOutput, + hasExpectedOutput + ? "Expected pip3 command to be wrapped by safe-chain" + : `Output did not contain \"no malware found.\": \n${result.output}` + ); + }); + + it(`setup-ci routes python -m pip 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 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 python3 -m pip 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( + "python3 -m 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 pip 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( + "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( + result.output.includes("no malware found."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + } +}); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 5d046a7..3d3b4dd 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -323,4 +323,12 @@ describe("E2E: pip coverage", () => { `Should not have SSL/certificate errors for tunneled hosts. Output was:\n${result.output}` ); }); + + it(`pip3 install requests with --safe-chain-logging=verbose`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "pip3 install --break-system-packages requests --safe-chain-logging=verbose" + ); + assert.ok(result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}`); + }); });