diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 08f714a..36dad7b 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -144,6 +144,8 @@ jobs: with: node-version: "lts/*" registry-url: "https://registry.npmjs.org/" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - name: Setup safe-chain run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci diff --git a/.github/workflows/bump-endpoint.yml b/.github/workflows/bump-endpoint.yml deleted file mode 100644 index 8c61826..0000000 --- a/.github/workflows/bump-endpoint.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Bump Device Protection Automatically - -on: - schedule: - - cron: '0 * * * *' # every hour - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - bump-endpoint: - runs-on: open-source-releaser - steps: - - uses: actions/checkout@v4 - - - name: Get latest safechain-internals release - id: latest - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION=$(gh api repos/AikidoSec/safechain-internals/releases/latest --jq '.tag_name') - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Get current version from install script - id: current - run: | - CURRENT=$(grep -oP '(?<=releases/download/)[^/]+(?=/EndpointProtection\.pkg)' install-scripts/install-endpoint-mac.sh) - echo "version=$CURRENT" >> $GITHUB_OUTPUT - - - name: Download assets and compute checksums - if: steps.latest.outputs.version != steps.current.outputs.version - id: checksums - run: | - VERSION="${{ steps.latest.outputs.version }}" - BASE="https://github.com/AikidoSec/safechain-internals/releases/download/${VERSION}" - curl -fsSL "${BASE}/EndpointProtection.pkg" -o /tmp/EndpointProtection.pkg - curl -fsSL "${BASE}/EndpointProtection.msi" -o /tmp/EndpointProtection.msi - echo "mac=$(sha256sum /tmp/EndpointProtection.pkg | cut -d' ' -f1)" >> $GITHUB_OUTPUT - echo "win=$(sha256sum /tmp/EndpointProtection.msi | cut -d' ' -f1)" >> $GITHUB_OUTPUT - - - name: Update install scripts - if: steps.latest.outputs.version != steps.current.outputs.version - run: | - NEW="${{ steps.latest.outputs.version }}" - OLD="${{ steps.current.outputs.version }}" - MAC_SHA="${{ steps.checksums.outputs.mac }}" - WIN_SHA="${{ steps.checksums.outputs.win }}" - - sed -i "s|${OLD}/EndpointProtection.pkg|${NEW}/EndpointProtection.pkg|" install-scripts/install-endpoint-mac.sh - sed -i "s|^DOWNLOAD_SHA256=\"[^\"]*\"|DOWNLOAD_SHA256=\"${MAC_SHA}\"|" install-scripts/install-endpoint-mac.sh - - sed -i "s|${OLD}/EndpointProtection.msi|${NEW}/EndpointProtection.msi|" install-scripts/install-endpoint-windows.ps1 - sed -i 's|^\$DownloadSha256 = "[^"]*"|\$DownloadSha256 = "'"${WIN_SHA}"'"|' install-scripts/install-endpoint-windows.ps1 - - - name: Open PR - if: steps.latest.outputs.version != steps.current.outputs.version - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - run: | - NEW="${{ steps.latest.outputs.version }}" - OLD="${{ steps.current.outputs.version }}" - BRANCH="bump/endpoint-${NEW}" - - if git ls-remote --exit-code --heads origin "$BRANCH" &>/dev/null; then - echo "Branch $BRANCH already exists, skipping." - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add install-scripts/install-endpoint-mac.sh install-scripts/install-endpoint-windows.ps1 - git commit -m "Bump Endpoint to ${NEW}" - git push origin "$BRANCH" - PR_URL="https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1" - - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H "Content-Type: application/json" \ - -d "{\"text\": \"update to ${NEW} - ${PR_URL}\"}" diff --git a/README.md b/README.md index cb8f34b..039e355 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ Aikido Safe Chain supports the following package managers: - πŸ“¦ **poetry** - πŸ“¦ **uvx** - πŸ“¦ **pipx** -- πŸ“¦ **pdm** # Usage @@ -78,7 +77,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, rush, rushx, bun, bunx, pip, pip3, poetry, uv, uvx, pipx and pdm 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, rush, rushx, bun, bunx, pip, pip3, poetry, uv, uvx and pipx 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: @@ -109,7 +108,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`, `rush`, `rushx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `uvx`, `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. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `rush`, `rushx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `uvx`, `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: @@ -121,7 +120,7 @@ safe-chain --version ### Malware Blocking -The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, pip, pip3, uv, uvx, poetry, pipx or pdm commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine. +The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, pip, pip3, uv, uvx, poetry or pipx commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine. ### Minimum package age @@ -140,7 +139,7 @@ By default, the minimum package age is 48 hours. This provides an additional sec ### Shell Integration -The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, and Python package managers (pip, uv, uvx, poetry, pipx, pdm). 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, rush, rushx, bun, bunx, and Python package managers (pip, uv, uvx, 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** - βœ… **Zsh** diff --git a/install-scripts/install-endpoint-mac.sh b/install-scripts/install-endpoint-mac.sh index 4cb9e9a..feabeb1 100755 --- a/install-scripts/install-endpoint-mac.sh +++ b/install-scripts/install-endpoint-mac.sh @@ -7,8 +7,8 @@ set -e # Exit on error # Configuration -INSTALL_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.5.4/EndpointProtection.pkg" -DOWNLOAD_SHA256="ad800f9e476b0a75bf32b1c079f060ecb98bc16972a4e8cca29cf165388ea9fe" +INSTALL_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.3.4/EndpointProtection.pkg" +DOWNLOAD_SHA256="f2ea55588d42e4aa17545ad787f46dd36001009e2ddb9655c497b1a36edf3581" TOKEN_FILE="/tmp/aikido_endpoint_token.txt" # Colors for output diff --git a/install-scripts/install-endpoint-windows.ps1 b/install-scripts/install-endpoint-windows.ps1 index 05da8b4..29bc873 100644 --- a/install-scripts/install-endpoint-windows.ps1 +++ b/install-scripts/install-endpoint-windows.ps1 @@ -7,8 +7,8 @@ param( ) # Configuration -$InstallUrl = "https://github.com/AikidoSec/safechain-internals/releases/download/v1.5.4/EndpointProtection.msi" -$DownloadSha256 = "e2750c59124f53456a8f9cdb9e81fd9ce2f2491869f68f01602444ad519be5be" +$InstallUrl = "https://github.com/AikidoSec/safechain-internals/releases/download/v1.3.4/EndpointProtection.msi" +$DownloadSha256 = "0699379716a9a8b1531befa538befb237252af9f7fd780b33f4dce73588c6f83" # Ensure TLS 1.2 is enabled for downloads [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 310e28f..8148344 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -3129,7 +3129,6 @@ "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", diff --git a/packages/safe-chain/bin/aikido-pdm.js b/packages/safe-chain/bin/aikido-pdm.js deleted file mode 100755 index 9c6cf94..0000000 --- a/packages/safe-chain/bin/aikido-pdm.js +++ /dev/null @@ -1,13 +0,0 @@ -#!/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); -})(); diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 6ff33a0..900bd83 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -1,11 +1,5 @@ #!/usr/bin/env node -// Strip PKG_EXECPATH from the environment so any child process safe-chain -// spawns (npm, uv, pip, …) doesn't inherit it. If it leaks into a subsequent -// safe-chain invocation (e.g. via a shim) the yao-pkg bootstrap would treat -// argv[1] as a script path and fail with MODULE_NOT_FOUND. -delete process.env.PKG_EXECPATH; - import chalk from "chalk"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; @@ -21,7 +15,7 @@ import { main } from "../src/main.js"; import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; -import { knownAikidoTools, getPackageManagerList } from "../src/shell-integration/helpers.js"; +import { knownAikidoTools } from "../src/shell-integration/helpers.js"; import { getInstalledSafeChainDir } from "../src/installLocation.js"; /** @type {string} */ @@ -114,7 +108,7 @@ function writeHelp() { ui.writeInformation( `- ${chalk.cyan( "safe-chain setup", - )}: This will setup your shell to wrap safe-chain around ${getPackageManagerList()}.`, + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, pip and pip3.`, ); ui.writeInformation( `- ${chalk.cyan( diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 72f9bac..f7ae933 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -25,7 +25,6 @@ "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", @@ -40,7 +39,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), [rush](https://rushjs.io/), [rushx](https://rushjs.io/pages/commands/rushx/), [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, rush, rushx, bun, bunx, uv, uvx, pip/pip3, or pdm 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), [rush](https://rushjs.io/), [rushx](https://rushjs.io/pages/commands/rushx/), [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, rush, rushx, bun, bunx, uv, uvx, or pip/pip3 from downloading or running the malware.", "dependencies": { "certifi": "14.5.15", "chalk": "5.4.1", diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index bf91d88..90050d3 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -13,7 +13,6 @@ 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"; import { createRushPackageManager } from "./rush/createRushPackageManager.js"; import { createRushxPackageManager } from "./rushx/createRushxPackageManager.js"; import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js"; @@ -70,8 +69,6 @@ export function initializePackageManager(packageManagerName, context) { state.packageManagerName = createPoetryPackageManager(); } else if (packageManagerName === "pipx") { state.packageManagerName = createPipXPackageManager(); - } else if (packageManagerName === "pdm") { - state.packageManagerName = createPdmPackageManager(); } else if (packageManagerName === "rush") { state.packageManagerName = createRushPackageManager(); } else if (packageManagerName === "rushx") { diff --git a/packages/safe-chain/src/packagemanager/pdm/createPdmPackageManager.js b/packages/safe-chain/src/packagemanager/pdm/createPdmPackageManager.js deleted file mode 100644 index 1649a89..0000000 --- a/packages/safe-chain/src/packagemanager/pdm/createPdmPackageManager.js +++ /dev/null @@ -1,72 +0,0 @@ -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"); - } -} diff --git a/packages/safe-chain/src/packagemanager/pdm/createPdmPackageManager.spec.js b/packages/safe-chain/src/packagemanager/pdm/createPdmPackageManager.spec.js deleted file mode 100644 index 2b2266b..0000000 --- a/packages/safe-chain/src/packagemanager/pdm/createPdmPackageManager.spec.js +++ /dev/null @@ -1,14 +0,0 @@ -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"); - }); -}); diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index bc4ef6c..dd10f3f 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -120,12 +120,6 @@ 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 ]; diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh index 30ab833..5b318ff 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -20,10 +20,7 @@ remove_shim_from_path() { } if command -v safe-chain >/dev/null 2>&1; then - # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops. - # Unset PKG_EXECPATH so the yao-pkg bootstrap inside the safe-chain binary doesn't - # mistake argv[1] for a script path and try to resolve "{{PACKAGE_MANAGER}}" against cwd. - unset PKG_EXECPATH + # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@" else # safe-chain is not reachable β€” warn the user so they know protection is inactive diff --git a/packages/safe-chain/src/shell-integration/pkg-execpath-cleanup.spec.js b/packages/safe-chain/src/shell-integration/pkg-execpath-cleanup.spec.js deleted file mode 100644 index 4057224..0000000 --- a/packages/safe-chain/src/shell-integration/pkg-execpath-cleanup.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(__dirname, "..", ".."); - -describe("PKG_EXECPATH cleanup", () => { - it("unix shim template unsets PKG_EXECPATH before invoking safe-chain", () => { - const file = path.join( - repoRoot, - "src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh", - ); - const content = fs.readFileSync(file, "utf-8"); - assert.match( - content, - /unset PKG_EXECPATH[\s\S]*exec safe-chain/, - "unix-wrapper.template.sh must `unset PKG_EXECPATH` before `exec safe-chain`", - ); - }); - - it("posix shell function unsets PKG_EXECPATH before invoking safe-chain", () => { - const file = path.join( - repoRoot, - "src/shell-integration/startup-scripts/init-posix.sh", - ); - const content = fs.readFileSync(file, "utf-8"); - // Scoped subshell so we don't mutate the user's interactive env. - assert.match( - content, - /\(unset PKG_EXECPATH;\s*safe-chain "\$@"\)/, - "init-posix.sh must invoke safe-chain in a subshell that unsets PKG_EXECPATH", - ); - }); - - it("fish shell function unsets PKG_EXECPATH before invoking safe-chain", () => { - const file = path.join( - repoRoot, - "src/shell-integration/startup-scripts/init-fish.fish", - ); - const content = fs.readFileSync(file, "utf-8"); - assert.match( - content, - /env -u PKG_EXECPATH safe-chain/, - "init-fish.fish must invoke safe-chain via `env -u PKG_EXECPATH`", - ); - }); - - it("safe-chain entry point deletes PKG_EXECPATH from process.env", () => { - const file = path.join(repoRoot, "bin/safe-chain.js"); - const content = fs.readFileSync(file, "utf-8"); - assert.match( - content, - /delete process\.env\.PKG_EXECPATH/, - "bin/safe-chain.js must delete process.env.PKG_EXECPATH so spawned children don't inherit it", - ); - }); -}); diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index 697cc80..728aff1 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -84,10 +84,6 @@ function pipx wrapSafeChainCommand "pipx" $argv end -function pdm - wrapSafeChainCommand "pdm" $argv -end - function printSafeChainWarning set original_cmd $argv[1] @@ -124,10 +120,8 @@ function wrapSafeChainCommand end if type -q safe-chain - # If the safe-chain command is available, just run it with the provided arguments. - # Unset PKG_EXECPATH for this invocation so the yao-pkg bootstrap inside the - # safe-chain binary doesn't mistake argv[1] for a script path to resolve against cwd. - env -u PKG_EXECPATH safe-chain $original_cmd $cmd_args + # If the safe-chain command is available, just run it with the provided arguments + safe-chain $original_cmd $cmd_args else # If the safe-chain command is not available, print a warning and run the original command printSafeChainWarning $original_cmd diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index df5623d..cde8f48 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -89,10 +89,6 @@ 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 @@ -113,10 +109,8 @@ function wrapSafeChainCommand() { fi if command -v safe-chain > /dev/null 2>&1; then - # If the aikido command is available, just run it with the provided arguments. - # Unset PKG_EXECPATH so the yao-pkg bootstrap inside the safe-chain binary doesn't - # mistake argv[1] for a script path and try to resolve it against cwd. - (unset PKG_EXECPATH; safe-chain "$@") + # If the aikido command is available, just run it with the provided arguments + safe-chain "$@" else # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index fb63ce8..7aad2fc 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -83,10 +83,6 @@ 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) diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 290922d..0e38110 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -79,10 +79,6 @@ 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 diff --git a/test/e2e/bun.e2e.spec.js b/test/e2e/bun.e2e.spec.js index 589d863..fb6e99a 100644 --- a/test/e2e/bun.e2e.spec.js +++ b/test/e2e/bun.e2e.spec.js @@ -46,9 +46,8 @@ describe("E2E: bun coverage", () => { var result = await shell.runCommand("bun install"); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( @@ -66,9 +65,8 @@ describe("E2E: bun coverage", () => { const result = await shell.runCommand("bunx safe-chain-test"); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index 810359e..e8ba7c8 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -70,9 +70,8 @@ describe("E2E: npm coverage", () => { var result = await shell.runCommand("npm install"); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( diff --git a/test/e2e/pdm.e2e.spec.js b/test/e2e/pdm.e2e.spec.js deleted file mode 100644 index 94bb5e0..0000000 --- a/test/e2e/pdm.e2e.spec.js +++ /dev/null @@ -1,300 +0,0 @@ -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 numpy==2.4.4" - ); - - assert.ok( - result.output.includes("blocked") && result.output.includes("malicious package downloads"), - `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 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 numpy==2.4.4 2>&1" - ); - - assert.ok( - result.output.includes("blocked") && result.output.includes("malicious package downloads"), - `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 numpy==2.4.4 requests 2>&1" - ); - - assert.ok( - result.output.includes("blocked") && result.output.includes("malicious package downloads"), - `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}` - ); - }); -}); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 8044a0f..af979dc 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -131,9 +131,8 @@ describe("E2E: pip coverage", () => { "pip3 install --break-system-packages numpy==2.4.4 --safe-chain-logging=verbose" ); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads:/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( diff --git a/test/e2e/pnpm.e2e.spec.js b/test/e2e/pnpm.e2e.spec.js index 6f9dacf..a15250a 100644 --- a/test/e2e/pnpm.e2e.spec.js +++ b/test/e2e/pnpm.e2e.spec.js @@ -70,9 +70,8 @@ describe("E2E: pnpm coverage", () => { var result = await shell.runCommand("pnpm install"); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( diff --git a/test/e2e/rush.e2e.spec.js b/test/e2e/rush.e2e.spec.js index fb6895f..a5471a0 100644 --- a/test/e2e/rush.e2e.spec.js +++ b/test/e2e/rush.e2e.spec.js @@ -109,7 +109,7 @@ describe("E2E: rush coverage", () => { assert.match( result.output, - /blocked [1-9]\d* malicious package downloads/, + /blocked \d+ malicious package downloads/, `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( diff --git a/test/e2e/rushx.e2e.spec.js b/test/e2e/rushx.e2e.spec.js index ec5ff75..b7d5078 100644 --- a/test/e2e/rushx.e2e.spec.js +++ b/test/e2e/rushx.e2e.spec.js @@ -57,7 +57,7 @@ describe("E2E: rushx coverage", () => { assert.match( result.output, - /blocked [1-9]\d* malicious package downloads/, + /blocked \d+ malicious package downloads/, `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( diff --git a/test/e2e/safe-chain-cli-python.e2e.spec.js b/test/e2e/safe-chain-cli-python.e2e.spec.js index 43187d8..cf3fda2 100644 --- a/test/e2e/safe-chain-cli-python.e2e.spec.js +++ b/test/e2e/safe-chain-cli-python.e2e.spec.js @@ -100,9 +100,8 @@ describe("E2E: safe-chain CLI python/pip support", () => { "safe-chain pip3 install --break-system-packages numpy==2.4.4" ); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), `Should have blocked malware. Output was:\n${result.output}` ); }); diff --git a/test/e2e/uv.e2e.spec.js b/test/e2e/uv.e2e.spec.js index 728d4c5..5536e22 100644 --- a/test/e2e/uv.e2e.spec.js +++ b/test/e2e/uv.e2e.spec.js @@ -129,9 +129,8 @@ describe("E2E: uv coverage", () => { "uv pip install --system --break-system-packages numpy==2.4.4" ); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads:/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( @@ -417,9 +416,8 @@ describe("E2E: uv coverage", () => { "cd test-project-malware && uv add numpy==2.4.4" ); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads:/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( @@ -449,9 +447,8 @@ describe("E2E: uv coverage", () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("uv tool install numpy==2.4.4"); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads:/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok( @@ -488,9 +485,8 @@ describe("E2E: uv coverage", () => { "uv run --with numpy==2.4.4 test_script2.py" ); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads:/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/yarn.e2e.spec.js b/test/e2e/yarn.e2e.spec.js index e70d6fc..5e56d12 100644 --- a/test/e2e/yarn.e2e.spec.js +++ b/test/e2e/yarn.e2e.spec.js @@ -70,9 +70,8 @@ describe("E2E: yarn coverage", () => { var result = await shell.runCommand("yarn"); - assert.match( - result.output, - /blocked [1-9]\d* malicious package downloads/, + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), `Output did not include expected text. Output was:\n${result.output}` ); assert.ok(