mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Skeleton
This commit is contained in:
parent
fc5df6cd14
commit
dc6fcb9761
6 changed files with 697 additions and 0 deletions
16
packages/safe-chain/bin/aikido-pipx.js
Executable file
16
packages/safe-chain/bin/aikido-pipx.js
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/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("pipx");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Pass through only user-supplied pipx args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { runPipX } from "./runPipXCommand.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createPipXPackageManager() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*/
|
||||||
|
runCommand: (args) => {
|
||||||
|
return runPipX("pipx", args);
|
||||||
|
},
|
||||||
|
// For uv, rely solely on MITM
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { createPipXPackageManager } from "./createPipXPackageManager.js";
|
||||||
|
|
||||||
|
test("createPipXPackageManager", async (t) => {
|
||||||
|
await t.test("should create package manager with required interface", () => {
|
||||||
|
const pm = createPipXPackageManager();
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 pipx.
|
||||||
|
*
|
||||||
|
* @param {NodeJS.ProcessEnv} env - Env object
|
||||||
|
* @param {string} combinedCaPath - Path to the combined CA bundle
|
||||||
|
*/
|
||||||
|
function setPipXCaBundleEnvironmentVariables(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 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) {
|
||||||
|
try {
|
||||||
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
|
const combinedCaPath = getCombinedCaBundlePath();
|
||||||
|
setPipXCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
|
// Note: pipx 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -94,6 +94,12 @@ export const knownAikidoTools = [
|
||||||
ecoSystem: ECOSYSTEM_PY,
|
ecoSystem: ECOSYSTEM_PY,
|
||||||
internalPackageManagerName: "pip",
|
internalPackageManagerName: "pip",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tool: "pipx",
|
||||||
|
aikidoCommand: "aikido-pipx",
|
||||||
|
ecoSystem: ECOSYSTEM_PY,
|
||||||
|
internalPackageManagerName: "pip",
|
||||||
|
}
|
||||||
// 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
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
572
test/e2e/pipx.e2e.spec.js
Normal file
572
test/e2e/pipx.e2e.spec.js
Normal file
|
|
@ -0,0 +1,572 @@
|
||||||
|
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||||
|
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("E2E: pipx 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");
|
||||||
|
|
||||||
|
// Clear uv cache
|
||||||
|
await installationShell.runCommand("uv cache clean");
|
||||||
|
});
|
||||||
|
|
||||||
|
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 --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 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 () => {
|
||||||
|
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 --safe-chain-logging=verbose"
|
||||||
|
);
|
||||||
|
|
||||||
|
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 --safe-chain-logging=verbose"
|
||||||
|
);
|
||||||
|
|
||||||
|
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 --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"
|
||||||
|
);
|
||||||
|
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
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 --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");
|
||||||
|
|
||||||
|
// 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 --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 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 --safe-chain-logging=verbose && rm -rf .venv && uv sync --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 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 --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 --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 --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 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 --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 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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue