Add redirecting for explicit python(3) commands

This commit is contained in:
Reinier Criel 2025-10-27 13:00:18 -07:00
parent f6381f5e91
commit 57bbb06f39
6 changed files with 143 additions and 5 deletions

View file

@ -1,6 +1,6 @@
# Aikido Safe Chain
The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token.
The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3, including `python -m pip[...]` and `python3 -m pip[...]` where available) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token.
The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, or pip/pip3 from downloading or running the malware.
@ -40,7 +40,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
```
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`, and on Windows `py -m pip ...`). If any malware is detected, it will prompt you to exit the command.
You can check the installed version by running:
@ -50,7 +50,7 @@ safe-chain --version
## How it works
The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, bun, bunx, `pip`, or `pip3` commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, bun, bunx, `pip`, or `pip3` commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. Python `-m pip[...]` invocations are also routed when invoked by command name. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:

View file

@ -2,7 +2,7 @@
## Overview
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`, and on Windows PowerShell `py -m pip`/`py -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
## Supported Shells
@ -29,6 +29,7 @@ This command:
- Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
- Detects all supported shells on your system
- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3`
- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` (and `py -m pip[...]` on Windows PowerShell) route through Safe Chain when invoked by name
❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly.
@ -80,6 +81,12 @@ This means the shell functions are working but the Aikido commands aren't instal
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist
- Check that these commands are in your system's PATH
**`python -m pip` is not being intercepted:**
- Ensure you are invoking `python`/`python3`/`py` by name (not via an absolute path). Shell function interception only occurs for command names resolved through PATH and wont catch absolute paths like `/usr/bin/python -m pip`.
- Restart your terminal so the updated startup scripts are sourced.
- On Windows PowerShell, verify `python`, `python3` or `py` resolves by running `Get-Command python` / `Get-Command py`.
### Manual Verification
To verify the integration is working, follow these steps:
@ -99,6 +106,7 @@ To verify the integration is working, follow these steps:
- `npm --version` - Should show output from the Aikido-wrapped version
- `type npm` - Should show that `npm` is a function
- Optionally: `python -m pip --version` (or `python3 -m pip --version`) should show Safe Chain output at the end
3. **If you need to remove the integration manually:**
@ -121,3 +129,28 @@ npm() {
```
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
To intercept Python module invocations for pip without altering Python itself, you can add small forwarding functions:
```bash
# Example for Bash/Zsh
python() {
if [[ "$1" == "-m" && "$2" == pip* ]]; then
local mod="$2"; shift 2
if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi
else
command python "$@"
fi
}
python3() {
if [[ "$1" == "-m" && "$2" == pip* ]]; then
local mod="$2"; shift 2
if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi
else
command python3 "$@"
fi
}
```
Limitations: these only apply when invoking `python`/`python3` by name. Absolute paths (e.g., `/usr/bin/python -m pip`) bypass shell functions.

View file

@ -76,3 +76,33 @@ end
function pip3
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
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
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 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 python3 $argv
end
end

View file

@ -68,3 +68,33 @@ function pip() {
function pip3() {
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
}
# Intercept `python -m pip[...]` so it routes through safe-chain without changing python itself.
# Supports: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -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
}
function python3() {
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 python3 "$@"
fi
}

View file

@ -94,3 +94,28 @@ function pip {
function pip3 {
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
}
# `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)?$') {
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $Args[2..($Args.Length-1)] }
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $Args[2..($Args.Length-1)] }
}
else {
Invoke-RealCommand '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)?$') {
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $Args[2..($Args.Length-1)] }
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $Args[2..($Args.Length-1)] }
}
else {
Invoke-RealCommand 'python3' $Args
}
}

View file

@ -86,4 +86,24 @@ describe("E2E: pip coverage", () => {
);
});
it(`python3 -m pip install routes through safe-chain`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand('python3 -m pip install requests');
assert.ok(
result.output.includes("no malicious packages found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`python3 -m pip download routes through safe-chain`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand('python3 -m pip download requests');
assert.ok(
result.output.includes("no malicious packages found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
});