mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add pdm package manager support
PDM is a modern Python package manager using pyproject.toml (PEP 621). Uses the same MITM-only proxy approach as poetry/uv/pipx — all malware detection and minimum package age enforcement happens at the proxy layer by intercepting PyPI requests.
This commit is contained in:
parent
3f47ae890c
commit
1eb4fe05fd
13 changed files with 448 additions and 3 deletions
|
|
@ -24,6 +24,7 @@ Aikido Safe Chain supports the following package managers:
|
|||
- 📦 **uv**
|
||||
- 📦 **poetry**
|
||||
- 📦 **pipx**
|
||||
- 📦 **pdm**
|
||||
|
||||
# Usage
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
|||
### Verify the installation
|
||||
|
||||
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, pip, pip3, poetry, uv and pipx 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, pipx and pdm are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
||||
|
||||
2. **Verify the installation** by running the verification command:
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
|||
|
||||
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
||||
|
||||
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `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.
|
||||
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry`, `pipx` and `pdm` 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:
|
||||
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -3108,6 +3108,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4018,6 +4019,7 @@
|
|||
"aikido-bunx": "bin/aikido-bunx.js",
|
||||
"aikido-npm": "bin/aikido-npm.js",
|
||||
"aikido-npx": "bin/aikido-npx.js",
|
||||
"aikido-pdm": "bin/aikido-pdm.js",
|
||||
"aikido-pip": "bin/aikido-pip.js",
|
||||
"aikido-pip3": "bin/aikido-pip3.js",
|
||||
"aikido-pipx": "bin/aikido-pipx.js",
|
||||
|
|
|
|||
13
packages/safe-chain/bin/aikido-pdm.js
Normal file
13
packages/safe-chain/bin/aikido-pdm.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/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";
|
||||
|
||||
setEcoSystem(ECOSYSTEM_PY);
|
||||
initializePackageManager("pdm");
|
||||
|
||||
(async () => {
|
||||
var exitCode = await main(process.argv.slice(2));
|
||||
process.exit(exitCode);
|
||||
})();
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
"aikido-python3": "bin/aikido-python3.js",
|
||||
"aikido-poetry": "bin/aikido-poetry.js",
|
||||
"aikido-pipx": "bin/aikido-pipx.js",
|
||||
"aikido-pdm": "bin/aikido-pdm.js",
|
||||
"safe-chain": "bin/safe-chain.js"
|
||||
},
|
||||
"type": "module",
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
"keywords": [],
|
||||
"author": "Aikido Security",
|
||||
"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/), [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.",
|
||||
"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), [pip](https://pip.pypa.io/), and [pdm](https://pdm-project.org/) 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, pip/pip3, or pdm from downloading or running the malware.",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"certifi": "14.5.15",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { createPipPackageManager } from "./pip/createPackageManager.js";
|
|||
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
||||
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
||||
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
|
||||
import { createPdmPackageManager } from "./pdm/createPdmPackageManager.js";
|
||||
|
||||
/**
|
||||
* @type {{packageManagerName: PackageManager | null}}
|
||||
|
|
@ -64,6 +65,8 @@ export function initializePackageManager(packageManagerName, context) {
|
|||
state.packageManagerName = createPoetryPackageManager();
|
||||
} else if (packageManagerName === "pipx") {
|
||||
state.packageManagerName = createPipXPackageManager();
|
||||
} else if (packageManagerName === "pdm") {
|
||||
state.packageManagerName = createPdmPackageManager();
|
||||
} else {
|
||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { ui } from "../../environment/userInteraction.js";
|
||||
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
||||
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||
|
||||
/**
|
||||
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||
*/
|
||||
export function createPdmPackageManager() {
|
||||
return {
|
||||
runCommand: (args) => runPdmCommand(args),
|
||||
|
||||
// MITM only approach for PDM
|
||||
isSupportedCommand: () => false,
|
||||
getDependencyUpdatesForCommand: () => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets CA bundle environment variables used by PDM and Python libraries.
|
||||
* PDM uses httpx (via unearth) which respects SSL_CERT_FILE through Python's ssl module.
|
||||
*
|
||||
* @param {NodeJS.ProcessEnv} env - Environment object to modify
|
||||
* @param {string} combinedCaPath - Path to the combined CA bundle
|
||||
*/
|
||||
function setPdmCaBundleEnvironmentVariables(env, combinedCaPath) {
|
||||
// SSL_CERT_FILE: Used by Python SSL libraries and httpx (which PDM uses)
|
||||
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 (PDM plugins may use it)
|
||||
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: PDM may use pip internally
|
||||
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 pdm command with safe-chain's certificate bundle and proxy configuration.
|
||||
*
|
||||
* PDM respects standard HTTP_PROXY/HTTPS_PROXY environment variables through
|
||||
* httpx which it uses for package downloads.
|
||||
*
|
||||
* @param {string[]} args - Command line arguments to pass to pdm
|
||||
* @returns {Promise<{status: number}>} Exit status of the pdm command
|
||||
*/
|
||||
async function runPdmCommand(args) {
|
||||
try {
|
||||
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||
|
||||
const combinedCaPath = getCombinedCaBundlePath();
|
||||
setPdmCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||
|
||||
const result = await safeSpawn("pdm", args, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
|
||||
return { status: result.status };
|
||||
} catch (/** @type any */ error) {
|
||||
return reportCommandExecutionFailure(error, "pdm");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { createPdmPackageManager } from "./createPdmPackageManager.js";
|
||||
|
||||
test("createPdmPackageManager", async (t) => {
|
||||
await t.test("should create package manager with required interface", () => {
|
||||
const pm = createPdmPackageManager();
|
||||
|
||||
assert.ok(pm);
|
||||
assert.strictEqual(typeof pm.runCommand, "function");
|
||||
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||
});
|
||||
});
|
||||
|
|
@ -102,6 +102,12 @@ export const knownAikidoTools = [
|
|||
ecoSystem: ECOSYSTEM_PY,
|
||||
internalPackageManagerName: "pipx",
|
||||
},
|
||||
{
|
||||
tool: "pdm",
|
||||
aikidoCommand: "aikido-pdm",
|
||||
ecoSystem: ECOSYSTEM_PY,
|
||||
internalPackageManagerName: "pdm",
|
||||
},
|
||||
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@ function pipx
|
|||
wrapSafeChainCommand "pipx" $argv
|
||||
end
|
||||
|
||||
function pdm
|
||||
wrapSafeChainCommand "pdm" $argv
|
||||
end
|
||||
|
||||
function printSafeChainWarning
|
||||
set original_cmd $argv[1]
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ function pipx() {
|
|||
wrapSafeChainCommand "pipx" "$@"
|
||||
}
|
||||
|
||||
function pdm() {
|
||||
wrapSafeChainCommand "pdm" "$@"
|
||||
}
|
||||
|
||||
function printSafeChainWarning() {
|
||||
# \033[43;30m is used to set the background color to yellow and text color to black
|
||||
# \033[0m is used to reset the text formatting
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ function pipx {
|
|||
Invoke-WrappedCommand "pipx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
|
||||
}
|
||||
|
||||
function pdm {
|
||||
Invoke-WrappedCommand "pdm" $args $MyInvocation.Line $MyInvocation.OffsetInLine
|
||||
}
|
||||
|
||||
function Write-SafeChainWarning {
|
||||
param([string]$Command)
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ RUN apt-get update && apt-get install -y pipx && \
|
|||
pipx install poetry && \
|
||||
ln -sf /root/.local/bin/poetry /usr/local/bin/poetry
|
||||
|
||||
# Install PDM
|
||||
RUN pipx install pdm && \
|
||||
ln -sf /root/.local/bin/pdm /usr/local/bin/pdm
|
||||
|
||||
# Copy and install Safe chain
|
||||
COPY --from=builder /app/*.tgz /pkgs/
|
||||
RUN npm install -g /pkgs/*.tgz
|
||||
|
|
|
|||
317
test/e2e/pdm.e2e.spec.js
Normal file
317
test/e2e/pdm.e2e.spec.js
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("E2E: pdm 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");
|
||||
|
||||
// Clear pdm cache
|
||||
await installationShell.runCommand("command pdm cache clear");
|
||||
});
|
||||
|
||||
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 pdm add`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Initialize a new pdm project
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-project && cd /tmp/test-pdm-project");
|
||||
await shell.runCommand("cd /tmp/test-pdm-project && pdm init --non-interactive");
|
||||
|
||||
// Add a safe package
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-project && pdm add requests"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Installing"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm add with specific version`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-version && cd /tmp/test-pdm-version");
|
||||
await shell.runCommand("cd /tmp/test-pdm-version && pdm init --non-interactive");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-version && pdm add requests==2.32.3"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Installing"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`safe-chain blocks installation of malicious Python packages via pdm`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-malware && cd /tmp/test-pdm-malware");
|
||||
await shell.runCommand("cd /tmp/test-pdm-malware && pdm init --non-interactive");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-malware && pdm add safe-chain-pi-test"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked by safe-chain"),
|
||||
`Expected malware to be blocked. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Expected exit message. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm install installs dependencies from pyproject.toml`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-install && cd /tmp/test-pdm-install");
|
||||
await shell.runCommand("cd /tmp/test-pdm-install && pdm init --non-interactive");
|
||||
await shell.runCommand("cd /tmp/test-pdm-install && pdm add requests");
|
||||
|
||||
// Now remove the virtualenv and run install
|
||||
await shell.runCommand("cd /tmp/test-pdm-install && rm -rf .venv");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-install && pdm install"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Installing"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm update updates dependencies`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-update && cd /tmp/test-pdm-update");
|
||||
await shell.runCommand("cd /tmp/test-pdm-update && pdm init --non-interactive");
|
||||
await shell.runCommand("cd /tmp/test-pdm-update && pdm add requests");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-update && pdm update"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Updating"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm update with specific packages`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-update-specific && cd /tmp/test-pdm-update-specific");
|
||||
await shell.runCommand("cd /tmp/test-pdm-update-specific && pdm init --non-interactive");
|
||||
await shell.runCommand("cd /tmp/test-pdm-update-specific && pdm add requests certifi");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-update-specific && pdm update requests"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Updating"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm add with multiple packages`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-multi && cd /tmp/test-pdm-multi");
|
||||
await shell.runCommand("cd /tmp/test-pdm-multi && pdm init --non-interactive");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-multi && pdm add requests certifi"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Installing"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm add with extras`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-extras && cd /tmp/test-pdm-extras");
|
||||
await shell.runCommand("cd /tmp/test-pdm-extras && pdm init --non-interactive");
|
||||
|
||||
// Use quotes to prevent shell expansion of square brackets
|
||||
const result = await shell.runCommand(
|
||||
'cd /tmp/test-pdm-extras && pdm add "requests[security]"'
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Installing"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm add with development group`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-dev && cd /tmp/test-pdm-dev");
|
||||
await shell.runCommand("cd /tmp/test-pdm-dev && pdm init --non-interactive");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-dev && pdm add -dG dev pytest"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Installing"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm lock creates/updates lock file`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-lock && cd /tmp/test-pdm-lock");
|
||||
await shell.runCommand("cd /tmp/test-pdm-lock && pdm init --non-interactive");
|
||||
await shell.runCommand("cd /tmp/test-pdm-lock && pdm add requests");
|
||||
await shell.runCommand("cd /tmp/test-pdm-lock && rm pdm.lock");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-lock && pdm lock"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("no malware found.") || result.output.includes("Resolving") || result.output.includes("lock file"),
|
||||
`Output did not include expected text. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm remove does not download packages`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-remove && cd /tmp/test-pdm-remove");
|
||||
await shell.runCommand("cd /tmp/test-pdm-remove && pdm init --non-interactive");
|
||||
await shell.runCommand("cd /tmp/test-pdm-remove && pdm add requests");
|
||||
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-remove && pdm remove requests"
|
||||
);
|
||||
|
||||
// Remove should succeed - it doesn't download packages, just modifies pyproject.toml
|
||||
assert.ok(
|
||||
!result.output.includes("blocked"),
|
||||
`Remove command should not trigger downloads. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`blocks malware during pdm install`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
// Create a project with malware in dependencies
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-install-malware && cd /tmp/test-pdm-install-malware");
|
||||
await shell.runCommand("cd /tmp/test-pdm-install-malware && pdm init --non-interactive");
|
||||
|
||||
// Add malware package - this will create lock file and attempt download
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-install-malware && pdm add safe-chain-pi-test 2>&1"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked by safe-chain"),
|
||||
`Expected malware to be blocked during add (which triggers install). Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Expected exit message. Output was:\n${result.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`blocks malware when adding malicious dependency alongside safe one`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-batch && cd /tmp/test-pdm-batch");
|
||||
await shell.runCommand("cd /tmp/test-pdm-batch && pdm init --non-interactive");
|
||||
|
||||
// Try to add malware alongside safe package
|
||||
const result = await shell.runCommand(
|
||||
"cd /tmp/test-pdm-batch && pdm add safe-chain-pi-test requests 2>&1"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.output.includes("blocked by safe-chain"),
|
||||
`Expected malware to be blocked. Output was:\n${result.output}`
|
||||
);
|
||||
assert.ok(
|
||||
result.output.includes("Exiting without installing malicious packages."),
|
||||
`Expected exit message. Output was:\n${result.output}`
|
||||
);
|
||||
|
||||
// Verify safe package was also not installed due to malware in batch
|
||||
const listResult = await shell.runCommand("cd /tmp/test-pdm-batch && pdm list");
|
||||
assert.ok(
|
||||
!listResult.output.includes("requests"),
|
||||
`Safe package should not be installed when batch includes malware. Output was:\n${listResult.output}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`pdm non-network commands work correctly`, async () => {
|
||||
const shell = await container.openShell("zsh");
|
||||
|
||||
await shell.runCommand("mkdir /tmp/test-pdm-nonnetwork && cd /tmp/test-pdm-nonnetwork");
|
||||
await shell.runCommand("cd /tmp/test-pdm-nonnetwork && pdm init --non-interactive");
|
||||
await shell.runCommand("cd /tmp/test-pdm-nonnetwork && pdm add requests");
|
||||
|
||||
// Test pdm --version
|
||||
const versionResult = await shell.runCommand("pdm --version");
|
||||
assert.ok(
|
||||
versionResult.output.includes("PDM") || versionResult.output.includes("pdm"),
|
||||
`Expected version output. Output was:\n${versionResult.output}`
|
||||
);
|
||||
|
||||
// Test pdm list (list installed packages)
|
||||
const listResult = await shell.runCommand("cd /tmp/test-pdm-nonnetwork && pdm list");
|
||||
assert.ok(
|
||||
listResult.output.includes("requests"),
|
||||
`Expected to see installed package. Output was:\n${listResult.output}`
|
||||
);
|
||||
|
||||
// Test pdm info (show project info)
|
||||
const infoResult = await shell.runCommand("cd /tmp/test-pdm-nonnetwork && pdm info");
|
||||
assert.ok(
|
||||
infoResult.output.includes("PDM") || infoResult.output.includes("Python") || infoResult.output.includes("Project"),
|
||||
`Expected project info. Output was:\n${infoResult.output}`
|
||||
);
|
||||
|
||||
// Test pdm config (show configuration)
|
||||
const configResult = await shell.runCommand("pdm config");
|
||||
assert.ok(
|
||||
configResult.output.length > 0,
|
||||
`Expected configuration output. Output was:\n${configResult.output}`
|
||||
);
|
||||
|
||||
// Test pdm run (execute command in virtualenv) - non-network command
|
||||
const runResult = await shell.runCommand("cd /tmp/test-pdm-nonnetwork && pdm run python --version");
|
||||
assert.ok(
|
||||
runResult.output.includes("Python"),
|
||||
`Expected Python version output. Output was:\n${runResult.output}`
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue