Fix tests and add command support

This commit is contained in:
Reinier Criel 2025-12-18 10:33:31 +01:00
parent b9de94f0f1
commit d2fc531c81
14 changed files with 198 additions and 462 deletions

View file

@ -23,6 +23,7 @@ Aikido Safe Chain supports the following package managers:
- 📦 **pip3**
- 📦 **uv**
- 📦 **poetry**
- 📦 **pipx**
# Usage
@ -64,7 +65,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
1. **❗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, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
2. **Verify the installation** by running one of the following commands:
@ -82,7 +83,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
- 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`, `uv`, `pip`, `pip3` or `poetry` 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`, `uv`, `pip`, `pip3`, `poetry` and `pipx` 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.
You can check the installed version by running:
@ -100,11 +101,11 @@ The Aikido Safe Chain works by running a lightweight proxy server that intercept
For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours (by default) until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can configure this threshold or bypass this protection entirely - see the [Minimum Package Age Configuration](#minimum-package-age) section below.
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3, poetry).
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3, poetry, pipx).
### Shell Integration
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (uv, pip). 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:
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (uv, pip, poetry, pipx). 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:
- ✅ **Bash**
- ✅ **Zsh**

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. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. 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`, `uv`, `poetry`, `pipx`) 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`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
## Supported Shells
@ -28,7 +28,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`
- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry` and `pipx`
- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` 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.
@ -78,7 +78,7 @@ The system modifies the following files to source Safe Chain startup scripts:
This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
- Make sure Aikido Safe Chain is properly installed on your system
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, `aikido-pip3`, `aikido-uv`, `aikido-poetry` and `aikido-pipx` commands exist
- Check that these commands are in your system's PATH
### Manual Verification
@ -121,7 +121,7 @@ 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.
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry` and `pipx` 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:

1
package-lock.json generated
View file

@ -3131,6 +3131,7 @@
"aikido-npx": "bin/aikido-npx.js",
"aikido-pip": "bin/aikido-pip.js",
"aikido-pip3": "bin/aikido-pip3.js",
"aikido-pipx": "bin/aikido-pipx.js",
"aikido-pnpm": "bin/aikido-pnpm.js",
"aikido-pnpx": "bin/aikido-pnpx.js",
"aikido-poetry": "bin/aikido-poetry.js",

View file

@ -21,6 +21,7 @@
"aikido-python": "bin/aikido-python.js",
"aikido-python3": "bin/aikido-python3.js",
"aikido-poetry": "bin/aikido-poetry.js",
"aikido-pipx": "bin/aikido-pipx.js",
"safe-chain": "bin/safe-chain.js"
},
"type": "module",

View file

@ -10,6 +10,7 @@ import {
} from "./pnpm/createPackageManager.js";
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
import { createPipPackageManager } from "./pip/createPackageManager.js";
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
@ -61,6 +62,8 @@ export function initializePackageManager(packageManagerName, context) {
state.packageManagerName = createUvPackageManager();
} else if (packageManagerName === "poetry") {
state.packageManagerName = createPoetryPackageManager();
} else if (packageManagerName === "pipx") {
state.packageManagerName = createPipXPackageManager();
} else {
throw new Error("Unsupported package manager: " + packageManagerName);
}

View file

@ -11,7 +11,7 @@ export function createPipXPackageManager() {
runCommand: (args) => {
return runPipX("pipx", args);
},
// For uv, rely solely on MITM
// MITM only
isSupportedCommand: () => false,
getDependencyUpdatesForCommand: () => [],
};

View file

@ -16,7 +16,7 @@ function setPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
}
env.SSL_CERT_FILE = combinedCaPath;
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
// REQUESTS_CA_BUNDLE: Used by the requests library (may be used by tooling under pipx)
if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
}
@ -30,18 +30,11 @@ function setPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
}
/**
* Runs a uv command with safe-chain's certificate bundle and proxy configuration.
* Runs a pipx command with safe-chain's certificate bundle and proxy configuration.
*
* uv respects standard environment variables for proxy and TLS configuration:
* - HTTP_PROXY / HTTPS_PROXY: Proxy settings
* - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
*
* Unlike pip (which requires a temporary config file for cert configuration), uv directly
* honors environment variables, so no config/ini file is needed.
*
* @param {string} command - The pipx command to execute
* @param {string[]} args - Command line arguments to pass to pipx
* @returns {Promise<{status: number}>} Exit status of the pipx command
* @param {string} command - The command to execute
* @param {string[]} args - Command line arguments
* @returns {Promise<{status: number}>} Exit status of the command
*/
export async function runPipX(command, args) {
try {

View file

@ -0,0 +1,100 @@
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
describe("runPipXCommand", () => {
let runPipX;
let safeSpawnMock;
let warnMock;
let errorMock;
let mergeCalls;
let mergedEnvReturn;
beforeEach(async () => {
mergeCalls = [];
mergedEnvReturn = {
HTTPS_PROXY: "http://localhost:8080",
HTTP_PROXY: "",
};
safeSpawnMock = mock.fn(async () => ({ status: 0 }));
warnMock = mock.fn();
errorMock = mock.fn();
mock.module("../../environment/userInteraction.js", {
namedExports: {
ui: {
writeWarning: warnMock,
writeError: errorMock,
writeInfo: () => {},
writeVerbose: () => {},
writeSuccess: () => {},
},
},
});
mock.module("../../registryProxy/registryProxy.js", {
namedExports: {
mergeSafeChainProxyEnvironmentVariables: (env) => {
mergeCalls.push(env);
return { ...env, ...mergedEnvReturn };
},
},
});
mock.module("../../registryProxy/certBundle.js", {
namedExports: {
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
},
});
mock.module("../../utils/safeSpawn.js", {
namedExports: {
safeSpawn: safeSpawnMock,
},
});
const mod = await import("./runPipXCommand.js");
runPipX = mod.runPipX;
});
afterEach(() => {
mock.reset();
});
it("sets CA env vars and proxies before spawning", async () => {
const res = await runPipX("pipx", ["install", "ruff"]);
assert.strictEqual(res.status, 0);
assert.strictEqual(safeSpawnMock.mock.calls.length, 1, "safeSpawn should be called once");
const [, , options] = safeSpawnMock.mock.calls[0].arguments;
const env = options.env;
assert.strictEqual(env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem");
assert.strictEqual(env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem");
assert.strictEqual(env.PIP_CERT, "/tmp/test-combined-ca.pem");
assert.strictEqual(env.HTTPS_PROXY, "http://localhost:8080");
assert.strictEqual(env.HTTP_PROXY, "");
assert.ok(mergeCalls.length >= 1, "proxy merge should be invoked");
});
it("overwrites user CA env vars and warns", async () => {
mergedEnvReturn = {
HTTPS_PROXY: "http://localhost:8080",
HTTP_PROXY: "",
SSL_CERT_FILE: "user-ssl",
REQUESTS_CA_BUNDLE: "user-requests",
PIP_CERT: "user-pip",
};
await runPipX("pipx", ["install", "ruff"]);
const [, , options] = safeSpawnMock.mock.calls[0].arguments;
const env = options.env;
assert.strictEqual(env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", "SSL cert should be overwritten");
assert.strictEqual(env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem", "requests bundle should be overwritten");
assert.strictEqual(env.PIP_CERT, "/tmp/test-combined-ca.pem", "pip cert should be overwritten");
assert.strictEqual(warnMock.mock.calls.length, 3, "should warn for each overwritten var");
});
});

View file

@ -98,7 +98,7 @@ export const knownAikidoTools = [
tool: "pipx",
aikidoCommand: "aikido-pipx",
ecoSystem: ECOSYSTEM_PY,
internalPackageManagerName: "pip",
internalPackageManagerName: "pipx",
}
// When adding a new tool here, also update the documentation for the new tool in the README.md
];

View file

@ -39,7 +39,6 @@ function npm
wrapSafeChainCommand "npm" $argv
end
function pip
wrapSafeChainCommand "pip" $argv
end
@ -66,6 +65,10 @@ function python3
wrapSafeChainCommand "python3" $argv
end
function pipx
wrapSafeChainCommand "pipx" $argv
end
function printSafeChainWarning
set original_cmd $argv[1]

View file

@ -35,7 +35,6 @@ function npm() {
wrapSafeChainCommand "npm" "$@"
}
function pip() {
wrapSafeChainCommand "pip" "$@"
}
@ -62,6 +61,10 @@ function python3() {
wrapSafeChainCommand "python3" "$@"
}
function pipx() {
wrapSafeChainCommand "pipx" "$@"
}
function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black
# \033[0m is used to reset the text formatting

View file

@ -66,6 +66,9 @@ function python3 {
Invoke-WrappedCommand 'python3' $args
}
function pipx {
Invoke-WrappedCommand "pipx" $args
}
function Write-SafeChainWarning {
param([string]$Command)

View file

@ -10,563 +10,191 @@ describe("E2E: pipx coverage", () => {
});
beforeEach(async () => {
// Run a new Docker container for each test
container = new DockerTestContainer();
await container.start();
const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup --include-python");
// Clear uv cache
await installationShell.runCommand("uv cache clean");
await installationShell.runCommand("safe-chain setup");
});
afterEach(async () => {
// Stop and clean up the container after each test
if (container) {
await container.stop();
container = null;
}
});
it(`successfully installs known safe packages with uv pip install`, async () => {
it(`successfully installs known safe packages with pipx install`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages requests --safe-chain-logging=verbose"
"pipx install ruff --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
result.output.includes("no malware found.") || result.output.includes("installed successfully"),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`pipx install with specific version`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages requests==2.32.3 --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`pipx install with version specifiers (>=)`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
'uv pip install --system --break-system-packages "Jinja2>=3.1" --safe-chain-logging=verbose'
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip install with extras such as requests[socks]`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
'uv pip install --system --break-system-packages "requests[socks]==2.32.3" --safe-chain-logging=verbose'
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip install multiple packages`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages requests certifi urllib3 --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip install from requirements file`, async () => {
const shell = await container.openShell("zsh");
// Create a requirements.txt file
await shell.runCommand("echo 'requests==2.32.3' > requirements.txt");
await shell.runCommand("echo 'certifi>=2024.0.0' >> requirements.txt");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages -r requirements.txt --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip sync with requirements file`, async () => {
const shell = await container.openShell("zsh");
// Create a requirements.txt file
await shell.runCommand("echo 'requests==2.32.3' > requirements-sync.txt");
const result = await shell.runCommand(
"uv pip sync --system --break-system-packages requirements-sync.txt --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`safe-chain blocks installation of malicious Python packages via uv`, async () => {
it(`safe-chain blocks installation of malicious Python packages via pipx`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages safe-chain-pi-test"
"pipx install 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}`
result.output.includes("blocked by safe-chain"),
`Expected malware to be blocked. 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("uv pip list --system");
assert.ok(
!listResult.output.includes("safe-chain-pi-test"),
`Malicious package was installed despite safe-chain protection. Output of 'uv pip list' was:\n${listResult.output}`
`Expected exit message. Output was:\n${result.output}`
);
});
it(`uv pip install from GitHub URL using the CA bundle`, async () => {
it(`pipx upgrade upgrades installed packages`, async () => {
const shell = await container.openShell("zsh");
await shell.runCommand("pipx install ruff==0.1.0");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages git+https://github.com/psf/requests.git@v2.32.3 --safe-chain-logging=verbose"
"pipx upgrade ruff"
);
assert.ok(
result.output.includes("no malware found."),
result.output.includes("no malware found.") || result.output.includes("Upgraded") || result.output.includes("upgraded"),
`Output did not include expected text. Output was:\n${result.output}`
);
// Verify installation succeeded (would fail if certificate validation via env CA bundle broke)
assert.ok(
result.output.includes("Installed") ||
result.output.includes("installed"),
`Installation from GitHub failed - CA bundle may not be working. Output was:\n${result.output}`
);
});
it(`uv pip successfully validates certificates for HTTPS downloads`, async () => {
it(`pipx run downloads and executes a safe tool`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages certifi --safe-chain-logging=verbose"
"pipx run ruff --version"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
// Verify successful installation (would fail with SSL/certificate errors if the env CA bundle wasn't working)
assert.ok(
result.output.includes("Installed") ||
result.output.includes("installed"),
`Installation should succeed with proper certificate validation. Output was:\n${result.output}`
);
// Should NOT contain SSL or certificate errors
assert.ok(
!result.output.match(
/SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i
),
`Should not have SSL/certificate errors. Output was:\n${result.output}`
result.output.includes("no malware found.") || /ruff/i.test(result.output),
`Expected safe run to succeed. Output was:\n${result.output}`
);
});
it(`uv pip install from direct HTTPS wheel URL`, async () => {
it(`pipx run blocks malicious tool download`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("Installed") ||
result.output.includes("installed"),
`Installation from direct HTTPS URL failed. Output was:\n${result.output}`
);
});
it(`uv pip install with --upgrade flag`, async () => {
const shell = await container.openShell("zsh");
// First install a package
await shell.runCommand(
"uv pip install --system --break-system-packages requests==2.31.0"
);
// Then upgrade it
const result = await shell.runCommand(
"uv pip install --system --break-system-packages --upgrade requests --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip install with --no-deps flag`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages --no-deps requests --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip install with --editable flag from local directory`, async () => {
const shell = await container.openShell("zsh");
// Create a simple package structure
await shell.runCommand("mkdir -p /tmp/test-pkg");
await shell.runCommand(
"echo 'from setuptools import setup' > /tmp/test-pkg/setup.py"
);
await shell.runCommand(
"echo \"setup(name='test-pkg', version='0.1.0')\" >> /tmp/test-pkg/setup.py"
);
const result = await shell.runCommand(
"uv pip install --system --break-system-packages -e /tmp/test-pkg --safe-chain-logging=verbose"
"pipx run safe-chain-pi-test --version"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip compile creates locked requirements`, async () => {
const shell = await container.openShell("zsh");
// Create an input requirements file
await shell.runCommand("echo 'requests' > requirements.in");
const result = await shell.runCommand("uv pip compile requirements.in");
// uv pip compile doesn't install packages, just resolves dependencies
// It should complete successfully and output resolved requirements
assert.ok(
result.output.includes("requests==") || result.output.includes("# via"),
`Output did not include compiled requirements. Output was:\n${result.output}`
);
});
it(`uv pip install with --index-url for alternate registry`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages --index-url https://test.pypi.org/simple certifi --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
// Should succeed if CA bundle properly handles tunneled hosts
assert.ok(
result.output.includes("Installed") ||
result.output.includes("installed"),
`Installation from Test PyPI failed. This may indicate the CA bundle lacks public roots. Output was:\n${result.output}`
);
});
it(`uv pip install with --safe-chain-logging=verbose`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --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}`
);
});
it(`uv pip install with version range constraint`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
'uv pip install --system --break-system-packages "requests>=2.31.0,<2.33.0" --safe-chain-logging=verbose'
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip list shows installed packages`, async () => {
const shell = await container.openShell("zsh");
// Install a package first
await shell.runCommand(
"uv pip install --system --break-system-packages requests"
);
// Then list packages - this shouldn't trigger safe-chain scanning
const result = await shell.runCommand("uv pip list --system");
// List command should work without malware scanning
assert.ok(
result.output.includes("requests") || result.output.length > 0,
`Output did not show package list. Output was:\n${result.output}`
);
});
it(`uv add installs package and updates project`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project and add package in same command
const result = await shell.runCommand(
"uv init test-project && cd test-project && uv add requests --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv add with specific version`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-project-version");
const result = await shell.runCommand(
"cd test-project-version && uv add requests==2.32.3 --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv add --dev for development dependencies`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-project-dev");
const result = await shell.runCommand(
"cd test-project-dev && uv add --dev pytest --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv add multiple packages at once`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-project-multi");
const result = await shell.runCommand(
"cd test-project-multi && uv add requests certifi urllib3 --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`safe-chain blocks malicious packages via uv add`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-project-malware");
const result = await shell.runCommand(
"cd test-project-malware && uv add 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}`
result.output.includes("blocked by safe-chain"),
`Expected malicious run to be blocked. 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}`
`Expected exit message. Output was:\n${result.output}`
);
});
it(`uv tool install installs a global tool`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv tool install ruff --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found.") ||
result.output.includes("Installed"),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`safe-chain blocks malicious packages via uv tool install`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("uv tool install 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}`
);
});
it(`uv run --with installs ephemeral dependency`, async () => {
it(`pipx runpip installs safe dependency inside an app venv`, async () => {
const shell = await container.openShell("zsh");
// Create a simple Python script
await shell.runCommand(
"echo 'import requests; print(requests.__version__)' > test_script.py"
);
// Prepare an app environment
await shell.runCommand("pipx install ruff");
const result = await shell.runCommand(
"uv run --with requests test_script.py --safe-chain-logging=verbose"
"pipx runpip ruff install requests==2.32.3"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
result.output.includes("no malware found.") || /Successfully installed/i.test(result.output) || /requests/i.test(result.output),
`Expected safe dependency install inside app venv. Output was:\n${result.output}`
);
});
it(`safe-chain blocks malicious packages via uv run --with`, async () => {
it(`pipx runpip blocks malicious dependency install`, async () => {
const shell = await container.openShell("zsh");
// Create a simple Python script
await shell.runCommand("echo 'print(\"test\")' > test_script2.py");
// Prepare an app environment
await shell.runCommand("pipx install ruff");
const result = await shell.runCommand(
"uv run --with safe-chain-pi-test test_script2.py"
"pipx runpip ruff install 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}`
result.output.includes("blocked by safe-chain"),
`Expected malicious dependency to be blocked. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("Exiting without installing malicious packages."),
`Expected exit message. Output was:\n${result.output}`
);
});
it(`uv sync syncs project dependencies`, async () => {
it(`pipx list shows installed packages`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project, add a dependency, remove venv, and sync in one command chain
await shell.runCommand("pipx install ruff");
const result = await shell.runCommand(
"uv init test-sync-project && cd test-sync-project && uv add requests --safe-chain-logging=verbose && rm -rf .venv && uv sync --safe-chain-logging=verbose"
"pipx list"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
result.output.includes("ruff"),
`Expected ruff in list output. Output was:\n${result.output}`
);
});
it(`uv add from git URL`, async () => {
it(`pipx uninstall removes packages`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-git-add");
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
await shell.runCommand("pipx uninstall ruff --safe-chain-logging=verbose");
const result = await shell.runCommand(
"cd test-git-add && uv add git+https://github.com/psf/requests.git@v2.32.3 --safe-chain-logging=verbose"
"pipx list"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
!result.output.includes("ruff"),
`Expected ruff to be removed from list. Output was:\n${result.output}`
);
});
it(`uv add with --optional group`, async () => {
it('pipx inject installs safe packages into existing venvs', async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-optional");
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
const result = await shell.runCommand(
"cd test-optional && uv add --optional dev pytest --safe-chain-logging=verbose"
"pipx inject ruff requests==2.32.3 --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
result.output.includes("no malware found.") || /Successfully installed/i.test(result.output) || /requests/i.test(result.output),
`Expected safe package to be injected. Output was:\n${result.output}`
);
});
it(`uv run --with-requirements installs from requirements file`, async () => {
it('pipx inject blocks malicious packages from being installed into existing venvs', async () => {
const shell = await container.openShell("zsh");
// Create requirements file and script
await shell.runCommand("echo 'requests' > run_requirements.txt");
await shell.runCommand(
"echo 'import requests; print(requests.__version__)' > run_script.py"
);
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
const result = await shell.runCommand(
"uv run --with-requirements run_requirements.txt run_script.py --safe-chain-logging=verbose"
"pipx inject ruff safe-chain-pi-test --safe-chain-logging=verbose"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
result.output.includes("blocked by safe-chain"),
`Expected malicious package to be blocked. Output was:\n${result.output}`
);
});
it(`uv sync --all-extras syncs all optional dependencies`, async () => {
const shell = await container.openShell("zsh");
// Initialize project with optional dependency and sync in one command chain
const result = await shell.runCommand(
"uv init test-extras && cd test-extras && uv add --optional dev pytest --safe-chain-logging=verbose && uv sync --all-extras"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
result.output.includes("Exiting without installing malicious packages."),
`Expected exit message. Output was:\n${result.output}`
);
});
});