From d0f2edec0a4079cb5285d1f5d13bc6797e2286f6 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 21 Oct 2025 15:25:12 -0700 Subject: [PATCH 01/52] Skeleton --- package-lock.json | 33 +++++++------------ packages/safe-chain/bin/aikido-pip.js | 9 +++++ packages/safe-chain/package.json | 1 + .../pip/createPipPackageManager.js | 31 +++++++++++++++++ .../startup-scripts/init-posix.sh | 8 +++++ 5 files changed, 60 insertions(+), 22 deletions(-) create mode 100755 packages/safe-chain/bin/aikido-pip.js create mode 100644 packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js diff --git a/package-lock.json b/package-lock.json index 88e9fb5..cc210a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,8 +154,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@oven/bun-darwin-x64": { "version": "1.2.21", @@ -168,8 +167,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@oven/bun-darwin-x64-baseline": { "version": "1.2.21", @@ -182,8 +180,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-aarch64": { "version": "1.2.21", @@ -196,8 +193,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-aarch64-musl": { "version": "1.2.21", @@ -210,8 +206,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64": { "version": "1.2.21", @@ -224,8 +219,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64-baseline": { "version": "1.2.21", @@ -238,8 +232,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64-musl": { "version": "1.2.21", @@ -252,8 +245,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64-musl-baseline": { "version": "1.2.21", @@ -266,8 +258,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-windows-x64": { "version": "1.2.21", @@ -280,8 +271,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@oven/bun-windows-x64-baseline": { "version": "1.2.21", @@ -294,8 +284,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@oxlint/darwin-arm64": { "version": "1.22.0", diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js new file mode 100755 index 0000000..04da636 --- /dev/null +++ b/packages/safe-chain/bin/aikido-pip.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +const packageManagerName = "pip"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 98ccd52..d9c5547 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -14,6 +14,7 @@ "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-bun": "bin/aikido-bun.js", "aikido-bunx": "bin/aikido-bunx.js", + "aikido-pip": "bin/aikido-pip.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", diff --git a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js new file mode 100644 index 0000000..53cd630 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js @@ -0,0 +1,31 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; + +export function createPipPackageManager() { + return { + runCommand: (args) => runPipCommand("pip3", args), + + // For pip, set proxy server + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +async function runPipCommand(command, args) { + try { + console.log("**createPipPackageManager.js** Running pip command"); + const result = await safeSpawn(command, args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; + } catch (error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} 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 353c6c0..7bee44e 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 @@ -50,6 +50,14 @@ function bunx() { wrapSafeChainCommand "bunx" "aikido-bunx" "$@" } +function pip() { + wrapSafeChainCommand "pip" "aikido-pip" "$@" +} + +function pip3() { + wrapSafeChainCommand "pip3" "aikido-pip" "$@" +} + function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command From f086aeb2be163d1887239f6e021fc3ed9e7a59fd Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 06:59:32 -0700 Subject: [PATCH 02/52] Skeleton --- package-lock.json | 1 + packages/safe-chain/bin/aikido-pip.js | 29 +++++++++++++++++-- packages/safe-chain/src/main.js | 1 + .../packagemanager/currentPackageManager.js | 5 ++++ .../pip/createPipPackageManager.js | 9 ++++-- .../startup-scripts/init-posix.sh | 5 ++-- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc210a6..0d64f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1724,6 +1724,7 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", + "aikido-pip": "bin/aikido-pip.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 04da636..99fbbb6 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -2,8 +2,33 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; -const packageManagerName = "pip"; + +// Defaults +let packageManagerName = "pip"; +let targetVersionMajor; + +// Copy argv so we can mutate while parsing +const argv = process.argv.slice(2); + +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + + // --target-version-major + if (a === "--target-version-major" && i + 1 < argv.length) { + console.log("Setting targetVersionMajor from CLI arg:", argv[i + 1]); + targetVersionMajor = argv[i + 1]; + argv.splice(i, 2); + i -= 1; + continue; + } +} + +// If the user explicitly called python3, prefer pip3 +if (targetVersionMajor && String(targetVersionMajor).trim() === "3") { + packageManagerName = "pip3"; +} + initializePackageManager(packageManagerName); -var exitCode = await main(process.argv.slice(2)); +var exitCode = await main(argv); process.exit(exitCode); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index e106e83..4eaf8d2 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -12,6 +12,7 @@ export async function main(args) { await proxy.startServer(); try { + console.log(chalk.blueBright.bold("main.js: Scanning for malicious packages...")); // This parses all the --safe-chain arguments and removes them from the args array args = initializeCliArguments(args); diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2f019a1..2c78d06 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -9,11 +9,14 @@ import { createPnpxPackageManager, } from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; +import { createPipPackageManager } from "./pip/createPipPackageManager.js"; const state = { packageManagerName: null, }; +const PIP_COMMANDS = new Set(["pip", "pip3"]); + export function initializePackageManager(packageManagerName) { if (packageManagerName === "npm") { state.packageManagerName = createNpmPackageManager(); @@ -29,6 +32,8 @@ export function initializePackageManager(packageManagerName) { state.packageManagerName = createBunPackageManager(); } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); + } else if (PIP_COMMANDS.has(packageManagerName)) { + state.packageManagerName = createPipPackageManager(packageManagerName); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js index 53cd630..6aaf986 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js @@ -2,9 +2,14 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -export function createPipPackageManager() { +/** + * Creates a package manager interface for Python's pip package installer + * + * @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip" + */ +export function createPipPackageManager(command = "pip") { return { - runCommand: (args) => runPipCommand("pip3", args), + runCommand: (args) => runPipCommand(command, args), // For pip, set proxy server isSupportedCommand: () => false, 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 7bee44e..d1df130 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 @@ -51,13 +51,14 @@ function bunx() { } function pip() { - wrapSafeChainCommand "pip" "aikido-pip" "$@" + wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" "$@" } function pip3() { - wrapSafeChainCommand "pip3" "aikido-pip" "$@" + wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" "$@" } + function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command From 8b9ffc28ed45409f6ea93439f178cef0e81192e9 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 07:04:35 -0700 Subject: [PATCH 03/52] Some cleanup --- packages/safe-chain/bin/aikido-pip.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 99fbbb6..e5e6206 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -10,6 +10,8 @@ let targetVersionMajor; // Copy argv so we can mutate while parsing const argv = process.argv.slice(2); +console.log("** aikido-pip ** Original arguments:", process.argv.slice(2)); + for (let i = 0; i < argv.length; i++) { const a = argv[i]; @@ -28,6 +30,8 @@ if (targetVersionMajor && String(targetVersionMajor).trim() === "3") { packageManagerName = "pip3"; } +console.log("** aikido-pip ** Final arguments (after processing):", argv); + initializePackageManager(packageManagerName); var exitCode = await main(argv); From 1f707c1e13df368b963a8731bdbbb4a70de08a39 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 09:43:40 -0700 Subject: [PATCH 04/52] Add cert --- packages/safe-chain/src/registryProxy/registryProxy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index b0e8dd1..e4ec3a6 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -33,6 +33,11 @@ function getSafeChainProxyEnvironmentVariables() { HTTPS_PROXY: `http://localhost:${state.port}`, GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`, NODE_EXTRA_CA_CERTS: getCaCertPath(), + + // Following env vars point pip and Python's requests/urllib at a CA bundle file. + PIP_CERT: getCaCertPath(), + REQUESTS_CA_BUNDLE: getCaCertPath(), + SSL_CERT_FILE: getCaCertPath(), }; } From fbb7e0f95f08adb574bed89d8be9ad0f5e061695 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 14:51:44 -0700 Subject: [PATCH 05/52] Add tests --- package-lock.json | 1664 ++++++++++++++++- package.json | 5 + packages/safe-chain/bin/aikido-pip.js | 4 + packages/safe-chain/src/api/aikido.js | 24 +- packages/safe-chain/src/config/settings.js | 12 + .../src/registryProxy/parsePackageFromUrl.js | 102 +- .../registryProxy/parsePackageFromUrl.spec.js | 78 + .../src/registryProxy/registryProxy.js | 13 +- .../registryProxy/registryProxy.mitm.spec.js | 53 + .../safe-chain/src/scanning/audit/index.js | 1 + 10 files changed, 1934 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d64f79..feddfae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,11 @@ "packages/*", "test/e2e" ], + "dependencies": { + "express": "^5.1.0", + "lodash": "^4.17.21", + "webpack": "^5.102.1" + }, "devDependencies": { "oxlint": "^1.22.0" } @@ -106,6 +111,51 @@ "node": ">=18.0.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -400,6 +450,243 @@ "node": ">=14" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -409,6 +696,52 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -439,6 +772,35 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz", + "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -447,6 +809,46 @@ "balanced-match": "^1.0.0" } }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/bun": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", @@ -482,6 +884,15 @@ "@oven/bun-windows-x64-baseline": "1.2.21" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -505,6 +916,55 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -525,6 +985,15 @@ "node": ">=18" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -570,6 +1039,51 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -601,18 +1115,62 @@ } } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.238", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.238.tgz", + "integrity": "sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==", + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -623,17 +1181,17 @@ "iconv-lite": "^0.6.2" } }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", - "optional": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, "node_modules/err-code": { @@ -642,6 +1200,199 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -658,6 +1409,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -670,6 +1439,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -682,6 +1460,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -702,6 +1517,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -717,6 +1538,57 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", @@ -735,6 +1607,31 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -761,6 +1658,18 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -770,6 +1679,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -783,6 +1698,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -804,6 +1728,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -837,12 +1767,38 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -852,6 +1808,25 @@ ], "license": "MIT" }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -908,6 +1883,63 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1096,6 +2128,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -1115,6 +2153,12 @@ "nan": "^2.17.0" } }, + "node_modules/node-releases": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "license": "MIT" + }, "node_modules/npm-package-arg": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", @@ -1149,6 +2193,39 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -1274,6 +2351,15 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1299,6 +2385,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -1321,6 +2423,92 @@ "node": ">=10" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -1361,12 +2549,66 @@ "node": ">= 4" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "optional": true + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } }, "node_modules/semver": { "version": "7.7.2", @@ -1380,6 +2622,58 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1401,6 +2695,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1451,6 +2817,25 @@ "node": ">= 14" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -1469,6 +2854,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -1535,6 +2929,34 @@ "node": ">=8" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", @@ -1552,6 +2974,87 @@ "node": ">=18" } }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -1576,6 +3079,45 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/validate-npm-package-name": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", @@ -1585,6 +3127,106 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1697,6 +3339,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 0193a82..a0c235a 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,10 @@ "license": "AGPL-3.0-or-later", "devDependencies": { "oxlint": "^1.22.0" + }, + "dependencies": { + "express": "^5.1.0", + "lodash": "^4.17.21", + "webpack": "^5.102.1" } } diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index e5e6206..e4669b3 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -2,6 +2,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem } from "../src/config/settings.js"; // Defaults let packageManagerName = "pip"; @@ -32,6 +33,9 @@ if (targetVersionMajor && String(targetVersionMajor).trim() === "3") { console.log("** aikido-pip ** Final arguments (after processing):", argv); +// Set eco system +setEcoSystem("py"); + initializePackageManager(packageManagerName); var exitCode = await main(argv); diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index c9eeea0..2cabbe0 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -1,12 +1,20 @@ import fetch from "make-fetch-happen"; +import { getEcoSystem } from "../config/settings.js"; -const malwareDatabaseUrl = - "https://malware-list.aikido.dev/malware_predictions.json"; +const malwareDatabaseUrls = { + js: "https://malware-list.aikido.dev/malware_predictions.json", + python: "https://malware-list.aikido.dev/malware_predictions_python.json", +}; export async function fetchMalwareDatabase() { + const ecosystem = getEcoSystem() || "js"; + if (ecosystem === "py") { + console.log("**aikido.js** Using 'python' ecosystem for malware database fetch"); + } + const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl); if (!response.ok) { - throw new Error(`Error fetching malware database: ${response.statusText}`); + throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`); } try { @@ -16,17 +24,23 @@ export async function fetchMalwareDatabase() { version: response.headers.get("etag") || undefined, }; } catch (error) { - throw new Error(`Error parsing malware database: ${error.message}`); + throw new Error(`Error parsing ${ecosystem} malware database: ${error.message}`); } } export async function fetchMalwareDatabaseVersion() { + const ecosystem = getEcoSystem() || "js"; + if (ecosystem === "py") { + console.log("**aikido.js** Using 'python' ecosystem for malware database fetch"); + } + + const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl, { method: "HEAD", }); if (!response.ok) { throw new Error( - `Error fetching malware database version: ${response.statusText}` + `Error fetching ${ecosystem} malware database version: ${response.statusText}` ); } return response.headers.get("etag") || undefined; diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ed2cae2..0e03dd5 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -12,3 +12,15 @@ export function getMalwareAction() { export const MALWARE_ACTION_BLOCK = "block"; export const MALWARE_ACTION_PROMPT = "prompt"; + +// Default to JavaScript ecosystem +const ecosystemSettings = { + ecoSystem: "js", +}; + +export function getEcoSystem() { + return ecosystemSettings.ecoSystem; +} +export function setEcoSystem(setting) { + ecosystemSettings.ecoSystem = setting; +} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 7368b35..583c439 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -1,15 +1,41 @@ -export const knownRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; +import { parse } from "semver"; + +export const knownNpmRegistries = ["registry.npmjs.org"]; +export const knownYarnRegistries = ["registry.yarnpkg.com"]; +export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"]; export function parsePackageFromUrl(url) { - let packageName, version, registry; + let registry; - for (const knownRegistry of knownRegistries) { + for (const knownRegistry of knownNpmRegistries) { if (url.includes(knownRegistry)) { registry = knownRegistry; - break; + return parseNpmYarnPackageFromUrl(url, registry); } } + for (const knownRegistry of knownPipRegistries) { + console.log("**parsePackageFromUrl.js** Checking pip registry:", knownRegistry); + if (url.includes(knownRegistry)) { + console.log("**parsePackageFromUrl.js** Matched pip registry:", knownRegistry); + registry = knownRegistry; + return parsePipPackageFromUrl(url, registry); + } + } + + for (const knownRegistry of knownYarnRegistries) { + if (url.includes(knownRegistry)) { + registry = knownRegistry; + return parseNpmYarnPackageFromUrl(url, registry); + } + } + + // If no known registry matched, return { packageName: undefined, version: undefined } + return { packageName: undefined, version: undefined }; +} + +function parseNpmYarnPackageFromUrl(url, registry) { + let packageName, version; if (!registry || !url.endsWith(".tgz")) { return { packageName, version }; } @@ -44,5 +70,73 @@ export function parsePackageFromUrl(url) { } } + console.log("**parsePackageFromUrl.js** Parsed package:", { packageName, version }); return { packageName, version }; } + +function parsePipPackageFromUrl(url, registry) { + let packageName, version + + // Basic validation + if (!registry || typeof url !== "string") { + console.log("**parsePackageFromUrl.js** Invalid registry or URL"); + return { packageName, version}; + } + + // Quick sanity check on the URL + parse + let u; + try { + u = new URL(url); + } catch { + console.log("**parsePackageFromUrl.js** Malformed URL:", url); + return { packageName, version}; + } + + // Get the last path segment (filename) and decode it (strip query & fragment automatically) + const lastSegment = u.pathname.split("/").filter(Boolean).pop(); + if (!lastSegment){ + console.log("**parsePackageFromUrl.js** No filename in URL path:", url); + return { packageName, version}; + } + + const filename = decodeURIComponent(lastSegment); + + // Wheel (.whl) + if (filename.endsWith(".whl")) { + const base = filename.slice(0, -4); // remove ".whl" + const firstDash = base.indexOf("-"); + if (firstDash > 0) { + const dist = base.slice(0, firstDash); // may contain underscores + const rest = base.slice(firstDash + 1); // version + the rest of tags + const secondDash = rest.indexOf("-"); + const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest; + packageName = dist; // preserve underscores + version = rawVersion; + if (version === "latest" || !packageName || !version) { + return { packageName: undefined, version: undefined }; + } + console.log("**parsePackageFromUrl.js** Parsed package:", { packageName, version }); + return { packageName, version }; + } + } + + // Source dist (sdist) + const sdistExtMatch = filename.match(/\.(tar\.gz|zip|tar\.bz2|tar\.xz)$/i); + if (sdistExtMatch) { + const base = filename.slice(0, -sdistExtMatch[0].length); + const lastDash = base.lastIndexOf("-"); + if (lastDash > 0 && lastDash < base.length - 1) { + packageName = base.slice(0, lastDash); + version = base.slice(lastDash + 1); + if (version === "latest" || !packageName || !version) { + return { packageName: undefined, version: undefined }; + } + console.log("**parsePackageFromUrl.js** Parsed package:", { packageName, version }); + return { packageName, version }; + } + } + + // Unknown file type or invalid + console.log("**parsePackageFromUrl.js** Unknown file type for URL:", url); + return { packageName: undefined, version: undefined }; +} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js index 0b8f700..ee4c6b3 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js @@ -112,3 +112,81 @@ describe("parsePackageFromUrl", () => { }); }); }); + +describe("parsePackageFromUrl - pip URLs", () => { + const pipTestCases = [ + // Valid pip URLs + { + url: "https://files.pythonhosted.org/packages/xx/yy/foobar-1.2.3.tar.gz", + expected: { packageName: "foobar", version: "1.2.3" }, + }, + { + url: "https://pypi.org/packages/source/f/foobar/foobar-1.2.3.tar.gz", + expected: { packageName: "foobar", version: "1.2.3" }, + }, + { + url: "https://pypi.org/packages/source/f/foo-bar/foo-bar-0.9.0.tar.gz", + expected: { packageName: "foo-bar", version: "0.9.0" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-py3-none-any.whl", + expected: { packageName: "foo_bar", version: "2.0.0" }, + }, + { + url: "https://files.pythonhosted.org/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl", + expected: { packageName: "foo_bar", version: "2.0.0" }, + }, + { + url: "https://pypi.org/packages/source/f/foo.bar/foo.bar-1.0.0.tar.gz", + expected: { packageName: "foo.bar", version: "1.0.0" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0b1.tar.gz", + expected: { packageName: "foo_bar", version: "2.0.0b1" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0rc1.tar.gz", + expected: { packageName: "foo_bar", version: "2.0.0rc1" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0.post1.tar.gz", + expected: { packageName: "foo_bar", version: "2.0.0.post1" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0.dev1.tar.gz", + expected: { packageName: "foo_bar", version: "2.0.0.dev1" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz", + expected: { packageName: "foo_bar", version: "2.0.0a1" }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-cp38-cp38-manylinux1_x86_64.whl", + expected: { packageName: "foo_bar", version: "2.0.0" }, + }, + // Invalid pip URLs + { + url: "https://pypi.org/simple/", + expected: { packageName: undefined, version: undefined }, + }, + { + url: "https://pypi.org/project/foobar/", + expected: { packageName: undefined, version: undefined }, + }, + { + url: "https://files.pythonhosted.org/packages/xx/yy/foobar-latest.tar.gz", + expected: { packageName: undefined, version: undefined }, + }, + { + url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-latest.tar.gz", + expected: { packageName: undefined, version: undefined }, + }, + ]; + + pipTestCases.forEach(({ url, expected }, index) => { + it(`should parse pip URL ${index + 1}: ${url}`, () => { + const result = parsePackageFromUrl(url); + assert.deepEqual(result, expected); + }); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index e4ec3a6..3809312 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -4,7 +4,7 @@ import { mitmConnect } from "./mitmRequestHandler.js"; import { handleHttpProxyRequest } from "./plainHttpProxy.js"; import { getCaCertPath } from "./certUtils.js"; import { auditChanges } from "../scanning/audit/index.js"; -import { knownRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; +import { knownNpmRegistries, knownYarnRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; @@ -108,10 +108,11 @@ function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL - if (knownRegistries.some((reg) => req.url.includes(reg))) { - // For npm and yarn registries, we want to intercept and inspect the traffic - // so we can block packages with malware - mitmConnect(req, clientSocket, isAllowedUrl); + console.log("**registryProxy.js** Handling CONNECT request for:", req.url); + if ((knownNpmRegistries.some((reg) => req.url.includes(reg))) + || (knownYarnRegistries.some((reg) => req.url.includes(reg))) + || (knownPipRegistries.some((reg) => req.url.includes(reg)))) { + mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket tunnelRequest(req, clientSocket, head); @@ -124,6 +125,7 @@ async function isAllowedUrl(url) { // packageName and version are undefined when the URL is not a package download // In that case, we can allow the request to proceed if (!packageName || !version) { + console.log("**registryProxy.js** Non-package URL, allowing:", url); return true; } @@ -132,6 +134,7 @@ async function isAllowedUrl(url) { ]); if (!auditResult.isAllowed) { + console.log("**registryProxy.js** Blocking malicious package:", { packageName, version, url }); state.blockedRequests.push({ packageName, version, url }); return false; } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index 515284c..3b2a20c 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -140,6 +140,59 @@ describe("registryProxy.mitm", () => { // Same hostname should get the same certificate (fingerprint) assert.strictEqual(cert1.fingerprint, cert2.fingerprint); }); + + // --- Pip registry MITM and env var tests --- + it("should set pip CA trust environment variables", () => { + const envVars = mergeSafeChainProxyEnvironmentVariables([]); + const caPath = getCaCertPath(); + assert.strictEqual(envVars.PIP_CERT, caPath); + assert.strictEqual(envVars.REQUESTS_CA_BUNDLE, caPath); + assert.strictEqual(envVars.SSL_CERT_FILE, caPath); + }); + + it("should intercept HTTPS requests to pypi.org for pip package", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "pypi.org", + "/packages/source/f/foo_bar/foo_bar-2.0.0.tar.gz" + ); + assert.notStrictEqual(response.statusCode, 403); + assert.ok(typeof response.body === "string"); + }); + + it("should intercept HTTPS requests to files.pythonhosted.org for pip wheel", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "files.pythonhosted.org", + "/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl" + ); + assert.notStrictEqual(response.statusCode, 403); + assert.ok(typeof response.body === "string"); + }); + + it("should handle pip package with a1 version", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "pypi.org", + "/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz" + ); + assert.notStrictEqual(response.statusCode, 403); + assert.ok(typeof response.body === "string"); + }); + + it("should handle pip package with latest version (should not block)", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "pypi.org", + "/packages/source/f/foo_bar/foo_bar-latest.tar.gz" + ); + assert.notStrictEqual(response.statusCode, 403); + assert.ok(typeof response.body === "string"); + }); }); async function makeRegistryRequest(proxyHost, proxyPort, targetHost, path) { diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 215bfa0..be46fdb 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -7,6 +7,7 @@ export async function auditChanges(changes) { const allowedChanges = []; const disallowedChanges = []; + console.log("**audit/index.js** Auditing changes:", changes); var malwarePackages = await getPackagesWithMalware( changes.filter( (change) => change.type === "add" || change.type === "change" From 982da4aa775193ed45bd0f80bf1413b949b51cfc Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 15:16:53 -0700 Subject: [PATCH 06/52] more cleanup --- packages/safe-chain/bin/aikido-pip.js | 2 -- .../pip/createPipPackageManager.js | 1 - .../src/registryProxy/parsePackageFromUrl.js | 25 +++---------------- .../src/registryProxy/registryProxy.js | 7 ++---- 4 files changed, 6 insertions(+), 29 deletions(-) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index e4669b3..7834bc5 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -11,8 +11,6 @@ let targetVersionMajor; // Copy argv so we can mutate while parsing const argv = process.argv.slice(2); -console.log("** aikido-pip ** Original arguments:", process.argv.slice(2)); - for (let i = 0; i < argv.length; i++) { const a = argv[i]; diff --git a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js index 6aaf986..93d0fcc 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js @@ -19,7 +19,6 @@ export function createPipPackageManager(command = "pip") { async function runPipCommand(command, args) { try { - console.log("**createPipPackageManager.js** Running pip command"); const result = await safeSpawn(command, args, { stdio: "inherit", env: mergeSafeChainProxyEnvironmentVariables(process.env), diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 583c439..4061578 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -1,40 +1,30 @@ import { parse } from "semver"; -export const knownNpmRegistries = ["registry.npmjs.org"]; -export const knownYarnRegistries = ["registry.yarnpkg.com"]; +export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"]; export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"]; export function parsePackageFromUrl(url) { let registry; - for (const knownRegistry of knownNpmRegistries) { + for (const knownRegistry of knownJsRegistries) { if (url.includes(knownRegistry)) { registry = knownRegistry; - return parseNpmYarnPackageFromUrl(url, registry); + return parseJsPackageFromUrl(url, registry); } } for (const knownRegistry of knownPipRegistries) { - console.log("**parsePackageFromUrl.js** Checking pip registry:", knownRegistry); if (url.includes(knownRegistry)) { - console.log("**parsePackageFromUrl.js** Matched pip registry:", knownRegistry); registry = knownRegistry; return parsePipPackageFromUrl(url, registry); } } - for (const knownRegistry of knownYarnRegistries) { - if (url.includes(knownRegistry)) { - registry = knownRegistry; - return parseNpmYarnPackageFromUrl(url, registry); - } - } - // If no known registry matched, return { packageName: undefined, version: undefined } return { packageName: undefined, version: undefined }; } -function parseNpmYarnPackageFromUrl(url, registry) { +function parseJsPackageFromUrl(url, registry) { let packageName, version; if (!registry || !url.endsWith(".tgz")) { return { packageName, version }; @@ -70,7 +60,6 @@ function parseNpmYarnPackageFromUrl(url, registry) { } } - console.log("**parsePackageFromUrl.js** Parsed package:", { packageName, version }); return { packageName, version }; } @@ -79,7 +68,6 @@ function parsePipPackageFromUrl(url, registry) { // Basic validation if (!registry || typeof url !== "string") { - console.log("**parsePackageFromUrl.js** Invalid registry or URL"); return { packageName, version}; } @@ -88,14 +76,12 @@ function parsePipPackageFromUrl(url, registry) { try { u = new URL(url); } catch { - console.log("**parsePackageFromUrl.js** Malformed URL:", url); return { packageName, version}; } // Get the last path segment (filename) and decode it (strip query & fragment automatically) const lastSegment = u.pathname.split("/").filter(Boolean).pop(); if (!lastSegment){ - console.log("**parsePackageFromUrl.js** No filename in URL path:", url); return { packageName, version}; } @@ -115,7 +101,6 @@ function parsePipPackageFromUrl(url, registry) { if (version === "latest" || !packageName || !version) { return { packageName: undefined, version: undefined }; } - console.log("**parsePackageFromUrl.js** Parsed package:", { packageName, version }); return { packageName, version }; } } @@ -131,12 +116,10 @@ function parsePipPackageFromUrl(url, registry) { if (version === "latest" || !packageName || !version) { return { packageName: undefined, version: undefined }; } - console.log("**parsePackageFromUrl.js** Parsed package:", { packageName, version }); return { packageName, version }; } } // Unknown file type or invalid - console.log("**parsePackageFromUrl.js** Unknown file type for URL:", url); return { packageName: undefined, version: undefined }; } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3809312..2dfb1b5 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -4,7 +4,7 @@ import { mitmConnect } from "./mitmRequestHandler.js"; import { handleHttpProxyRequest } from "./plainHttpProxy.js"; import { getCaCertPath } from "./certUtils.js"; import { auditChanges } from "../scanning/audit/index.js"; -import { knownNpmRegistries, knownYarnRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; +import { knownJsRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; @@ -109,8 +109,7 @@ function handleConnect(req, clientSocket, head) { // It establishes a tunnel to the server identified by the request URL console.log("**registryProxy.js** Handling CONNECT request for:", req.url); - if ((knownNpmRegistries.some((reg) => req.url.includes(reg))) - || (knownYarnRegistries.some((reg) => req.url.includes(reg))) + if ((knownJsRegistries.some((reg) => req.url.includes(reg))) || (knownPipRegistries.some((reg) => req.url.includes(reg)))) { mitmConnect(req, clientSocket, isAllowedUrl); } else { @@ -125,7 +124,6 @@ async function isAllowedUrl(url) { // packageName and version are undefined when the URL is not a package download // In that case, we can allow the request to proceed if (!packageName || !version) { - console.log("**registryProxy.js** Non-package URL, allowing:", url); return true; } @@ -134,7 +132,6 @@ async function isAllowedUrl(url) { ]); if (!auditResult.isAllowed) { - console.log("**registryProxy.js** Blocking malicious package:", { packageName, version, url }); state.blockedRequests.push({ packageName, version, url }); return false; } From 1b82aeb6b0a365c27bb834c5af864c14fc8d4511 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 15:28:27 -0700 Subject: [PATCH 07/52] Adapt the structure to parse the initial pip commands --- .../pip/createPipPackageManager.js | 61 ++++++++++------- .../commandArgumentScanner.js | 27 ++++++++ .../pip/dependencyScanner/nullScanner.js | 10 +++ .../parsing/parsePackagesFromInstallArgs.js | 62 +++++++++++++++++ .../parsePackagesFromInstallArgs.spec.js | 40 +++++++++++ .../src/packagemanager/pip/runPipCommand.js | 66 +++++++++++++++++++ .../packagemanager/pip/utils/pipCommands.js | 29 ++++++++ 7 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js create mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js create mode 100644 packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js create mode 100644 packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js create mode 100644 packages/safe-chain/src/packagemanager/pip/runPipCommand.js create mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js diff --git a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js index 93d0fcc..7861d16 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js @@ -1,6 +1,11 @@ -import { ui } from "../../environment/userInteraction.js"; -import { safeSpawn } from "../../utils/safeSpawn.js"; -import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; +import { nullScanner } from "./dependencyScanner/nullScanner.js"; +import { runPip } from "./runPipCommand.js"; +import { + getPipCommandForArgs, + pipInstallCommand, + pipUninstallCommand, +} from "./utils/pipCommands.js"; /** * Creates a package manager interface for Python's pip package installer @@ -8,28 +13,40 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg * @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip" */ export function createPipPackageManager(command = "pip") { - return { - runCommand: (args) => runPipCommand(command, args), + function isSupportedCommand(args) { + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); + return scanner.shouldScan(args); + } - // For pip, set proxy server - isSupportedCommand: () => false, - getDependencyUpdatesForCommand: () => [], + function getDependencyUpdatesForCommand(args) { + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); + return scanner.scan(args); + } + + return { + runCommand: (args) => runPip(command, args), + isSupportedCommand, + getDependencyUpdatesForCommand, }; } -async function runPipCommand(command, args) { - try { - const result = await safeSpawn(command, args, { - stdio: "inherit", - env: mergeSafeChainProxyEnvironmentVariables(process.env), - }); - return { status: result.status }; - } catch (error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } +const commandScannerMapping = { + [pipInstallCommand]: commandArgumentScanner(), + [pipUninstallCommand]: nullScanner(), // Uninstall doesn't need scanning +}; + +function findDependencyScannerForCommand(scanners, args) { + const command = getPipCommandForArgs(args); + if (!command) { + return nullScanner(); } + + const scanner = scanners[command]; + return scanner ? scanner : nullScanner(); } diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js new file mode 100644 index 0000000..26429f9 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -0,0 +1,27 @@ +/** + * Scanner for pip command arguments to detect package installations + * + * @param {Object} options - Scanner options + * @param {boolean} [options.ignoreDryRun=false] - Whether to ignore dry-run flag + * @returns {Object} Scanner interface + */ +export function commandArgumentScanner(options = {}) { + const { ignoreDryRun = false } = options; + + function shouldScan(args) { + // For now, pip scanning is not yet implemented + // This would need to detect 'install' commands and package arguments + return false; + } + + function scan(args) { + // Future implementation would parse pip install arguments + // and return array of {name, version, type} objects + return []; + } + + return { + shouldScan, + scan, + }; +} diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js new file mode 100644 index 0000000..ec3ba12 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js @@ -0,0 +1,10 @@ +/** + * Null scanner that returns no dependencies + * Used when a command is not supported for scanning + */ +export function nullScanner() { + return { + shouldScan: () => false, + scan: () => [], + }; +} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js new file mode 100644 index 0000000..109f994 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -0,0 +1,62 @@ +/** + * Parses package specifications from pip install arguments + * + * Pip supports various package specification formats: + * - package_name + * - package_name==version + * - package_name>=version + * - package_name~=version + * - git+https://... + * - -r requirements.txt + * - . (local directory) + * + * @param {string[]} args - pip install command arguments + * @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications + */ +export function parsePackagesFromInstallArgs(args) { + const packages = []; + let skipNext = false; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (skipNext) { + skipNext = false; + continue; + } + + // Skip the command itself (install, uninstall, etc.) + if (i === 0 && !arg.startsWith("-")) { + continue; + } + + // Skip flags and their values + if (arg.startsWith("-")) { + // Some flags take a value, skip the next arg for those + if (arg === "-r" || arg === "--requirement" || + arg === "-c" || arg === "--constraint" || + arg === "-e" || arg === "--editable" || + arg === "-t" || arg === "--target" || + arg === "-i" || arg === "--index-url" || + arg === "--extra-index-url") { + skipNext = true; + } + continue; + } + + // TODO: Implement full parsing logic + // For now, this is a placeholder that would need to handle: + // - Version specifiers (==, >=, <=, ~=, !=, <, >) + // - VCS urls (git+, hg+, svn+, bzr+) + // - Local file paths + // - Requirements files (-r, --requirement) + // - Extras (package[extra1,extra2]) + + packages.push({ + name: arg, + type: "add", + }); + } + + return packages; +} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js new file mode 100644 index 0000000..1e3782c --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js @@ -0,0 +1,40 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { parsePackagesFromInstallArgs } from "./parsePackagesFromInstallArgs.js"; + +describe("parsePackagesFromInstallArgs", () => { + it("should parse simple package name", () => { + const result = parsePackagesFromInstallArgs(["install", "requests"]); + assert.deepEqual(result, [ + { name: "requests", type: "add" }, + ]); + }); + + it("should parse package with version specifier", () => { + const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]); + assert.deepEqual(result, [ + { name: "requests==2.28.0", type: "add" }, + ]); + }); + + it("should skip flags", () => { + const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]); + assert.deepEqual(result, [ + { name: "requests", type: "add" }, + ]); + }); + + it("should parse multiple packages", () => { + const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]); + assert.deepEqual(result, [ + { name: "requests", type: "add" }, + { name: "flask", type: "add" }, + { name: "django==4.0", type: "add" }, + ]); + }); + + it("should return empty array for no packages", () => { + const result = parsePackagesFromInstallArgs(["install", "--help"]); + assert.deepEqual(result, []); + }); +}); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js new file mode 100644 index 0000000..859326a --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -0,0 +1,66 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; + +/** + * Runs a pip command with the specified arguments + * + * @param {string} command - The pip command to use (e.g., "pip", "pip3") + * @param {string[]} args - Command arguments + * @returns {Promise<{status: number}>} Result object with status code + */ +export async function runPip(command, args) { + try { + const result = await safeSpawn(command, args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; + } catch (error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} + +/** + * Runs a pip command in dry-run mode and captures output + * Note: pip doesn't have a native --dry-run flag, so this may need adjustment + * + * @param {string} command - The pip command to use + * @param {string[]} args - Command arguments + * @returns {Promise<{status: number, output: string}>} Result with status and output + */ +export async function dryRunPipCommandAndOutput(command, args) { + try { + // Note: pip doesn't have a --dry-run flag like npm + // This would need to be implemented differently if dry-run functionality is needed + const result = await safeSpawn( + command, + args, + { + stdio: "pipe", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + } + ); + return { + status: result.status, + output: result.status === 0 ? result.stdout : result.stderr, + }; + } catch (error) { + if (error.status) { + const output = + error.stdout?.toString() ?? + error.stderr?.toString() ?? + error.message ?? + ""; + return { status: error.status, output }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js new file mode 100644 index 0000000..99ce14b --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -0,0 +1,29 @@ +/** + * Pip command constants + */ +export const pipInstallCommand = "install"; +export const pipUninstallCommand = "uninstall"; +export const pipListCommand = "list"; +export const pipShowCommand = "show"; +export const pipFreeze = "freeze"; + +/** + * Gets the pip command from the arguments array + * + * @param {string[]} args - Command line arguments + * @returns {string|null} The pip command or null if not found + */ +export function getPipCommandForArgs(args) { + if (!args || args.length === 0) { + return null; + } + + // The first non-flag argument is typically the command + for (const arg of args) { + if (!arg.startsWith("-")) { + return arg; + } + } + + return null; +} From 1fdb15a39206231ae2acc242757d72675fb42ec1 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 23 Oct 2025 09:14:05 -0700 Subject: [PATCH 08/52] Fix some border cases --- .../packagemanager/currentPackageManager.js | 2 +- ...kageManager.js => createPackageManager.js} | 7 +- .../pip/createPackageManager.spec.js | 117 ++++++++++ .../commandArgumentScanner.js | 36 ++- .../commandArgumentScanner.spec.js | 215 ++++++++++++++++++ .../parsing/parsePackagesFromInstallArgs.js | 160 ++++++++++--- .../parsePackagesFromInstallArgs.spec.js | 88 ++++++- .../packagemanager/pip/utils/pipCommands.js | 18 +- .../pip/utils/pipCommands.spec.js | 88 +++++++ 9 files changed, 685 insertions(+), 46 deletions(-) rename packages/safe-chain/src/packagemanager/pip/{createPipPackageManager.js => createPackageManager.js} (81%) create mode 100644 packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js create mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js create mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2c78d06..ff2481d 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -9,7 +9,7 @@ import { createPnpxPackageManager, } from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; -import { createPipPackageManager } from "./pip/createPipPackageManager.js"; +import { createPipPackageManager } from "./pip/createPackageManager.js"; const state = { packageManagerName: null, diff --git a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js similarity index 81% rename from packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js rename to packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 7861d16..2f0fc0d 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPipPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -4,7 +4,8 @@ import { runPip } from "./runPipCommand.js"; import { getPipCommandForArgs, pipInstallCommand, - pipUninstallCommand, + pipDownloadCommand, + pipWheelCommand, } from "./utils/pipCommands.js"; /** @@ -38,7 +39,9 @@ export function createPipPackageManager(command = "pip") { const commandScannerMapping = { [pipInstallCommand]: commandArgumentScanner(), - [pipUninstallCommand]: nullScanner(), // Uninstall doesn't need scanning + [pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI + [pipWheelCommand]: commandArgumentScanner(), // wheel downloads and builds packages + // Other commands (uninstall, list, etc.) will use nullScanner() by default }; function findDependencyScannerForCommand(scanners, args) { diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js new file mode 100644 index 0000000..d525e2c --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js @@ -0,0 +1,117 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { createPipPackageManager } from "./createPackageManager.js"; + +test("createPipPackageManager", async (t) => { + await t.test("should create package manager with default pip command", () => { + const pm = createPipPackageManager(); + + assert.ok(pm); + assert.strictEqual(typeof pm.runCommand, "function"); + assert.strictEqual(typeof pm.isSupportedCommand, "function"); + assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function"); + }); + + await t.test("should create package manager with custom pip3 command", () => { + const pm = createPipPackageManager("pip3"); + + assert.ok(pm); + assert.strictEqual(typeof pm.runCommand, "function"); + }); + + await t.test("should recognize install command as supported", () => { + const pm = createPipPackageManager(); + + // Note: Currently returns false because commandArgumentScanner is not yet implemented + // When implemented, this should return true + const result = pm.isSupportedCommand(["install", "requests"]); + assert.strictEqual(typeof result, "boolean"); + }); + + await t.test("should recognize download command as supported", () => { + const pm = createPipPackageManager(); + + const result = pm.isSupportedCommand(["download", "requests"]); + assert.strictEqual(typeof result, "boolean"); + }); + + await t.test("should recognize wheel command as supported", () => { + const pm = createPipPackageManager(); + + const result = pm.isSupportedCommand(["wheel", "requests"]); + assert.strictEqual(typeof result, "boolean"); + }); + + await t.test("should not support uninstall command", () => { + const pm = createPipPackageManager(); + + const result = pm.isSupportedCommand(["uninstall", "requests"]); + assert.strictEqual(result, false); + }); + + await t.test("should not support list command", () => { + const pm = createPipPackageManager(); + + const result = pm.isSupportedCommand(["list"]); + assert.strictEqual(result, false); + }); + + await t.test("should not support show command", () => { + const pm = createPipPackageManager(); + + const result = pm.isSupportedCommand(["show", "requests"]); + assert.strictEqual(result, false); + }); + + await t.test("should return empty array for getDependencyUpdatesForCommand on install", () => { + const pm = createPipPackageManager(); + + // Note: Currently returns [] because commandArgumentScanner is not yet implemented + const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]); + assert.ok(Array.isArray(result)); + }); + + await t.test("should return empty array for getDependencyUpdatesForCommand on download", () => { + const pm = createPipPackageManager(); + + const result = pm.getDependencyUpdatesForCommand(["download", "flask"]); + assert.ok(Array.isArray(result)); + }); + + await t.test("should return empty array for getDependencyUpdatesForCommand on wheel", () => { + const pm = createPipPackageManager(); + + const result = pm.getDependencyUpdatesForCommand(["wheel", "django"]); + assert.ok(Array.isArray(result)); + }); + + await t.test("should return empty array for unsupported commands", () => { + const pm = createPipPackageManager(); + + const result = pm.getDependencyUpdatesForCommand(["uninstall", "requests"]); + assert.strictEqual(Array.isArray(result), true); + assert.strictEqual(result.length, 0); + }); + + await t.test("should handle empty args array", () => { + const pm = createPipPackageManager(); + + const supported = pm.isSupportedCommand([]); + assert.strictEqual(supported, false); + + const deps = pm.getDependencyUpdatesForCommand([]); + assert.ok(Array.isArray(deps)); + assert.strictEqual(deps.length, 0); + }); + + await t.test("should handle args with only flags", () => { + const pm = createPipPackageManager(); + + const supported = pm.isSupportedCommand(["--version"]); + assert.strictEqual(supported, false); + + const deps = pm.getDependencyUpdatesForCommand(["-h", "--help"]); + assert.ok(Array.isArray(deps)); + assert.strictEqual(deps.length, 0); + }); +}); diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js index 26429f9..f0e47f1 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -1,3 +1,6 @@ +import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js"; +import { hasDryRunArg } from "../utils/pipCommands.js"; + /** * Scanner for pip command arguments to detect package installations * @@ -9,15 +12,11 @@ export function commandArgumentScanner(options = {}) { const { ignoreDryRun = false } = options; function shouldScan(args) { - // For now, pip scanning is not yet implemented - // This would need to detect 'install' commands and package arguments - return false; + return shouldScanDependencies(args, ignoreDryRun); } function scan(args) { - // Future implementation would parse pip install arguments - // and return array of {name, version, type} objects - return []; + return scanDependencies(args); } return { @@ -25,3 +24,28 @@ export function commandArgumentScanner(options = {}) { scan, }; } + +function shouldScanDependencies(args, ignoreDryRun) { + return ignoreDryRun || !hasDryRunArg(args); +} + +function scanDependencies(args) { + return checkChangesFromArgs(args); +} + +/** + * Extracts package changes from pip command arguments + * + * Unlike npm, pip's parser already returns exact versions (== or ===) + * or "latest" for unversioned packages, so no version resolution is needed. + * + * @param {string[]} args - Command line arguments + * @returns {Array<{name: string, version: string, type: string}>} Package changes + */ +export function checkChangesFromArgs(args) { + const packageUpdates = parsePackagesFromInstallArgs(args); + + // Parser already provides exact versions or "latest", no need to resolve + // Just return the packages with type "add" + return packageUpdates; +} diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js new file mode 100644 index 0000000..6e69386 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js @@ -0,0 +1,215 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { commandArgumentScanner, checkChangesFromArgs } from "./commandArgumentScanner.js"; + +test("commandArgumentScanner", async (t) => { + await t.test("should create scanner with default options", () => { + const scanner = commandArgumentScanner(); + + assert.ok(scanner); + assert.strictEqual(typeof scanner.shouldScan, "function"); + assert.strictEqual(typeof scanner.scan, "function"); + }); + + await t.test("should create scanner with ignoreDryRun option", () => { + const scanner = commandArgumentScanner({ ignoreDryRun: true }); + + assert.ok(scanner); + assert.strictEqual(typeof scanner.shouldScan, "function"); + assert.strictEqual(typeof scanner.scan, "function"); + }); +}); + +test("shouldScan", async (t) => { + await t.test("should return true for normal install command", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.shouldScan(["install", "requests"]); + assert.strictEqual(result, true); + }); + + await t.test("should return false for install with --dry-run", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.shouldScan(["install", "--dry-run", "requests"]); + assert.strictEqual(result, false); + }); + + await t.test("should return true for install with --dry-run when ignoreDryRun is true", () => { + const scanner = commandArgumentScanner({ ignoreDryRun: true }); + + const result = scanner.shouldScan(["install", "--dry-run", "requests"]); + assert.strictEqual(result, true); + }); + + await t.test("should return true for download command", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.shouldScan(["download", "flask"]); + assert.strictEqual(result, true); + }); + + await t.test("should return true for wheel command", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.shouldScan(["wheel", "django"]); + assert.strictEqual(result, true); + }); +}); + +test("scan", async (t) => { + await t.test("should scan simple package installation", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["install", "requests"]); + assert.ok(Array.isArray(result)); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "requests", + version: "latest", + type: "add", + }); + }); + + await t.test("should scan package with exact version", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["install", "requests==2.28.0"]); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "requests", + version: "2.28.0", + type: "add", + }); + }); + + await t.test("should scan multiple packages", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["install", "requests==2.28.0", "flask"]); + assert.strictEqual(result.length, 2); + assert.deepEqual(result[0], { + name: "requests", + version: "2.28.0", + type: "add", + }); + assert.deepEqual(result[1], { + name: "flask", + version: "latest", + type: "add", + }); + }); + + await t.test("should skip packages with range specifiers", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["install", "requests>=2.0.0", "flask==2.0.0"]); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "flask", + version: "2.0.0", + type: "add", + }); + }); + + await t.test("should skip flags with parameters", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan([ + "install", + "-r", + "requirements.txt", + "requests==2.28.0", + ]); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "requests", + version: "2.28.0", + type: "add", + }); + }); + + await t.test("should work with download command", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["download", "django==4.2.0"]); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "django", + version: "4.2.0", + type: "add", + }); + }); + + await t.test("should work with wheel command", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["wheel", "numpy==1.24.0"]); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "numpy", + version: "1.24.0", + type: "add", + }); + }); + + await t.test("should parse packages even for unsupported commands", () => { + const scanner = commandArgumentScanner(); + + // Note: The parser treats the first non-flag arg as the command and skips it + // So "uninstall" is treated as the command, and "requests" is parsed as a package + // The scanner itself doesn't filter by command type - that's done at a higher level + const result = scanner.scan(["uninstall", "requests"]); + assert.ok(Array.isArray(result)); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "requests", + version: "latest", + type: "add", + }); + }); + + await t.test("should handle === exact version specifier", () => { + const scanner = commandArgumentScanner(); + + const result = scanner.scan(["install", "requests===2.28.0"]); + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + name: "requests", + version: "2.28.0", + type: "add", + }); + }); +}); + +test("checkChangesFromArgs", async (t) => { + await t.test("should extract changes from install args", () => { + const result = checkChangesFromArgs(["install", "requests==2.28.0", "flask"]); + + assert.strictEqual(result.length, 2); + assert.deepEqual(result[0], { + name: "requests", + version: "2.28.0", + type: "add", + }); + assert.deepEqual(result[1], { + name: "flask", + version: "latest", + type: "add", + }); + }); + + await t.test("should return empty array for commands with no packages", () => { + const result = checkChangesFromArgs(["install", "-r", "requirements.txt"]); + + assert.ok(Array.isArray(result)); + assert.strictEqual(result.length, 0); + }); + + await t.test("should handle empty args", () => { + const result = checkChangesFromArgs([]); + + assert.ok(Array.isArray(result)); + assert.strictEqual(result.length, 0); + }); +}); diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index 109f994..3dc46ed 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -1,17 +1,26 @@ /** * Parses package specifications from pip install arguments * - * Pip supports various package specification formats: - * - package_name - * - package_name==version - * - package_name>=version - * - package_name~=version - * - git+https://... - * - -r requirements.txt - * - . (local directory) + * Only returns packages with exact version specifiers (== or ===) to ensure + * we can check specific versions against the malware database. + * + * Supported formats that will be returned: + * - package_name (no version) + * - package_name==version (exact version) + * - package_name===version (exact version, PEP 440) + * + * Skipped formats (won't be returned): + * - package_name>=version (range specifier) + * - package_name<=version (range specifier) + * - package_name>version (range specifier) + * - package_name} Array of package specifications + * @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications with exact versions only */ export function parsePackagesFromInstallArgs(args) { const packages = []; @@ -32,31 +41,126 @@ export function parsePackagesFromInstallArgs(args) { // Skip flags and their values if (arg.startsWith("-")) { - // Some flags take a value, skip the next arg for those - if (arg === "-r" || arg === "--requirement" || - arg === "-c" || arg === "--constraint" || - arg === "-e" || arg === "--editable" || - arg === "-t" || arg === "--target" || - arg === "-i" || arg === "--index-url" || - arg === "--extra-index-url") { + // Flags that take a value - skip the next arg for those + if (isPipOptionWithParameter(arg)) { skipNext = true; } continue; } - // TODO: Implement full parsing logic - // For now, this is a placeholder that would need to handle: - // - Version specifiers (==, >=, <=, ~=, !=, <, >) - // - VCS urls (git+, hg+, svn+, bzr+) - // - Local file paths - // - Requirements files (-r, --requirement) - // - Extras (package[extra1,extra2]) - - packages.push({ - name: arg, - type: "add", - }); + const parsed = parsePipSpec(arg); + if (parsed) { + packages.push({ ...parsed, type: "add" }); + } } return packages; } + +// Check if a pip flag takes a parameter +function isPipOptionWithParameter(arg) { + const optionsWithParameters = [ + // Install options + "-r", + "--requirement", + "-c", + "--constraint", + "-e", + "--editable", + "-t", + "--target", + "--platform", + "--python-version", + "--implementation", + "--abi", + "--root", + "--prefix", + "--src", + "--upgrade-strategy", + "--progress-bar", + "--root-user-action", + "--report", + "--group", + // Package index options + "-i", + "--index-url", + "--extra-index-url", + "-f", + "--find-links", + // General options + "--python", + "--log", + "--keyring-provider", + "--proxy", + "--retries", + "--timeout", + "--exists-action", + "--trusted-host", + "--cert", + "--client-cert", + "--cache-dir", + "--use-feature", + "--use-deprecated", + "--resume-retries", + ]; + + return optionsWithParameters.includes(arg); +} + +// Parse a single pip requirement spec +// Always returns { name, version } where version defaults to "latest" if not specified +function parsePipSpec(spec) { + + // Ignore obvious URLs and paths + // These cannot be scanned from the malware database + const lower = spec.toLowerCase(); + if ( + lower.startsWith("git+") || + lower.startsWith("hg+") || + lower.startsWith("svn+") || + lower.startsWith("bzr+") || + lower.startsWith("http:") || + lower.startsWith("https:") || + lower.startsWith("file:") || + spec.startsWith("./") || + spec.startsWith("../") || + spec.startsWith("/") + ) { + return { name: spec, version: "latest" }; + } + + // Strip extras: package[extra1,extra2] + const extrasStart = spec.indexOf("["); + const extrasEnd = extrasStart >= 0 ? spec.indexOf("]", extrasStart) : -1; + let base = spec; + if (extrasStart >= 0 && extrasEnd > extrasStart) { + base = spec.slice(0, extrasStart) + spec.slice(extrasEnd + 1); + } + + // Split on first occurrence of a comparator or comma spec + // Support multi-constraint lists like ">=1,<2" by detecting the first comparator + const comparatorRegex = /(===|==|!=|~=|>=|<=|<|>)/; + const m = base.match(comparatorRegex); + if (!m) { + // No comparator => just a name, use "latest" as version + return { name: base, version: "latest" }; + } + + const idx = m.index; + const name = base.slice(0, idx); + const versionPart = base.slice(idx); // e.g. '==2.28.0' or '>=1,<2' + + // Normalize whitespace inside versionPart + const versionWithOperator = versionPart.replace(/\s+/g, ""); + + // Only return packages with exact version specifiers (== or ===) + // Skip range specifiers (<, >, <=, >=, ~=, !=) since they don't provide a specific version + if (!versionWithOperator.startsWith("==")) { + return null; + } + + // Strip the == or === operator to get just the version number + const version = versionWithOperator.replace(/^===?/, ""); + + return { name, version }; +} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js index 1e3782c..6a0098b 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js @@ -6,30 +6,82 @@ describe("parsePackagesFromInstallArgs", () => { it("should parse simple package name", () => { const result = parsePackagesFromInstallArgs(["install", "requests"]); assert.deepEqual(result, [ - { name: "requests", type: "add" }, + { name: "requests", version: "latest", type: "add" }, ]); }); it("should parse package with version specifier", () => { const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]); assert.deepEqual(result, [ - { name: "requests==2.28.0", type: "add" }, + { name: "requests", version: "2.28.0", type: "add" }, ]); }); it("should skip flags", () => { const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]); assert.deepEqual(result, [ - { name: "requests", type: "add" }, + { name: "requests", version: "latest", type: "add" }, ]); }); it("should parse multiple packages", () => { const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]); assert.deepEqual(result, [ - { name: "requests", type: "add" }, - { name: "flask", type: "add" }, - { name: "django==4.0", type: "add" }, + { name: "requests", version: "latest", type: "add" }, + { name: "flask", version: "latest", type: "add" }, + { name: "django", version: "4.0", type: "add" }, + ]); + }); + + it("should parse extras and strip them from name", () => { + const result = parsePackagesFromInstallArgs(["install", "django[postgres]==4.2.1"]); + assert.deepEqual(result, [ + { name: "django", version: "4.2.1", type: "add" }, + ]); + }); + + it("should parse multiple constraints", () => { + const result = parsePackagesFromInstallArgs(["install", "requests>=2,<3"]); + // Range specifiers should be skipped since they don't provide exact versions + assert.deepEqual(result, []); + }); + + it("should skip packages with range specifiers", () => { + const result = parsePackagesFromInstallArgs([ + "install", + "requests>=2.0.0", + "flask>1.0", + "django<=4.0", + "numpy~=1.20", + "scipy!=1.5.0", + "pandas==1.3.0", + ]); + // Only pandas with exact version (==) should be returned + assert.deepEqual(result, [ + { name: "pandas", version: "1.3.0", type: "add" }, + ]); + }); + + it("should support === exact version specifier", () => { + const result = parsePackagesFromInstallArgs(["install", "requests===2.28.0"]); + assert.deepEqual(result, [ + { name: "requests", version: "2.28.0", type: "add" }, + ]); + }); + + it("should treat VCS/URL/path specs as names (no version)", () => { + const result = parsePackagesFromInstallArgs([ + "install", + "git+https://github.com/pallets/flask.git", + "https://files.pythonhosted.org/packages/foo/bar.whl", + "file:/tmp/pkg.whl", + "./localpkg", + ]); + assert.deepEqual(result, [ + { name: "git+https://github.com/pallets/flask.git", version: "latest", type: "add" }, + { name: "https://files.pythonhosted.org/packages/foo/bar.whl", version: "latest", type: "add" }, + { name: "file:/tmp/pkg.whl", version: "latest", type: "add" }, + { name: "./localpkg", version: "latest", type: "add" }, ]); }); @@ -37,4 +89,28 @@ describe("parsePackagesFromInstallArgs", () => { const result = parsePackagesFromInstallArgs(["install", "--help"]); assert.deepEqual(result, []); }); + + it("should skip all flags with parameters", () => { + const result = parsePackagesFromInstallArgs([ + "install", + "--target", + "/tmp/target", + "--platform", + "linux", + "--python-version", + "3.9", + "--index-url", + "https://pypi.org/simple", + "--trusted-host", + "pypi.org", + "requests==2.28.0", + "--cache-dir", + "/tmp/cache", + "flask", + ]); + assert.deepEqual(result, [ + { name: "requests", version: "2.28.0", type: "add" }, + { name: "flask", version: "latest", type: "add" }, + ]); + }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js index 99ce14b..e88b262 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -1,11 +1,13 @@ /** * Pip command constants + * + * Note: Unlike npm, pip does not support command aliases or abbreviations. + * Commands must be spelled out fully (e.g., "install", not "i" or "add"). */ export const pipInstallCommand = "install"; +export const pipDownloadCommand = "download"; +export const pipWheelCommand = "wheel"; export const pipUninstallCommand = "uninstall"; -export const pipListCommand = "list"; -export const pipShowCommand = "show"; -export const pipFreeze = "freeze"; /** * Gets the pip command from the arguments array @@ -27,3 +29,13 @@ export function getPipCommandForArgs(args) { return null; } + +/** + * Checks if the arguments contain the --dry-run flag + * + * @param {string[]} args - Command line arguments + * @returns {boolean} True if --dry-run is present + */ +export function hasDryRunArg(args) { + return args.some((arg) => arg === "--dry-run"); +} diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js new file mode 100644 index 0000000..bfe8339 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js @@ -0,0 +1,88 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { + getPipCommandForArgs, + hasDryRunArg, + pipInstallCommand, + pipDownloadCommand, + pipWheelCommand, + pipUninstallCommand, +} from "./pipCommands.js"; + +test("getPipCommandForArgs", async (t) => { + await t.test("should return null for empty args", () => { + assert.strictEqual(getPipCommandForArgs([]), null); + }); + + await t.test("should return null for null args", () => { + assert.strictEqual(getPipCommandForArgs(null), null); + }); + + await t.test("should return the first non-flag argument", () => { + assert.strictEqual(getPipCommandForArgs(["install"]), "install"); + }); + + await t.test("should skip flags and return command", () => { + assert.strictEqual( + getPipCommandForArgs(["-v", "--verbose", "install"]), + "install" + ); + }); + + await t.test("should return install command", () => { + assert.strictEqual( + getPipCommandForArgs(["install", "requests"]), + "install" + ); + }); + + await t.test("should return uninstall command", () => { + assert.strictEqual( + getPipCommandForArgs(["uninstall", "requests"]), + "uninstall" + ); + }); + + await t.test("should return null if only flags", () => { + assert.strictEqual(getPipCommandForArgs(["--version", "-v"]), null); + }); +}); + +test("hasDryRunArg", async (t) => { + await t.test("should return false for empty args", () => { + assert.strictEqual(hasDryRunArg([]), false); + }); + + await t.test("should return true if --dry-run is present", () => { + assert.strictEqual(hasDryRunArg(["install", "--dry-run", "requests"]), true); + }); + + await t.test("should return false if --dry-run is not present", () => { + assert.strictEqual(hasDryRunArg(["install", "requests"]), false); + }); + + await t.test("should return true for --dry-run with other flags", () => { + assert.strictEqual( + hasDryRunArg(["install", "-v", "--dry-run", "--upgrade", "requests"]), + true + ); + }); +}); + +test("command constants", async (t) => { + await t.test("should have correct install command", () => { + assert.strictEqual(pipInstallCommand, "install"); + }); + + await t.test("should have correct download command", () => { + assert.strictEqual(pipDownloadCommand, "download"); + }); + + await t.test("should have correct wheel command", () => { + assert.strictEqual(pipWheelCommand, "wheel"); + }); + + await t.test("should have correct uninstall command", () => { + assert.strictEqual(pipUninstallCommand, "uninstall"); + }); +}); From c85802dd2e3f077984139528520b046da994d901 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 23 Oct 2025 09:17:13 -0700 Subject: [PATCH 09/52] Undo unnecessary changes --- package-lock.json | 1698 +-------------------------------------------- package.json | 5 - 2 files changed, 30 insertions(+), 1673 deletions(-) diff --git a/package-lock.json b/package-lock.json index feddfae..88e9fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,6 @@ "packages/*", "test/e2e" ], - "dependencies": { - "express": "^5.1.0", - "lodash": "^4.17.21", - "webpack": "^5.102.1" - }, "devDependencies": { "oxlint": "^1.22.0" } @@ -111,51 +106,6 @@ "node": ">=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -204,7 +154,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-darwin-x64": { "version": "1.2.21", @@ -217,7 +168,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-darwin-x64-baseline": { "version": "1.2.21", @@ -230,7 +182,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-aarch64": { "version": "1.2.21", @@ -243,7 +196,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-aarch64-musl": { "version": "1.2.21", @@ -256,7 +210,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64": { "version": "1.2.21", @@ -269,7 +224,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-baseline": { "version": "1.2.21", @@ -282,7 +238,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-musl": { "version": "1.2.21", @@ -295,7 +252,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-musl-baseline": { "version": "1.2.21", @@ -308,7 +266,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-windows-x64": { "version": "1.2.21", @@ -321,7 +280,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@oven/bun-windows-x64-baseline": { "version": "1.2.21", @@ -334,7 +294,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@oxlint/darwin-arm64": { "version": "1.22.0", @@ -450,243 +411,6 @@ "node": ">=14" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -696,52 +420,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -772,35 +450,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz", - "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -809,46 +458,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, "node_modules/bun": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", @@ -884,15 +493,6 @@ "@oven/bun-windows-x64-baseline": "1.2.21" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -916,55 +516,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -985,15 +536,6 @@ "node": ">=18" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -1039,51 +581,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1115,62 +612,18 @@ } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.238", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.238.tgz", - "integrity": "sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==", - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -1181,17 +634,17 @@ "iconv-lite": "^0.6.2" } }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", + "optional": true, "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=0.10.0" } }, "node_modules/err-code": { @@ -1200,199 +653,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1409,24 +669,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -1439,15 +681,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -1460,43 +693,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -1517,12 +713,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -1538,57 +728,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hosted-git-info": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", @@ -1607,31 +746,6 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -1658,18 +772,6 @@ "node": ">= 14" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1679,12 +781,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -1698,15 +794,6 @@ "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1728,12 +815,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -1767,38 +848,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -1808,25 +863,6 @@ ], "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -1883,63 +919,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -2128,12 +1107,6 @@ "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -2153,12 +1126,6 @@ "nan": "^2.17.0" } }, - "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", - "license": "MIT" - }, "node_modules/npm-package-arg": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", @@ -2193,39 +1160,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -2351,15 +1285,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2385,22 +1310,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -2423,92 +1332,6 @@ "node": ">=10" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -2549,66 +1372,12 @@ "node": ">= 4" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } + "optional": true }, "node_modules/semver": { "version": "7.7.2", @@ -2622,58 +1391,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2695,78 +1412,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2817,25 +1462,6 @@ "node": ">= 14" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -2854,15 +1480,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -2929,34 +1546,6 @@ "node": ">=8" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tar": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", @@ -2974,87 +1563,6 @@ "node": ">=18" } }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -3079,45 +1587,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/validate-npm-package-name": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", @@ -3127,106 +1596,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3339,12 +1708,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -3372,7 +1735,6 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", - "aikido-pip": "bin/aikido-pip.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", diff --git a/package.json b/package.json index a0c235a..0193a82 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,5 @@ "license": "AGPL-3.0-or-later", "devDependencies": { "oxlint": "^1.22.0" - }, - "dependencies": { - "express": "^5.1.0", - "lodash": "^4.17.21", - "webpack": "^5.102.1" } } From f817bf887af8c8da19d94013d30ff3c828053b8e Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 23 Oct 2025 10:23:42 -0700 Subject: [PATCH 10/52] Update documentation --- README.md | 20 ++++++++++++----- packages/safe-chain/bin/safe-chain.js | 2 +- .../src/shell-integration/helpers.js | 1 + .../src/shell-integration/helpers.spec.js | 18 +++++++++++++++ .../src/shell-integration/setup-ci.js | 22 +++++++++++++++---- .../startup-scripts/init-fish.fish | 10 +++++++++ .../startup-scripts/init-posix.sh | 17 +++++++------- .../startup-scripts/init-pwsh.ps1 | 12 ++++++++++ 8 files changed, 82 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1083c0e..3129b71 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Aikido Safe Chain -The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm, pnpx, bun, and bunx. It's **free** to use and does not require any token. +The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip. It's **free** to use and does not require any token. -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/), and [bunx](https://bun.sh/docs/cli/bunx) 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, or bunx from downloading or running the malware. +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), 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, or pip from downloading or running the malware. ![demo](./docs/safe-package-manager-demo.png) @@ -15,6 +15,7 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - ✅ **pnpx** - ✅ **bun** - ✅ **bunx** +- ✅ **pip** (pip and pip3) # Usage @@ -31,14 +32,14 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: safe-chain setup ``` 3. **❗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, and bunx 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, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. 4. **Verify the installation** by running: ```shell npm install safe-chain-test ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, or `bunx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, or `pip` (including `pip3`) commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. You can check the installed version by running: ```shell @@ -47,9 +48,9 @@ safe-chain --version ## How it works -The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry. When you run npm, npx, yarn, pnpm, pnpx, bun, or bunx 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, bun, bunx, or pip 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 integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, and bunx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support: +The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip commands. 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** @@ -59,6 +60,13 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md). +### Python / pip support + +- Supports `pip` and `pip3` commands. +- Scans Python packages fetched by `pip install`, `pip download`, and `pip wheel`. +- Intercepts downloads from PyPI and checks them against Aikido's malware intelligence before they reach your machine. +- Included automatically when you run `safe-chain setup` (shell integration); **CI integration is not yet available for pip/pip3**. + ## Uninstallation To uninstall the Aikido Safe Chain, you can run the following command: diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index ad88c08..b416f43 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -54,7 +54,7 @@ function writeHelp() { ui.writeInformation( `- ${chalk.cyan( "safe-chain setup" - )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun and bunx.` + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx and pip.` ); ui.writeInformation( `- ${chalk.cyan( diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 2345022..e08227e 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -11,6 +11,7 @@ export const knownAikidoTools = [ { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, { tool: "bun", aikidoCommand: "aikido-bun" }, { tool: "bunx", aikidoCommand: "aikido-bunx" }, + { tool: "pip", aikidoCommand: "aikido-pip" }, // 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/helpers.spec.js b/packages/safe-chain/src/shell-integration/helpers.spec.js index 4f18c36..d7013db 100644 --- a/packages/safe-chain/src/shell-integration/helpers.spec.js +++ b/packages/safe-chain/src/shell-integration/helpers.spec.js @@ -181,4 +181,22 @@ describe("removeLinesMatchingPatternTests", () => { const resultLines = result.split("\n"); assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines"); }); + + it("should include pip in knownAikidoTools and in the package manager list", async () => { + // Import helpers after setting up the mock + const { knownAikidoTools, getPackageManagerList } = await import("./helpers.js"); + + // Verify pip tool + const hasPip = knownAikidoTools.some( + (t) => t.tool === "pip" && t.aikidoCommand === "aikido-pip" + ); + assert.ok(hasPip, "knownAikidoTools should include pip"); + + // Verify pip appears in the human-readable list + const list = getPackageManagerList(); + assert.ok( + /(^|[,\s])pip(,|\s| and)/.test(list) && /commands$/.test(list), + `getPackageManagerList should include 'pip' (actual: ${list})` + ); + }); }); diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 0449ac4..6b1d357 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -46,8 +46,14 @@ function createUnixShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool + // Create a shim for each tool except pip for now. + // TODO(pip): Enable pip and pip3 CI support + let created = 0; for (const toolInfo of knownAikidoTools) { + if (toolInfo.tool === "pip") { + continue; // Skip pip shims in CI for now + } + const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); @@ -57,10 +63,11 @@ function createUnixShims(shimsDir) { // Make the shim executable on Unix systems fs.chmodSync(shimPath, 0o755); + created++; } ui.writeInformation( - `Created ${knownAikidoTools.length} Unix shim(s) in ${shimsDir}` + `Created ${created} Unix shim(s) in ${shimsDir}` ); } @@ -82,18 +89,25 @@ function createWindowsShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool + // Create a shim for each tool except pip for now. + // TODO(pip): Enable pip and pip3 CI support + let created = 0; for (const toolInfo of knownAikidoTools) { + if (toolInfo.tool === "pip") { + continue; // Skip pip shims in CI for now + } + const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); const shimPath = path.join(shimsDir, `${toolInfo.tool}.cmd`); fs.writeFileSync(shimPath, shimContent, "utf-8"); + created++; } ui.writeInformation( - `Created ${knownAikidoTools.length} Windows shim(s) in ${shimsDir}` + `Created ${created} Windows shim(s) in ${shimsDir}` ); } 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 29d6bf3..efc03ca 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 @@ -68,3 +68,13 @@ function npm wrapSafeChainCommand "npm" "aikido-npm" $argv end + +function pip + # Default to Python 2 major version when explicitly calling pip + wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" $argv +end + +function pip3 + # Route to Python 3 when calling pip3 + wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" $argv +end 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 d1df130..a433154 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 @@ -50,15 +50,6 @@ function bunx() { wrapSafeChainCommand "bunx" "aikido-bunx" "$@" } -function pip() { - wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" "$@" -} - -function pip3() { - wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" "$@" -} - - function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command @@ -69,3 +60,11 @@ function npm() { wrapSafeChainCommand "npm" "aikido-npm" "$@" } + +function pip() { + wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" "$@" +} + +function pip3() { + wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" "$@" +} 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 a449405..199fccc 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 @@ -86,3 +86,15 @@ function npm { Invoke-WrappedCommand "npm" "aikido-npm" $args } + +function pip { + # Default to Python 2 major version when explicitly calling pip + $forward = @("--target-version-major", "2") + $args + Invoke-WrappedCommand "pip" "aikido-pip" $forward +} + +function pip3 { + # Route to Python 3 when calling pip3 + $forward = @("--target-version-major", "3") + $args + Invoke-WrappedCommand "pip3" "aikido-pip" $forward +} From 059cba06bc0f53aa2d7caa0df1aa02ed3300d854 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 23 Oct 2025 11:41:13 -0700 Subject: [PATCH 11/52] Implement e2e tests --- package-lock.json | 34 +++---- packages/safe-chain/bin/aikido-pip.js | 23 ++--- packages/safe-chain/src/api/aikido.js | 23 +++-- packages/safe-chain/src/main.js | 1 - .../packagemanager/currentPackageManager.js | 4 +- .../pip/createPackageManager.js | 15 +-- .../pip/createPackageManager.spec.js | 93 ++++--------------- .../commandArgumentScanner.js | 18 +--- .../commandArgumentScanner.spec.js | 81 +--------------- .../pip/dependencyScanner/nullScanner.js | 10 -- .../parsing/parsePackagesFromInstallArgs.js | 32 +++---- .../src/packagemanager/pip/runPipCommand.js | 20 +--- .../packagemanager/pip/utils/pipCommands.js | 19 ---- .../pip/utils/pipCommands.spec.js | 5 - .../safe-chain/src/scanning/audit/index.js | 1 - test/e2e/Dockerfile | 6 ++ test/e2e/pip.e2e.spec.js | 71 ++++++++++++++ 17 files changed, 163 insertions(+), 293 deletions(-) delete mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js create mode 100644 test/e2e/pip.e2e.spec.js diff --git a/package-lock.json b/package-lock.json index 88e9fb5..0d64f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,8 +154,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@oven/bun-darwin-x64": { "version": "1.2.21", @@ -168,8 +167,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@oven/bun-darwin-x64-baseline": { "version": "1.2.21", @@ -182,8 +180,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-aarch64": { "version": "1.2.21", @@ -196,8 +193,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-aarch64-musl": { "version": "1.2.21", @@ -210,8 +206,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64": { "version": "1.2.21", @@ -224,8 +219,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64-baseline": { "version": "1.2.21", @@ -238,8 +232,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64-musl": { "version": "1.2.21", @@ -252,8 +245,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-linux-x64-musl-baseline": { "version": "1.2.21", @@ -266,8 +258,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@oven/bun-windows-x64": { "version": "1.2.21", @@ -280,8 +271,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@oven/bun-windows-x64-baseline": { "version": "1.2.21", @@ -294,8 +284,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@oxlint/darwin-arm64": { "version": "1.22.0", @@ -1735,6 +1724,7 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", + "aikido-pip": "bin/aikido-pip.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 7834bc5..29c68bc 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -8,20 +8,19 @@ import { setEcoSystem } from "../src/config/settings.js"; let packageManagerName = "pip"; let targetVersionMajor; -// Copy argv so we can mutate while parsing +// Copy argv so we can modify it const argv = process.argv.slice(2); for (let i = 0; i < argv.length; i++) { - const a = argv[i]; + const a = argv[i]; - // --target-version-major - if (a === "--target-version-major" && i + 1 < argv.length) { - console.log("Setting targetVersionMajor from CLI arg:", argv[i + 1]); - targetVersionMajor = argv[i + 1]; - argv.splice(i, 2); - i -= 1; - continue; - } + // --target-version-major tells us which pip version is being used (2 or 3) + if (a === "--target-version-major" && i + 1 < argv.length) { + targetVersionMajor = argv[i + 1]; + argv.splice(i, 2); + i -= 1; + continue; + } } // If the user explicitly called python3, prefer pip3 @@ -29,12 +28,10 @@ if (targetVersionMajor && String(targetVersionMajor).trim() === "3") { packageManagerName = "pip3"; } -console.log("** aikido-pip ** Final arguments (after processing):", argv); - // Set eco system setEcoSystem("py"); initializePackageManager(packageManagerName); -var exitCode = await main(argv); +const exitCode = await main(argv); process.exit(exitCode); diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 2cabbe0..9e5f6bd 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -3,16 +3,22 @@ import { getEcoSystem } from "../config/settings.js"; const malwareDatabaseUrls = { js: "https://malware-list.aikido.dev/malware_predictions.json", - python: "https://malware-list.aikido.dev/malware_predictions_python.json", + py: "https://malware-list.aikido.dev/malware_predictions_python.json", }; export async function fetchMalwareDatabase() { const ecosystem = getEcoSystem() || "js"; - if (ecosystem === "py") { - console.log("**aikido.js** Using 'python' ecosystem for malware database fetch"); - } const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl); + + // Python malware database doesn't exist yet, return empty database + if (!response.ok && ecosystem === "py" && response.status === 403) { + return { + malwareDatabase: [], + version: undefined, + }; + } + if (!response.ok) { throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`); } @@ -30,14 +36,17 @@ export async function fetchMalwareDatabase() { export async function fetchMalwareDatabaseVersion() { const ecosystem = getEcoSystem() || "js"; - if (ecosystem === "py") { - console.log("**aikido.js** Using 'python' ecosystem for malware database fetch"); - } const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl, { method: "HEAD", }); + + // Python malware database doesn't exist yet, return undefined + if (!response.ok && ecosystem === "py" && response.status === 403) { + return undefined; + } + if (!response.ok) { throw new Error( `Error fetching ${ecosystem} malware database version: ${response.statusText}` diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 4eaf8d2..e106e83 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -12,7 +12,6 @@ export async function main(args) { await proxy.startServer(); try { - console.log(chalk.blueBright.bold("main.js: Scanning for malicious packages...")); // This parses all the --safe-chain arguments and removes them from the args array args = initializeCliArguments(args); diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index ff2481d..665ac92 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -15,8 +15,6 @@ const state = { packageManagerName: null, }; -const PIP_COMMANDS = new Set(["pip", "pip3"]); - export function initializePackageManager(packageManagerName) { if (packageManagerName === "npm") { state.packageManagerName = createNpmPackageManager(); @@ -32,7 +30,7 @@ export function initializePackageManager(packageManagerName) { state.packageManagerName = createBunPackageManager(); } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); - } else if (PIP_COMMANDS.has(packageManagerName)) { + } else if (packageManagerName === "pip" || packageManagerName === "pip3") { state.packageManagerName = createPipPackageManager(packageManagerName); } else { throw new Error("Unsupported package manager: " + packageManagerName); diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 2f0fc0d..2eaab01 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -1,5 +1,4 @@ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; -import { nullScanner } from "./dependencyScanner/nullScanner.js"; import { runPip } from "./runPipCommand.js"; import { getPipCommandForArgs, @@ -9,8 +8,7 @@ import { } from "./utils/pipCommands.js"; /** - * Creates a package manager interface for Python's pip package installer - * + * Creates a package manager * @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip" */ export function createPipPackageManager(command = "pip") { @@ -41,15 +39,20 @@ const commandScannerMapping = { [pipInstallCommand]: commandArgumentScanner(), [pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI [pipWheelCommand]: commandArgumentScanner(), // wheel downloads and builds packages - // Other commands (uninstall, list, etc.) will use nullScanner() by default + // Other commands return null scanner by default +}; + +const NULL_SCANNER = { + shouldScan: () => false, + scan: () => [], }; function findDependencyScannerForCommand(scanners, args) { const command = getPipCommandForArgs(args); if (!command) { - return nullScanner(); + return NULL_SCANNER; } const scanner = scanners[command]; - return scanner ? scanner : nullScanner(); + return scanner || NULL_SCANNER; } diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js index d525e2c..2d38b0d 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js @@ -3,7 +3,7 @@ import assert from "node:assert"; import { createPipPackageManager } from "./createPackageManager.js"; test("createPipPackageManager", async (t) => { - await t.test("should create package manager with default pip command", () => { + await t.test("should create package manager with required interface", () => { const pm = createPipPackageManager(); assert.ok(pm); @@ -12,106 +12,49 @@ test("createPipPackageManager", async (t) => { assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function"); }); - await t.test("should create package manager with custom pip3 command", () => { + await t.test("should accept pip3 as command parameter", () => { const pm = createPipPackageManager("pip3"); - assert.ok(pm); - assert.strictEqual(typeof pm.runCommand, "function"); }); - await t.test("should recognize install command as supported", () => { + await t.test("should support install, download, and wheel commands", () => { const pm = createPipPackageManager(); - // Note: Currently returns false because commandArgumentScanner is not yet implemented - // When implemented, this should return true - const result = pm.isSupportedCommand(["install", "requests"]); - assert.strictEqual(typeof result, "boolean"); + assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), true); + assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), true); + assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), true); }); - await t.test("should recognize download command as supported", () => { + await t.test("should not support uninstall and info commands", () => { const pm = createPipPackageManager(); - const result = pm.isSupportedCommand(["download", "requests"]); - assert.strictEqual(typeof result, "boolean"); + assert.strictEqual(pm.isSupportedCommand(["uninstall", "requests"]), false); + assert.strictEqual(pm.isSupportedCommand(["list"]), false); + assert.strictEqual(pm.isSupportedCommand(["show", "requests"]), false); }); - await t.test("should recognize wheel command as supported", () => { + await t.test("should extract packages from install command", () => { const pm = createPipPackageManager(); - const result = pm.isSupportedCommand(["wheel", "requests"]); - assert.strictEqual(typeof result, "boolean"); - }); - - await t.test("should not support uninstall command", () => { - const pm = createPipPackageManager(); - - const result = pm.isSupportedCommand(["uninstall", "requests"]); - assert.strictEqual(result, false); - }); - - await t.test("should not support list command", () => { - const pm = createPipPackageManager(); - - const result = pm.isSupportedCommand(["list"]); - assert.strictEqual(result, false); - }); - - await t.test("should not support show command", () => { - const pm = createPipPackageManager(); - - const result = pm.isSupportedCommand(["show", "requests"]); - assert.strictEqual(result, false); - }); - - await t.test("should return empty array for getDependencyUpdatesForCommand on install", () => { - const pm = createPipPackageManager(); - - // Note: Currently returns [] because commandArgumentScanner is not yet implemented const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]); assert.ok(Array.isArray(result)); - }); - - await t.test("should return empty array for getDependencyUpdatesForCommand on download", () => { - const pm = createPipPackageManager(); - - const result = pm.getDependencyUpdatesForCommand(["download", "flask"]); - assert.ok(Array.isArray(result)); - }); - - await t.test("should return empty array for getDependencyUpdatesForCommand on wheel", () => { - const pm = createPipPackageManager(); - - const result = pm.getDependencyUpdatesForCommand(["wheel", "django"]); - assert.ok(Array.isArray(result)); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, "requests"); + assert.strictEqual(result[0].version, "2.28.0"); }); await t.test("should return empty array for unsupported commands", () => { const pm = createPipPackageManager(); const result = pm.getDependencyUpdatesForCommand(["uninstall", "requests"]); - assert.strictEqual(Array.isArray(result), true); + assert.ok(Array.isArray(result)); assert.strictEqual(result.length, 0); }); - await t.test("should handle empty args array", () => { + await t.test("should handle empty args gracefully", () => { const pm = createPipPackageManager(); - const supported = pm.isSupportedCommand([]); - assert.strictEqual(supported, false); - - const deps = pm.getDependencyUpdatesForCommand([]); - assert.ok(Array.isArray(deps)); - assert.strictEqual(deps.length, 0); - }); - - await t.test("should handle args with only flags", () => { - const pm = createPipPackageManager(); - - const supported = pm.isSupportedCommand(["--version"]); - assert.strictEqual(supported, false); - - const deps = pm.getDependencyUpdatesForCommand(["-h", "--help"]); - assert.ok(Array.isArray(deps)); - assert.strictEqual(deps.length, 0); + assert.strictEqual(pm.isSupportedCommand([]), false); + assert.deepStrictEqual(pm.getDependencyUpdatesForCommand([]), []); }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js index f0e47f1..dbe92d6 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -1,13 +1,6 @@ import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js"; import { hasDryRunArg } from "../utils/pipCommands.js"; -/** - * Scanner for pip command arguments to detect package installations - * - * @param {Object} options - Scanner options - * @param {boolean} [options.ignoreDryRun=false] - Whether to ignore dry-run flag - * @returns {Object} Scanner interface - */ export function commandArgumentScanner(options = {}) { const { ignoreDryRun = false } = options; @@ -33,18 +26,9 @@ function scanDependencies(args) { return checkChangesFromArgs(args); } -/** - * Extracts package changes from pip command arguments - * - * Unlike npm, pip's parser already returns exact versions (== or ===) - * or "latest" for unversioned packages, so no version resolution is needed. - * - * @param {string[]} args - Command line arguments - * @returns {Array<{name: string, version: string, type: string}>} Package changes - */ export function checkChangesFromArgs(args) { const packageUpdates = parsePackagesFromInstallArgs(args); - + // Parser already provides exact versions or "latest", no need to resolve // Just return the packages with type "add" return packageUpdates; diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js index 6e69386..9570756 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js @@ -2,22 +2,14 @@ import { test } from "node:test"; import assert from "node:assert"; import { commandArgumentScanner, checkChangesFromArgs } from "./commandArgumentScanner.js"; -test("commandArgumentScanner", async (t) => { - await t.test("should create scanner with default options", () => { +test("commandArgumentScanner factory", async (t) => { + await t.test("should create scanner with required interface", () => { const scanner = commandArgumentScanner(); assert.ok(scanner); assert.strictEqual(typeof scanner.shouldScan, "function"); assert.strictEqual(typeof scanner.scan, "function"); }); - - await t.test("should create scanner with ignoreDryRun option", () => { - const scanner = commandArgumentScanner({ ignoreDryRun: true }); - - assert.ok(scanner); - assert.strictEqual(typeof scanner.shouldScan, "function"); - assert.strictEqual(typeof scanner.scan, "function"); - }); }); test("shouldScan", async (t) => { @@ -41,20 +33,6 @@ test("shouldScan", async (t) => { const result = scanner.shouldScan(["install", "--dry-run", "requests"]); assert.strictEqual(result, true); }); - - await t.test("should return true for download command", () => { - const scanner = commandArgumentScanner(); - - const result = scanner.shouldScan(["download", "flask"]); - assert.strictEqual(result, true); - }); - - await t.test("should return true for wheel command", () => { - const scanner = commandArgumentScanner(); - - const result = scanner.shouldScan(["wheel", "django"]); - assert.strictEqual(result, true); - }); }); test("scan", async (t) => { @@ -129,46 +107,6 @@ test("scan", async (t) => { }); }); - await t.test("should work with download command", () => { - const scanner = commandArgumentScanner(); - - const result = scanner.scan(["download", "django==4.2.0"]); - assert.strictEqual(result.length, 1); - assert.deepEqual(result[0], { - name: "django", - version: "4.2.0", - type: "add", - }); - }); - - await t.test("should work with wheel command", () => { - const scanner = commandArgumentScanner(); - - const result = scanner.scan(["wheel", "numpy==1.24.0"]); - assert.strictEqual(result.length, 1); - assert.deepEqual(result[0], { - name: "numpy", - version: "1.24.0", - type: "add", - }); - }); - - await t.test("should parse packages even for unsupported commands", () => { - const scanner = commandArgumentScanner(); - - // Note: The parser treats the first non-flag arg as the command and skips it - // So "uninstall" is treated as the command, and "requests" is parsed as a package - // The scanner itself doesn't filter by command type - that's done at a higher level - const result = scanner.scan(["uninstall", "requests"]); - assert.ok(Array.isArray(result)); - assert.strictEqual(result.length, 1); - assert.deepEqual(result[0], { - name: "requests", - version: "latest", - type: "add", - }); - }); - await t.test("should handle === exact version specifier", () => { const scanner = commandArgumentScanner(); @@ -182,8 +120,8 @@ test("scan", async (t) => { }); }); -test("checkChangesFromArgs", async (t) => { - await t.test("should extract changes from install args", () => { +test("checkChangesFromArgs helper", async (t) => { + await t.test("should extract packages from args", () => { const result = checkChangesFromArgs(["install", "requests==2.28.0", "flask"]); assert.strictEqual(result.length, 2); @@ -199,17 +137,8 @@ test("checkChangesFromArgs", async (t) => { }); }); - await t.test("should return empty array for commands with no packages", () => { - const result = checkChangesFromArgs(["install", "-r", "requirements.txt"]); - - assert.ok(Array.isArray(result)); - assert.strictEqual(result.length, 0); - }); - await t.test("should handle empty args", () => { const result = checkChangesFromArgs([]); - - assert.ok(Array.isArray(result)); - assert.strictEqual(result.length, 0); + assert.deepStrictEqual(result, []); }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js deleted file mode 100644 index ec3ba12..0000000 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Null scanner that returns no dependencies - * Used when a command is not supported for scanning - */ -export function nullScanner() { - return { - shouldScan: () => false, - scan: () => [], - }; -} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index 3dc46ed..71c99c0 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -1,26 +1,18 @@ /** - * Parses package specifications from pip install arguments - * - * Only returns packages with exact version specifiers (== or ===) to ensure - * we can check specific versions against the malware database. - * * Supported formats that will be returned: * - package_name (no version) * - package_name==version (exact version) * - package_name===version (exact version, PEP 440) * - * Skipped formats (won't be returned): - * - package_name>=version (range specifier) - * - package_name<=version (range specifier) - * - package_name>version (range specifier) - * - package_name=version + * - package_name<=version + * - package_name>version + * - package_name} Array of package specifications with exact versions only */ export function parsePackagesFromInstallArgs(args) { const packages = []; @@ -34,14 +26,13 @@ export function parsePackagesFromInstallArgs(args) { continue; } - // Skip the command itself (install, uninstall, etc.) + // Skip the command itself (install, etc.) if (i === 0 && !arg.startsWith("-")) { continue; } // Skip flags and their values if (arg.startsWith("-")) { - // Flags that take a value - skip the next arg for those if (isPipOptionWithParameter(arg)) { skipNext = true; } @@ -57,8 +48,10 @@ export function parsePackagesFromInstallArgs(args) { return packages; } -// Check if a pip flag takes a parameter function isPipOptionWithParameter(arg) { + + // Check if a pip flag takes a parameter + // TODO it would be better to query pip itself for this info const optionsWithParameters = [ // Install options "-r", @@ -107,10 +100,7 @@ function isPipOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } -// Parse a single pip requirement spec -// Always returns { name, version } where version defaults to "latest" if not specified function parsePipSpec(spec) { - // Ignore obvious URLs and paths // These cannot be scanned from the malware database const lower = spec.toLowerCase(); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 859326a..878d856 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,13 +2,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -/** - * Runs a pip command with the specified arguments - * - * @param {string} command - The pip command to use (e.g., "pip", "pip3") - * @param {string[]} args - Command arguments - * @returns {Promise<{status: number}>} Result object with status code - */ + export async function runPip(command, args) { try { const result = await safeSpawn(command, args, { @@ -26,18 +20,10 @@ export async function runPip(command, args) { } } -/** - * Runs a pip command in dry-run mode and captures output - * Note: pip doesn't have a native --dry-run flag, so this may need adjustment - * - * @param {string} command - The pip command to use - * @param {string[]} args - Command arguments - * @returns {Promise<{status: number, output: string}>} Result with status and output - */ export async function dryRunPipCommandAndOutput(command, args) { try { - // Note: pip doesn't have a --dry-run flag like npm - // This would need to be implemented differently if dry-run functionality is needed + // Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not. + // We don't mutate args here — callers should include --dry-run when appropriate. const result = await safeSpawn( command, args, diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js index e88b262..2818c87 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -1,20 +1,7 @@ -/** - * Pip command constants - * - * Note: Unlike npm, pip does not support command aliases or abbreviations. - * Commands must be spelled out fully (e.g., "install", not "i" or "add"). - */ export const pipInstallCommand = "install"; export const pipDownloadCommand = "download"; export const pipWheelCommand = "wheel"; -export const pipUninstallCommand = "uninstall"; -/** - * Gets the pip command from the arguments array - * - * @param {string[]} args - Command line arguments - * @returns {string|null} The pip command or null if not found - */ export function getPipCommandForArgs(args) { if (!args || args.length === 0) { return null; @@ -30,12 +17,6 @@ export function getPipCommandForArgs(args) { return null; } -/** - * Checks if the arguments contain the --dry-run flag - * - * @param {string[]} args - Command line arguments - * @returns {boolean} True if --dry-run is present - */ export function hasDryRunArg(args) { return args.some((arg) => arg === "--dry-run"); } diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js index bfe8339..346ad8f 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js @@ -6,7 +6,6 @@ import { pipInstallCommand, pipDownloadCommand, pipWheelCommand, - pipUninstallCommand, } from "./pipCommands.js"; test("getPipCommandForArgs", async (t) => { @@ -81,8 +80,4 @@ test("command constants", async (t) => { await t.test("should have correct wheel command", () => { assert.strictEqual(pipWheelCommand, "wheel"); }); - - await t.test("should have correct uninstall command", () => { - assert.strictEqual(pipUninstallCommand, "uninstall"); - }); }); diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index be46fdb..215bfa0 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -7,7 +7,6 @@ export async function auditChanges(changes) { const allowedChanges = []; const disallowedChanges = []; - console.log("**audit/index.js** Auditing changes:", changes); var malwarePackages = await getPackagesWithMalware( changes.filter( (change) => change.type === "add" || change.type === "change" diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 484f5fe..e590d19 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -25,6 +25,7 @@ ARG NODE_VERSION=latest ARG NPM_VERSION=latest ARG YARN_VERSION=latest ARG PNPM_VERSION=latest +ARG PYTHON_VERSION=3 SHELL ["/bin/bash", "-c"] ENV BASH_ENV=~/.bashrc @@ -49,6 +50,11 @@ RUN volta install pnpm@${PNPM_VERSION} # Install Bun RUN curl -fsSL https://bun.sh/install | bash +# Install Python and pip (pip3) +RUN apt-get update && apt-get install -y python${PYTHON_VERSION} python3-pip && \ + ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 && \ + ln -sf /usr/bin/pip3 /usr/local/bin/pip3 + # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ RUN npm install -g /pkgs/*.tgz diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js new file mode 100644 index 0000000..50619ff --- /dev/null +++ b/test/e2e/pip.e2e.spec.js @@ -0,0 +1,71 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +// Note: These tests require Docker. If Docker isn't available locally, +// they will be skipped by the runner or fail to build the image. +describe("E2E: pip 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"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`safe-chain successfully installs safe packages with pip3`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("pip3 install requests"); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`pip3 download works with safe-chain proxy`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("pip3 download requests"); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`pip3 wheel works with safe-chain proxy`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("pip3 wheel requests"); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`pip3 install --dry-run is respected by scanner`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("pip3 install --dry-run requests"); + + // Scanner intentionally skips when --dry-run is present for install + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); +}); From b5988e19c1ae7b9721c9df0b91c9bcd18e7300ae Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 23 Oct 2025 13:11:51 -0700 Subject: [PATCH 12/52] Some more cleanup --- packages/safe-chain/src/registryProxy/registryProxy.js | 5 ++++- test/e2e/pip.e2e.spec.js | 9 +++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 2dfb1b5..ebb315b 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -34,7 +34,10 @@ function getSafeChainProxyEnvironmentVariables() { GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`, NODE_EXTRA_CA_CERTS: getCaCertPath(), - // Following env vars point pip and Python's requests/urllib at a CA bundle file. + // Following env vars point pip and Python's requests/urllib at a CA Cert file. + // pip checks PIP_CERT first + // If pip uses requests library internally, it needs REQUESTS_CA_BUNDLE + // Other Python packages or pip's fallback SSL code may use SSL_CERT_FILE PIP_CERT: getCaCertPath(), REQUESTS_CA_BUNDLE: getCaCertPath(), SSL_CERT_FILE: getCaCertPath(), diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 50619ff..767c8fb 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -2,8 +2,6 @@ import { describe, it, before, beforeEach, afterEach } from "node:test"; import { DockerTestContainer } from "./DockerTestContainer.js"; import assert from "node:assert"; -// Note: These tests require Docker. If Docker isn't available locally, -// they will be skipped by the runner or fail to build the image. describe("E2E: pip coverage", () => { let container; @@ -28,7 +26,7 @@ describe("E2E: pip coverage", () => { } }); - it(`safe-chain successfully installs safe packages with pip3`, async () => { + it(`successfully installs known safe packages with pip3`, async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("pip3 install requests"); @@ -38,7 +36,7 @@ describe("E2E: pip coverage", () => { ); }); - it(`pip3 download works with safe-chain proxy`, async () => { + it(`pip3 download`, async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("pip3 download requests"); @@ -48,7 +46,7 @@ describe("E2E: pip coverage", () => { ); }); - it(`pip3 wheel works with safe-chain proxy`, async () => { + it(`pip3 .whl`, async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("pip3 wheel requests"); @@ -62,7 +60,6 @@ describe("E2E: pip coverage", () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("pip3 install --dry-run requests"); - // Scanner intentionally skips when --dry-run is present for install assert.ok( result.output.includes("no malicious packages found."), `Output did not include expected text. Output was:\n${result.output}` From 15785fad73fb91f6bdb1873dd5ddc4ef730ad814 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 24 Oct 2025 09:59:53 -0700 Subject: [PATCH 13/52] Make sure we use a different version.txt to prevent having to redownload DB --- packages/safe-chain/src/api/aikido.js | 18 ++------------ packages/safe-chain/src/config/configFile.js | 7 ++++-- .../src/registryProxy/registryProxy.js | 1 - .../src/scanning/malwareDatabase.js | 24 ++++++++++++++++--- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 9e5f6bd..b38a4cc 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -3,22 +3,13 @@ import { getEcoSystem } from "../config/settings.js"; const malwareDatabaseUrls = { js: "https://malware-list.aikido.dev/malware_predictions.json", - py: "https://malware-list.aikido.dev/malware_predictions_python.json", + py: "https://malware-list.aikido.dev/malware_pypi.json", }; export async function fetchMalwareDatabase() { const ecosystem = getEcoSystem() || "js"; const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl); - - // Python malware database doesn't exist yet, return empty database - if (!response.ok && ecosystem === "py" && response.status === 403) { - return { - malwareDatabase: [], - version: undefined, - }; - } - if (!response.ok) { throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`); } @@ -41,12 +32,7 @@ export async function fetchMalwareDatabaseVersion() { const response = await fetch(malwareDatabaseUrl, { method: "HEAD", }); - - // Python malware database doesn't exist yet, return undefined - if (!response.ok && ecosystem === "py" && response.status === 403) { - return undefined; - } - + if (!response.ok) { throw new Error( `Error fetching ${ecosystem} malware database version: ${response.statusText}` diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 2feb307..1091eb0 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -2,6 +2,7 @@ import fs from "fs"; import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; +import { getEcoSystem } from "./settings.js"; export function getScanTimeout() { const config = readConfigFile(); @@ -68,12 +69,14 @@ function readConfigFile() { function getDatabasePath() { const aikidoDir = getAikidoDirectory(); - return path.join(aikidoDir, "malwareDatabase.json"); + const ecosystem = getEcoSystem() || "js"; + return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`); } function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); - return path.join(aikidoDir, "version.txt"); + const ecosystem = getEcoSystem() || "js"; + return path.join(aikidoDir, `version_${ecosystem}.txt`); } function getConfigFilePath() { diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index ebb315b..013c470 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -111,7 +111,6 @@ function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL - console.log("**registryProxy.js** Handling CONNECT request for:", req.url); if ((knownJsRegistries.some((reg) => req.url.includes(reg))) || (knownPipRegistries.some((reg) => req.url.includes(reg)))) { mitmConnect(req, clientSocket, isAllowedUrl); diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 1cb781b..b21733a 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -7,9 +7,24 @@ import { writeDatabaseToLocalCache, } from "../config/configFile.js"; import { ui } from "../environment/userInteraction.js"; +import { getEcoSystem } from "../config/settings.js"; let cachedMalwareDatabase = null; +/** + * Normalize package name for comparison. + * For Python packages (PEP-503): lowercase and replace _, -, . with - + * For js packages: keep as-is (case-sensitive) + */ +function normalizePackageName(name) { + const ecosystem = getEcoSystem(); + if (ecosystem === "py") { + return name.toLowerCase().replace(/[-_.]+/g, "-"); + } + + return name; +} + export async function openMalwareDatabase() { if (cachedMalwareDatabase) { return cachedMalwareDatabase; @@ -18,10 +33,13 @@ export async function openMalwareDatabase() { const malwareDatabase = await getMalwareDatabase(); function getPackageStatus(name, version) { + const normalizedName = normalizePackageName(name); const packageData = malwareDatabase.find( - (pkg) => - pkg.package_name === name && - (pkg.version === version || pkg.version === "*") + (pkg) => { + const normalizedPkgName = normalizePackageName(pkg.package_name); + return normalizedPkgName === normalizedName && + (pkg.version === version || pkg.version === "*"); + } ); if (!packageData) { From 6b2db6dace79abde4c662ae403745990ecf506ab Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 24 Oct 2025 13:14:57 -0700 Subject: [PATCH 14/52] Fix ranges issue --- .../src/packagemanager/pip/runPipCommand.js | 6 +- .../safe-chain/src/scanning/audit/index.js | 1 + .../src/scanning/malwareDatabase.js | 3 +- packages/safe-chain/src/utils/safeSpawn.js | 32 +++++++++ .../safe-chain/src/utils/safeSpawn.spec.js | 70 +++++++++++++++++++ test/e2e/pip.e2e.spec.js | 20 ++++++ 6 files changed, 128 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 878d856..b09fc50 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,11 +1,11 @@ import { ui } from "../../environment/userInteraction.js"; -import { safeSpawn } from "../../utils/safeSpawn.js"; +import { safeSpawnPy } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; export async function runPip(command, args) { try { - const result = await safeSpawn(command, args, { + const result = await safeSpawnPy(command, args, { stdio: "inherit", env: mergeSafeChainProxyEnvironmentVariables(process.env), }); @@ -24,7 +24,7 @@ export async function dryRunPipCommandAndOutput(command, args) { try { // Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not. // We don't mutate args here — callers should include --dry-run when appropriate. - const result = await safeSpawn( + const result = await safeSpawnPy( command, args, { diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 215bfa0..e67ee0c 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -14,6 +14,7 @@ export async function auditChanges(changes) { ); for (const change of changes) { + console.log("**** Auditing change:", change); const malwarePackage = malwarePackages.find( (pkg) => pkg.name === change.name && pkg.version === change.version ); diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index b21733a..976b386 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -90,7 +90,8 @@ async function getMalwareDatabase() { function isMalwareStatus(status) { let malwareStatus = status.toUpperCase(); - return malwareStatus === MALWARE_STATUS_MALWARE; + return malwareStatus === MALWARE_STATUS_MALWARE + || malwareStatus === MALWARE_STATUS_TELEMETRY; } export const MALWARE_STATUS_OK = "OK"; diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 253417c..1fcaef8 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -43,3 +43,35 @@ export async function safeSpawn(command, args, options = {}) { }); }); } + +/** + * To avoid any regression issues on the JS ecosystem, + * a py-friendly safeSpawn that avoids shell interpolation + * issues (e.g., '<', '>' in version specs). + * + * TL;DR: add support for shell::false + */ +export async function safeSpawnPy(command, args, options = {}) { + return new Promise((resolve) => { + const child = spawn(command, args, { ...options, shell: false }); + + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + resolve({ status: code, stdout, stderr }); + }); + + child.on("error", (error) => { + resolve({ status: 1, stdout: "", stderr: error.message || String(error) }); + }); + }); +} diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 6417084..847a009 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -87,3 +87,73 @@ describe("safeSpawn", () => { assert.strictEqual(spawnCalls[0].options.shell, true); }); }); + +describe("safeSpawnPy", () => { + let safeSpawnPy; + let spawnCalls = []; + + beforeEach(async () => { + spawnCalls = []; + + // Mock child_process for argument-array spawn signature + mock.module("child_process", { + namedExports: { + spawn: (command, args = [], options = {}) => { + spawnCalls.push({ command, args, options }); + const stdoutListeners = []; + const stderrListeners = []; + const stdout = { on: (event, cb) => { if (event === "data") stdoutListeners.push(cb); } }; + const stderr = { on: (event, cb) => { if (event === "data") stderrListeners.push(cb); } }; + const obj = { + stdout, + stderr, + on: (event, callback) => { + if (event === 'close') { + // Emit one chunk to stdout and stderr to verify piping works, then close with success + setTimeout(() => { + stdoutListeners.forEach((cb) => cb(Buffer.from("STDOUT-TEST"))); + stderrListeners.forEach((cb) => cb(Buffer.from(""))); + callback(0); + }, 0); + } + } + }; + return obj; + }, + }, + }); + + // Import after mocking; use a query to avoid ESM cache collisions with previous import + const safeSpawnModule = await import("./safeSpawn.js?py"); + safeSpawnPy = safeSpawnModule.safeSpawnPy; + }); + + afterEach(() => { + mock.reset(); + }); + + it("spawns without a shell and preserves args (inherit)", async () => { + const result = await safeSpawnPy("pip3", ["install", "Jinja2>=3.1,<3.2"], { stdio: "inherit" }); + + // Verifies no throw and status 0 + assert.strictEqual(result.status, 0); + + // Verify spawn signature + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, "pip3"); + assert.deepStrictEqual(spawnCalls[0].args, ["install", "Jinja2>=3.1,<3.2"]); + assert.strictEqual(spawnCalls[0].options.shell, false); + assert.strictEqual(spawnCalls[0].options.stdio, "inherit"); + }); + + it("captures stdout when stdio=pipe", async () => { + const result = await safeSpawnPy("pip3", ["install", "idna!=3.5,>=3.0", "--dry-run"], { stdio: "pipe" }); + + assert.strictEqual(result.status, 0); + assert.match(result.stdout || "", /STDOUT-TEST/); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].options.shell, false); + assert.strictEqual(spawnCalls[0].options.stdio, "pipe"); + }); +}); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 767c8fb..524c472 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -65,4 +65,24 @@ describe("E2E: pip coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + it(`pip3 install with extras such as requests[socks]`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('pip3 install "requests[socks]==2.32.3"'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`pip3 install with range version specifier`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('pip3 install "Jinja2>=3.1,<3.2"'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); }); From 9914c0ccb31ed892bc31a8cc4890571bc5f32317 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 24 Oct 2025 13:47:22 -0700 Subject: [PATCH 15/52] Some fixes --- README.md | 6 +++--- .../src/packagemanager/pip/createPackageManager.js | 4 ---- .../pip/parsing/parsePackagesFromInstallArgs.js | 5 ++--- .../pip/parsing/parsePackagesFromInstallArgs.spec.js | 12 +++--------- .../registryProxy.connect-tunnel.spec.js | 3 +++ 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3129b71..57d8c89 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - ✅ **pnpx** - ✅ **bun** - ✅ **bunx** -- ✅ **pip** (pip and pip3) +- ✅ **pip** # Usage @@ -39,7 +39,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, or `pip` (including `pip3`) commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, or `pip` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. You can check the installed version by running: ```shell @@ -60,7 +60,7 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md). -### Python / pip support +### Python support - Supports `pip` and `pip3` commands. - Scans Python packages fetched by `pip install`, `pip download`, and `pip wheel`. diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 2eaab01..3c0e974 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -7,10 +7,6 @@ import { pipWheelCommand, } from "./utils/pipCommands.js"; -/** - * Creates a package manager - * @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip" - */ export function createPipPackageManager(command = "pip") { function isSupportedCommand(args) { const scanner = findDependencyScannerForCommand( diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index 71c99c0..b0b2f6c 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -101,8 +101,7 @@ function isPipOptionWithParameter(arg) { } function parsePipSpec(spec) { - // Ignore obvious URLs and paths - // These cannot be scanned from the malware database + // Ignore obvious URLs and paths, rely on mitm scanner const lower = spec.toLowerCase(); if ( lower.startsWith("git+") || @@ -116,7 +115,7 @@ function parsePipSpec(spec) { spec.startsWith("../") || spec.startsWith("/") ) { - return { name: spec, version: "latest" }; + return null; } // Strip extras: package[extra1,extra2] diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js index 6a0098b..8a653c9 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js @@ -40,9 +40,8 @@ describe("parsePackagesFromInstallArgs", () => { ]); }); - it("should parse multiple constraints", () => { + it("should skip ranges", () => { const result = parsePackagesFromInstallArgs(["install", "requests>=2,<3"]); - // Range specifiers should be skipped since they don't provide exact versions assert.deepEqual(result, []); }); @@ -69,7 +68,7 @@ describe("parsePackagesFromInstallArgs", () => { ]); }); - it("should treat VCS/URL/path specs as names (no version)", () => { + it("should skip VCS/URL/path)", () => { const result = parsePackagesFromInstallArgs([ "install", "git+https://github.com/pallets/flask.git", @@ -77,12 +76,7 @@ describe("parsePackagesFromInstallArgs", () => { "file:/tmp/pkg.whl", "./localpkg", ]); - assert.deepEqual(result, [ - { name: "git+https://github.com/pallets/flask.git", version: "latest", type: "add" }, - { name: "https://files.pythonhosted.org/packages/foo/bar.whl", version: "latest", type: "add" }, - { name: "file:/tmp/pkg.whl", version: "latest", type: "add" }, - { name: "./localpkg", version: "latest", type: "add" }, - ]); + assert.deepEqual(result, []); }); it("should return empty array for no packages", () => { diff --git a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js index e5f5902..a1fea55 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js @@ -147,6 +147,9 @@ function sendHttpsRequestThroughTunnel(socket, verb, url) { { socket: socket, servername: url.hostname, + // Tests should focus on tunnel behavior, not system CA state; + // disable CA verification to avoid flakiness on machines without full roots. + rejectUnauthorized: false, }, () => { tlsSocket.write( From 30a347d0b3dd6e5bd1435c59bf27733c94e39231 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 24 Oct 2025 13:51:54 -0700 Subject: [PATCH 16/52] Cleanup readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57d8c89..ead3c6d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Aikido Safe Chain -The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip. It's **free** to use and does not require any token. +The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token. 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), 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, or pip from downloading or running the malware. @@ -32,7 +32,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: safe-chain setup ``` 3. **❗Restart your terminal** to start using the Aikido Safe Chain. - - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. + - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip are loaded correctly. If you do not restart your terminal, the aliases will not be available. 4. **Verify the installation** by running: ```shell npm install safe-chain-test From 41fda7f6edc0c234c9d33a9113346ebe20a36ca6 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Sat, 25 Oct 2025 13:35:18 -0700 Subject: [PATCH 17/52] Update logging for audit --- packages/safe-chain/src/scanning/audit/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index e67ee0c..92c1f17 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -14,7 +14,7 @@ export async function auditChanges(changes) { ); for (const change of changes) { - console.log("**** Auditing change:", change); + console.log(" Safe-chain: auditing package:", change); const malwarePackage = malwarePackages.find( (pkg) => pkg.name === change.name && pkg.version === change.version ); From 38d3b46939f3f142c9411cae01aa2638efcf4e4d Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Sat, 25 Oct 2025 14:03:19 -0700 Subject: [PATCH 18/52] Some more cleanup --- packages/safe-chain/bin/aikido-pip.js | 1 + .../safe-chain/src/packagemanager/pip/utils/pipCommands.js | 2 +- packages/safe-chain/src/scanning/audit/index.js | 3 ++- packages/safe-chain/src/scanning/malwareDatabase.js | 3 +-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 29c68bc..c90355d 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -29,6 +29,7 @@ if (targetVersionMajor && String(targetVersionMajor).trim() === "3") { } // Set eco system +// This can be used in other parts of the code to determine which eco system we are working with setEcoSystem("py"); initializePackageManager(packageManagerName); diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js index 2818c87..5db1cc5 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -7,7 +7,7 @@ export function getPipCommandForArgs(args) { return null; } - // The first non-flag argument is typically the command + // The first non-flag argument is the command for (const arg of args) { if (!arg.startsWith("-")) { return arg; diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 92c1f17..cc87e17 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -14,7 +14,8 @@ export async function auditChanges(changes) { ); for (const change of changes) { - console.log(" Safe-chain: auditing package:", change); + //Uncomment next line during manual testing + //console.log(" Safe-chain: auditing package:", change); const malwarePackage = malwarePackages.find( (pkg) => pkg.name === change.name && pkg.version === change.version ); diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 976b386..b21733a 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -90,8 +90,7 @@ async function getMalwareDatabase() { function isMalwareStatus(status) { let malwareStatus = status.toUpperCase(); - return malwareStatus === MALWARE_STATUS_MALWARE - || malwareStatus === MALWARE_STATUS_TELEMETRY; + return malwareStatus === MALWARE_STATUS_MALWARE; } export const MALWARE_STATUS_OK = "OK"; From 598ddc17fa2f766c4b81a3c24d7fd40edf18b877 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Sat, 25 Oct 2025 14:14:36 -0700 Subject: [PATCH 19/52] Fix linting issue --- .../src/registryProxy/parsePackageFromUrl.js | 2 -- .../src/shell-integration/helpers.spec.js | 18 ------------------ 2 files changed, 20 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 4061578..4b440af 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -1,5 +1,3 @@ -import { parse } from "semver"; - export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"]; export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"]; diff --git a/packages/safe-chain/src/shell-integration/helpers.spec.js b/packages/safe-chain/src/shell-integration/helpers.spec.js index d7013db..4f18c36 100644 --- a/packages/safe-chain/src/shell-integration/helpers.spec.js +++ b/packages/safe-chain/src/shell-integration/helpers.spec.js @@ -181,22 +181,4 @@ describe("removeLinesMatchingPatternTests", () => { const resultLines = result.split("\n"); assert.strictEqual(resultLines.length, 5, "Should have exactly 5 lines"); }); - - it("should include pip in knownAikidoTools and in the package manager list", async () => { - // Import helpers after setting up the mock - const { knownAikidoTools, getPackageManagerList } = await import("./helpers.js"); - - // Verify pip tool - const hasPip = knownAikidoTools.some( - (t) => t.tool === "pip" && t.aikidoCommand === "aikido-pip" - ); - assert.ok(hasPip, "knownAikidoTools should include pip"); - - // Verify pip appears in the human-readable list - const list = getPackageManagerList(); - assert.ok( - /(^|[,\s])pip(,|\s| and)/.test(list) && /commands$/.test(list), - `getPackageManagerList should include 'pip' (actual: ${list})` - ); - }); }); From 9dacf5cff3ec539ab7c1fadd29581d58bc0d5c4d Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Sat, 25 Oct 2025 14:27:05 -0700 Subject: [PATCH 20/52] Revert package-lock.json to match main --- package-lock.json | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d64f79..88e9fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,7 +154,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-darwin-x64": { "version": "1.2.21", @@ -167,7 +168,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-darwin-x64-baseline": { "version": "1.2.21", @@ -180,7 +182,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-aarch64": { "version": "1.2.21", @@ -193,7 +196,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-aarch64-musl": { "version": "1.2.21", @@ -206,7 +210,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64": { "version": "1.2.21", @@ -219,7 +224,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-baseline": { "version": "1.2.21", @@ -232,7 +238,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-musl": { "version": "1.2.21", @@ -245,7 +252,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-musl-baseline": { "version": "1.2.21", @@ -258,7 +266,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-windows-x64": { "version": "1.2.21", @@ -271,7 +280,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@oven/bun-windows-x64-baseline": { "version": "1.2.21", @@ -284,7 +294,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@oxlint/darwin-arm64": { "version": "1.22.0", @@ -1724,7 +1735,6 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", - "aikido-pip": "bin/aikido-pip.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", From 190607de9235727c1c06eb02af563b3be68ea136 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 09:23:47 -0700 Subject: [PATCH 21/52] Adapt per review --- README.md | 16 ++--- docs/shell-integration.md | 8 +-- packages/safe-chain/bin/aikido-bun.js | 2 + packages/safe-chain/bin/aikido-bunx.js | 2 + packages/safe-chain/bin/aikido-npm.js | 2 + packages/safe-chain/bin/aikido-npx.js | 2 + packages/safe-chain/bin/aikido-pip.js | 25 +------- packages/safe-chain/bin/aikido-pip3.js | 19 ++++++ packages/safe-chain/bin/aikido-pnpm.js | 2 + packages/safe-chain/bin/aikido-pnpx.js | 2 + packages/safe-chain/bin/aikido-yarn.js | 2 + packages/safe-chain/package.json | 1 + packages/safe-chain/src/api/aikido.js | 11 ++-- packages/safe-chain/src/config/configFile.js | 4 +- packages/safe-chain/src/config/settings.js | 5 +- .../src/packagemanager/pip/runPipCommand.js | 31 ---------- .../src/registryProxy/parsePackageFromUrl.js | 42 +++++++++---- .../registryProxy/parsePackageFromUrl.spec.js | 11 +++- .../registryProxy.connect-tunnel.spec.js | 62 ++++++++++++++++++- .../src/registryProxy/registryProxy.js | 16 ++++- .../registryProxy/registryProxy.mitm.spec.js | 11 ++++ .../src/scanning/malwareDatabase.js | 4 +- .../src/shell-integration/setup-ci.js | 6 +- .../startup-scripts/init-fish.fish | 6 +- .../startup-scripts/init-posix.sh | 4 +- .../startup-scripts/init-pwsh.ps1 | 8 +-- test/e2e/pip.e2e.spec.js | 1 + 27 files changed, 191 insertions(+), 114 deletions(-) create mode 100644 packages/safe-chain/bin/aikido-pip3.js diff --git a/README.md b/README.md index ead3c6d..c18be58 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token. -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), 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, or pip from downloading or running the malware. +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), 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, or pip/pip3 from downloading or running the malware. ![demo](./docs/safe-package-manager-demo.png) @@ -16,6 +16,7 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - ✅ **bun** - ✅ **bunx** - ✅ **pip** +- ✅ **pip3** # Usage @@ -32,14 +33,14 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: safe-chain setup ``` 3. **❗Restart your terminal** to start using the Aikido Safe Chain. - - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip 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, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. 4. **Verify the installation** by running: ```shell npm install safe-chain-test ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, or `pip` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. You can check the installed version by running: ```shell @@ -48,7 +49,7 @@ safe-chain --version ## How it works -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, bun, bunx, or pip 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, bun, bunx, `pip`, or `pip3` 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 integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip commands. 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: @@ -60,13 +61,6 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md). -### Python support - -- Supports `pip` and `pip3` commands. -- Scans Python packages fetched by `pip install`, `pip download`, and `pip wheel`. -- Intercepts downloads from PyPI and checks them against Aikido's malware intelligence before they reach your machine. -- Included automatically when you run `safe-chain setup` (shell integration); **CI integration is not yet available for pip/pip3**. - ## Uninstallation To uninstall the Aikido Safe Chain, you can run the following command: diff --git a/docs/shell-integration.md b/docs/shell-integration.md index 4a6ac99..f1f64e7 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -2,7 +2,7 @@ ## Overview -The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. +The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. ## Supported Shells @@ -28,7 +28,7 @@ This command: - Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`) - Detects all supported shells on your system -- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `bunx` +- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly. @@ -77,7 +77,7 @@ The system modifies the following files to source Safe Chain startup scripts: This means the shell functions are working but the Aikido commands aren't installed or available in your PATH: - Make sure Aikido Safe Chain is properly installed on your system -- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, and `aikido-bunx` commands exist +- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist - Check that these commands are in your system's PATH ### Manual Verification @@ -120,4 +120,4 @@ npm() { } ``` -Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `bunx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes. +Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes. diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index 01e3972..c128445 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "bun"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index fb378e5..2e83793 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "bunx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 0e9f302..a50d9b5 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "npm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index d3dfdd6..e1687d3 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "npx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index c90355d..8d483f3 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -2,35 +2,16 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; -import { setEcoSystem } from "../src/config/settings.js"; +import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; // Defaults let packageManagerName = "pip"; -let targetVersionMajor; - -// Copy argv so we can modify it +// Pass through user args as-is const argv = process.argv.slice(2); -for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - - // --target-version-major tells us which pip version is being used (2 or 3) - if (a === "--target-version-major" && i + 1 < argv.length) { - targetVersionMajor = argv[i + 1]; - argv.splice(i, 2); - i -= 1; - continue; - } -} - -// If the user explicitly called python3, prefer pip3 -if (targetVersionMajor && String(targetVersionMajor).trim() === "3") { - packageManagerName = "pip3"; -} - // Set eco system // This can be used in other parts of the code to determine which eco system we are working with -setEcoSystem("py"); +setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); const exitCode = await main(argv); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js new file mode 100644 index 0000000..31da8bd --- /dev/null +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -0,0 +1,19 @@ +#!/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"; + +// Explicit pip3 entrypoint +const packageManagerName = "pip3"; + +// Copy argv as-is +const argv = process.argv.slice(2); + +// Set ecosystem to Python +setEcoSystem(ECOSYSTEM_PY); + +initializePackageManager(packageManagerName); +const exitCode = await main(argv); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index 0a06217..cf5125e 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "pnpm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index cdb6504..6182810 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "pnpx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index fd87606..eee14e8 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -2,7 +2,9 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js"; +setEcoSystem(ECOSYSTEM_JS); const packageManagerName = "yarn"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index d9c5547..ee95737 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -15,6 +15,7 @@ "aikido-bun": "bin/aikido-bun.js", "aikido-bunx": "bin/aikido-bunx.js", "aikido-pip": "bin/aikido-pip.js", + "aikido-pip3": "bin/aikido-pip3.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index b38a4cc..04d117e 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -1,13 +1,13 @@ import fetch from "make-fetch-happen"; -import { getEcoSystem } from "../config/settings.js"; +import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; const malwareDatabaseUrls = { - js: "https://malware-list.aikido.dev/malware_predictions.json", - py: "https://malware-list.aikido.dev/malware_pypi.json", + [ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json", + [ECOSYSTEM_PY]: "https://malware-list.aikido.dev/malware_pypi.json", }; export async function fetchMalwareDatabase() { - const ecosystem = getEcoSystem() || "js"; + const ecosystem = getEcoSystem(); const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl); if (!response.ok) { @@ -26,8 +26,7 @@ export async function fetchMalwareDatabase() { } export async function fetchMalwareDatabaseVersion() { - const ecosystem = getEcoSystem() || "js"; - + const ecosystem = getEcoSystem(); const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem]; const response = await fetch(malwareDatabaseUrl, { method: "HEAD", diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 1091eb0..5123d83 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -69,13 +69,13 @@ function readConfigFile() { function getDatabasePath() { const aikidoDir = getAikidoDirectory(); - const ecosystem = getEcoSystem() || "js"; + const ecosystem = getEcoSystem(); return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`); } function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); - const ecosystem = getEcoSystem() || "js"; + const ecosystem = getEcoSystem(); return path.join(aikidoDir, `version_${ecosystem}.txt`); } diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 0e03dd5..9690ae8 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -13,9 +13,12 @@ export function getMalwareAction() { export const MALWARE_ACTION_BLOCK = "block"; export const MALWARE_ACTION_PROMPT = "prompt"; +export const ECOSYSTEM_JS = "js"; +export const ECOSYSTEM_PY = "py"; + // Default to JavaScript ecosystem const ecosystemSettings = { - ecoSystem: "js", + ecoSystem: ECOSYSTEM_JS, }; export function getEcoSystem() { diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index b09fc50..e17eeb9 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -19,34 +19,3 @@ export async function runPip(command, args) { } } } - -export async function dryRunPipCommandAndOutput(command, args) { - try { - // Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not. - // We don't mutate args here — callers should include --dry-run when appropriate. - const result = await safeSpawnPy( - command, - args, - { - stdio: "pipe", - env: mergeSafeChainProxyEnvironmentVariables(process.env), - } - ); - return { - status: result.status, - output: result.status === 0 ? result.stdout : result.stderr, - }; - } catch (error) { - if (error.status) { - const output = - error.stdout?.toString() ?? - error.stderr?.toString() ?? - error.message ?? - ""; - return { status: error.status, output }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } - } -} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 4b440af..f7e9f82 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -1,20 +1,26 @@ +import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; + export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"]; export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"]; export function parsePackageFromUrl(url) { + const ecosystem = getEcoSystem(); let registry; - for (const knownRegistry of knownJsRegistries) { - if (url.includes(knownRegistry)) { - registry = knownRegistry; - return parseJsPackageFromUrl(url, registry); + // Only check registries that match the current ecosystem + if (ecosystem === ECOSYSTEM_JS) { + for (const knownRegistry of knownJsRegistries) { + if (url.includes(knownRegistry)) { + registry = knownRegistry; + return parseJsPackageFromUrl(url, registry); + } } - } - - for (const knownRegistry of knownPipRegistries) { - if (url.includes(knownRegistry)) { - registry = knownRegistry; - return parsePipPackageFromUrl(url, registry); + } else if (ecosystem === ECOSYSTEM_PY) { + for (const knownRegistry of knownPipRegistries) { + if (url.includes(knownRegistry)) { + registry = knownRegistry; + return parsePipPackageFromUrl(url, registry); + } } } @@ -70,21 +76,25 @@ function parsePipPackageFromUrl(url, registry) { } // Quick sanity check on the URL + parse - let u; + let urlObj; try { - u = new URL(url); + urlObj = new URL(url); } catch { return { packageName, version}; } // Get the last path segment (filename) and decode it (strip query & fragment automatically) - const lastSegment = u.pathname.split("/").filter(Boolean).pop(); + const lastSegment = urlObj.pathname.split("/").filter(Boolean).pop(); if (!lastSegment){ return { packageName, version}; } const filename = decodeURIComponent(lastSegment); + // Parse Python package downloads from PyPI/pythonhosted.org + // Example wheel: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1-py3-none-any.whl + // Example sdist: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1.tar.gz + // Wheel (.whl) if (filename.endsWith(".whl")) { const base = filename.slice(0, -4); // remove ".whl" @@ -96,6 +106,9 @@ function parsePipPackageFromUrl(url, registry) { const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest; packageName = dist; // preserve underscores version = rawVersion; + // Reject "latest" as it's a placeholder, not a real version + // When version is "latest", this signals the URL doesn't contain actual version info + // Returning undefined allows the request (see registryProxy.js isAllowedUrl) if (version === "latest" || !packageName || !version) { return { packageName: undefined, version: undefined }; } @@ -111,6 +124,9 @@ function parsePipPackageFromUrl(url, registry) { if (lastDash > 0 && lastDash < base.length - 1) { packageName = base.slice(0, lastDash); version = base.slice(lastDash + 1); + // Reject "latest" as it's a placeholder, not a real version + // When version is "latest", this signals the URL doesn't contain actual version info + // Returning undefined allows the request (see registryProxy.js isAllowedUrl) if (version === "latest" || !packageName || !version) { return { packageName: undefined, version: undefined }; } diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js index ee4c6b3..d052e9d 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js @@ -1,8 +1,13 @@ -import { describe, it } from "node:test"; +import { describe, it, beforeEach } from "node:test"; import assert from "node:assert"; import { parsePackageFromUrl } from "./parsePackageFromUrl.js"; +import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; describe("parsePackageFromUrl", () => { + beforeEach(() => { + setEcoSystem(ECOSYSTEM_JS); + }); + const testCases = [ // Regular packages { @@ -114,6 +119,10 @@ describe("parsePackageFromUrl", () => { }); describe("parsePackageFromUrl - pip URLs", () => { + beforeEach(() => { + setEcoSystem(ECOSYSTEM_PY); + }); + const pipTestCases = [ // Valid pip URLs { diff --git a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js index a1fea55..45fd96a 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js @@ -49,6 +49,32 @@ describe("registryProxy.connectTunnel", () => { socket.destroy(); }); + it("should use destination's real certificate (not safe-chain's self-signed CA)", async () => { + const socket = await connectToProxy(proxyHost, proxyPort); + await establishHttpsTunnel(socket, "postman-echo.com", 443); + + // Verifies that tunnel requests pass through the destination's real certificate + // without interception by the safe-chain MITM proxy. + const certInfo = await getTlsCertificateInfo( + socket, + new URL("https://postman-echo.com") + ); + + // Verify the certificate is NOT issued by our safe-chain CA + // Our self-signed CA would have issuer: "Safe-Chain Proxy CA" + assert.ok(certInfo.issuer !== undefined, "Certificate should have an issuer"); + assert.ok( + !certInfo.issuer.includes("Safe-Chain"), + `Tunnel should use destination's real certificate, not safe-chain CA. Issuer: ${certInfo.issuer}` + ); + + // Verify it's a real certificate with proper hostname + assert.strictEqual(certInfo.subject.includes("postman-echo.com"), true, + `Certificate subject should include postman-echo.com, got: ${certInfo.subject}`); + + socket.destroy(); + }); + describe("Error Handling", () => { it("should return 502 Bad Gateway for invalid hostname", async () => { const socket = await connectToProxy(proxyHost, proxyPort); @@ -141,7 +167,7 @@ function establishHttpsTunnel(socket, targetHost, targetPort) { }); } -function sendHttpsRequestThroughTunnel(socket, verb, url) { +function sendHttpsRequestThroughTunnel(socket, verb, url, rejectUnauthorized = false) { return new Promise((resolve, reject) => { const tlsSocket = tls.connect( { @@ -149,7 +175,7 @@ function sendHttpsRequestThroughTunnel(socket, verb, url) { servername: url.hostname, // Tests should focus on tunnel behavior, not system CA state; // disable CA verification to avoid flakiness on machines without full roots. - rejectUnauthorized: false, + rejectUnauthorized: rejectUnauthorized, }, () => { tlsSocket.write( @@ -173,3 +199,35 @@ function sendHttpsRequestThroughTunnel(socket, verb, url) { }); }); } + +function getTlsCertificateInfo(socket, url) { + return new Promise((resolve, reject) => { + const tlsSocket = tls.connect( + { + socket: socket, + servername: url.hostname, + // Don't reject unauthorized to avoid system CA issues in CI + // We just want to inspect the certificate + rejectUnauthorized: false, + }, + () => { + const cert = tlsSocket.getPeerCertificate(); + + // Extract issuer and subject information + const issuer = cert.issuer ? + Object.entries(cert.issuer).map(([k, v]) => `${k}=${v}`).join(", ") : + "unknown"; + const subject = cert.subject ? + Object.entries(cert.subject).map(([k, v]) => `${k}=${v}`).join(", ") : + "unknown"; + + tlsSocket.end(); + resolve({ issuer, subject }); + } + ); + + tlsSocket.on("error", (err) => { + reject(err); + }); + }); +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 013c470..f7ef83b 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -5,6 +5,7 @@ import { handleHttpProxyRequest } from "./plainHttpProxy.js"; import { getCaCertPath } from "./certUtils.js"; import { auditChanges } from "../scanning/audit/index.js"; import { knownJsRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js"; +import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; @@ -111,9 +112,18 @@ function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL - if ((knownJsRegistries.some((reg) => req.url.includes(reg))) - || (knownPipRegistries.some((reg) => req.url.includes(reg)))) { - mitmConnect(req, clientSocket, isAllowedUrl); + const ecosystem = getEcoSystem(); + const url = req.url || ""; + + let isKnownRegistry = false; + if (ecosystem === ECOSYSTEM_JS) { + isKnownRegistry = knownJsRegistries.some((reg) => url.includes(reg)); + } else if (ecosystem === ECOSYSTEM_PY) { + isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg)); + } + + if (isKnownRegistry) { + mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket tunnelRequest(req, clientSocket, head); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index 3b2a20c..130d94c 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -7,6 +7,7 @@ import { mergeSafeChainProxyEnvironmentVariables, } from "./registryProxy.js"; import { getCaCertPath } from "./certUtils.js"; +import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; import fs from "fs"; describe("registryProxy.mitm", () => { @@ -19,6 +20,8 @@ describe("registryProxy.mitm", () => { const proxyUrl = new URL(envVars.HTTPS_PROXY); proxyHost = proxyUrl.hostname; proxyPort = parseInt(proxyUrl.port, 10); + // Default to JS ecosystem for JS registry tests + setEcoSystem(ECOSYSTEM_JS); }); after(async () => { @@ -151,6 +154,8 @@ describe("registryProxy.mitm", () => { }); it("should intercept HTTPS requests to pypi.org for pip package", async () => { + // Switch to Python ecosystem for pip registry MITM tests + setEcoSystem(ECOSYSTEM_PY); const response = await makeRegistryRequest( proxyHost, proxyPort, @@ -162,6 +167,8 @@ describe("registryProxy.mitm", () => { }); it("should intercept HTTPS requests to files.pythonhosted.org for pip wheel", async () => { + // Ensure Python ecosystem + setEcoSystem(ECOSYSTEM_PY); const response = await makeRegistryRequest( proxyHost, proxyPort, @@ -173,6 +180,8 @@ describe("registryProxy.mitm", () => { }); it("should handle pip package with a1 version", async () => { + // Ensure Python ecosystem + setEcoSystem(ECOSYSTEM_PY); const response = await makeRegistryRequest( proxyHost, proxyPort, @@ -184,6 +193,8 @@ describe("registryProxy.mitm", () => { }); it("should handle pip package with latest version (should not block)", async () => { + // Ensure Python ecosystem + setEcoSystem(ECOSYSTEM_PY); const response = await makeRegistryRequest( proxyHost, proxyPort, diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index b21733a..7b4d6e6 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -7,7 +7,7 @@ import { writeDatabaseToLocalCache, } from "../config/configFile.js"; import { ui } from "../environment/userInteraction.js"; -import { getEcoSystem } from "../config/settings.js"; +import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js"; let cachedMalwareDatabase = null; @@ -18,7 +18,7 @@ let cachedMalwareDatabase = null; */ function normalizePackageName(name) { const ecosystem = getEcoSystem(); - if (ecosystem === "py") { + if (ecosystem === ECOSYSTEM_PY) { return name.toLowerCase().replace(/[-_.]+/g, "-"); } diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 6b1d357..eac75ab 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -46,8 +46,7 @@ function createUnixShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool except pip for now. - // TODO(pip): Enable pip and pip3 CI support + // Create a shim for each tool except pip (CI support not yet implemented) let created = 0; for (const toolInfo of knownAikidoTools) { if (toolInfo.tool === "pip") { @@ -89,8 +88,7 @@ function createWindowsShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool except pip for now. - // TODO(pip): Enable pip and pip3 CI support + // Create a shim for each tool except pip (CI support not yet implemented) let created = 0; for (const toolInfo of knownAikidoTools) { if (toolInfo.tool === "pip") { 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 efc03ca..9b6deaf 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 @@ -70,11 +70,9 @@ function npm end function pip - # Default to Python 2 major version when explicitly calling pip - wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" $argv + wrapSafeChainCommand "pip" "aikido-pip" $argv end function pip3 - # Route to Python 3 when calling pip3 - wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" $argv + wrapSafeChainCommand "pip3" "aikido-pip3" $argv end 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 a433154..820c34a 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 @@ -62,9 +62,9 @@ function npm() { } function pip() { - wrapSafeChainCommand "pip" "aikido-pip" --target-version-major "2" "$@" + wrapSafeChainCommand "pip" "aikido-pip" "$@" } function pip3() { - wrapSafeChainCommand "pip3" "aikido-pip" --target-version-major "3" "$@" + wrapSafeChainCommand "pip3" "aikido-pip3" "$@" } 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 199fccc..47c996d 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 @@ -88,13 +88,9 @@ function npm { } function pip { - # Default to Python 2 major version when explicitly calling pip - $forward = @("--target-version-major", "2") + $args - Invoke-WrappedCommand "pip" "aikido-pip" $forward + Invoke-WrappedCommand "pip" "aikido-pip" $args } function pip3 { - # Route to Python 3 when calling pip3 - $forward = @("--target-version-major", "3") + $args - Invoke-WrappedCommand "pip3" "aikido-pip" $forward + Invoke-WrappedCommand "pip3" "aikido-pip3" $args } diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 524c472..b864da9 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -85,4 +85,5 @@ describe("E2E: pip coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + }); From 8f877742d0844f03e0f3392b5aafe6dd8e73240a Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 11:48:30 -0700 Subject: [PATCH 22/52] Fix permissions issue with aikido-pip3 --- packages/safe-chain/bin/aikido-pip3.js | 0 packages/safe-chain/src/shell-integration/helpers.js | 1 + 2 files changed, 1 insertion(+) mode change 100644 => 100755 packages/safe-chain/bin/aikido-pip3.js diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js old mode 100644 new mode 100755 diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index e08227e..4af92fb 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -12,6 +12,7 @@ export const knownAikidoTools = [ { tool: "bun", aikidoCommand: "aikido-bun" }, { tool: "bunx", aikidoCommand: "aikido-bunx" }, { tool: "pip", aikidoCommand: "aikido-pip" }, + { tool: "pip3", aikidoCommand: "aikido-pip3" }, // When adding a new tool here, also update the documentation for the new tool in the README.md ]; From f6381f5e91ed0f205b356f4dd1509aaec470a595 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 12:09:41 -0700 Subject: [PATCH 23/52] Correct package-lock.json --- package-lock.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 88e9fb5..5aa28e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1735,6 +1735,8 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", + "aikido-pip": "bin/aikido-pip.js", + "aikido-pip3": "bin/aikido-pip3.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", From 57bbb06f395122edb8b7e5cfeec3ceac057d9297 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 13:00:18 -0700 Subject: [PATCH 24/52] Add redirecting for explicit python(3) commands --- README.md | 6 +-- docs/shell-integration.md | 37 ++++++++++++++++++- .../startup-scripts/init-fish.fish | 30 +++++++++++++++ .../startup-scripts/init-posix.sh | 30 +++++++++++++++ .../startup-scripts/init-pwsh.ps1 | 25 +++++++++++++ test/e2e/pip.e2e.spec.js | 20 ++++++++++ 6 files changed, 143 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 176a6b0..5b393de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Aikido Safe Chain -The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token. +The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Python ecosystem (through pip or pip3, including `python -m pip[...]` and `python3 -m pip[...]` where available) or in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token. 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), 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, or pip/pip3 from downloading or running the malware. @@ -40,7 +40,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` 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 ...`, and on Windows `py -m pip ...`). If any malware is detected, it will prompt you to exit the command. You can check the installed version by running: @@ -50,7 +50,7 @@ safe-chain --version ## How it works -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, bun, bunx, `pip`, or `pip3` 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, bun, bunx, `pip`, or `pip3` 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)**. Python `-m pip[...]` invocations are also routed when invoked by command name. 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 integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip commands. 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: diff --git a/docs/shell-integration.md b/docs/shell-integration.md index f1f64e7..97c96c1 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -2,7 +2,7 @@ ## Overview -The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. +The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`, and on Windows PowerShell `py -m pip`/`py -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. ## Supported Shells @@ -29,6 +29,7 @@ This command: - Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`) - Detects all supported shells on your system - Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` +- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` (and `py -m pip[...]` on Windows PowerShell) route through Safe Chain when invoked by name ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly. @@ -80,6 +81,12 @@ This means the shell functions are working but the Aikido commands aren't instal - Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist - Check that these commands are in your system's PATH +**`python -m pip` is not being intercepted:** + +- Ensure you are invoking `python`/`python3`/`py` by name (not via an absolute path). Shell function interception only occurs for command names resolved through PATH and won’t catch absolute paths like `/usr/bin/python -m pip`. +- Restart your terminal so the updated startup scripts are sourced. +- On Windows PowerShell, verify `python`, `python3` or `py` resolves by running `Get-Command python` / `Get-Command py`. + ### Manual Verification To verify the integration is working, follow these steps: @@ -98,7 +105,8 @@ To verify the integration is working, follow these steps: After restarting your terminal, run these commands: - `npm --version` - Should show output from the Aikido-wrapped version - - `type npm` - Should show that `npm` is a function + - `type npm` - Should show that `npm` is a function + - Optionally: `python -m pip --version` (or `python3 -m pip --version`) should show Safe Chain output at the end 3. **If you need to remove the integration manually:** @@ -121,3 +129,28 @@ npm() { ``` Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes. + +To intercept Python module invocations for pip without altering Python itself, you can add small forwarding functions: + +```bash +# Example for Bash/Zsh +python() { + if [[ "$1" == "-m" && "$2" == pip* ]]; then + local mod="$2"; shift 2 + if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi + else + command python "$@" + fi +} + +python3() { + if [[ "$1" == "-m" && "$2" == pip* ]]; then + local mod="$2"; shift 2 + if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi + else + command python3 "$@" + fi +} +``` + +Limitations: these only apply when invoking `python`/`python3` by name. Absolute paths (e.g., `/usr/bin/python -m pip`) bypass shell functions. 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 9b6deaf..40b8ef6 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 @@ -76,3 +76,33 @@ end function pip3 wrapSafeChainCommand "pip3" "aikido-pip3" $argv end + +# `python -m pip`, `python -m pip3`. +function python + if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] + set mod $argv[2] + set args $argv[3..-1] + if test $mod = "pip3" + wrapSafeChainCommand "pip3" "aikido-pip3" $args + else + wrapSafeChainCommand "pip" "aikido-pip" $args + end + else + command python $argv + end +end + +# `python3 -m pip`, `python3 -m pip3'. +function python3 + if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] + set mod $argv[2] + set args $argv[3..-1] + if test $mod = "pip3" + wrapSafeChainCommand "pip3" "aikido-pip3" $args + else + wrapSafeChainCommand "pip" "aikido-pip" $args + end + else + command python3 $argv + end +end 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 820c34a..2415fb0 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 @@ -68,3 +68,33 @@ function pip() { function pip3() { wrapSafeChainCommand "pip3" "aikido-pip3" "$@" } + +# Intercept `python -m pip[...]` so it routes through safe-chain without changing python itself. +# Supports: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. +function python() { + if [[ "$1" == "-m" && "$2" == pip* ]]; then + local mod="$2" + shift 2 + if [[ "$mod" == "pip3" ]]; then + wrapSafeChainCommand "pip3" "aikido-pip3" "$@" + else + wrapSafeChainCommand "pip" "aikido-pip" "$@" + fi + else + command python "$@" + fi +} + +function python3() { + if [[ "$1" == "-m" && "$2" == pip* ]]; then + local mod="$2" + shift 2 + if [[ "$mod" == "pip3" ]]; then + wrapSafeChainCommand "pip3" "aikido-pip3" "$@" + else + wrapSafeChainCommand "pip" "aikido-pip" "$@" + fi + else + command python3 "$@" + fi +} 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 47c996d..eeeb459 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 @@ -94,3 +94,28 @@ function pip { function pip3 { Invoke-WrappedCommand "pip3" "aikido-pip3" $args } + +# `python -m pip`, `python -m pip3`. +function python { + param([Parameter(ValueFromRemainingArguments=$true)]$Args) + if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { + if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $Args[2..($Args.Length-1)] } + else { Invoke-WrappedCommand 'pip' 'aikido-pip' $Args[2..($Args.Length-1)] } + } + else { + Invoke-RealCommand 'python' $Args + } +} + +# `python3 -m pip`, `python3 -m pip3'. +function python3 { + param([Parameter(ValueFromRemainingArguments=$true)]$Args) + if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { + if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $Args[2..($Args.Length-1)] } + else { Invoke-WrappedCommand 'pip' 'aikido-pip' $Args[2..($Args.Length-1)] } + } + else { + Invoke-RealCommand 'python3' $Args + } +} + diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index b864da9..0ef88d3 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -86,4 +86,24 @@ describe("E2E: pip coverage", () => { ); }); + it(`python3 -m pip install routes through safe-chain`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('python3 -m pip install requests'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`python3 -m pip download routes through safe-chain`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('python3 -m pip download requests'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + }); From a438175e8ae4e958c240d39af78764f074629f21 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 13:28:35 -0700 Subject: [PATCH 25/52] Fix tests --- README.md | 4 ++-- docs/shell-integration.md | 13 +++---------- .../shell-integration/startup-scripts/init-posix.sh | 4 ++-- packages/safe-chain/src/utils/safeSpawn.spec.js | 9 +++++++++ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5b393de..e438475 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ``` - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. -When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` 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 ...`, and on Windows `py -m pip ...`). If any malware is detected, it will prompt you to exit the command. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` 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: @@ -50,7 +50,7 @@ safe-chain --version ## How it works -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, bun, bunx, `pip`, or `pip3` 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)**. Python `-m pip[...]` invocations are also routed when invoked by command name. 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, bun, bunx, `pip`, or `pip3` 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 integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip commands. 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: diff --git a/docs/shell-integration.md b/docs/shell-integration.md index 97c96c1..e7afbe5 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -2,7 +2,7 @@ ## Overview -The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`, and on Windows PowerShell `py -m pip`/`py -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. +The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents. ## Supported Shells @@ -29,7 +29,7 @@ This command: - Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`) - Detects all supported shells on your system - Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` -- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` (and `py -m pip[...]` on Windows PowerShell) route through Safe Chain when invoked by name +- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` route through Safe Chain when invoked by name ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly. @@ -81,12 +81,6 @@ This means the shell functions are working but the Aikido commands aren't instal - Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist - Check that these commands are in your system's PATH -**`python -m pip` is not being intercepted:** - -- Ensure you are invoking `python`/`python3`/`py` by name (not via an absolute path). Shell function interception only occurs for command names resolved through PATH and won’t catch absolute paths like `/usr/bin/python -m pip`. -- Restart your terminal so the updated startup scripts are sourced. -- On Windows PowerShell, verify `python`, `python3` or `py` resolves by running `Get-Command python` / `Get-Command py`. - ### Manual Verification To verify the integration is working, follow these steps: @@ -105,8 +99,7 @@ To verify the integration is working, follow these steps: After restarting your terminal, run these commands: - `npm --version` - Should show output from the Aikido-wrapped version - - `type npm` - Should show that `npm` is a function - - Optionally: `python -m pip --version` (or `python3 -m pip --version`) should show Safe Chain output at the end + - `type npm` - Should show that `npm` is a function 3. **If you need to remove the integration manually:** 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 2415fb0..e4e0362 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 @@ -69,8 +69,7 @@ function pip3() { wrapSafeChainCommand "pip3" "aikido-pip3" "$@" } -# Intercept `python -m pip[...]` so it routes through safe-chain without changing python itself. -# Supports: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. +# `python -m pip`, `python -m pip3`. function python() { if [[ "$1" == "-m" && "$2" == pip* ]]; then local mod="$2" @@ -85,6 +84,7 @@ function python() { fi } +# `python3 -m pip`, `python3 -m pip3'. function python3() { if [[ "$1" == "-m" && "$2" == pip* ]]; then local mod="$2" diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index a07907d..9e25997 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -245,6 +245,15 @@ describe("safeSpawnPy", () => { }; return obj; }, + // Provide execSync so the module under test can import it without ESM errors. + // We don't actually execute it in safeSpawnPy flows, but Node's module loader + // validates the presence of the named export during import. + execSync: (cmd) => { + // Minimal stub: emulate `command -v ` returning a path + const match = /command -v (.*)/.exec(String(cmd) || ""); + const bin = match?.[1] || "mockbin"; + return `/usr/bin/${bin}\n`; + }, }, }); From 3c109fb5fdd9be863572eee53c170651ff255892 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 15:19:48 -0700 Subject: [PATCH 26/52] Fix issue seen during Windows testing --- .../shell-integration/startup-scripts/init-pwsh.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 eeeb459..b467d9e 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 @@ -99,8 +99,9 @@ function pip3 { function python { param([Parameter(ValueFromRemainingArguments=$true)]$Args) if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { - if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $Args[2..($Args.Length-1)] } - else { Invoke-WrappedCommand 'pip' 'aikido-pip' $Args[2..($Args.Length-1)] } + $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } + if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } + else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs } } else { Invoke-RealCommand 'python' $Args @@ -111,8 +112,9 @@ function python { function python3 { param([Parameter(ValueFromRemainingArguments=$true)]$Args) if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { - if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $Args[2..($Args.Length-1)] } - else { Invoke-WrappedCommand 'pip' 'aikido-pip' $Args[2..($Args.Length-1)] } + $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } + if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } + else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs } } else { Invoke-RealCommand 'python3' $Args From c2e632ead21730af20a79969fbb2b5cbeddc1248 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 08:46:07 -0700 Subject: [PATCH 27/52] Add e2e test for malware blocking + python3 fix --- README.md | 12 ++- .../startup-scripts/init-fish.fish | 9 +- .../startup-scripts/init-posix.sh | 9 +- .../startup-scripts/init-pwsh.ps1 | 5 +- packages/safe-chain/src/utils/safeSpawn.js | 9 ++ test/e2e/pip.e2e.spec.js | 87 +++++++++++++++++++ 6 files changed, 115 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e438475..1faa1f6 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,19 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ``` 3. **❗Restart your terminal** to start using the Aikido Safe Chain. - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. -4. **Verify the installation** by running: +4. **Verify the installation** by running one of the following commands: + + For JavaScript/Node.js: ```shell npm install safe-chain-test ``` - - The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. + + For Python: + ```shell + pip3 install safe-chain-pi-test + ``` + + - 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`, or `pip3` 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. 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 40b8ef6..699a057 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 @@ -82,6 +82,7 @@ function python if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] set mod $argv[2] set args $argv[3..-1] + # python -m pip → aikido-pip, python -m pip3 → aikido-pip3 if test $mod = "pip3" wrapSafeChainCommand "pip3" "aikido-pip3" $args else @@ -95,13 +96,9 @@ end # `python3 -m pip`, `python3 -m pip3'. function python3 if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] - set mod $argv[2] set args $argv[3..-1] - if test $mod = "pip3" - wrapSafeChainCommand "pip3" "aikido-pip3" $args - else - wrapSafeChainCommand "pip" "aikido-pip" $args - end + # python3 always uses pip3, regardless of whether user types `pip` or `pip3` + wrapSafeChainCommand "pip3" "aikido-pip3" $args else command python3 $argv end 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 e4e0362..43413c3 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 @@ -74,6 +74,7 @@ function python() { if [[ "$1" == "-m" && "$2" == pip* ]]; then local mod="$2" shift 2 + # python -m pip → aikido-pip, python -m pip3 → aikido-pip3 if [[ "$mod" == "pip3" ]]; then wrapSafeChainCommand "pip3" "aikido-pip3" "$@" else @@ -87,13 +88,9 @@ function python() { # `python3 -m pip`, `python3 -m pip3'. function python3() { if [[ "$1" == "-m" && "$2" == pip* ]]; then - local mod="$2" shift 2 - if [[ "$mod" == "pip3" ]]; then - wrapSafeChainCommand "pip3" "aikido-pip3" "$@" - else - wrapSafeChainCommand "pip" "aikido-pip" "$@" - fi + # python3 always uses pip3, regardless of whether user types `pip` or `pip3` + wrapSafeChainCommand "pip3" "aikido-pip3" "$@" else command python3 "$@" fi 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 b467d9e..6727bcb 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 @@ -99,6 +99,7 @@ function pip3 { function python { param([Parameter(ValueFromRemainingArguments=$true)]$Args) if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { + # python -m pip → aikido-pip, python -m pip3 → aikido-pip3 $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs } @@ -112,9 +113,9 @@ function python { function python3 { param([Parameter(ValueFromRemainingArguments=$true)]$Args) if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { + # python3 always uses pip3, regardless of whether user types `pip` or `pip3` $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } - if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } - else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs } + Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } else { Invoke-RealCommand 'python3' $Args diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index b88a3b1..b4602d2 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -126,6 +126,15 @@ export async function safeSpawnPy(command, args, options = {}) { }); child.on("error", (error) => { + // When stdio is inherited and spawn fails (e.g., command not found), + // we need to write the error to stderr manually since there's no child process + if (options.stdio === "inherit") { + if (error.code === "ENOENT") { + process.stderr.write(`Error: Command '${command}' not found. Please ensure it is installed and available in your PATH.\n`); + } else { + process.stderr.write(`Error: ${error.message}\n`); + } + } resolve({ status: 1, stdout: "", stderr: error.message || String(error) }); }); }); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 0ef88d3..7013121 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -106,4 +106,91 @@ describe("E2E: pip coverage", () => { ); }); + it(`safe-chain blocks installation of malicious Python packages`, async () => { + const shell = await container.openShell("zsh"); + // Clear pip cache to ensure network download through proxy + await shell.runCommand("pip3 cache purge"); + + const result = await shell.runCommand("pip3 install --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("pip3 list"); + assert.ok( + !listResult.output.includes("safe-chain-pi-test"), + `Malicious package was installed despite safe-chain protection. Output of 'pip3 list' was:\n${listResult.output}` + ); + }); + + it(`python -m pip routes to aikido-pip (uses pip command)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('python -m pip install --break-system-packages requests'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + // Verify it completed successfully (would fail if routing was incorrect) + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation did not succeed. Output was:\n${result.output}` + ); + }); + + it(`python -m pip3 routes to aikido-pip3 (uses pip3 command)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('python -m pip3 install --break-system-packages requests'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + // Verify it completed successfully (would fail if routing was incorrect) + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation did not succeed. Output was:\n${result.output}` + ); + }); + + it(`python3 -m pip routes to aikido-pip3 (uses pip3 command)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('python3 -m pip install --break-system-packages requests'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + // Verify it completed successfully (would fail if routing was incorrect) + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation did not succeed. Output was:\n${result.output}` + ); + }); + + it(`python3 -m pip3 routes to aikido-pip3 (uses pip3 command)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand('python3 -m pip3 install --break-system-packages requests'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + // Verify it completed successfully (would fail if routing was incorrect) + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation did not succeed. Output was:\n${result.output}` + ); + }); + }); From 684edd27a2d8d0c96b039b81657f40dc8c3d9708 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 09:39:05 -0700 Subject: [PATCH 28/52] Fix scanning issue --- packages/safe-chain/src/utils/safeSpawn.js | 24 ++++++++++++++++++- .../safe-chain/src/utils/safeSpawn.spec.js | 5 ++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index b4602d2..18f7eb1 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -107,8 +107,30 @@ export async function safeSpawn(command, args, options = {}) { * TL;DR: add support for shell::false */ export async function safeSpawnPy(command, args, options = {}) { + // The command is always one of our supported package managers. + // It should always be alphanumeric or _ or - + // Reject any command names with suspicious characters + if (!/^[a-zA-Z0-9_-]+$/.test(command)) { + throw new Error(`Invalid command name: ${command}`); + } + return new Promise((resolve) => { - const child = spawn(command, args, { ...options, shell: false }); + // On Unix/macOS resolve to full path to avoid PATH ambiguity; keep shell disabled everywhere + let cmdToRun = command; + if (os.platform() !== "win32") { + try { + cmdToRun = resolveCommandPath(command); + } catch (e) { + if (options.stdio === "inherit") { + process.stderr.write( + `Error: Command '${command}' not found. Please ensure it is installed and available in your PATH.\n` + ); + } + return resolve({ status: 1, stdout: "", stderr: e.message || String(e) }); + } + } + + const child = spawn(cmdToRun, args, { ...options, shell: false }); let stdout = ""; let stderr = ""; diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 9e25997..40d9847 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -273,8 +273,9 @@ describe("safeSpawnPy", () => { assert.strictEqual(result.status, 0); // Verify spawn signature - assert.strictEqual(spawnCalls.length, 1); - assert.strictEqual(spawnCalls[0].command, "pip3"); + assert.strictEqual(spawnCalls.length, 1); + // Allow either bare command or resolved full path + assert.match(spawnCalls[0].command, /(^|\/)pip3$/); assert.deepStrictEqual(spawnCalls[0].args, ["install", "Jinja2>=3.1,<3.2"]); assert.strictEqual(spawnCalls[0].options.shell, false); assert.strictEqual(spawnCalls[0].options.stdio, "inherit"); From ccd59a2f176d54f95217d47c9c1b976e913e36d6 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 09:45:24 -0700 Subject: [PATCH 29/52] Clean up code --- .../src/shell-integration/startup-scripts/init-fish.fish | 1 - .../src/shell-integration/startup-scripts/init-posix.sh | 1 - .../src/shell-integration/startup-scripts/init-pwsh.ps1 | 1 - packages/safe-chain/src/utils/safeSpawn.js | 1 - 4 files changed, 4 deletions(-) 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 699a057..13494d1 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 @@ -82,7 +82,6 @@ function python if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2] set mod $argv[2] set args $argv[3..-1] - # python -m pip → aikido-pip, python -m pip3 → aikido-pip3 if test $mod = "pip3" wrapSafeChainCommand "pip3" "aikido-pip3" $args else 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 43413c3..05b8b81 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 @@ -74,7 +74,6 @@ function python() { if [[ "$1" == "-m" && "$2" == pip* ]]; then local mod="$2" shift 2 - # python -m pip → aikido-pip, python -m pip3 → aikido-pip3 if [[ "$mod" == "pip3" ]]; then wrapSafeChainCommand "pip3" "aikido-pip3" "$@" else 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 6727bcb..6425f2f 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 @@ -99,7 +99,6 @@ function pip3 { function python { param([Parameter(ValueFromRemainingArguments=$true)]$Args) if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') { - # python -m pip → aikido-pip, python -m pip3 → aikido-pip3 $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs } else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs } diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 18f7eb1..8e1accd 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -115,7 +115,6 @@ export async function safeSpawnPy(command, args, options = {}) { } return new Promise((resolve) => { - // On Unix/macOS resolve to full path to avoid PATH ambiguity; keep shell disabled everywhere let cmdToRun = command; if (os.platform() !== "win32") { try { From b886bb1cfe978fb09fba535152a5e88f3a123a08 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 13:38:31 -0700 Subject: [PATCH 30/52] Call safeSpawn iso safeSpawnPy --- .../src/packagemanager/pip/runPipCommand.js | 4 +- packages/safe-chain/src/utils/safeSpawn.js | 62 --------- .../safe-chain/src/utils/safeSpawn.spec.js | 120 ++++++------------ 3 files changed, 44 insertions(+), 142 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index e17eeb9..456ff5d 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,11 +1,11 @@ import { ui } from "../../environment/userInteraction.js"; -import { safeSpawnPy } from "../../utils/safeSpawn.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; export async function runPip(command, args) { try { - const result = await safeSpawnPy(command, args, { + const result = await safeSpawn(command, args, { stdio: "inherit", env: mergeSafeChainProxyEnvironmentVariables(process.env), }); diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 8e1accd..c398ac2 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -98,65 +98,3 @@ export async function safeSpawn(command, args, options = {}) { }); }); } - -/** - * To avoid any regression issues on the JS ecosystem, - * a py-friendly safeSpawn that avoids shell interpolation - * issues (e.g., '<', '>' in version specs). - * - * TL;DR: add support for shell::false - */ -export async function safeSpawnPy(command, args, options = {}) { - // The command is always one of our supported package managers. - // It should always be alphanumeric or _ or - - // Reject any command names with suspicious characters - if (!/^[a-zA-Z0-9_-]+$/.test(command)) { - throw new Error(`Invalid command name: ${command}`); - } - - return new Promise((resolve) => { - let cmdToRun = command; - if (os.platform() !== "win32") { - try { - cmdToRun = resolveCommandPath(command); - } catch (e) { - if (options.stdio === "inherit") { - process.stderr.write( - `Error: Command '${command}' not found. Please ensure it is installed and available in your PATH.\n` - ); - } - return resolve({ status: 1, stdout: "", stderr: e.message || String(e) }); - } - } - - const child = spawn(cmdToRun, args, { ...options, shell: false }); - - let stdout = ""; - let stderr = ""; - - child.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - - child.stderr?.on("data", (data) => { - stderr += data.toString(); - }); - - child.on("close", (code) => { - resolve({ status: code, stdout, stderr }); - }); - - child.on("error", (error) => { - // When stdio is inherited and spawn fails (e.g., command not found), - // we need to write the error to stderr manually since there's no child process - if (options.stdio === "inherit") { - if (error.code === "ENOENT") { - process.stderr.write(`Error: Command '${command}' not found. Please ensure it is installed and available in your PATH.\n`); - } else { - process.stderr.write(`Error: ${error.message}\n`); - } - } - resolve({ status: 1, stdout: "", stderr: error.message || String(error) }); - }); - }); -} diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 40d9847..cbc5583 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -13,8 +13,13 @@ describe("safeSpawn", () => { // Mock child_process module to capture what command string gets built mock.module("child_process", { namedExports: { - spawn: (command, options) => { - spawnCalls.push({ command, options }); + spawn: (command, argsOrOptions, options) => { + // Handle both signatures: spawn(cmd, {opts}) and spawn(cmd, [args], {opts}) + if (Array.isArray(argsOrOptions)) { + spawnCalls.push({ command, args: argsOrOptions, options: options || {} }); + } else { + spawnCalls.push({ command, options: argsOrOptions || {} }); + } return { on: (event, callback) => { if (event === "close") { @@ -211,84 +216,43 @@ describe("safeSpawn", () => { assert.strictEqual(spawnCalls.length, 1); assert.strictEqual(spawnCalls[0].command, "valid_command-123"); }); -}); -describe("safeSpawnPy", () => { - let safeSpawnPy; - let spawnCalls = []; - - beforeEach(async () => { - spawnCalls = []; - - // Mock child_process for argument-array spawn signature - mock.module("child_process", { - namedExports: { - spawn: (command, args = [], options = {}) => { - spawnCalls.push({ command, args, options }); - const stdoutListeners = []; - const stderrListeners = []; - const stdout = { on: (event, cb) => { if (event === "data") stdoutListeners.push(cb); } }; - const stderr = { on: (event, cb) => { if (event === "data") stderrListeners.push(cb); } }; - const obj = { - stdout, - stderr, - on: (event, callback) => { - if (event === 'close') { - // Emit one chunk to stdout and stderr to verify piping works, then close with success - setTimeout(() => { - stdoutListeners.forEach((cb) => cb(Buffer.from("STDOUT-TEST"))); - stderrListeners.forEach((cb) => cb(Buffer.from(""))); - callback(0); - }, 0); - } - } - }; - return obj; - }, - // Provide execSync so the module under test can import it without ESM errors. - // We don't actually execute it in safeSpawnPy flows, but Node's module loader - // validates the presence of the named export during import. - execSync: (cmd) => { - // Minimal stub: emulate `command -v ` returning a path - const match = /command -v (.*)/.exec(String(cmd) || ""); - const bin = match?.[1] || "mockbin"; - return `/usr/bin/${bin}\n`; - }, - }, - }); - - // Import after mocking; use a query to avoid ESM cache collisions with previous import - const safeSpawnModule = await import("./safeSpawn.js?py"); - safeSpawnPy = safeSpawnModule.safeSpawnPy; - }); - - afterEach(() => { - mock.reset(); - }); - - it("spawns without a shell and preserves args (inherit)", async () => { - const result = await safeSpawnPy("pip3", ["install", "Jinja2>=3.1,<3.2"], { stdio: "inherit" }); - - // Verifies no throw and status 0 - assert.strictEqual(result.status, 0); - - // Verify spawn signature - assert.strictEqual(spawnCalls.length, 1); - // Allow either bare command or resolved full path - assert.match(spawnCalls[0].command, /(^|\/)pip3$/); - assert.deepStrictEqual(spawnCalls[0].args, ["install", "Jinja2>=3.1,<3.2"]); - assert.strictEqual(spawnCalls[0].options.shell, false); - assert.strictEqual(spawnCalls[0].options.stdio, "inherit"); - }); - - it("captures stdout when stdio=pipe", async () => { - const result = await safeSpawnPy("pip3", ["install", "idna!=3.5,>=3.0", "--dry-run"], { stdio: "pipe" }); - - assert.strictEqual(result.status, 0); - assert.match(result.stdout || "", /STDOUT-TEST/); + it("should handle Python version specifiers with comparison operators on Windows", async () => { + os = "win32"; + await safeSpawn("pip3", ["install", "Jinja2>=3.1,<3.2"]); assert.strictEqual(spawnCalls.length, 1); - assert.strictEqual(spawnCalls[0].options.shell, false); - assert.strictEqual(spawnCalls[0].options.stdio, "pipe"); + // On Windows, args are built into a command string with proper escaping + assert.strictEqual(spawnCalls[0].command, 'pip3 install "Jinja2>=3.1,<3.2"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); + + it("should handle Python version specifiers with comparison operators on Unix", async () => { + os = "darwin"; // or "linux" + await safeSpawn("pip3", ["install", "Jinja2>=3.1,<3.2"]); + + assert.strictEqual(spawnCalls.length, 1); + // On Unix, resolves full path and passes args as array (no shell interpretation) + assert.strictEqual(spawnCalls[0].command, "/usr/bin/pip3"); + assert.deepStrictEqual(spawnCalls[0].args, ["install", "Jinja2>=3.1,<3.2"]); + assert.deepStrictEqual(spawnCalls[0].options, {}); + }); + + it("should handle Python not-equal version specifiers", async () => { + os = "win32"; + await safeSpawn("pip3", ["install", "idna!=3.5,>=3.0"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'pip3 install "idna!=3.5,>=3.0"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); + + it("should handle Python extras with square brackets", async () => { + os = "win32"; + await safeSpawn("pip3", ["install", "requests[socks]"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'pip3 install "requests[socks]"'); + assert.strictEqual(spawnCalls[0].options.shell, true); }); }); From 70dc89c3e834a02fdf832435d9194884a49a3d51 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 13:56:27 -0700 Subject: [PATCH 31/52] Simplify setting certificates --- .../src/packagemanager/pip/runPipCommand.js | 13 +++- .../packagemanager/pip/runPipCommand.spec.js | 63 +++++++++++++++++++ .../src/registryProxy/registryProxy.js | 8 --- .../registryProxy/registryProxy.mitm.spec.js | 9 ++- 4 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 456ff5d..4de1512 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,13 +1,20 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; - +import { getCaCertPath } from "../../registryProxy/certUtils.js"; export async function runPip(command, args) { try { - const result = await safeSpawn(command, args, { + const env = mergeSafeChainProxyEnvironmentVariables(process.env); + + // Pass --cert with our CA to pip so it trusts our MITM for known registries. + // pip will append this to its default CA bundle, so it still validates + // non-registry HTTPS (GitHub, custom mirrors) against system CAs. + const finalArgs = [...args, "--cert", getCaCertPath()]; + + const result = await safeSpawn(command, finalArgs, { stdio: "inherit", - env: mergeSafeChainProxyEnvironmentVariables(process.env), + env, }); return { status: result.status }; } catch (error) { diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js new file mode 100644 index 0000000..52a4148 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -0,0 +1,63 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; + +describe("runPipCommand --cert handling", () => { + let runPip; + let capturedArgs = null; + + beforeEach(async () => { + capturedArgs = null; + + // Mock safeSpawn to capture args + mock.module("../../utils/safeSpawn.js", { + namedExports: { + safeSpawn: async (command, args, options) => { + capturedArgs = { command, args, options }; + return { status: 0 }; + }, + }, + }); + + // Mock proxy env merge + mock.module("../../registryProxy/registryProxy.js", { + namedExports: { + mergeSafeChainProxyEnvironmentVariables: (env) => ({ + ...env, + HTTPS_PROXY: "http://localhost:8080", + }), + }, + }); + + // Mock certUtils to point to a test CA path + mock.module("../../registryProxy/certUtils.js", { + namedExports: { + getCaCertPath: () => "/tmp/test-ca.pem", + }, + }); + + const mod = await import("./runPipCommand.js"); + runPip = mod.runPip; + }); + + afterEach(() => { + mock.reset(); + }); + + it("should append --cert with our CA path to pip args", async () => { + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + + // safeSpawn should be called with --cert flag + assert.ok(capturedArgs, "safeSpawn should have been called"); + + const idx = capturedArgs.args.indexOf("--cert"); + assert.ok(idx >= 0, "--cert flag should be present in pip args"); + + const certPath = capturedArgs.args[idx + 1]; + assert.strictEqual(certPath, "/tmp/test-ca.pem", "CA path should match getCaCertPath()"); + + // Original args should be preserved before --cert + assert.strictEqual(capturedArgs.args[0], "install"); + assert.strictEqual(capturedArgs.args[1], "requests"); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 636878f..85408e6 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -34,14 +34,6 @@ function getSafeChainProxyEnvironmentVariables() { HTTPS_PROXY: `http://localhost:${state.port}`, GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`, NODE_EXTRA_CA_CERTS: getCaCertPath(), - - // Following env vars point pip and Python's requests/urllib at a CA Cert file. - // pip checks PIP_CERT first - // If pip uses requests library internally, it needs REQUESTS_CA_BUNDLE - // Other Python packages or pip's fallback SSL code may use SSL_CERT_FILE - PIP_CERT: getCaCertPath(), - REQUESTS_CA_BUNDLE: getCaCertPath(), - SSL_CERT_FILE: getCaCertPath(), }; } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index 130d94c..72da0c6 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -145,12 +145,11 @@ describe("registryProxy.mitm", () => { }); // --- Pip registry MITM and env var tests --- - it("should set pip CA trust environment variables", () => { + it("should NOT set global Python CA environment variables", () => { const envVars = mergeSafeChainProxyEnvironmentVariables([]); - const caPath = getCaCertPath(); - assert.strictEqual(envVars.PIP_CERT, caPath); - assert.strictEqual(envVars.REQUESTS_CA_BUNDLE, caPath); - assert.strictEqual(envVars.SSL_CERT_FILE, caPath); + assert.strictEqual(envVars.PIP_CERT, undefined); + assert.strictEqual(envVars.REQUESTS_CA_BUNDLE, undefined); + assert.strictEqual(envVars.SSL_CERT_FILE, undefined); }); it("should intercept HTTPS requests to pypi.org for pip package", async () => { From a17e14c988566af50846eb2ff847efe4ddfc9730 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 15:02:59 -0700 Subject: [PATCH 32/52] Ensure that --cert parameters do not get overriden --- .../src/packagemanager/pip/runPipCommand.js | 14 ++-- .../packagemanager/pip/runPipCommand.spec.js | 24 +++++++ test/e2e/pip.e2e.spec.js | 69 +++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 4de1512..e8d8d26 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -7,10 +7,16 @@ export async function runPip(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - // Pass --cert with our CA to pip so it trusts our MITM for known registries. - // pip will append this to its default CA bundle, so it still validates - // non-registry HTTPS (GitHub, custom mirrors) against system CAs. - const finalArgs = [...args, "--cert", getCaCertPath()]; + // If the user already provided --cert, respect their choice and do not override. + // Support both "--cert " and "--cert=" forms. + const hasUserCert = args.some((a, i) => { + if (a === "--cert") return true; + return typeof a === "string" && a.startsWith("--cert="); + }); + + // By default, pass --cert with our CA so pip trusts our MITM for known registries. + // Note: pip treats --cert as the CA bundle to use for TLS (it does not merge with system CAs). + const finalArgs = hasUserCert ? [...args] : [...args, "--cert", getCaCertPath()]; const result = await safeSpawn(command, finalArgs, { stdio: "inherit", diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 52a4148..f9ef43d 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -60,4 +60,28 @@ describe("runPipCommand --cert handling", () => { assert.strictEqual(capturedArgs.args[0], "install"); assert.strictEqual(capturedArgs.args[1], "requests"); }); + + it("should not override user-provided --cert ", async () => { + const res = await runPip("pip3", ["install", "requests", "--cert", "/tmp/user-ca.pem"]); + assert.strictEqual(res.status, 0); + + // Ensure only the user-provided --cert is present + const certIndices = capturedArgs.args + .map((a, i) => (a === "--cert" ? i : -1)) + .filter((i) => i >= 0); + assert.strictEqual(certIndices.length, 1, "should not inject an extra --cert"); + const userPath = capturedArgs.args[certIndices[0] + 1]; + assert.strictEqual(userPath, "/tmp/user-ca.pem", "should preserve user-provided cert path"); + }); + + it("should not override user-provided --cert=", async () => { + const res = await runPip("pip3", ["install", "requests", "--cert=/tmp/user-ca.pem"]); + assert.strictEqual(res.status, 0); + + // Ensure args contain the inline --cert= and no extra --cert token + const hasInline = capturedArgs.args.some((a) => typeof a === "string" && a.startsWith("--cert=")); + assert.ok(hasInline, "should keep inline --cert="); + const injectedIndex = capturedArgs.args.indexOf("--cert"); + assert.strictEqual(injectedIndex, -1, "should not inject separate --cert when inline is provided"); + }); }); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 7013121..05bdde9 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -193,4 +193,73 @@ describe("E2E: pip coverage", () => { ); }); + it(`pip3 can install from GitHub URL using system CAs`, async () => { + const shell = await container.openShell("zsh"); + // Install a simple package from GitHub - this should use TCP tunnel, not MITM + // Using a popular, small package for testing + const result = await shell.runCommand('pip3 install --break-system-packages git+https://github.com/psf/requests.git@v2.32.3'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + // Verify installation succeeded (would fail if certificate validation broke) + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation from GitHub failed - system CAs may not be working. Output was:\n${result.output}` + ); + + // Verify package was actually installed + const listResult = await shell.runCommand("pip3 list"); + assert.ok( + listResult.output.includes("requests"), + `Package from GitHub was not installed. Output was:\n${listResult.output}` + ); + }); + + it(`pip3 successfully validates certificates for HTTPS downloads`, async () => { + const shell = await container.openShell("zsh"); + // Clear cache to force network download through proxy + await shell.runCommand("pip3 cache purge"); + + const result = await shell.runCommand('pip3 install --break-system-packages certifi'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + // Verify successful installation (would fail with SSL/certificate errors if --cert wasn't working) + assert.ok( + result.output.includes("Successfully 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(`pip3 handles external HTTPS correctly (e.g., downloading from CDN)`, async () => { + const shell = await container.openShell("zsh"); + // Test installing from a direct HTTPS URL (not a registry) + // This validates that non-registry HTTPS traffic works with system CAs + const result = await shell.runCommand('pip3 install --break-system-packages https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl'); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + // Since this is from pythonhosted.org, it should be MITM'd by safe-chain + // But the certificate validation should still work + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation from direct HTTPS URL failed. Output was:\n${result.output}` + ); + }); + }); From 86ce7ac45ee012baa9bcb1404ed625ab1114b102 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 28 Oct 2025 15:44:36 -0700 Subject: [PATCH 33/52] Remove unused var --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index e8d8d26..969363b 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -9,7 +9,7 @@ export async function runPip(command, args) { // If the user already provided --cert, respect their choice and do not override. // Support both "--cert " and "--cert=" forms. - const hasUserCert = args.some((a, i) => { + const hasUserCert = args.some((a) => { if (a === "--cert") return true; return typeof a === "string" && a.startsWith("--cert="); }); From 8b7784ecc0fed5ea82047a80e609b1cf13118c58 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 30 Oct 2025 12:36:32 -0700 Subject: [PATCH 34/52] Omly pass --cert when using known registry --- package-lock.json | 12 ++- packages/safe-chain/package.json | 1 + .../src/packagemanager/pip/runPipCommand.js | 74 ++++++++++++++++--- .../packagemanager/pip/runPipCommand.spec.js | 41 +++++++++- test/e2e/pip.e2e.spec.js | 25 +++++++ 5 files changed, 142 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5aa28e8..dee28f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1717,6 +1717,15 @@ "node": ">=18" } }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", @@ -1728,7 +1737,8 @@ "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", "ora": "8.2.0", - "semver": "7.7.2" + "semver": "7.7.2", + "yargs-parser": "^21.1.1" }, "bin": { "aikido-bun": "bin/aikido-bun.js", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index ee95737..08d8a19 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -32,6 +32,7 @@ "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/), and [bunx](https://bun.sh/docs/cli/bunx) 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, or bunx from downloading or running the malware.", "dependencies": { + "yargs-parser": "^21.1.1", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 969363b..c4a1ac6 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,21 +2,77 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCaCertPath } from "../../registryProxy/certUtils.js"; +import { knownPipRegistries } from "../../registryProxy/parsePackageFromUrl.js"; +import yargsParser from "yargs-parser"; + +function extractHostsFromPipArgs(args) { + function hostFromString(input) { + if (typeof input !== "string" || input.length === 0) return undefined; + try { + const u = new URL(input); + return u.hostname || undefined; + } catch { + // ignore: not a valid absolute URL + } + // Try adding a scheme if it's a schemeless URL-like value + try { + const u2 = new URL(`https://${input}`); + return u2.hostname || undefined; + } catch { + // ignore: not a valid schemeless URL either + } + return undefined; + } + + const parsed = yargsParser(args, { + configuration: { + "short-option-groups": true, + "camel-case-expansion": false, + "dot-notation": false, + "duplicate-arguments-array": true, + "flatten-duplicate-arrays": false, + "greedy-arrays": false, + "unknown-options-as-args": true, + }, + }); + const toArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]); + const candidateUrls = [ + ...toArray(parsed.i), + ...toArray(parsed["index-url"]), + ...toArray(parsed["extra-index-url"]), + ...toArray(parsed["find-links"]), + ...toArray(parsed._).filter( + (a) => typeof a === "string" && (a.startsWith("https://") || a.startsWith("http://")) + ), + ]; + const hosts = new Set(); + for (const u of candidateUrls) { + const h = hostFromString(u); + if (h) hosts.add(h); + } + return Array.from(hosts); +} + export async function runPip(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); + // Re-introduce conditional --cert injection: only for known registries (MITM). + // No global env overrides for Python trust. + const hosts = extractHostsFromPipArgs(args); + const allKnown = hosts.length === 0 + ? true // No explicit sources => default PyPI (known) -> MITM + : hosts.every((h) => knownPipRegistries.includes(h)); - // If the user already provided --cert, respect their choice and do not override. - // Support both "--cert " and "--cert=" forms. - const hasUserCert = args.some((a) => { - if (a === "--cert") return true; - return typeof a === "string" && a.startsWith("--cert="); - }); + // Respect user-provided --cert: detect both "--cert " and "--cert=" + const hasUserCert = args.some( + (a) => a === "--cert" || (typeof a === "string" && a.startsWith("--cert=")) + ); - // By default, pass --cert with our CA so pip trusts our MITM for known registries. - // Note: pip treats --cert as the CA bundle to use for TLS (it does not merge with system CAs). - const finalArgs = hasUserCert ? [...args] : [...args, "--cert", getCaCertPath()]; + let finalArgs = [...args]; + if (allKnown && !hasUserCert) { + finalArgs = [...args, "--cert", getCaCertPath()]; + } const result = await safeSpawn(command, finalArgs, { stdio: "inherit", diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index f9ef43d..1aecc30 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -43,7 +43,7 @@ describe("runPipCommand --cert handling", () => { mock.reset(); }); - it("should append --cert with our CA path to pip args", async () => { + it("should append --cert with our CA path to pip args by default (PyPI)", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); @@ -59,6 +59,10 @@ describe("runPipCommand --cert handling", () => { // Original args should be preserved before --cert assert.strictEqual(capturedArgs.args[0], "install"); assert.strictEqual(capturedArgs.args[1], "requests"); + + // No Python CA env overrides expected + assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, undefined); + assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, undefined); }); it("should not override user-provided --cert ", async () => { @@ -72,6 +76,9 @@ describe("runPipCommand --cert handling", () => { assert.strictEqual(certIndices.length, 1, "should not inject an extra --cert"); const userPath = capturedArgs.args[certIndices[0] + 1]; assert.strictEqual(userPath, "/tmp/user-ca.pem", "should preserve user-provided cert path"); + // No Python CA env overrides expected + assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, undefined); + assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, undefined); }); it("should not override user-provided --cert=", async () => { @@ -83,5 +90,37 @@ describe("runPipCommand --cert handling", () => { assert.ok(hasInline, "should keep inline --cert="); const injectedIndex = capturedArgs.args.indexOf("--cert"); assert.strictEqual(injectedIndex, -1, "should not inject separate --cert when inline is provided"); + // No Python CA env overrides expected + assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, undefined); + assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, undefined); + }); + + it("should inject --cert when explicit index is a known PyPI host", async () => { + const res = await runPip("pip3", ["install", "requests", "--index-url", "https://pypi.org/simple"]); + assert.strictEqual(res.status, 0); + const idx = capturedArgs.args.indexOf("--cert"); + assert.ok(idx >= 0, "--cert should be present for known registries"); + }); + + it("should NOT inject --cert when index points to an unknown external mirror (tunneled)", async () => { + const res = await runPip("pip3", [ + "install", + "certifi", + "--index-url", + "https://pypi.tuna.tsinghua.edu.cn/simple", + ]); + assert.strictEqual(res.status, 0); + const idx = capturedArgs.args.indexOf("--cert"); + assert.strictEqual(idx, -1, "--cert should be omitted for tunneled external hosts"); + }); + + it("should NOT inject --cert when installing from a direct external URL", async () => { + const res = await runPip("pip3", [ + "install", + "https://example.com/pkg-1.0.0-py3-none-any.whl", + ]); + assert.strictEqual(res.status, 0); + const idx = capturedArgs.args.indexOf("--cert"); + assert.strictEqual(idx, -1, "--cert should be omitted for direct external URLs"); }); }); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 05bdde9..153eca5 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -262,4 +262,29 @@ describe("E2E: pip coverage", () => { ); }); + it(`pip3 can install from alternate PyPI mirror (tunneled, not MITM)`, async () => { + const shell = await container.openShell("zsh"); + // Use Tsinghua PyPI mirror which is NOT in knownPipRegistries + // This tests tunneled HTTPS with --cert containing only Safe Chain CA + // If the CA bundle doesn't include public roots, this will fail with CERTIFICATE_VERIFY_FAILED + const result = await shell.runCommand('pip3 install --break-system-packages --index-url https://pypi.tuna.tsinghua.edu.cn/simple certifi'); + + assert.ok( + result.output.includes("no malicious packages 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("Successfully installed") || result.output.includes("Requirement already satisfied"), + `Installation from PyPI mirror failed. This may indicate --cert CA bundle lacks public roots. Output was:\n${result.output}` + ); + + // Should NOT contain certificate verification errors + assert.ok( + !result.output.match(/SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i), + `Should not have SSL/certificate errors for tunneled hosts. Output was:\n${result.output}` + ); + }); + }); From 1755fe829c5269bc9c5e48d4d1a5cc102f496985 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 30 Oct 2025 12:52:10 -0700 Subject: [PATCH 35/52] Make test a little safer --- .../safe-chain/src/packagemanager/pip/runPipCommand.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 1aecc30..36abdf1 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -107,7 +107,7 @@ describe("runPipCommand --cert handling", () => { "install", "certifi", "--index-url", - "https://pypi.tuna.tsinghua.edu.cn/simple", + "https://test.pypi.org/simple", ]); assert.strictEqual(res.status, 0); const idx = capturedArgs.args.indexOf("--cert"); From f38a12c6d55d7431434982c38fdf1d4d669b908f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 30 Oct 2025 16:00:32 -0700 Subject: [PATCH 36/52] Combine certificates --- package-lock.json | 22 ++-- packages/safe-chain/package.json | 2 +- .../src/packagemanager/pip/runPipCommand.js | 78 ++---------- .../packagemanager/pip/runPipCommand.spec.js | 117 ++++++++---------- .../packagemanager/pip/utils/pipCaBundle.js | 90 ++++++++++++++ .../pip/utils/pipCaBundle.spec.js | 71 +++++++++++ .../registryProxy/registryProxy.mitm.spec.js | 2 +- test/e2e/pip.e2e.spec.js | 14 +-- 8 files changed, 243 insertions(+), 153 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js create mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js diff --git a/package-lock.json b/package-lock.json index dee28f5..d3dda09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -516,6 +516,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/certifi": { + "version": "14.5.15", + "resolved": "https://registry.npmjs.org/certifi/-/certifi-14.5.15.tgz", + "integrity": "sha512-NeLXuKCqSzwQNjpJ+WaSp5m8ntdTKJ8HnBu+eA7DxHfgzU7F1sjwrJFang+4U38+vmWbiFUpPZMV3uwwnHAisQ==", + "license": "MPL-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -1717,28 +1726,19 @@ "node": ">=18" } }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { + "certifi": "^14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", "ora": "8.2.0", - "semver": "7.7.2", - "yargs-parser": "^21.1.1" + "semver": "7.7.2" }, "bin": { "aikido-bun": "bin/aikido-bun.js", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 08d8a19..5724878 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -32,7 +32,7 @@ "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/), and [bunx](https://bun.sh/docs/cli/bunx) 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, or bunx from downloading or running the malware.", "dependencies": { - "yargs-parser": "^21.1.1", + "certifi": "^14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index c4a1ac6..4e12282 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,80 +1,22 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCaCertPath } from "../../registryProxy/certUtils.js"; -import { knownPipRegistries } from "../../registryProxy/parsePackageFromUrl.js"; -import yargsParser from "yargs-parser"; - -function extractHostsFromPipArgs(args) { - function hostFromString(input) { - if (typeof input !== "string" || input.length === 0) return undefined; - try { - const u = new URL(input); - return u.hostname || undefined; - } catch { - // ignore: not a valid absolute URL - } - // Try adding a scheme if it's a schemeless URL-like value - try { - const u2 = new URL(`https://${input}`); - return u2.hostname || undefined; - } catch { - // ignore: not a valid schemeless URL either - } - return undefined; - } - - const parsed = yargsParser(args, { - configuration: { - "short-option-groups": true, - "camel-case-expansion": false, - "dot-notation": false, - "duplicate-arguments-array": true, - "flatten-duplicate-arrays": false, - "greedy-arrays": false, - "unknown-options-as-args": true, - }, - }); - const toArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]); - const candidateUrls = [ - ...toArray(parsed.i), - ...toArray(parsed["index-url"]), - ...toArray(parsed["extra-index-url"]), - ...toArray(parsed["find-links"]), - ...toArray(parsed._).filter( - (a) => typeof a === "string" && (a.startsWith("https://") || a.startsWith("http://")) - ), - ]; - const hosts = new Set(); - for (const u of candidateUrls) { - const h = hostFromString(u); - if (h) hosts.add(h); - } - return Array.from(hosts); -} - +import { getCombinedCaBundlePath } from "./utils/pipCaBundle.js"; +// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) +// so that any network request made by pip, including those outside explicit CLI args, +// validates correctly under both MITM'd and tunneled HTTPS. export async function runPip(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - // Re-introduce conditional --cert injection: only for known registries (MITM). - // No global env overrides for Python trust. - const hosts = extractHostsFromPipArgs(args); - const allKnown = hosts.length === 0 - ? true // No explicit sources => default PyPI (known) -> MITM - : hosts.every((h) => knownPipRegistries.includes(h)); - // Respect user-provided --cert: detect both "--cert " and "--cert=" - const hasUserCert = args.some( - (a) => a === "--cert" || (typeof a === "string" && a.startsWith("--cert=")) - ); + // Always set Python CA env vars to a combined bundle that includes Safe Chain CA, + // Mozilla roots (certifi), and Node built-in root CAs. + const combinedCaPath = getCombinedCaBundlePath(); + env.REQUESTS_CA_BUNDLE = combinedCaPath; + env.SSL_CERT_FILE = combinedCaPath; - let finalArgs = [...args]; - if (allKnown && !hasUserCert) { - finalArgs = [...args, "--cert", getCaCertPath()]; - } - - const result = await safeSpawn(command, finalArgs, { + const result = await safeSpawn(command, args, { stdio: "inherit", env, }); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 36abdf1..56863ef 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -1,7 +1,7 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; -describe("runPipCommand --cert handling", () => { +describe("runPipCommand environment variable handling", () => { let runPip; let capturedArgs = null; @@ -28,10 +28,10 @@ describe("runPipCommand --cert handling", () => { }, }); - // Mock certUtils to point to a test CA path - mock.module("../../registryProxy/certUtils.js", { + // Mock pipCaBundle to return a test combined bundle path + mock.module("./utils/pipCaBundle.js", { namedExports: { - getCaCertPath: () => "/tmp/test-ca.pem", + getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem", }, }); @@ -43,66 +43,29 @@ describe("runPipCommand --cert handling", () => { mock.reset(); }); - it("should append --cert with our CA path to pip args by default (PyPI)", async () => { + it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - // safeSpawn should be called with --cert flag assert.ok(capturedArgs, "safeSpawn should have been called"); - const idx = capturedArgs.args.indexOf("--cert"); - assert.ok(idx >= 0, "--cert flag should be present in pip args"); + // Check environment variables are set + assert.strictEqual( + capturedArgs.options.env.REQUESTS_CA_BUNDLE, + "/tmp/test-combined-ca.pem", + "REQUESTS_CA_BUNDLE should be set to combined bundle path" + ); + assert.strictEqual( + capturedArgs.options.env.SSL_CERT_FILE, + "/tmp/test-combined-ca.pem", + "SSL_CERT_FILE should be set to combined bundle path" + ); - const certPath = capturedArgs.args[idx + 1]; - assert.strictEqual(certPath, "/tmp/test-ca.pem", "CA path should match getCaCertPath()"); - - // Original args should be preserved before --cert - assert.strictEqual(capturedArgs.args[0], "install"); - assert.strictEqual(capturedArgs.args[1], "requests"); - - // No Python CA env overrides expected - assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, undefined); - assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, undefined); + // Args should be unchanged (no arg injection) + assert.deepStrictEqual(capturedArgs.args, ["install", "requests"]); }); - it("should not override user-provided --cert ", async () => { - const res = await runPip("pip3", ["install", "requests", "--cert", "/tmp/user-ca.pem"]); - assert.strictEqual(res.status, 0); - - // Ensure only the user-provided --cert is present - const certIndices = capturedArgs.args - .map((a, i) => (a === "--cert" ? i : -1)) - .filter((i) => i >= 0); - assert.strictEqual(certIndices.length, 1, "should not inject an extra --cert"); - const userPath = capturedArgs.args[certIndices[0] + 1]; - assert.strictEqual(userPath, "/tmp/user-ca.pem", "should preserve user-provided cert path"); - // No Python CA env overrides expected - assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, undefined); - assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, undefined); - }); - - it("should not override user-provided --cert=", async () => { - const res = await runPip("pip3", ["install", "requests", "--cert=/tmp/user-ca.pem"]); - assert.strictEqual(res.status, 0); - - // Ensure args contain the inline --cert= and no extra --cert token - const hasInline = capturedArgs.args.some((a) => typeof a === "string" && a.startsWith("--cert=")); - assert.ok(hasInline, "should keep inline --cert="); - const injectedIndex = capturedArgs.args.indexOf("--cert"); - assert.strictEqual(injectedIndex, -1, "should not inject separate --cert when inline is provided"); - // No Python CA env overrides expected - assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, undefined); - assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, undefined); - }); - - it("should inject --cert when explicit index is a known PyPI host", async () => { - const res = await runPip("pip3", ["install", "requests", "--index-url", "https://pypi.org/simple"]); - assert.strictEqual(res.status, 0); - const idx = capturedArgs.args.indexOf("--cert"); - assert.ok(idx >= 0, "--cert should be present for known registries"); - }); - - it("should NOT inject --cert when index points to an unknown external mirror (tunneled)", async () => { + it("should set CA environment variables even for external/test PyPI mirror (covers non-CLI traffic)", async () => { const res = await runPip("pip3", [ "install", "certifi", @@ -110,17 +73,41 @@ describe("runPipCommand --cert handling", () => { "https://test.pypi.org/simple", ]); assert.strictEqual(res.status, 0); - const idx = capturedArgs.args.indexOf("--cert"); - assert.strictEqual(idx, -1, "--cert should be omitted for tunneled external hosts"); + // Env vars should be set unconditionally + assert.strictEqual( + capturedArgs.options.env.REQUESTS_CA_BUNDLE, + "/tmp/test-combined-ca.pem" + ); + assert.strictEqual( + capturedArgs.options.env.SSL_CERT_FILE, + "/tmp/test-combined-ca.pem" + ); }); - it("should NOT inject --cert when installing from a direct external URL", async () => { - const res = await runPip("pip3", [ - "install", - "https://example.com/pkg-1.0.0-py3-none-any.whl", - ]); + it("should still set CA env vars for PyPI even with user --cert flag", async () => { + // For default PyPI, we still set env vars; pip CLI --cert takes precedence + const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - const idx = capturedArgs.args.indexOf("--cert"); - assert.strictEqual(idx, -1, "--cert should be omitted for direct external URLs"); + + // Environment variables still set (pip CLI --cert takes precedence) + assert.strictEqual( + capturedArgs.options.env.REQUESTS_CA_BUNDLE, + "/tmp/test-combined-ca.pem" + ); + assert.strictEqual( + capturedArgs.options.env.SSL_CERT_FILE, + "/tmp/test-combined-ca.pem" + ); + }); + + it("should preserve HTTPS_PROXY from proxy merge", async () => { + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + + assert.strictEqual( + capturedArgs.options.env.HTTPS_PROXY, + "http://localhost:8080", + "HTTPS_PROXY should be set by proxy merge" + ); }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js new file mode 100644 index 0000000..984a8ea --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js @@ -0,0 +1,90 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import certifi from "certifi"; +import tls from "node:tls"; +import { X509Certificate } from "node:crypto"; +import { getCaCertPath } from "../../../registryProxy/certUtils.js"; + +/** + * Check if a PEM string contains only parsable cert blocks. + */ +function isParsable(pem) { + if (!pem || typeof pem !== "string") return false; + const begin = "-----BEGIN CERTIFICATE-----"; + const end = "-----END CERTIFICATE-----"; + const blocks = []; + + let idx = 0; + while (idx < pem.length) { + const start = pem.indexOf(begin, idx); + if (start === -1) break; + const stop = pem.indexOf(end, start + begin.length); + if (stop === -1) break; + const blockEnd = stop + end.length; + blocks.push(pem.slice(start, blockEnd)); + idx = blockEnd; + } + + if (blocks.length === 0) return false; + try { + for (const b of blocks) { + // throw if invalid + new X509Certificate(b); + } + return true; + } catch { + return false; + } +} + +let cachedPath = null; + +/** + * Build a combined CA bundle specifically for pip flows. + * - Includes Safe Chain CA (for MITM of known registries) + * - Includes Mozilla roots via npm `certifi` (public HTTPS) + * - Includes Node's built-in root certificates as a portable fallback + * */ +export function getCombinedCaBundlePath() { + if (cachedPath && fs.existsSync(cachedPath)) return cachedPath; + + // Concatenate PEM files + const parts = []; + + // 1) Safe Chain CA (for MITM'd registries) + const safeChainPath = getCaCertPath(); + try { + const safeChainPem = fs.readFileSync(safeChainPath, "utf8"); + if (isParsable(safeChainPem)) parts.push(safeChainPem.trim()); + } catch { + // Ignore if Safe Chain CA is not available + } + + // 2) certifi (Mozilla CA bundle for all public HTTPS) + try { + const certifiPem = fs.readFileSync(certifi, "utf8"); + if (isParsable(certifiPem)) parts.push(certifiPem.trim()); + } catch { + // Ignore if certifi bundle is not available + } + + // 3) Node's built-in root certificates + try { + const nodeRoots = tls.rootCertificates; + if (Array.isArray(nodeRoots) && nodeRoots.length) { + for (const rootPem of nodeRoots) { + if (typeof rootPem !== "string") continue; + if (isParsable(rootPem)) parts.push(rootPem.trim()); + } + } + } catch { + // Ignore if unavailable + } + + const combined = parts.filter(Boolean).join("\n"); + const target = path.join(os.tmpdir(), "safe-chain-python-ca-bundle.pem"); + fs.writeFileSync(target, combined, { encoding: "utf8" }); + cachedPath = target; + return cachedPath; +} diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js new file mode 100644 index 0000000..a88eeec --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js @@ -0,0 +1,71 @@ +import { describe, it, beforeEach, mock } from "node:test"; +import assert from "node:assert"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import tls from "node:tls"; + +// Utility to remove the generated bundle so the module rebuilds it on demand +function removeBundleIfExists() { + const target = path.join(os.tmpdir(), "safe-chain-python-ca-bundle.pem"); + try { + if (fs.existsSync(target)) fs.unlinkSync(target); + } catch { + // ignore + } +} + +describe("pipCaBundle.getCombinedCaBundlePath", () => { + beforeEach(() => { + mock.restoreAll(); + removeBundleIfExists(); + }); + + it("includes Safe Chain CA when parsable and produces a PEM bundle", async () => { + // Prepare a temporary Safe Chain CA file with a recognizable marker and a valid cert block + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-")); + const safeChainPath = path.join(tmpDir, "safechain-ca.pem"); + const marker = "# SAFE_CHAIN_TEST_MARKER"; + const rootPem = typeof tls.rootCertificates?.[0] === "string" ? tls.rootCertificates[0] : ""; + assert.ok(rootPem.includes("BEGIN CERTIFICATE"), "Environment lacks Node root certificates for test"); + fs.writeFileSync(safeChainPath, `${marker}\n${rootPem}`, "utf8"); + + // Mock the certUtils.getCaCertPath to return our temp file + mock.module("../../../registryProxy/certUtils.js", { + namedExports: { + getCaCertPath: () => safeChainPath, + }, + }); + + const { getCombinedCaBundlePath } = await import("./pipCaBundle.js"); + const bundlePath = getCombinedCaBundlePath(); + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + assert.match(contents, /-----BEGIN CERTIFICATE-----/); + assert.ok(contents.includes(marker), "Bundle should include Safe Chain CA content when parsable"); + }); + + it("ignores invalid Safe Chain CA but still builds from other sources", async () => { + // Write an invalid file (no cert blocks) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pipcabundle-")); + const safeChainPath = path.join(tmpDir, "safechain-invalid.pem"); + const invalidMarker = "INVALID_SAFE_CHAIN_CONTENT"; + fs.writeFileSync(safeChainPath, invalidMarker, "utf8"); + + // Mock the certUtils.getCaCertPath to return our invalid file + mock.module("../../../registryProxy/certUtils.js", { + namedExports: { + getCaCertPath: () => safeChainPath, + }, + }); + + // Ensure fresh build + removeBundleIfExists(); + const { getCombinedCaBundlePath } = await import("./pipCaBundle.js"); + const bundlePath = getCombinedCaBundlePath(); + assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); + const contents = fs.readFileSync(bundlePath, "utf8"); + assert.match(contents, /-----BEGIN CERTIFICATE-----/, "Bundle should contain certificate blocks from certifi/Node roots"); + assert.ok(!contents.includes(invalidMarker), "Bundle should not include invalid Safe Chain content"); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js index 72da0c6..df4332e 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -145,7 +145,7 @@ describe("registryProxy.mitm", () => { }); // --- Pip registry MITM and env var tests --- - it("should NOT set global Python CA environment variables", () => { + it("should NOT set Python CA environment variables in proxy merge (handled by runPipCommand)", () => { const envVars = mergeSafeChainProxyEnvironmentVariables([]); assert.strictEqual(envVars.PIP_CERT, undefined); assert.strictEqual(envVars.REQUESTS_CA_BUNDLE, undefined); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 153eca5..bdc5a14 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -193,7 +193,7 @@ describe("E2E: pip coverage", () => { ); }); - it(`pip3 can install from GitHub URL using system CAs`, async () => { + it(`pip3 can install from GitHub URL using the CA bundle`, async () => { const shell = await container.openShell("zsh"); // Install a simple package from GitHub - this should use TCP tunnel, not MITM // Using a popular, small package for testing @@ -204,10 +204,10 @@ describe("E2E: pip coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); - // Verify installation succeeded (would fail if certificate validation broke) + // Verify installation succeeded (would fail if certificate validation via env CA bundle broke) assert.ok( result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), - `Installation from GitHub failed - system CAs may not be working. Output was:\n${result.output}` + `Installation from GitHub failed - CA bundle may not be working. Output was:\n${result.output}` ); // Verify package was actually installed @@ -230,7 +230,7 @@ describe("E2E: pip coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); - // Verify successful installation (would fail with SSL/certificate errors if --cert wasn't working) + // Verify successful installation (would fail with SSL/certificate errors if the env CA bundle wasn't working) assert.ok( result.output.includes("Successfully installed"), `Installation should succeed with proper certificate validation. Output was:\n${result.output}` @@ -246,7 +246,7 @@ describe("E2E: pip coverage", () => { it(`pip3 handles external HTTPS correctly (e.g., downloading from CDN)`, async () => { const shell = await container.openShell("zsh"); // Test installing from a direct HTTPS URL (not a registry) - // This validates that non-registry HTTPS traffic works with system CAs + // This validates that non-registry HTTPS traffic works with our env-provided CA bundle const result = await shell.runCommand('pip3 install --break-system-packages https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl'); assert.ok( @@ -265,7 +265,7 @@ describe("E2E: pip coverage", () => { it(`pip3 can install from alternate PyPI mirror (tunneled, not MITM)`, async () => { const shell = await container.openShell("zsh"); // Use Tsinghua PyPI mirror which is NOT in knownPipRegistries - // This tests tunneled HTTPS with --cert containing only Safe Chain CA + // This tests tunneled HTTPS with our env-provided CA bundle (Safe Chain CA + Mozilla + Node roots) // If the CA bundle doesn't include public roots, this will fail with CERTIFICATE_VERIFY_FAILED const result = await shell.runCommand('pip3 install --break-system-packages --index-url https://pypi.tuna.tsinghua.edu.cn/simple certifi'); @@ -277,7 +277,7 @@ describe("E2E: pip coverage", () => { // Should succeed if CA bundle properly handles tunneled hosts assert.ok( result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), - `Installation from PyPI mirror failed. This may indicate --cert CA bundle lacks public roots. Output was:\n${result.output}` + `Installation from PyPI mirror failed. This may indicate the CA bundle lacks public roots. Output was:\n${result.output}` ); // Should NOT contain certificate verification errors From c9e7bd2ab473d2a53cf80f91698e681d9eb94e89 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 30 Oct 2025 20:15:58 -0700 Subject: [PATCH 37/52] Adapt e2e test to use test.pypi --- test/e2e/pip.e2e.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index bdc5a14..c647d30 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -264,10 +264,10 @@ describe("E2E: pip coverage", () => { it(`pip3 can install from alternate PyPI mirror (tunneled, not MITM)`, async () => { const shell = await container.openShell("zsh"); - // Use Tsinghua PyPI mirror which is NOT in knownPipRegistries + // Use Test PyPI which is NOT in knownPipRegistries // This tests tunneled HTTPS with our env-provided CA bundle (Safe Chain CA + Mozilla + Node roots) // If the CA bundle doesn't include public roots, this will fail with CERTIFICATE_VERIFY_FAILED - const result = await shell.runCommand('pip3 install --break-system-packages --index-url https://pypi.tuna.tsinghua.edu.cn/simple certifi'); + const result = await shell.runCommand('pip3 install --break-system-packages --index-url https://test.pypi.org/simple certifi'); assert.ok( result.output.includes("no malicious packages found."), @@ -277,7 +277,7 @@ describe("E2E: pip coverage", () => { // Should succeed if CA bundle properly handles tunneled hosts assert.ok( result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), - `Installation from PyPI mirror failed. This may indicate the CA bundle lacks public roots. Output was:\n${result.output}` + `Installation from Test PyPI failed. This may indicate the CA bundle lacks public roots. Output was:\n${result.output}` ); // Should NOT contain certificate verification errors From d691c614ac287983beab6a3611e47f8b4cb09968 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 30 Oct 2025 20:19:16 -0700 Subject: [PATCH 38/52] Cleanup --- .../safe-chain/src/packagemanager/pip/runPipCommand.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 4e12282..552749a 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,16 +2,14 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "./utils/pipCaBundle.js"; -// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) -// so that any network request made by pip, including those outside explicit CLI args, -// validates correctly under both MITM'd and tunneled HTTPS. export async function runPip(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - // Always set Python CA env vars to a combined bundle that includes Safe Chain CA, - // Mozilla roots (certifi), and Node built-in root CAs. + // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) + // so that any network request made by pip, including those outside explicit CLI args, + // validates correctly under both MITM'd and tunneled HTTPS. const combinedCaPath = getCombinedCaBundlePath(); env.REQUESTS_CA_BUNDLE = combinedCaPath; env.SSL_CERT_FILE = combinedCaPath; From c2a9cc27337ac1ad46e9e0ac28f2114edb7ec0d4 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 31 Oct 2025 07:51:26 -0700 Subject: [PATCH 39/52] Move pipCaBundle to central location --- .../src/packagemanager/pip/runPipCommand.js | 2 +- .../src/packagemanager/pip/runPipCommand.spec.js | 4 ++-- .../pipCaBundle.js => registryProxy/certBundle.js} | 6 +++--- .../certBundle.spec.js} | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) rename packages/safe-chain/src/{packagemanager/pip/utils/pipCaBundle.js => registryProxy/certBundle.js} (94%) rename packages/safe-chain/src/{packagemanager/pip/utils/pipCaBundle.spec.js => registryProxy/certBundle.spec.js} (87%) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 552749a..18c8f99 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -1,7 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; -import { getCombinedCaBundlePath } from "./utils/pipCaBundle.js"; +import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; export async function runPip(command, args) { try { diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 56863ef..d7a0f93 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -28,8 +28,8 @@ describe("runPipCommand environment variable handling", () => { }, }); - // Mock pipCaBundle to return a test combined bundle path - mock.module("./utils/pipCaBundle.js", { + // Mock certBundle to return a test combined bundle path + mock.module("../../registryProxy/certBundle.js", { namedExports: { getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem", }, diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js similarity index 94% rename from packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js rename to packages/safe-chain/src/registryProxy/certBundle.js index 984a8ea..5b38250 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -4,7 +4,7 @@ import path from "node:path"; import certifi from "certifi"; import tls from "node:tls"; import { X509Certificate } from "node:crypto"; -import { getCaCertPath } from "../../../registryProxy/certUtils.js"; +import { getCaCertPath } from "./certUtils.js"; /** * Check if a PEM string contains only parsable cert blocks. @@ -41,11 +41,11 @@ function isParsable(pem) { let cachedPath = null; /** - * Build a combined CA bundle specifically for pip flows. + * Build a combined CA bundle for Python and Node HTTPS flows. * - Includes Safe Chain CA (for MITM of known registries) * - Includes Mozilla roots via npm `certifi` (public HTTPS) * - Includes Node's built-in root certificates as a portable fallback - * */ + */ export function getCombinedCaBundlePath() { if (cachedPath && fs.existsSync(cachedPath)) return cachedPath; diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js b/packages/safe-chain/src/registryProxy/certBundle.spec.js similarity index 87% rename from packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js rename to packages/safe-chain/src/registryProxy/certBundle.spec.js index a88eeec..2f26d51 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCaBundle.spec.js +++ b/packages/safe-chain/src/registryProxy/certBundle.spec.js @@ -7,7 +7,7 @@ import tls from "node:tls"; // Utility to remove the generated bundle so the module rebuilds it on demand function removeBundleIfExists() { - const target = path.join(os.tmpdir(), "safe-chain-python-ca-bundle.pem"); + const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem"); try { if (fs.existsSync(target)) fs.unlinkSync(target); } catch { @@ -15,7 +15,7 @@ function removeBundleIfExists() { } } -describe("pipCaBundle.getCombinedCaBundlePath", () => { +describe("certBundle.getCombinedCaBundlePath", () => { beforeEach(() => { mock.restoreAll(); removeBundleIfExists(); @@ -31,13 +31,13 @@ describe("pipCaBundle.getCombinedCaBundlePath", () => { fs.writeFileSync(safeChainPath, `${marker}\n${rootPem}`, "utf8"); // Mock the certUtils.getCaCertPath to return our temp file - mock.module("../../../registryProxy/certUtils.js", { + mock.module("./certUtils.js", { namedExports: { getCaCertPath: () => safeChainPath, }, }); - const { getCombinedCaBundlePath } = await import("./pipCaBundle.js"); + const { getCombinedCaBundlePath } = await import("./certBundle.js"); const bundlePath = getCombinedCaBundlePath(); assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); const contents = fs.readFileSync(bundlePath, "utf8"); @@ -53,7 +53,7 @@ describe("pipCaBundle.getCombinedCaBundlePath", () => { fs.writeFileSync(safeChainPath, invalidMarker, "utf8"); // Mock the certUtils.getCaCertPath to return our invalid file - mock.module("../../../registryProxy/certUtils.js", { + mock.module("./certUtils.js", { namedExports: { getCaCertPath: () => safeChainPath, }, @@ -61,7 +61,7 @@ describe("pipCaBundle.getCombinedCaBundlePath", () => { // Ensure fresh build removeBundleIfExists(); - const { getCombinedCaBundlePath } = await import("./pipCaBundle.js"); + const { getCombinedCaBundlePath } = await import("./certBundle.js"); const bundlePath = getCombinedCaBundlePath(); assert.ok(fs.existsSync(bundlePath), "Bundle path should exist"); const contents = fs.readFileSync(bundlePath, "utf8"); From be5c4fb3821adc1ab89d67897cf4d8df95dd2ad2 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 31 Oct 2025 08:07:06 -0700 Subject: [PATCH 40/52] Fix renaming --- packages/safe-chain/src/registryProxy/certBundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js index 5b38250..d3d7a91 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -83,7 +83,7 @@ export function getCombinedCaBundlePath() { } const combined = parts.filter(Boolean).join("\n"); - const target = path.join(os.tmpdir(), "safe-chain-python-ca-bundle.pem"); + const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem"); fs.writeFileSync(target, combined, { encoding: "utf8" }); cachedPath = target; return cachedPath; From 27ca2153b0eaa567a190562145e6f036bc499243 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 06:51:14 -0800 Subject: [PATCH 41/52] Fix warnings --- .../src/packagemanager/pip/runPipCommand.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 18c8f99..99ee97c 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -3,9 +3,13 @@ import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +/** + * @param {string} command + * @param {string[]} args + */ export async function runPip(command, args) { try { - const env = mergeSafeChainProxyEnvironmentVariables(process.env); + const env = mergeSafeChainProxyEnvironmentVariables(/** @type {Record} */ (process.env)); // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) // so that any network request made by pip, including those outside explicit CLI args, @@ -20,10 +24,11 @@ export async function runPip(command, args) { }); return { status: result.status }; } catch (error) { - if (error.status) { - return { status: error.status }; + if (error && typeof error === "object" && "status" in error) { + return { status: /** @type {any} */ (error).status }; } else { - ui.writeError("Error executing command:", error.message); + const message = error && typeof error === "object" && "message" in error ? /** @type {any} */ (error).message : String(error); + ui.writeError("Error executing command:", message); return { status: 1 }; } } From 3d98bb5084dc001f7bcdd5f66b7a3f54988df2fe Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 07:07:41 -0800 Subject: [PATCH 42/52] Fix package-lock.json --- package-lock.json | 707 +++++++++++++++++++++++++--------------------- 1 file changed, 378 insertions(+), 329 deletions(-) diff --git a/package-lock.json b/package-lock.json index f44df75..c6876ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,6 @@ { "name": "aikido-safe-chain-workspace", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { @@ -43,6 +44,56 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -93,9 +144,9 @@ } }, "node_modules/@oven/bun-darwin-aarch64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.1.tgz", - "integrity": "sha512-7Rap1BHNWqgnexc4wLjjdZeVRQKtk534iGuJ7qZ42i/q1B+cxJZ6zSnrFsYmo+zreH7dUyUXL3AHuXGrl2772Q==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.2.21.tgz", + "integrity": "sha512-SihfZ3czKeWz6Z3m5rUDrMlarwOXjnkUg+7tIiSB9VZCFSvWEItMfdAF170eCXxZmEh7A1dw20a3lW37lkmlrA==", "cpu": [ "arm64" ], @@ -103,12 +154,13 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-darwin-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.1.tgz", - "integrity": "sha512-wpqmgT/8w+tEr5YMGt1u1sEAMRHhyA2SKZddC6GCPasHxSqkCWOPQvYIHIApnTsoSsxhxP0x6Cpe93+4c7hq/w==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.2.21.tgz", + "integrity": "sha512-iXr4y2ap6EmME7/EDoLMxSRKAh9yswKfrHDb9sF+ExHbk1C+XsNGxMY73ckQe2w0SIH6NXz2cRMTORbZ8LNjig==", "cpu": [ "x64" ], @@ -116,12 +168,13 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-darwin-x64-baseline": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.1.tgz", - "integrity": "sha512-mJo715WvwEHmJ6khNymWyxi0QrFzU94wolsUmxolViNHrk+2ugzIkVIJhTnxf7pHnarxxHwyJ/kgatuV//QILQ==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.2.21.tgz", + "integrity": "sha512-3KeslC5z3vpXxluYBqh6EDwojxTSyWJQeYPJFf7y/Z5QJuAN7g33l8jrx072X8P/G8CBzU1lJky14vhhnqWd7A==", "cpu": [ "x64" ], @@ -129,12 +182,13 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-aarch64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.1.tgz", - "integrity": "sha512-ACn038SZL8del+sFnqCjf+haGB02//j2Ez491IMmPTvbv4a/D0iiNz9xiIB3ICbQd3EwQzi+Ut/om3Ba/KoHbQ==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.2.21.tgz", + "integrity": "sha512-jpUFKGUpim4h4KOqI1VYYgvifZVrWNQZFrmVPfSqGb0ZzF/p5L2qc9Hy2aUL3Lo+zHMPylwbe0iLKElPYk0xoQ==", "cpu": [ "arm64" ], @@ -142,25 +196,27 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-aarch64-musl": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.1.tgz", - "integrity": "sha512-gKU3Wv3BTG5VMjqMMnRwqU6tipCveE9oyYNt62efy6cQK3Vo1DOBwY2SmjbFw+yzj+Um20YoFOLGxghfQET4Ng==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.2.21.tgz", + "integrity": "sha512-7UoUHKACYDin3iR6kdqUrF1AOCCjTHPTv1xmzlX4rzwNQvFYSAR83AMrY7hkatKGzLYkI8EjXDAvFJpwF+ZxoA==", "cpu": [ - "arm64" + "aarch64" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.1.tgz", - "integrity": "sha512-cAUeM3I5CIYlu5Ur52eCOGg9yfqibQd4lzt9G1/rA0ajqcnCBaTuekhUDZETJJf5H9QV+Gm46CqQg2DpdJzJsw==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.2.21.tgz", + "integrity": "sha512-6RuXFaVU2ve0TVw1vfFo7ix/jh9IX7mMAEhwE2odX8EdX/ea55upiivYQ/EKeXt+Ij3STc2bCeV4vvRoEJAHdg==", "cpu": [ "x64" ], @@ -168,12 +224,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-baseline": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.1.tgz", - "integrity": "sha512-7+2aCrL81mtltZQbKdiPB58UL+Gr3DAIuPyUAKm0Ib/KG/Z8t7nD/eSMRY/q6b+NsAjYnVPiPwqSjC3edpMmmQ==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.2.21.tgz", + "integrity": "sha512-oZ5FUMfeghwbQcL9oxajsKjwVI+1GnVvxcJ3z+pifuXaLMZr25NCr5h0q2j+ZxEFL3RtL/Pyj8/HLfzGEIVAVg==", "cpu": [ "x64" ], @@ -181,12 +238,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-musl": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.1.tgz", - "integrity": "sha512-8AgEAHyuJ5Jm9MUo1L53K1SRYu0bNGqV0E0L5rB5DjkteO4GXrnWGBT8qsuwuy7WMuCMY3bj64/pFjlRkZuiXw==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.2.21.tgz", + "integrity": "sha512-ioZjU+2yyLJXaDA8FKoy+tj/fuZKovG9EMp+n9+EG7g3MULbe5nU8gdsS/dET28WzuPlDlSkqF8EUocvg4HajQ==", "cpu": [ "x64" ], @@ -194,12 +252,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-linux-x64-musl-baseline": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.1.tgz", - "integrity": "sha512-tP0WWcAqrMayvkggOHBGBoyyoK+QHAqgRUyj1F6x5/udiqc9vCXmIt1tlydxYV/NvyvUAmJ7MWT0af44Xm2kJw==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.2.21.tgz", + "integrity": "sha512-0NzMg4XdXgujDM2jZogiV6MgACXW0a0NfB+o6fxwmUzdmMBUk1ZMRzypUi4XKjGUe89mYcPJcVFQRRnNwzTK/Q==", "cpu": [ "x64" ], @@ -207,12 +266,13 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@oven/bun-windows-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.1.tgz", - "integrity": "sha512-xdUjOZRq6PwPbbz4/F2QEMLBZwintGp7AS50cWxgkHnyp7Omz5eJfV6/vWtN4qwZIyR3V3DT/2oXsY1+7p3rtg==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.2.21.tgz", + "integrity": "sha512-DZVCXrZGN/B4JnVnieZin1Kxse1wOkf+Fm2hDGpZHzs27ECbw5xPMFIc0r/oCpxTc/InxuvYO9UGoOmvhFaHsQ==", "cpu": [ "x64" ], @@ -220,12 +280,13 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@oven/bun-windows-x64-baseline": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.1.tgz", - "integrity": "sha512-dcA+Kj7hGFrY3G8NWyYf3Lj3/GMViknpttWUf5pI6p6RphltZaoDu0lY5Lr71PkMdRZTwL2NnZopa/x/NWCdKA==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.2.21.tgz", + "integrity": "sha512-sTnkLdThgsa6X8ib6eb3+zgy+CGJOibK6Th4wV2wmZFi5af6TM+digEi9i+q/X3nabGwPXm0V4vBiVpvcFilsA==", "cpu": [ "x64" ], @@ -233,115 +294,108 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@oxlint/darwin-arm64": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.25.0.tgz", - "integrity": "sha512-OLx4XyUv5SO7k8y5FzJIoTKan+iKK53T1Ws8fBIl4zblUIWI66ZIqSVG2A2rxOBA7XfINqCz8UipGzOW9yzKcg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.22.0.tgz", + "integrity": "sha512-vfgwTA1CowVaU3QXFBjfGjbPsHbdjAiJnWX5FBaq8uXS8tksGgl0ue14MK6fVnXncWK9j69LRnkteGTixxDAfA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@oxlint/darwin-x64": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.25.0.tgz", - "integrity": "sha512-srndNPiliA0rchYKqYfOdqA9kqyVQ6YChK3XJe9Lxo/YG8tTJ5K65g2A5SHTT2s1Nm5DnQa5AKZH7w+7KI/m8A==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.22.0.tgz", + "integrity": "sha512-70x7Y+e0Ddb2Cf2IZsYGnXZrnB/MZgOTi/VkyXZucbnQcpi2VoaYS4Ve662DaNkzvTxdKOGmyJVMmD/digdJLQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@oxlint/linux-arm64-gnu": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.25.0.tgz", - "integrity": "sha512-W9+DnHDbygprpGV586BolwWES+o2raOcSJv404nOFPQjWZ09efG24nuXrg/fpyoMQb4YoW2W1fvlnyMVU+ADcw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.22.0.tgz", + "integrity": "sha512-Rv94lOyEV8WEuzhjJSpCW3DbL/tlOVizPxth1v5XAFuQdM5rgpOMs3TsAf/YFUn52/qenwVglyvQZL8oAUYlpg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oxlint/linux-arm64-musl": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.25.0.tgz", - "integrity": "sha512-1tIMpQhKlItm7uKzs3lluG7KorZR5ItoNKd1iFYF/IPmZ+i0/iuZ7MVWXRjBcgQMhMYSdfZpSVEdFKcFz2HDxA==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.22.0.tgz", + "integrity": "sha512-Aau6V6Osoyb3SFmRejP3rRhs1qhep4aJTdotFf1RVMVSLJkF7Ir0p+eGZSaIJyylFZuCCxHpud3hWasphmZnzw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oxlint/linux-x64-gnu": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.25.0.tgz", - "integrity": "sha512-xVkmk/zkIulc5o0OUWY04DyBfKotnq9+60O9I5c0DpdKAELVLhZkLmct0apx3jAX6Z/3yYPzhc6Lw1Ia3jU3VQ==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.22.0.tgz", + "integrity": "sha512-6eOtv+2gHrKw/hxUkV6hJdvYhzr0Dqzb4oc7sNlWxp64jU6I19tgMwSlmtn02r34YNSn+/NpZ/ECvQrycKUUFQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oxlint/linux-x64-musl": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.25.0.tgz", - "integrity": "sha512-IeO10dZosJV58YzN0gckhRYac+FM9s5VCKUx2ghgbKR91z/bpSRcRl8Sy5cWTkcVwu3ZTikhK8aXC6j7XIqKNw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.22.0.tgz", + "integrity": "sha512-c4O7qD7TCEfPE/FFKYvakF2sQoIP0LFZB8F5AQK4K9VYlyT1oENNRCdIiMu6irvLelOzJzkUM0XrvUCL9Kkxrw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oxlint/win32-arm64": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.25.0.tgz", - "integrity": "sha512-mpdiXZm2oNuSQAbTEPRDuSeR6v1DCD7Cl/xouR2ggHZu3AKZ4XYmm29hyrzIxrYVoQ/5j+182TGdOpGYn9xQJg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.22.0.tgz", + "integrity": "sha512-6DJwF5A9VoIbSWNexLYubbuteAL23l3YN00wUL7Wt4ZfEZu2f/lWtGB9yC9BfKLXzudq8MvGkrS0szmV0bc1VQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@oxlint/win32-x64": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.25.0.tgz", - "integrity": "sha512-opoIACOkcFloWQO6dubBLbcWwW52ML8+3deFdr0WE0PeM9UXdLB0jRMuLsEnplmBoy9TRvmxDJ+Pw8xc2PsOfQ==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.22.0.tgz", + "integrity": "sha512-nf8EZnIUgIrHlP9k26iOFMZZPoJG16KqZBXu5CG5YTAtVcu4CWlee9Q/cOS/rgQNGjLF+WPw8sVA5P3iGlYGQQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -362,7 +416,6 @@ "resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.4.tgz", "integrity": "sha512-jKzweQaEMMAi55ehvR1z0JF6aSVQm/h1BXBhPLOJriaeQBctjw5YbpIGs7zAx9dN0Sa2OO5bcXwCkrlgenoPEA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node-fetch": "*", "@types/retry": "*", @@ -370,13 +423,12 @@ } }, "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, "node_modules/@types/node-fetch": { @@ -384,7 +436,6 @@ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -395,7 +446,6 @@ "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -404,15 +454,13 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.4.tgz", "integrity": "sha512-vDgdbMy2QXHnAruzlv68pUtXCjmqUk3WrBAsRboRovsOmxbfn/WiYCjmecyKjGztnMps5dWp4Uq2prp+Ilo17Q==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/npm-registry-fetch": { "version": "8.0.9", "resolved": "https://registry.npmjs.org/@types/npm-registry-fetch/-/npm-registry-fetch-8.0.9.tgz", "integrity": "sha512-7NxvodR5Yrop3pb6+n8jhJNyzwOX0+6F+iagNEoi9u1CGxruYAwZD8pvGc9prIkL0+FdX5Xp0p80J9QPrGUp/g==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "@types/node-fetch": "*", @@ -426,7 +474,6 @@ "resolved": "https://registry.npmjs.org/@types/npmlog/-/npmlog-7.0.0.tgz", "integrity": "sha512-hJWbrKFvxKyWwSUXjZMYTINsSOY6IclhvGOZ97M8ac2tmR9hMwmTnYaMdpGhvju9ctWLTPhCS+eLfQNluiEjQQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -435,54 +482,51 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/ssri": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.5.tgz", "integrity": "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -492,8 +536,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/balanced-match": { "version": "1.0.2", @@ -505,18 +548,18 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/bun": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bun/-/bun-1.3.1.tgz", - "integrity": "sha512-enqkEb0RhNOgDzHQwv7uvnIhX3uSzmKzz779dL7kdH8SauyTdQvCz4O1UT2rU0UldQp2K9OlrJNdyDHayPEIvw==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", + "integrity": "sha512-y0lJ02dS90U3PJm+7KAKY8Se95AQvP5Xm77LouUwrpNOHpv59kBG4SK1+9iE1cAhpUaFipq+0EJ56S6MmE3row==", "cpu": [ "arm64", - "x64" + "x64", + "aarch64" ], "hasInstallScript": true, "license": "MIT", @@ -531,17 +574,17 @@ "bunx": "bin/bunx.exe" }, "optionalDependencies": { - "@oven/bun-darwin-aarch64": "1.3.1", - "@oven/bun-darwin-x64": "1.3.1", - "@oven/bun-darwin-x64-baseline": "1.3.1", - "@oven/bun-linux-aarch64": "1.3.1", - "@oven/bun-linux-aarch64-musl": "1.3.1", - "@oven/bun-linux-x64": "1.3.1", - "@oven/bun-linux-x64-baseline": "1.3.1", - "@oven/bun-linux-x64-musl": "1.3.1", - "@oven/bun-linux-x64-musl-baseline": "1.3.1", - "@oven/bun-windows-x64": "1.3.1", - "@oven/bun-windows-x64-baseline": "1.3.1" + "@oven/bun-darwin-aarch64": "1.2.21", + "@oven/bun-darwin-x64": "1.2.21", + "@oven/bun-darwin-x64-baseline": "1.2.21", + "@oven/bun-linux-aarch64": "1.2.21", + "@oven/bun-linux-aarch64-musl": "1.2.21", + "@oven/bun-linux-x64": "1.2.21", + "@oven/bun-linux-x64-baseline": "1.2.21", + "@oven/bun-linux-x64-musl": "1.2.21", + "@oven/bun-linux-x64-musl-baseline": "1.2.21", + "@oven/bun-windows-x64": "1.2.21", + "@oven/bun-windows-x64-baseline": "1.2.21" } }, "node_modules/cacache": { @@ -572,7 +615,6 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -581,20 +623,10 @@ "node": ">= 0.4" } }, - "node_modules/certifi": { - "version": "14.5.15", - "resolved": "https://registry.npmjs.org/certifi/-/certifi-14.5.15.tgz", - "integrity": "sha512-NeLXuKCqSzwQNjpJ+WaSp5m8ntdTKJ8HnBu+eA7DxHfgzU7F1sjwrJFang+4U38+vmWbiFUpPZMV3uwwnHAisQ==", - "license": "MPL-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -661,7 +693,6 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -684,9 +715,9 @@ } }, "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -705,7 +736,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -715,7 +745,6 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -732,9 +761,9 @@ "license": "MIT" }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/encoding": { @@ -747,6 +776,19 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -758,7 +800,6 @@ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -768,7 +809,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -778,7 +818,6 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -791,7 +830,6 @@ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -823,7 +861,6 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, - "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -852,15 +889,14 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "license": "MIT", "engines": { "node": ">=18" @@ -874,7 +910,6 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -899,7 +934,6 @@ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -928,12 +962,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -946,7 +994,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -959,7 +1006,6 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -975,7 +1021,6 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1027,19 +1072,6 @@ "node": ">= 14" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1050,10 +1082,14 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, "engines": { "node": ">= 12" } @@ -1112,6 +1148,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -1182,7 +1224,6 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1192,7 +1233,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1202,7 +1242,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1222,21 +1261,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -1366,9 +1390,9 @@ "license": "ISC" }, "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -1377,6 +1401,21 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1451,21 +1490,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -1489,10 +1513,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ora/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, "node_modules/ora/node_modules/string-width": { @@ -1512,12 +1548,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/oxlint": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.25.0.tgz", - "integrity": "sha512-O6iJ9xeuy9eQCi8/EghvsNO6lzSaUPs0FR1uLy51Exp3RkVpjvJKyPPhd9qv65KLnfG/BNd2HE/rH0NbEfVVzA==", - "dev": true, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/oxlint": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.22.0.tgz", + "integrity": "sha512-/HYT1Cfanveim9QUM6KlPKJe9y+WPnh3SxIB7z1InWnag9S0nzxLaWEUiW1P4UGzh/No3KvtNmBv2IOiwAl2/w==", + "dev": true, "bin": { "oxc_language_server": "bin/oxc_language_server", "oxlint": "bin/oxlint" @@ -1529,17 +1579,17 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/darwin-arm64": "1.25.0", - "@oxlint/darwin-x64": "1.25.0", - "@oxlint/linux-arm64-gnu": "1.25.0", - "@oxlint/linux-arm64-musl": "1.25.0", - "@oxlint/linux-x64-gnu": "1.25.0", - "@oxlint/linux-x64-musl": "1.25.0", - "@oxlint/win32-arm64": "1.25.0", - "@oxlint/win32-x64": "1.25.0" + "@oxlint/darwin-arm64": "1.22.0", + "@oxlint/darwin-x64": "1.22.0", + "@oxlint/linux-arm64-gnu": "1.22.0", + "@oxlint/linux-arm64-musl": "1.22.0", + "@oxlint/linux-x64-gnu": "1.22.0", + "@oxlint/linux-x64-musl": "1.22.0", + "@oxlint/win32-arm64": "1.22.0", + "@oxlint/win32-x64": "1.22.0" }, "peerDependencies": { - "oxlint-tsgolint": ">=0.4.0" + "oxlint-tsgolint": ">=0.2.0" }, "peerDependenciesMeta": { "oxlint-tsgolint": { @@ -1628,6 +1678,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -1700,12 +1765,12 @@ } }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { @@ -1727,6 +1792,12 @@ "node": ">= 14" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", @@ -1752,20 +1823,17 @@ } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -1783,22 +1851,7 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -1810,21 +1863,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -1838,25 +1876,17 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", - "license": "BlueOak-1.0.0", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.1.0", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", "yallist": "^5.0.0" }, "engines": { @@ -1868,7 +1898,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1878,11 +1907,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true }, "node_modules/unique-filename": { "version": "4.0.0", @@ -1909,9 +1937,9 @@ } }, "node_modules/validate-npm-package-name": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", - "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" @@ -1967,60 +1995,66 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/yallist": { @@ -2078,6 +2112,21 @@ "bun": ">=1.2.21" } }, + "packages/safe-chain/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "packages/safe-chain/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "test/e2e": { "name": "@aikidosec/safe-chain-e2e-tests", "version": "1.0.0", From a2fb94d0f05eac9dfd9ae87ca38139842fd5741c Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 07:13:36 -0800 Subject: [PATCH 43/52] Fix type check issues --- packages/safe-chain/bin/aikido-pip.js | 2 +- packages/safe-chain/bin/aikido-pip3.js | 2 +- .../pip/createPackageManager.js | 15 ++++++++++++++- .../commandArgumentScanner.js | 19 +++++++++++++++++++ .../parsing/parsePackagesFromInstallArgs.js | 9 +++++++++ .../packagemanager/pip/utils/pipCommands.js | 8 +++++++- .../src/registryProxy/parsePackageFromUrl.js | 8 ++++++++ .../src/scanning/malwareDatabase.js | 1 + 8 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 8d483f3..f5c250e 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -16,4 +16,4 @@ setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); const exitCode = await main(argv); -process.exit(exitCode); +process.exit(typeof exitCode === 'number' ? exitCode : 1); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index 31da8bd..b056764 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -16,4 +16,4 @@ setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); const exitCode = await main(argv); -process.exit(exitCode); +process.exit(typeof exitCode === 'number' ? exitCode : 1); diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 3c0e974..dce00ca 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -7,7 +7,13 @@ import { pipWheelCommand, } from "./utils/pipCommands.js"; +/** + * @param {string} [command] + */ export function createPipPackageManager(command = "pip") { + /** + * @param {string[]} args + */ function isSupportedCommand(args) { const scanner = findDependencyScannerForCommand( commandScannerMapping, @@ -16,6 +22,9 @@ export function createPipPackageManager(command = "pip") { return scanner.shouldScan(args); } + /** + * @param {string[]} args + */ function getDependencyUpdatesForCommand(args) { const scanner = findDependencyScannerForCommand( commandScannerMapping, @@ -25,7 +34,7 @@ export function createPipPackageManager(command = "pip") { } return { - runCommand: (args) => runPip(command, args), + runCommand: /** @param {string[]} args */ (args) => runPip(command, args), isSupportedCommand, getDependencyUpdatesForCommand, }; @@ -43,6 +52,10 @@ const NULL_SCANNER = { scan: () => [], }; +/** + * @param {Record} scanners + * @param {string[]} args + */ function findDependencyScannerForCommand(scanners, args) { const command = getPipCommandForArgs(args); if (!command) { diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js index dbe92d6..5e7031e 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -1,13 +1,22 @@ import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js"; import { hasDryRunArg } from "../utils/pipCommands.js"; +/** + * @param {{ ignoreDryRun?: boolean }} [options] + */ export function commandArgumentScanner(options = {}) { const { ignoreDryRun = false } = options; + /** + * @param {string[]} args + */ function shouldScan(args) { return shouldScanDependencies(args, ignoreDryRun); } + /** + * @param {string[]} args + */ function scan(args) { return scanDependencies(args); } @@ -18,14 +27,24 @@ export function commandArgumentScanner(options = {}) { }; } +/** + * @param {string[]} args + * @param {boolean} ignoreDryRun + */ function shouldScanDependencies(args, ignoreDryRun) { return ignoreDryRun || !hasDryRunArg(args); } +/** + * @param {string[]} args + */ function scanDependencies(args) { return checkChangesFromArgs(args); } +/** + * @param {string[]} args + */ export function checkChangesFromArgs(args) { const packageUpdates = parsePackagesFromInstallArgs(args); diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index b0b2f6c..5ec0426 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -14,6 +14,9 @@ * - git+https://... (VCS URLs - returned without version) * - -r requirements.txt (handled by flag skipping) */ +/** + * @param {string[]} args + */ export function parsePackagesFromInstallArgs(args) { const packages = []; let skipNext = false; @@ -48,6 +51,9 @@ export function parsePackagesFromInstallArgs(args) { return packages; } +/** + * @param {string} arg + */ function isPipOptionWithParameter(arg) { // Check if a pip flag takes a parameter @@ -100,6 +106,9 @@ function isPipOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} spec + */ function parsePipSpec(spec) { // Ignore obvious URLs and paths, rely on mitm scanner const lower = spec.toLowerCase(); diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js index 5db1cc5..5b28b48 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -2,6 +2,9 @@ export const pipInstallCommand = "install"; export const pipDownloadCommand = "download"; export const pipWheelCommand = "wheel"; +/** + * @param {string[]} args + */ export function getPipCommandForArgs(args) { if (!args || args.length === 0) { return null; @@ -17,6 +20,9 @@ export function getPipCommandForArgs(args) { return null; } +/** + * @param {string[]} args + */ export function hasDryRunArg(args) { - return args.some((arg) => arg === "--dry-run"); + return args.some(/** @param {string} arg */ (arg) => arg === "--dry-run"); } diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 5250b33..64ce99a 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -32,6 +32,10 @@ export function parsePackageFromUrl(url) { return { packageName: undefined, version: undefined }; } +/** + * @param {string} url + * @param {string} registry + */ function parseJsPackageFromUrl(url, registry) { let packageName, version; if (!registry || !url.endsWith(".tgz")) { @@ -71,6 +75,10 @@ function parseJsPackageFromUrl(url, registry) { return { packageName, version }; } +/** + * @param {string} url + * @param {string} registry + */ function parsePipPackageFromUrl(url, registry) { let packageName, version diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index dcd1833..a2e2fb7 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -22,6 +22,7 @@ let cachedMalwareDatabase = null; * Normalize package name for comparison. * For Python packages (PEP-503): lowercase and replace _, -, . with - * For js packages: keep as-is (case-sensitive) + * @param {string} name */ function normalizePackageName(name) { const ecosystem = getEcoSystem(); From bffb1995bd41548cf1d8d251e8f41a67b87e52fb Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 07:19:08 -0800 Subject: [PATCH 44/52] Fix lock file --- package-lock.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index c6876ad..ee38fa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -623,6 +623,15 @@ "node": ">= 0.4" } }, + "node_modules/certifi": { + "version": "14.5.15", + "resolved": "https://registry.npmjs.org/certifi/-/certifi-14.5.15.tgz", + "integrity": "sha512-NeLXuKCqSzwQNjpJ+WaSp5m8ntdTKJ8HnBu+eA7DxHfgzU7F1sjwrJFang+4U38+vmWbiFUpPZMV3uwwnHAisQ==", + "license": "MPL-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", From 9a0b6f45bb8e0e5c840741acf2059d7a81521808 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 08:12:48 -0800 Subject: [PATCH 45/52] Use comment iso type checking --- packages/safe-chain/bin/aikido-pip.js | 5 +++-- packages/safe-chain/bin/aikido-pip3.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index f5c250e..8edef59 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -14,6 +14,7 @@ const argv = process.argv.slice(2); setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); -const exitCode = await main(argv); +var exitCode = await main(argv); -process.exit(typeof exitCode === 'number' ? exitCode : 1); +// @ts-expect-error scanCommand can return an empty array in main +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index b056764..0c20df0 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -14,6 +14,7 @@ const argv = process.argv.slice(2); setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); -const exitCode = await main(argv); +var exitCode = await main(argv); -process.exit(typeof exitCode === 'number' ? exitCode : 1); +// @ts-expect-error scanCommand can return an empty array in main +process.exit(exitCode); From e65b857667abc90995d9955fbf507a183faf9f30 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 09:47:16 -0800 Subject: [PATCH 46/52] Adapt comments to align with other package managers --- .../pip/createPackageManager.js | 26 ++++++++++---- .../commandArgumentScanner.js | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index dce00ca..af3036f 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -9,10 +9,12 @@ import { /** * @param {string} [command] + * @returns {import("../currentPackageManager.js").PackageManager} */ export function createPipPackageManager(command = "pip") { /** * @param {string[]} args + * @returns {boolean} */ function isSupportedCommand(args) { const scanner = findDependencyScannerForCommand( @@ -24,6 +26,7 @@ export function createPipPackageManager(command = "pip") { /** * @param {string[]} args + * @returns {ReturnType} */ function getDependencyUpdatesForCommand(args) { const scanner = findDependencyScannerForCommand( @@ -40,6 +43,9 @@ export function createPipPackageManager(command = "pip") { }; } +/** + * @type {Record} + */ const commandScannerMapping = { [pipInstallCommand]: commandArgumentScanner(), [pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI @@ -47,21 +53,27 @@ const commandScannerMapping = { // Other commands return null scanner by default }; -const NULL_SCANNER = { - shouldScan: () => false, - scan: () => [], -}; +/** + * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ +function nullScanner() { + return { + shouldScan: () => false, + scan: () => [], + }; +} /** - * @param {Record} scanners + * @param {Record} scanners * @param {string[]} args + * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} */ function findDependencyScannerForCommand(scanners, args) { const command = getPipCommandForArgs(args); if (!command) { - return NULL_SCANNER; + return nullScanner(); } const scanner = scanners[command]; - return scanner || NULL_SCANNER; + return scanner || nullScanner(); } diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js index 5e7031e..542958d 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -1,9 +1,31 @@ import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js"; import { hasDryRunArg } from "../utils/pipCommands.js"; +/** + * @typedef {Object} ScanResult + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef {Object} ScannerOptions + * @property {boolean} [ignoreDryRun] + */ + +/** + * @typedef {Object} CommandArgumentScanner + * @property {(args: string[]) => Promise | ScanResult[]} scan + * @property {(args: string[]) => boolean} shouldScan + */ + /** * @param {{ ignoreDryRun?: boolean }} [options] */ +/** + * @param {ScannerOptions} [options] + * @returns {CommandArgumentScanner} + */ export function commandArgumentScanner(options = {}) { const { ignoreDryRun = false } = options; @@ -17,6 +39,10 @@ export function commandArgumentScanner(options = {}) { /** * @param {string[]} args */ + /** + * @param {string[]} args + * @returns {Promise | ScanResult[]} + */ function scan(args) { return scanDependencies(args); } @@ -38,6 +64,10 @@ function shouldScanDependencies(args, ignoreDryRun) { /** * @param {string[]} args */ +/** + * @param {string[]} args + * @returns {Promise | ScanResult[]} + */ function scanDependencies(args) { return checkChangesFromArgs(args); } @@ -45,6 +75,10 @@ function scanDependencies(args) { /** * @param {string[]} args */ +/** + * @param {string[]} args + * @returns {Promise | ScanResult[]} + */ export function checkChangesFromArgs(args) { const packageUpdates = parsePackagesFromInstallArgs(args); From 181470d76413288e5a5348d8133629e5e396ca89 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 09:49:06 -0800 Subject: [PATCH 47/52] Clean up --- packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js index 5b28b48..abda858 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -24,5 +24,5 @@ export function getPipCommandForArgs(args) { * @param {string[]} args */ export function hasDryRunArg(args) { - return args.some(/** @param {string} arg */ (arg) => arg === "--dry-run"); + return args.some((arg) => arg === "--dry-run"); } From dadb1a3fba3d1fa0d90d2130e73f7f198d6650c0 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 09:55:39 -0800 Subject: [PATCH 48/52] Adapt runPipCommand.js documentation --- .../src/packagemanager/pip/runPipCommand.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 99ee97c..ce75466 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -6,10 +6,13 @@ import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; /** * @param {string} command * @param {string[]} args + * + * @returns {Promise<{status: number}>} */ export async function runPip(command, args) { try { - const env = mergeSafeChainProxyEnvironmentVariables(/** @type {Record} */ (process.env)); + // @ts-expect-error values of process.env can be string | undefined + const env = mergeSafeChainProxyEnvironmentVariables(process.env); // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) // so that any network request made by pip, including those outside explicit CLI args, @@ -23,12 +26,11 @@ export async function runPip(command, args) { env, }); return { status: result.status }; - } catch (error) { - if (error && typeof error === "object" && "status" in error) { - return { status: /** @type {any} */ (error).status }; + } catch (/** @type any */ error) { + if (error.status) { + return { status: error.status }; } else { - const message = error && typeof error === "object" && "message" in error ? /** @type {any} */ (error).message : String(error); - ui.writeError("Error executing command:", message); + ui.writeError("Error executing command:", error.message); return { status: 1 }; } } From 2accf954cad76d1322341bc506bd6176fdd92b77 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 10:20:05 -0800 Subject: [PATCH 49/52] Fix more documentation issues --- .../parsing/parsePackagesFromInstallArgs.js | 26 +++++++++++++++---- .../packagemanager/pip/utils/pipCommands.js | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index 5ec0426..ee6f849 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -1,23 +1,37 @@ +/** + * @typedef {Object} PackageDetail + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef {Object} PipOption + * @property {string} name + * @property {number} numberOfParameters + */ + /** * Supported formats that will be returned: * - package_name (no version) * - package_name==version (exact version) * - package_name===version (exact version, PEP 440) - * - * "Ranges". Because they don't specify an exact version, the following formats are skipped and we will rely solely on the mitm scanner: + * + * Ranges: Because they don't specify an exact version, the following formats are skipped and we rely on the MITM scanner: * - package_name>=version * - package_name<=version * - package_name>version * - package_name arg === "--dry-run"); From f7e08bbea88129ff18664cfd1c472a4e3c976e9e Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 10:44:12 -0800 Subject: [PATCH 50/52] Fix more documentation issues --- packages/safe-chain/src/config/settings.js | 1 + .../pip/dependencyScanner/commandArgumentScanner.js | 13 +------------ .../pip/parsing/parsePackagesFromInstallArgs.js | 1 - 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 261f19a..d7ccf87 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -21,6 +21,7 @@ const ecosystemSettings = { ecoSystem: ECOSYSTEM_JS, }; +/** @returns {string} - The current ecosystem setting (ECOSYSTEM_JS or ECOSYSTEM_PY) */ export function getEcoSystem() { return ecosystemSettings.ecoSystem; } diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js index 542958d..27a07c2 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -19,11 +19,9 @@ import { hasDryRunArg } from "../utils/pipCommands.js"; * @property {(args: string[]) => boolean} shouldScan */ -/** - * @param {{ ignoreDryRun?: boolean }} [options] - */ /** * @param {ScannerOptions} [options] + * * @returns {CommandArgumentScanner} */ export function commandArgumentScanner(options = {}) { @@ -36,9 +34,6 @@ export function commandArgumentScanner(options = {}) { return shouldScanDependencies(args, ignoreDryRun); } - /** - * @param {string[]} args - */ /** * @param {string[]} args * @returns {Promise | ScanResult[]} @@ -61,9 +56,6 @@ function shouldScanDependencies(args, ignoreDryRun) { return ignoreDryRun || !hasDryRunArg(args); } -/** - * @param {string[]} args - */ /** * @param {string[]} args * @returns {Promise | ScanResult[]} @@ -72,9 +64,6 @@ function scanDependencies(args) { return checkChangesFromArgs(args); } -/** - * @param {string[]} args - */ /** * @param {string[]} args * @returns {Promise | ScanResult[]} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index ee6f849..ac3d99f 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -72,7 +72,6 @@ export function parsePackagesFromInstallArgs(args) { function isPipOptionWithParameter(arg) { // Check if a pip flag takes a parameter - // TODO it would be better to query pip itself for this info const optionsWithParameters = [ // Install options "-r", From 86f82d60658767aeeb509d7899a045937e15d384 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 10:53:35 -0800 Subject: [PATCH 51/52] Fix more documentation issues --- packages/safe-chain/src/registryProxy/certBundle.js | 1 + packages/safe-chain/src/registryProxy/parsePackageFromUrl.js | 2 ++ packages/safe-chain/src/scanning/malwareDatabase.js | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js index 60e7d23..956279d 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -49,6 +49,7 @@ let cachedPath = null; * - Includes Safe Chain CA (for MITM of known registries) * - Includes Mozilla roots via npm `certifi` (public HTTPS) * - Includes Node's built-in root certificates as a portable fallback + * @returns {string} Path to the combined CA bundle PEM file */ export function getCombinedCaBundlePath() { if (cachedPath && fs.existsSync(cachedPath)) return cachedPath; diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 64ce99a..1fda121 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -35,6 +35,7 @@ export function parsePackageFromUrl(url) { /** * @param {string} url * @param {string} registry + * @returns {{packageName: string | undefined, version: string | undefined}} */ function parseJsPackageFromUrl(url, registry) { let packageName, version; @@ -78,6 +79,7 @@ function parseJsPackageFromUrl(url, registry) { /** * @param {string} url * @param {string} registry + * @returns {{packageName: string | undefined, version: string | undefined}} */ function parsePipPackageFromUrl(url, registry) { let packageName, version diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index a2e2fb7..b11f8d8 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -23,6 +23,7 @@ let cachedMalwareDatabase = null; * For Python packages (PEP-503): lowercase and replace _, -, . with - * For js packages: keep as-is (case-sensitive) * @param {string} name + * @returns {string} */ function normalizePackageName(name) { const ecosystem = getEcoSystem(); From 2b6b9b6737eaf6fcbb52721752458d5adf6941c5 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 4 Nov 2025 06:59:45 -0800 Subject: [PATCH 52/52] Cleanup comments --- packages/safe-chain/bin/aikido-pip.js | 1 - packages/safe-chain/bin/aikido-pip3.js | 1 - packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 8edef59..92ba4e3 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -16,5 +16,4 @@ setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); var exitCode = await main(argv); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index 0c20df0..e24fda4 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -16,5 +16,4 @@ setEcoSystem(ECOSYSTEM_PY); initializePackageManager(packageManagerName); var exitCode = await main(argv); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index ce75466..6fae388 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -11,7 +11,6 @@ import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; */ export async function runPip(command, args) { try { - // @ts-expect-error values of process.env can be string | undefined const env = mergeSafeChainProxyEnvironmentVariables(process.env); // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots)