Merge pull request #168 from AikidoSec/feature/uv

Add uv (Astral Python Package Mgr) support
This commit is contained in:
bitterpanda 2025-11-26 13:20:45 +01:00 committed by GitHub
commit 5c3c3399d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 709 additions and 9 deletions

View file

@ -1,8 +1,8 @@
# Aikido Safe Chain # Aikido Safe Chain
The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing 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 Javascript and Python ecosystems (through npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, and pip). 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. 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), [uv](https://docs.astral.sh/uv/) (Python), 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, pip/pip3 or uv from downloading or running the malware.
Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers: Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers:
@ -15,6 +15,7 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi
- ✅ **bunx** - ✅ **bunx**
- ✅ **pip** (beta) - ✅ **pip** (beta)
- ✅ **pip3** (beta) - ✅ **pip3** (beta)
- ✅ **uv** (beta)
# Usage # Usage
@ -32,7 +33,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
safe-chain setup safe-chain setup
``` ```
To enable Python (pip/pip3) support (beta), use the `--include-python` flag: To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag:
```shell ```shell
safe-chain setup --include-python safe-chain setup --include-python
@ -58,7 +59,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 these test packages as they are flagged as malware. - 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`, `uv`, `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.
You can check the installed version by running: You can check the installed version by running:
@ -70,17 +71,17 @@ safe-chain --version
### Malware Blocking ### Malware Blocking
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, uv, `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.
### Minimum package age (npm only) ### Minimum package age (npm only)
For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours 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 bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag. For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours 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 bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag.
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip. ⚠️ 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).
### Shell Integration ### 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 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: 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:
- ✅ **Bash** - ✅ **Bash**
- ✅ **Zsh** - ✅ **Zsh**
@ -140,7 +141,7 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after
safe-chain setup-ci safe-chain setup-ci
``` ```
To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag: To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag:
```shell ```shell
safe-chain setup-ci --include-python safe-chain setup-ci --include-python

View file

@ -0,0 +1,14 @@
#!/usr/bin/env node
import { main } from "../src/main.js";
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
// Set eco system
setEcoSystem(ECOSYSTEM_PY);
initializePackageManager("uv");
// Pass through only user-supplied uv args
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);

View file

@ -15,6 +15,7 @@
"aikido-pnpx": "bin/aikido-pnpx.js", "aikido-pnpx": "bin/aikido-pnpx.js",
"aikido-bun": "bin/aikido-bun.js", "aikido-bun": "bin/aikido-bun.js",
"aikido-bunx": "bin/aikido-bunx.js", "aikido-bunx": "bin/aikido-bunx.js",
"aikido-uv": "bin/aikido-uv.js",
"aikido-pip": "bin/aikido-pip.js", "aikido-pip": "bin/aikido-pip.js",
"aikido-pip3": "bin/aikido-pip3.js", "aikido-pip3": "bin/aikido-pip3.js",
"aikido-python": "bin/aikido-python.js", "aikido-python": "bin/aikido-python.js",
@ -33,7 +34,7 @@
"keywords": [], "keywords": [],
"author": "Aikido Security", "author": "Aikido Security",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"description": "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/), and [bunx](https://bun.sh/docs/cli/bunx) 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, or bunx from downloading or running the malware.", "description": "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), [uv](https://docs.astral.sh/uv/) (Python), 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, uv, or pip/pip3 from downloading or running the malware.",
"dependencies": { "dependencies": {
"certifi": "14.5.15", "certifi": "14.5.15",
"chalk": "5.4.1", "chalk": "5.4.1",

View file

@ -10,6 +10,7 @@ import {
} from "./pnpm/createPackageManager.js"; } from "./pnpm/createPackageManager.js";
import { createYarnPackageManager } from "./yarn/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js";
import { createPipPackageManager } from "./pip/createPackageManager.js"; import { createPipPackageManager } from "./pip/createPackageManager.js";
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
/** /**
* @type {{packageManagerName: PackageManager | null}} * @type {{packageManagerName: PackageManager | null}}
@ -54,6 +55,8 @@ export function initializePackageManager(packageManagerName) {
state.packageManagerName = createBunxPackageManager(); state.packageManagerName = createBunxPackageManager();
} else if (packageManagerName === "pip") { } else if (packageManagerName === "pip") {
state.packageManagerName = createPipPackageManager(); state.packageManagerName = createPipPackageManager();
} else if (packageManagerName === "uv") {
state.packageManagerName = createUvPackageManager();
} else { } else {
throw new Error("Unsupported package manager: " + packageManagerName); throw new Error("Unsupported package manager: " + packageManagerName);
} }

View file

@ -0,0 +1,18 @@
import { runUv } from "./runUvCommand.js";
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createUvPackageManager() {
return {
/**
* @param {string[]} args
*/
runCommand: (args) => {
return runUv("uv", args);
},
// For uv, rely solely on MITM
isSupportedCommand: () => false,
getDependencyUpdatesForCommand: () => [],
};
}

View file

@ -0,0 +1,14 @@
import { test } from "node:test";
import assert from "node:assert";
import { createUvPackageManager } from "./createUvPackageManager.js";
test("createUvPackageManager", async (t) => {
await t.test("should create package manager with required interface", () => {
const pm = createUvPackageManager();
assert.ok(pm);
assert.strictEqual(typeof pm.runCommand, "function");
assert.strictEqual(typeof pm.isSupportedCommand, "function");
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
});
});

View file

@ -0,0 +1,71 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
/**
* Sets CA bundle environment variables used by Python libraries and uv.
*
* @param {NodeJS.ProcessEnv} env - Env object
* @param {string} combinedCaPath - Path to the combined CA bundle
*/
function setUvCaBundleEnvironmentVariables(env, combinedCaPath) {
// SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients
if (env.SSL_CERT_FILE) {
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
}
env.SSL_CERT_FILE = combinedCaPath;
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
}
env.REQUESTS_CA_BUNDLE = combinedCaPath;
// PIP_CERT: Some underlying pip operations may respect this
if (env.PIP_CERT) {
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
}
env.PIP_CERT = combinedCaPath;
}
/**
* Runs a uv 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 uv command to execute (typically 'uv')
* @param {string[]} args - Command line arguments to pass to uv
* @returns {Promise<{status: number}>} Exit status of the uv command
*/
export async function runUv(command, args) {
try {
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
const combinedCaPath = getCombinedCaBundlePath();
setUvCaBundleEnvironmentVariables(env, combinedCaPath);
// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
// These are already set by mergeSafeChainProxyEnvironmentVariables
const result = await safeSpawn(command, args, {
stdio: "inherit",
env,
});
return { status: result.status };
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {
ui.writeError(`Error executing command: ${error.message}`);
ui.writeError(`Is '${command}' installed and available on your system?`);
return { status: 1 };
}
}
}

View file

@ -22,6 +22,7 @@ export const knownAikidoTools = [
{ tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS }, { tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
{ tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS }, { tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS },
{ tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS }, { tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS },
{ tool: "uv", aikidoCommand: "aikido-uv", ecoSystem: ECOSYSTEM_PY },
{ tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY }, { tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY },
{ tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY }, { tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
{ tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY }, { tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },

View file

@ -77,6 +77,10 @@ function pip3
wrapSafeChainCommand "pip3" "aikido-pip3" $argv wrapSafeChainCommand "pip3" "aikido-pip3" $argv
end end
function uv
wrapSafeChainCommand "uv" "aikido-uv" $argv
end
# `python -m pip`, `python -m pip3`. # `python -m pip`, `python -m pip3`.
function python function python
wrapSafeChainCommand "python" "aikido-python" $argv wrapSafeChainCommand "python" "aikido-python" $argv

View file

@ -69,6 +69,10 @@ function pip3() {
wrapSafeChainCommand "pip3" "aikido-pip3" "$@" wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
} }
function uv() {
wrapSafeChainCommand "uv" "aikido-uv" "$@"
}
# `python -m pip`, `python -m pip3`. # `python -m pip`, `python -m pip3`.
function python() { function python() {
wrapSafeChainCommand "python" "aikido-python" "$@" wrapSafeChainCommand "python" "aikido-python" "$@"

View file

@ -95,6 +95,10 @@ function pip3 {
Invoke-WrappedCommand "pip3" "aikido-pip3" $args Invoke-WrappedCommand "pip3" "aikido-pip3" $args
} }
function uv {
Invoke-WrappedCommand "uv" "aikido-uv" $args
}
# `python -m pip`, `python -m pip3`. # `python -m pip`, `python -m pip3`.
function python { function python {
Invoke-WrappedCommand 'python' 'aikido-python' $args Invoke-WrappedCommand 'python' 'aikido-python' $args

View file

@ -67,6 +67,10 @@ except Exception as exc:
raise raise
EOF EOF
# Install uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
echo 'source $HOME/.local/bin/env' >> ~/.bashrc
# Copy and install Safe chain # Copy and install Safe chain
COPY --from=builder /app/*.tgz /pkgs/ COPY --from=builder /app/*.tgz /pkgs/
RUN npm install -g /pkgs/*.tgz RUN npm install -g /pkgs/*.tgz

561
test/e2e/uv.e2e.spec.js Normal file
View file

@ -0,0 +1,561 @@
import { describe, it, before, beforeEach, afterEach } from "node:test";
import { DockerTestContainer } from "./DockerTestContainer.js";
import assert from "node:assert";
describe("E2E: uv coverage", () => {
let container;
before(async () => {
DockerTestContainer.buildImage();
});
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");
});
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 () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages requests"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv pip 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"
);
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 specifiers (>=)`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
'uv pip install --system --break-system-packages "Jinja2>=3.1"'
);
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"'
);
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"
);
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"
);
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"
);
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 () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --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("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}`
);
});
it(`uv pip install from GitHub URL using the CA bundle`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages git+https://github.com/psf/requests.git@v2.32.3"
);
assert.ok(
result.output.includes("no malware found."),
`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 () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand(
"uv pip install --system --break-system-packages certifi"
);
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}`
);
});
it(`uv pip install from direct HTTPS wheel URL`, 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"
);
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"
);
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"
);
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"
);
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"
);
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"'
);
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"
);
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"
);
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"
);
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"
);
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}`
);
assert.ok(
result.output.includes("Exiting without installing malicious packages."),
`Output did not include expected text. 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"
);
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 () => {
const shell = await container.openShell("zsh");
// Create a simple Python script
await shell.runCommand("echo 'import requests; print(requests.__version__)' > test_script.py");
const result = await shell.runCommand(
"uv run --with requests test_script.py"
);
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 run --with`, async () => {
const shell = await container.openShell("zsh");
// Create a simple Python script
await shell.runCommand("echo 'print(\"test\")' > test_script2.py");
const result = await shell.runCommand(
"uv run --with safe-chain-pi-test test_script2.py"
);
assert.ok(
result.output.includes("blocked 1 malicious package downloads:"),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv sync syncs project dependencies`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project, add a dependency, remove venv, and sync in one command chain
const result = await shell.runCommand(
"uv init test-sync-project && cd test-sync-project && uv add requests && rm -rf .venv && uv sync"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv add from git URL`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-git-add");
const result = await shell.runCommand(
"cd test-git-add && uv add git+https://github.com/psf/requests.git@v2.32.3"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv add with --optional group`, async () => {
const shell = await container.openShell("zsh");
// Initialize a new uv project
await shell.runCommand("uv init test-optional");
const result = await shell.runCommand(
"cd test-optional && uv add --optional dev pytest"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`uv run --with-requirements installs from requirements file`, 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");
const result = await shell.runCommand(
"uv run --with-requirements run_requirements.txt run_script.py"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. 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 && uv sync --all-extras"
);
assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
});