mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Fix tests and add command support
This commit is contained in:
parent
b9de94f0f1
commit
d2fc531c81
14 changed files with 198 additions and 462 deletions
|
|
@ -23,6 +23,7 @@ Aikido Safe Chain supports the following package managers:
|
||||||
- 📦 **pip3**
|
- 📦 **pip3**
|
||||||
- 📦 **uv**
|
- 📦 **uv**
|
||||||
- 📦 **poetry**
|
- 📦 **poetry**
|
||||||
|
- 📦 **pipx**
|
||||||
|
|
||||||
# Usage
|
# 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.
|
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:
|
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.
|
- 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:
|
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.
|
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
|
### 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**
|
- ✅ **Bash**
|
||||||
- ✅ **Zsh**
|
- ✅ **Zsh**
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Overview
|
## 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
|
## Supported Shells
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ This command:
|
||||||
|
|
||||||
- Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
|
- Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
|
||||||
- Detects all supported shells on your system
|
- 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
|
- 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.
|
❗ 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:
|
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
|
- 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
|
- Check that these commands are in your system's PATH
|
||||||
|
|
||||||
### Manual Verification
|
### 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:
|
To intercept Python module invocations for pip without altering Python itself, you can add small forwarding functions:
|
||||||
|
|
||||||
|
|
|
||||||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -3131,6 +3131,7 @@
|
||||||
"aikido-npx": "bin/aikido-npx.js",
|
"aikido-npx": "bin/aikido-npx.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-pipx": "bin/aikido-pipx.js",
|
||||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||||
"aikido-poetry": "bin/aikido-poetry.js",
|
"aikido-poetry": "bin/aikido-poetry.js",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"aikido-python": "bin/aikido-python.js",
|
"aikido-python": "bin/aikido-python.js",
|
||||||
"aikido-python3": "bin/aikido-python3.js",
|
"aikido-python3": "bin/aikido-python3.js",
|
||||||
"aikido-poetry": "bin/aikido-poetry.js",
|
"aikido-poetry": "bin/aikido-poetry.js",
|
||||||
|
"aikido-pipx": "bin/aikido-pipx.js",
|
||||||
"safe-chain": "bin/safe-chain.js"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -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 { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
|
||||||
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
||||||
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
||||||
|
|
||||||
|
|
@ -61,6 +62,8 @@ export function initializePackageManager(packageManagerName, context) {
|
||||||
state.packageManagerName = createUvPackageManager();
|
state.packageManagerName = createUvPackageManager();
|
||||||
} else if (packageManagerName === "poetry") {
|
} else if (packageManagerName === "poetry") {
|
||||||
state.packageManagerName = createPoetryPackageManager();
|
state.packageManagerName = createPoetryPackageManager();
|
||||||
|
} else if (packageManagerName === "pipx") {
|
||||||
|
state.packageManagerName = createPipXPackageManager();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function createPipXPackageManager() {
|
||||||
runCommand: (args) => {
|
runCommand: (args) => {
|
||||||
return runPipX("pipx", args);
|
return runPipX("pipx", args);
|
||||||
},
|
},
|
||||||
// For uv, rely solely on MITM
|
// MITM only
|
||||||
isSupportedCommand: () => false,
|
isSupportedCommand: () => false,
|
||||||
getDependencyUpdatesForCommand: () => [],
|
getDependencyUpdatesForCommand: () => [],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { createPipXPackageManager } from "./createPipXPackageManager.js";
|
||||||
test("createPipXPackageManager", async (t) => {
|
test("createPipXPackageManager", async (t) => {
|
||||||
await t.test("should create package manager with required interface", () => {
|
await t.test("should create package manager with required interface", () => {
|
||||||
const pm = createPipXPackageManager();
|
const pm = createPipXPackageManager();
|
||||||
|
|
||||||
assert.ok(pm);
|
assert.ok(pm);
|
||||||
assert.strictEqual(typeof pm.runCommand, "function");
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ function setPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
|
||||||
}
|
}
|
||||||
env.SSL_CERT_FILE = 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) {
|
if (env.REQUESTS_CA_BUNDLE) {
|
||||||
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
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:
|
* @param {string} command - The command to execute
|
||||||
* - HTTP_PROXY / HTTPS_PROXY: Proxy settings
|
* @param {string[]} args - Command line arguments
|
||||||
* - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
|
* @returns {Promise<{status: number}>} Exit status of the command
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
export async function runPipX(command, args) {
|
export async function runPipX(command, args) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -98,7 +98,7 @@ export const knownAikidoTools = [
|
||||||
tool: "pipx",
|
tool: "pipx",
|
||||||
aikidoCommand: "aikido-pipx",
|
aikidoCommand: "aikido-pipx",
|
||||||
ecoSystem: ECOSYSTEM_PY,
|
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
|
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ function npm
|
||||||
wrapSafeChainCommand "npm" $argv
|
wrapSafeChainCommand "npm" $argv
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function pip
|
function pip
|
||||||
wrapSafeChainCommand "pip" $argv
|
wrapSafeChainCommand "pip" $argv
|
||||||
end
|
end
|
||||||
|
|
@ -66,6 +65,10 @@ function python3
|
||||||
wrapSafeChainCommand "python3" $argv
|
wrapSafeChainCommand "python3" $argv
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function pipx
|
||||||
|
wrapSafeChainCommand "pipx" $argv
|
||||||
|
end
|
||||||
|
|
||||||
function printSafeChainWarning
|
function printSafeChainWarning
|
||||||
set original_cmd $argv[1]
|
set original_cmd $argv[1]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ function npm() {
|
||||||
wrapSafeChainCommand "npm" "$@"
|
wrapSafeChainCommand "npm" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function pip() {
|
function pip() {
|
||||||
wrapSafeChainCommand "pip" "$@"
|
wrapSafeChainCommand "pip" "$@"
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +61,10 @@ function python3() {
|
||||||
wrapSafeChainCommand "python3" "$@"
|
wrapSafeChainCommand "python3" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pipx() {
|
||||||
|
wrapSafeChainCommand "pipx" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
function printSafeChainWarning() {
|
function printSafeChainWarning() {
|
||||||
# \033[43;30m is used to set the background color to yellow and text color to black
|
# \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
|
# \033[0m is used to reset the text formatting
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ function python3 {
|
||||||
Invoke-WrappedCommand 'python3' $args
|
Invoke-WrappedCommand 'python3' $args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pipx {
|
||||||
|
Invoke-WrappedCommand "pipx" $args
|
||||||
|
}
|
||||||
|
|
||||||
function Write-SafeChainWarning {
|
function Write-SafeChainWarning {
|
||||||
param([string]$Command)
|
param([string]$Command)
|
||||||
|
|
|
||||||
|
|
@ -10,563 +10,191 @@ describe("E2E: pipx coverage", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Run a new Docker container for each test
|
|
||||||
container = new DockerTestContainer();
|
container = new DockerTestContainer();
|
||||||
await container.start();
|
await container.start();
|
||||||
|
|
||||||
const installationShell = await container.openShell("zsh");
|
const installationShell = await container.openShell("zsh");
|
||||||
await installationShell.runCommand("safe-chain setup --include-python");
|
await installationShell.runCommand("safe-chain setup");
|
||||||
|
|
||||||
// Clear uv cache
|
|
||||||
await installationShell.runCommand("uv cache clean");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// Stop and clean up the container after each test
|
|
||||||
if (container) {
|
if (container) {
|
||||||
await container.stop();
|
await container.stop();
|
||||||
container = null;
|
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 shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
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}`
|
`Output did not include expected text. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`pipx install with specific version`, 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 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 () => {
|
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"uv pip install --system --break-system-packages safe-chain-pi-test"
|
"pipx install safe-chain-pi-test"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output.includes("blocked by safe-chain"),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Expected malware to be blocked. 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(
|
assert.ok(
|
||||||
result.output.includes("Exiting without installing malicious packages."),
|
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}`
|
||||||
);
|
|
||||||
|
|
||||||
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 () => {
|
it(`pipx upgrade upgrades installed packages`, async () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
|
await shell.runCommand("pipx install ruff==0.1.0");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
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}`
|
`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 shell = await container.openShell("zsh");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
"uv pip install --system --break-system-packages certifi --safe-chain-logging=verbose"
|
"pipx run ruff --version"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
result.output.includes("no malware found.") || /ruff/i.test(result.output),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Expected safe run to succeed. 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 () => {
|
it(`pipx run blocks malicious tool download`, async () => {
|
||||||
const shell = await container.openShell("zsh");
|
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(
|
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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
result.output.includes("blocked by safe-chain"),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`Expected malicious run to be blocked. 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}`
|
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
result.output.includes("Exiting without installing malicious packages."),
|
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 () => {
|
it(`pipx runpip installs safe dependency inside an app venv`, 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 () => {
|
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
// Create a simple Python script
|
// Prepare an app environment
|
||||||
await shell.runCommand(
|
await shell.runCommand("pipx install ruff");
|
||||||
"echo 'import requests; print(requests.__version__)' > test_script.py"
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
result.output.includes("no malware found.") || /Successfully installed/i.test(result.output) || /requests/i.test(result.output),
|
||||||
`Output did not include expected text. Output was:\n${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");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
// Create a simple Python script
|
// Prepare an app environment
|
||||||
await shell.runCommand("echo 'print(\"test\")' > test_script2.py");
|
await shell.runCommand("pipx install ruff");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
result.output.includes("blocked 1 malicious package downloads:"),
|
result.output.includes("blocked by safe-chain"),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`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");
|
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(
|
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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
result.output.includes("ruff"),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`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");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
// Initialize a new uv project
|
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
|
||||||
await shell.runCommand("uv init test-git-add");
|
await shell.runCommand("pipx uninstall ruff --safe-chain-logging=verbose");
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
!result.output.includes("ruff"),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`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");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
// Initialize a new uv project
|
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
|
||||||
await shell.runCommand("uv init test-optional");
|
|
||||||
|
|
||||||
const result = await shell.runCommand(
|
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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
result.output.includes("no malware found.") || /Successfully installed/i.test(result.output) || /requests/i.test(result.output),
|
||||||
`Output did not include expected text. Output was:\n${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");
|
const shell = await container.openShell("zsh");
|
||||||
|
|
||||||
// Create requirements file and script
|
await shell.runCommand("pipx install ruff --safe-chain-logging=verbose");
|
||||||
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(
|
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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
result.output.includes("blocked by safe-chain"),
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
`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(
|
assert.ok(
|
||||||
result.output.includes("no malware found."),
|
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}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue