mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add e2e test for malware blocking + python3 fix
This commit is contained in:
parent
3c109fb5fd
commit
c2e632ead2
6 changed files with 115 additions and 16 deletions
12
README.md
12
README.md
|
|
@ -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.
|
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.
|
- 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
|
```shell
|
||||||
npm install safe-chain-test
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2]
|
||||||
set mod $argv[2]
|
set mod $argv[2]
|
||||||
set args $argv[3..-1]
|
set args $argv[3..-1]
|
||||||
|
# python -m pip → aikido-pip, python -m pip3 → aikido-pip3
|
||||||
if test $mod = "pip3"
|
if test $mod = "pip3"
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" $args
|
wrapSafeChainCommand "pip3" "aikido-pip3" $args
|
||||||
else
|
else
|
||||||
|
|
@ -95,13 +96,9 @@ end
|
||||||
# `python3 -m pip`, `python3 -m pip3'.
|
# `python3 -m pip`, `python3 -m pip3'.
|
||||||
function python3
|
function python3
|
||||||
if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2]
|
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]
|
set args $argv[3..-1]
|
||||||
if test $mod = "pip3"
|
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" $args
|
wrapSafeChainCommand "pip3" "aikido-pip3" $args
|
||||||
else
|
|
||||||
wrapSafeChainCommand "pip" "aikido-pip" $args
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
command python3 $argv
|
command python3 $argv
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ function python() {
|
||||||
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
||||||
local mod="$2"
|
local mod="$2"
|
||||||
shift 2
|
shift 2
|
||||||
|
# python -m pip → aikido-pip, python -m pip3 → aikido-pip3
|
||||||
if [[ "$mod" == "pip3" ]]; then
|
if [[ "$mod" == "pip3" ]]; then
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
||||||
else
|
else
|
||||||
|
|
@ -87,13 +88,9 @@ function python() {
|
||||||
# `python3 -m pip`, `python3 -m pip3'.
|
# `python3 -m pip`, `python3 -m pip3'.
|
||||||
function python3() {
|
function python3() {
|
||||||
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
||||||
local mod="$2"
|
|
||||||
shift 2
|
shift 2
|
||||||
if [[ "$mod" == "pip3" ]]; then
|
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
||||||
else
|
|
||||||
wrapSafeChainCommand "pip" "aikido-pip" "$@"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
command python3 "$@"
|
command python3 "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ function pip3 {
|
||||||
function python {
|
function python {
|
||||||
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
|
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
|
||||||
if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') {
|
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 { @() }
|
$pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() }
|
||||||
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs }
|
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs }
|
||||||
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs }
|
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs }
|
||||||
|
|
@ -112,9 +113,9 @@ function python {
|
||||||
function python3 {
|
function python3 {
|
||||||
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
|
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
|
||||||
if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') {
|
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 { @() }
|
$pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() }
|
||||||
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs }
|
Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs
|
||||||
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs }
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Invoke-RealCommand 'python3' $Args
|
Invoke-RealCommand 'python3' $Args
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@ export async function safeSpawnPy(command, args, options = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", (error) => {
|
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) });
|
resolve({ status: 1, stdout: "", stderr: error.message || String(error) });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue