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
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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue