Add e2e test for malware blocking + python3 fix

This commit is contained in:
Reinier Criel 2025-10-28 08:46:07 -07:00
parent 3c109fb5fd
commit c2e632ead2
6 changed files with 115 additions and 16 deletions

View file

@ -34,11 +34,19 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
```
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
4. **Verify the installation** by running:
4. **Verify the installation** by running one of the following commands:
For JavaScript/Node.js:
```shell
npm install safe-chain-test
```
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
For Python:
```shell
pip3 install safe-chain-pi-test
```
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are 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. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`). If any malware is detected, it will prompt you to exit the command.

View file

@ -82,6 +82,7 @@ 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]
# python -m pip → aikido-pip, python -m pip3 → aikido-pip3
if test $mod = "pip3"
wrapSafeChainCommand "pip3" "aikido-pip3" $args
else
@ -95,13 +96,9 @@ 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
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
wrapSafeChainCommand "pip3" "aikido-pip3" $args
else
command python3 $argv
end

View file

@ -74,6 +74,7 @@ function python() {
if [[ "$1" == "-m" && "$2" == pip* ]]; then
local mod="$2"
shift 2
# python -m pip → aikido-pip, python -m pip3 → aikido-pip3
if [[ "$mod" == "pip3" ]]; then
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
else
@ -87,13 +88,9 @@ function python() {
# `python3 -m pip`, `python3 -m pip3'.
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
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
else
command python3 "$@"
fi

View file

@ -99,6 +99,7 @@ function pip3 {
function python {
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') {
# python -m pip → aikido-pip, python -m pip3 → aikido-pip3
$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 }
@ -112,9 +113,9 @@ function python {
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 { @() }
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs }
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs }
Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs
}
else {
Invoke-RealCommand 'python3' $Args

View file

@ -126,6 +126,15 @@ export async function safeSpawnPy(command, args, options = {}) {
});
child.on("error", (error) => {
// When stdio is inherited and spawn fails (e.g., command not found),
// we need to write the error to stderr manually since there's no child process
if (options.stdio === "inherit") {
if (error.code === "ENOENT") {
process.stderr.write(`Error: Command '${command}' not found. Please ensure it is installed and available in your PATH.\n`);
} else {
process.stderr.write(`Error: ${error.message}\n`);
}
}
resolve({ status: 1, stdout: "", stderr: error.message || String(error) });
});
});

View file

@ -106,4 +106,91 @@ describe("E2E: pip coverage", () => {
);
});
it(`safe-chain blocks installation of malicious Python packages`, async () => {
const shell = await container.openShell("zsh");
// Clear pip cache to ensure network download through proxy
await shell.runCommand("pip3 cache purge");
const result = await shell.runCommand("pip3 install --break-system-packages safe-chain-pi-test");
assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("safe_chain_pi_test@0.0.1"),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("Exiting without installing malicious packages."),
`Output did not include expected text. Output was:\n${result.output}`
);
const listResult = await shell.runCommand("pip3 list");
assert.ok(
!listResult.output.includes("safe-chain-pi-test"),
`Malicious package was installed despite safe-chain protection. Output of 'pip3 list' was:\n${listResult.output}`
);
});
it(`python -m pip routes to aikido-pip (uses pip command)`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand('python -m pip install --break-system-packages requests');
assert.ok(
result.output.includes("no malicious packages 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(`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 malicious packages 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('python3 -m pip install --break-system-packages requests');
assert.ok(
result.output.includes("no malicious packages 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 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 malicious packages 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}`
);
});
});