From 486a4b8f680f60100cebd1dcd0aa750e3924229b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 6 Oct 2025 16:25:12 +0200 Subject: [PATCH 001/345] Escape special chars in shell scripts --- packages/safe-chain/src/utils/safeSpawn.js | 12 +++++++++--- packages/safe-chain/src/utils/safeSpawn.spec.js | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 826ab7d..f45e4ff 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,9 +1,15 @@ import { spawnSync, spawn } from "child_process"; function escapeArg(arg) { - // If argument contains spaces or quotes, wrap in double quotes and escape double quotes - if (arg.includes(" ") || arg.includes('"') || arg.includes("'")) { - return '"' + arg.replaceAll('"', '\\"') + '"'; + // Shell metacharacters that need escaping + // These characters have special meaning in shells and need to be quoted + const shellMetaChars = /[ "&'|;<>()$`\\!*?[\]{}~#]/; + + // If argument contains shell metacharacters, wrap in double quotes + // and escape characters that are special even inside double quotes + if (shellMetaChars.test(arg)) { + // Inside double quotes, we need to escape: " $ ` \ + return '"' + arg.replace(/(["`$\\])/g, '\\$1') + '"'; } return arg; } diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index d325f8a..020b59a 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -105,5 +105,14 @@ describe("safeSpawn", () => { assert.strictEqual(spawnCalls[0].command, "npm install axios --save"); assert.strictEqual(spawnCalls[0].options.shell, true); }); + + it(`should escape ampersand character (${variant})`, async () => { + await runSafeSpawn(variant, "npx", ["cypress", "run", "--env", "password=foo&bar"]); + + assert.strictEqual(spawnCalls.length, 1); + // & should be escaped by wrapping the arg in quotes + assert.strictEqual(spawnCalls[0].command, 'npx cypress run --env "password=foo&bar"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); } }); \ No newline at end of file From d5620b2d126fc5483ad99a8485932795ae254946 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 9 Oct 2025 14:58:06 +0200 Subject: [PATCH 002/345] Don't set YARN_HTTPS_CA_FILE_PATH, it ignores all system CAs --- .../safe-chain/src/packagemanager/yarn/runYarnCommand.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 2c3795c..65c27a0 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -23,7 +23,9 @@ export async function runYarnCommand(args) { } async function fixYarnProxyEnvironmentVariables(env) { - // Yarn ignores standard proxy environment variables HTTPS_PROXY and NODE_EXTRA_CA_CERTS + // Yarn ignores standard proxy environment variable HTTPS_PROXY + // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. + // Don't use YARN_HTTPS_CA_FILE_PATH though, as it causes to ignore all system CAs // Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs // When setting all variables, yarn returns an error about conflicting variables @@ -35,10 +37,8 @@ async function fixYarnProxyEnvironmentVariables(env) { if (majorVersion >= 4) { env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - env.YARN_HTTPS_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS; } else if (majorVersion === 2 || majorVersion === 3) { env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - env.YARN_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS; } } From ad7e94dac4945e51a60be25b5d431efb8321d572 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 9 Oct 2025 15:35:43 +0200 Subject: [PATCH 003/345] Add unit tests for yarn environment variables --- .../yarn/runYarnCommand.spec.js | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js new file mode 100644 index 0000000..bd3d04d --- /dev/null +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js @@ -0,0 +1,152 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; + +describe("runYarnCommand", () => { + let runYarnCommand; + let capturedEnv; + let yarnVersion; + + beforeEach(async () => { + capturedEnv = null; + yarnVersion = "4.1.0"; // Default to v4 + + // Mock safeSpawn to capture env and control yarn version + mock.module("../../utils/safeSpawn.js", { + namedExports: { + safeSpawn: async (command, args, options) => { + if (args.includes("--version")) { + // Mock yarn version check + return { status: 0, stdout: yarnVersion }; + } + // Capture the env for assertions + capturedEnv = options.env; + return { status: 0 }; + }, + }, + }); + + // Mock mergeSafeChainProxyEnvironmentVariables to return test env + mock.module("../../registryProxy/registryProxy.js", { + namedExports: { + mergeSafeChainProxyEnvironmentVariables: (env) => { + return { + ...env, + HTTPS_PROXY: "http://localhost:8080", + NODE_EXTRA_CA_CERTS: "/path/to/ca-cert.pem", + }; + }, + }, + }); + + // Mock ui to prevent console output + mock.module("../../environment/userInteraction.js", { + namedExports: { + ui: { + writeError: () => {}, + }, + }, + }); + + const module = await import("./runYarnCommand.js"); + runYarnCommand = module.runYarnCommand; + }); + + afterEach(() => { + mock.reset(); + }); + + it("should set YARN_HTTPS_PROXY for Yarn v4+", async () => { + yarnVersion = "4.1.0"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + "http://localhost:8080", + "YARN_HTTPS_PROXY should be set to the HTTPS_PROXY value" + ); + assert.strictEqual( + capturedEnv.YARN_HTTPS_CA_FILE_PATH, + undefined, + "YARN_HTTPS_CA_FILE_PATH should NOT be set to avoid overriding system CAs" + ); + }); + + it("should set YARN_HTTPS_PROXY for Yarn v3", async () => { + yarnVersion = "3.6.4"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + "http://localhost:8080", + "YARN_HTTPS_PROXY should be set to the HTTPS_PROXY value" + ); + assert.strictEqual( + capturedEnv.YARN_CA_FILE_PATH, + undefined, + "YARN_CA_FILE_PATH should NOT be set to avoid overriding system CAs" + ); + }); + + it("should set YARN_HTTPS_PROXY for Yarn v2", async () => { + yarnVersion = "2.4.3"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + "http://localhost:8080", + "YARN_HTTPS_PROXY should be set to the HTTPS_PROXY value" + ); + assert.strictEqual( + capturedEnv.YARN_CA_FILE_PATH, + undefined, + "YARN_CA_FILE_PATH should NOT be set to avoid overriding system CAs" + ); + }); + + it("should not set Yarn-specific proxy vars for Yarn v1", async () => { + yarnVersion = "1.22.19"; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.YARN_HTTPS_PROXY, + undefined, + "YARN_HTTPS_PROXY should not be set for Yarn v1" + ); + assert.strictEqual( + capturedEnv.YARN_HTTPS_CA_FILE_PATH, + undefined, + "YARN_HTTPS_CA_FILE_PATH should not be set for Yarn v1" + ); + assert.strictEqual( + capturedEnv.YARN_CA_FILE_PATH, + undefined, + "YARN_CA_FILE_PATH should not be set for Yarn v1" + ); + }); + + it("should preserve NODE_EXTRA_CA_CERTS for all Yarn versions", async () => { + for (const version of ["4.1.0", "3.6.4", "2.4.3", "1.22.19"]) { + yarnVersion = version; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.NODE_EXTRA_CA_CERTS, + "/path/to/ca-cert.pem", + `NODE_EXTRA_CA_CERTS should be preserved for Yarn ${version}` + ); + } + }); + + it("should preserve HTTPS_PROXY for all Yarn versions", async () => { + for (const version of ["4.1.0", "3.6.4", "2.4.3", "1.22.19"]) { + yarnVersion = version; + await runYarnCommand(["add", "lodash"]); + + assert.strictEqual( + capturedEnv.HTTPS_PROXY, + "http://localhost:8080", + `HTTPS_PROXY should be preserved for Yarn ${version}` + ); + } + }); +}); From 0afea0eed6ef559b994810eb0a81dfde54e7badb Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 16:44:55 +0200 Subject: [PATCH 004/345] Remove `safeSpawnSync` (unused) --- packages/safe-chain/src/utils/safeSpawn.js | 5 ----- packages/safe-chain/src/utils/safeSpawn.spec.js | 13 ++++--------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index c5cd913..b8c9274 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -13,11 +13,6 @@ function buildCommand(command, args) { return `${command} ${escapedArgs.join(" ")}`; } -export function safeSpawnSync(command, args, options = {}) { - const fullCommand = buildCommand(command, args); - return spawnSync(fullCommand, { ...options, shell: true }); -} - export async function safeSpawn(command, args, options = {}) { const fullCommand = buildCommand(command, args); return new Promise((resolve, reject) => { diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index d325f8a..1ffdc25 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -2,7 +2,7 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; describe("safeSpawn", () => { - let safeSpawnSync, safeSpawn; + let safeSpawn; let spawnCalls = []; beforeEach(async () => { @@ -35,7 +35,6 @@ describe("safeSpawn", () => { // Import after mocking const safeSpawnModule = await import("./safeSpawn.js"); - safeSpawnSync = safeSpawnModule.safeSpawnSync; safeSpawn = safeSpawnModule.safeSpawn; }); @@ -45,14 +44,10 @@ describe("safeSpawn", () => { // Helper to run either sync or async variant async function runSafeSpawn(variant, command, args, options) { - if (variant === "sync") { - return safeSpawnSync(command, args, options); - } else { - return await safeSpawn(command, args, options); - } + return await safeSpawn(command, args, options); } - for (let variant of ["sync", "async"]) { + for (let variant of ["async"]) { it(`should pass basic command and arguments correctly (${variant})`, async () => { await runSafeSpawn(variant, "echo", ["hello"]); @@ -106,4 +101,4 @@ describe("safeSpawn", () => { assert.strictEqual(spawnCalls[0].options.shell, true); }); } -}); \ No newline at end of file +}); From 459f3a5b146004bc06f9d4c3778253c06294c268 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 17:35:29 +0200 Subject: [PATCH 005/345] Remove unused import --- packages/safe-chain/src/utils/safeSpawn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index b8c9274..253417c 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,4 +1,4 @@ -import { spawnSync, spawn } from "child_process"; +import { spawn } from "child_process"; function escapeArg(arg) { // If argument contains spaces or quotes, wrap in double quotes and escape double quotes From 41ab4b1edb9d63410d46f3cb616f7999ae96d538 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 18:03:45 +0200 Subject: [PATCH 006/345] Use oxlint instead of eslint - Less dev dependencies - Much faster - More helpful output - More sane defaults - Easier config --- .github/workflows/test-on-pr.yml | 2 +- .oxlintrc.json | 28 + eslint.config.js | 26 - package-lock.json | 3413 +---------------- package.json | 9 +- packages/safe-chain-bun/src/index.js | 1 + packages/safe-chain/package.json | 2 +- .../src/environment/userInteraction.js | 1 + 8 files changed, 164 insertions(+), 3318 deletions(-) create mode 100644 .oxlintrc.json delete mode 100644 eslint.config.js diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 5661f97..85d6aba 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -28,7 +28,7 @@ jobs: - name: Run unit tests run: npm test - - name: Run ESLint + - name: Run linting run: npm run lint - name: Create package tarball diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..a9dd70a --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,28 @@ +{ + "plugins": [ + "node", + "promise", + "eslint", + "unicorn", + "oxc", + "import" + ], + "env": { + "browser": false, + "node": true + }, + "rules": { + "eslint/no-console": "error", + "eslint/no-empty": "error" + }, + "overrides": [ + { + "files": [ + "*.spec.js" + ], + "rules": { + "eslint/no-console": "off" + } + } + ] +} diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 3db1b7f..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,26 +0,0 @@ -import js from "@eslint/js"; -import { defineConfig, globalIgnores } from "@eslint/config-helpers"; -import globals from "globals"; -import importPlugin from "eslint-plugin-import"; - -export default defineConfig([ - { - files: ["**/*.{js,mjs,cjs,ts}"], - plugins: { js }, - extends: ["js/recommended"], - }, - { - files: ["**/*.{js,mjs,cjs,ts}"], - languageOptions: { globals: globals.node }, - }, - importPlugin.flatConfigs.recommended, - { - files: ["**/*.{js,mjs,cjs}"], - languageOptions: { - ecmaVersion: "latest", - sourceType: "module", - }, - rules: {}, - }, - globalIgnores(['test/e2e', 'node_modules']), -]); diff --git a/package-lock.json b/package-lock.json index 6b74d53..0a8f765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,7 @@ "test/e2e" ], "devDependencies": { - "@eslint/js": "^9.35.0", - "eslint": "^9.35.0", - "eslint-plugin-import": "^2.32.0", - "globals": "^16.1.0", - "typescript-eslint": "^8.32.0" + "oxlint": "^1.22.0" } }, "node_modules/@aikidosec/safe-chain": { @@ -31,226 +27,6 @@ "resolved": "test/e2e", "link": true }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -330,44 +106,6 @@ "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -559,6 +297,110 @@ ], "peer": true }, + "node_modules/@oxlint/darwin-arm64": { + "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, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/darwin-x64": { + "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, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/linux-arm64-gnu": { + "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, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-arm64-musl": { + "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, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-gnu": { + "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, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-musl": { + "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, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/win32-arm64": { + "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, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/win32-x64": { + "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, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -569,230 +411,6 @@ "node": ">=14" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -802,29 +420,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -834,23 +429,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -875,161 +453,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1044,19 +467,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/bun": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", @@ -1115,66 +525,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "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/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -1254,60 +604,6 @@ "node": ">= 8" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1325,77 +621,6 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dev": true, - "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", @@ -1437,585 +662,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "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", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "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==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2044,47 +690,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==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "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", @@ -2097,63 +702,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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2174,19 +722,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2202,150 +737,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "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" - }, - "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==", - "dev": true, - "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", @@ -2390,33 +781,6 @@ "node": ">= 14" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2426,21 +790,6 @@ "node": ">=0.8.19" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -2454,167 +803,6 @@ "node": ">= 12" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "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", @@ -2624,38 +812,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -2668,158 +824,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -2832,59 +836,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2906,59 +857,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "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-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -2968,53 +872,6 @@ ], "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -3071,40 +928,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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -3117,29 +940,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3307,13 +1107,6 @@ "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -3376,121 +1169,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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -3564,54 +1242,38 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "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, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" + "bin": { + "oxc_language_server": "bin/oxc_language_server", + "oxlint": "bin/oxlint" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" + "url": "https://github.com/sponsors/Boshen" }, - "engines": { - "node": ">=10" + "optionalDependencies": { + "@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" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" + "peerDependencies": { + "oxlint-tsgolint": ">=0.2.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } } }, "node_modules/p-map": { @@ -3632,29 +1294,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3664,13 +1303,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -3687,39 +1319,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -3742,112 +1341,6 @@ "node": ">=10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "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/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -3888,96 +1381,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "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", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3997,55 +1400,6 @@ "node": ">=10" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4067,82 +1421,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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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", @@ -4223,20 +1501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4266,65 +1530,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4350,55 +1555,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", @@ -4416,193 +1572,6 @@ "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -4627,16 +1596,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.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", @@ -4661,105 +1620,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -4866,19 +1726,6 @@ "node": ">=18" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", @@ -4894,6 +1741,8 @@ "semver": "7.7.2" }, "bin": { + "aikido-bun": "bin/aikido-bun.js", + "aikido-bunx": "bin/aikido-bunx.js", "aikido-npm": "bin/aikido-npm.js", "aikido-npx": "bin/aikido-npx.js", "aikido-pnpm": "bin/aikido-pnpm.js", diff --git a/package.json b/package.json index ad71644..0193a82 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,6 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { - "@eslint/js": "^9.35.0", - "eslint": "^9.35.0", - "eslint-plugin-import": "^2.32.0", - "globals": "^16.1.0", - "typescript-eslint": "^8.32.0" - }, - "overrides": { - "brace-expansion@<=2.0.2": "2.0.2" + "oxlint": "^1.22.0" } } diff --git a/packages/safe-chain-bun/src/index.js b/packages/safe-chain-bun/src/index.js index fbd0f65..660e0bd 100644 --- a/packages/safe-chain-bun/src/index.js +++ b/packages/safe-chain-bun/src/index.js @@ -1,3 +1,4 @@ +// oxlint-disable no-console import { auditChanges } from "@aikidosec/safe-chain/scanning"; // Bun Security Scanner for Safe-Chain diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 6a927d4..95098b7 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "eslint ." + "lint": "oxlint" }, "bin": { "aikido-npm": "bin/aikido-npm.js", diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 5b1cb88..829afa1 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -1,3 +1,4 @@ +// oxlint-disable no-console import chalk from "chalk"; import ora from "ora"; import { createInterface } from "readline"; From 5e08461859cca1f9c9a71862963949e284b9e2b7 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 10 Oct 2025 11:41:42 +0200 Subject: [PATCH 007/345] Add $schema reference for autocompletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Timo Kössler --- .oxlintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.oxlintrc.json b/.oxlintrc.json index a9dd70a..b76f2ad 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,4 +1,5 @@ { + "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": [ "node", "promise", From 5518846e9612f52b1096dbf68001953211694578 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 10 Oct 2025 11:45:34 +0200 Subject: [PATCH 008/345] Update packages/safe-chain/package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Timo Kössler --- packages/safe-chain/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 95098b7..42bfb55 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "oxlint" + "lint": "oxlint --deny-warnings" }, "bin": { "aikido-npm": "bin/aikido-npm.js", From a377fd6caae67482cfba7f4db0c576e2e707723d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 13:55:39 +0200 Subject: [PATCH 009/345] Listen to error events on sockets --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 6 ++++++ .../safe-chain/src/registryProxy/tunnelRequestHandler.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 4be9987..63a8168 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,6 +5,12 @@ import { HttpsProxyAgent } from "https-proxy-agent"; export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); + clientSocket.on("error", () => { + // NO-OP + // This can happen if the client TCP socket sends RST instead of FIN. + // Not subscribing to 'close' event will cause node to throw and crash. + }); + const server = createHttpsServer(hostname, isAllowed); // Establish the connection diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index fa12aee..c28a022 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -24,6 +24,12 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); + clientSocket.on("error", () => { + // NO-OP + // This can happen if the client TCP socket sends RST instead of FIN. + // Not subscribing to 'close' event will cause node to throw and crash. + }); + const serverSocket = net.connect(port || 443, hostname, () => { clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); serverSocket.write(head); From 2fa14b82f3b05872540010de64fe0cac4547f04c Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 10 Oct 2025 14:57:28 +0200 Subject: [PATCH 010/345] Simplify tests --- .../safe-chain/src/utils/safeSpawn.spec.js | 97 ++++++++----------- 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 1ffdc25..6417084 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -11,14 +11,6 @@ describe("safeSpawn", () => { // Mock child_process module to capture what command string gets built mock.module("child_process", { namedExports: { - spawnSync: (command, options) => { - spawnCalls.push({ command, options }); - return { - status: 0, - stdout: Buffer.from(""), - stderr: Buffer.from(""), - }; - }, spawn: (command, options) => { spawnCalls.push({ command, options }); return { @@ -42,63 +34,56 @@ describe("safeSpawn", () => { mock.reset(); }); - // Helper to run either sync or async variant - async function runSafeSpawn(variant, command, args, options) { - return await safeSpawn(command, args, options); - } + it("should pass basic command and arguments correctly", async () => { + await safeSpawn("echo", ["hello"]); - for (let variant of ["async"]) { - it(`should pass basic command and arguments correctly (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ["hello"]); + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, "echo hello"); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - assert.strictEqual(spawnCalls[0].command, "echo hello"); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should escape arguments containing spaces", async () => { + await safeSpawn("echo", ["hello world"]); - it(`should escape arguments containing spaces (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ["hello world"]); + assert.strictEqual(spawnCalls.length, 1); + // Argument should be escaped to prevent shell interpretation + assert.strictEqual(spawnCalls[0].command, 'echo "hello world"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // Argument should be escaped to prevent shell interpretation - assert.strictEqual(spawnCalls[0].command, 'echo "hello world"'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should prevent shell injection attacks", async () => { + await safeSpawn("ls", ["; rm test123.txt"]); - it(`should prevent shell injection attacks (${variant})`, async () => { - await runSafeSpawn(variant, "ls", ["; rm test123.txt"]); + assert.strictEqual(spawnCalls.length, 1); + // Malicious command should be escaped to prevent execution + assert.strictEqual(spawnCalls[0].command, 'ls "; rm test123.txt"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // Malicious command should be escaped to prevent execution - assert.strictEqual(spawnCalls[0].command, 'ls "; rm test123.txt"'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should escape single quotes in arguments", async () => { + await safeSpawn("echo", ["don't break"]); - it(`should escape single quotes in arguments (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ["don't break"]); + assert.strictEqual(spawnCalls.length, 1); + // Single quote should be properly escaped with double quotes + assert.strictEqual(spawnCalls[0].command, 'echo "don\'t break"'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // Single quote should be properly escaped with double quotes - assert.strictEqual(spawnCalls[0].command, 'echo "don\'t break"'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should handle double quotes with simpler escaping", async () => { + await safeSpawn("echo", ['say "hello"']); - it(`should handle double quotes with simpler escaping (${variant})`, async () => { - await runSafeSpawn(variant, "echo", ['say "hello"']); + assert.strictEqual(spawnCalls.length, 1); + // If we switch to double quotes, this should be: "say \"hello\"" + assert.strictEqual(spawnCalls[0].command, 'echo "say \\"hello\\""'); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); - assert.strictEqual(spawnCalls.length, 1); - // If we switch to double quotes, this should be: "say \"hello\"" - assert.strictEqual(spawnCalls[0].command, 'echo "say \\"hello\\""'); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); + it("should not escape arguments with only safe characters", async () => { + await safeSpawn("npm", ["install", "axios", "--save"]); - it(`should not escape arguments with only safe characters (${variant})`, async () => { - await runSafeSpawn(variant, "npm", ["install", "axios", "--save"]); - - assert.strictEqual(spawnCalls.length, 1); - // Safe arguments (alphanumeric, dash, underscore, dot, slash) shouldn't be quoted - assert.strictEqual(spawnCalls[0].command, "npm install axios --save"); - assert.strictEqual(spawnCalls[0].options.shell, true); - }); - } + assert.strictEqual(spawnCalls.length, 1); + // Safe arguments (alphanumeric, dash, underscore, dot, slash) shouldn't be quoted + assert.strictEqual(spawnCalls[0].command, "npm install axios --save"); + assert.strictEqual(spawnCalls[0].options.shell, true); + }); }); From 4fc33d23874cd201b5ec7f90c4dbc81f0ebe5c20 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 15:34:33 +0200 Subject: [PATCH 011/345] Add command to get the safe-chain version --- README.md | 5 +++++ packages/safe-chain/bin/safe-chain.js | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd2cdff..1083c0e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: 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. +You can check the installed version by running: +```shell +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. diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 5a7d94b..ad88c08 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import chalk from "chalk"; +import { createRequire } from "module"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; @@ -26,6 +27,8 @@ if (command === "setup") { teardown(); } else if (command === "setup-ci") { setupCi(); +} else if (command === "--version" || command === "-v" || command === "-v") { + ui.writeInformation(`Current safe-chain version: ${getVersion()}`); } else { ui.writeError(`Unknown command: ${command}.`); ui.emptyLine(); @@ -43,13 +46,15 @@ function writeHelp() { ui.writeInformation( `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan( "teardown" - )}, ${chalk.cyan("help")}` + )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan( + "--version" + )}` ); ui.emptyLine(); ui.writeInformation( `- ${chalk.cyan( "safe-chain setup" - )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm and pnpx.` + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun and bunx.` ); ui.writeInformation( `- ${chalk.cyan( @@ -61,5 +66,16 @@ function writeHelp() { "safe-chain setup-ci" )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain --version" + )} (or ${chalk.cyan("-v")}): Display the current version of safe-chain.` + ); ui.emptyLine(); } + +function getVersion() { + const require = createRequire(import.meta.url); + const packageJson = require("../package.json"); + return packageJson.version; +} From 8aebb1b96b0f4d3412e3314af1547857608abd32 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 16:18:43 +0200 Subject: [PATCH 012/345] Remove dry-run scanner for npm, relying on the proxy to block maliscious package downloads instead --- packages/safe-chain/bin/aikido-npm.js | 13 +- packages/safe-chain/bin/aikido-npx.js | 2 +- packages/safe-chain/bin/aikido-pnpm.js | 2 +- packages/safe-chain/bin/aikido-pnpx.js | 2 +- packages/safe-chain/bin/aikido-yarn.js | 2 +- .../packagemanager/currentPackageManager.js | 4 +- .../npm/createPackageManager.js | 55 ++----- .../npm/dependencyScanner/dryRunScanner.js | 67 --------- .../dependencyScanner/dryRunScanner.spec.js | 139 ------------------ .../parsing/parseNpmInstallDryRunOutput.js | 57 ------- .../parseNpmInstallDryRunOutput.spec.js | 134 ----------------- test/e2e/npm.e2e.spec.js | 48 ++---- 12 files changed, 29 insertions(+), 496 deletions(-) delete mode 100644 packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js delete mode 100644 packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js delete mode 100644 packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js delete mode 100644 packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index d8b8c3e..0e9f302 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -1,21 +1,10 @@ #!/usr/bin/env node -import { execSync } from "child_process"; import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "npm"; -initializePackageManager(packageManagerName, getNpmVersion()); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); - -function getNpmVersion() { - try { - return execSync("npm --version").toString().trim(); - } catch { - // Default to 0.0.0 if npm is not found - // That way we don't use any unsupported features - return "0.0.0"; - } -} diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index 7f06c7c..d3dfdd6 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "npx"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index 7177159..0a06217 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "pnpm"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 4bb6840..cdb6504 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "pnpx"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index 002a956..fd87606 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -4,7 +4,7 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; const packageManagerName = "yarn"; -initializePackageManager(packageManagerName, process.versions.node); +initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2a10d86..2f019a1 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -14,9 +14,9 @@ const state = { packageManagerName: null, }; -export function initializePackageManager(packageManagerName, version) { +export function initializePackageManager(packageManagerName) { if (packageManagerName === "npm") { - state.packageManagerName = createNpmPackageManager(version); + state.packageManagerName = createNpmPackageManager(); } else if (packageManagerName === "npx") { state.packageManagerName = createNpxPackageManager(); } else if (packageManagerName === "yarn") { diff --git a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js index bf38209..731f406 100644 --- a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js @@ -1,34 +1,27 @@ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; -import { dryRunScanner } from "./dependencyScanner/dryRunScanner.js"; import { nullScanner } from "./dependencyScanner/nullScanner.js"; import { runNpm } from "./runNpmCommand.js"; import { getNpmCommandForArgs, npmInstallCommand, - npmCiCommand, - npmInstallTestCommand, - npmInstallCiTestCommand, npmUpdateCommand, - npmAuditCommand, npmExecCommand, } from "./utils/npmCommands.js"; -export function createNpmPackageManager(version) { - // From npm v10.4.0 onwards, the npm commands output detailed information - // when using the --dry-run flag. - // We use that information to scan for dependency changes. - // For older versions of npm we have to rely on parsing the command arguments. - const supportedScanners = isPriorToNpm10_4(version) - ? npm10_3AndBelowSupportedScanners - : npm10_4AndAboveSupportedScanners; - +export function createNpmPackageManager() { function isSupportedCommand(args) { - const scanner = findDependencyScannerForCommand(supportedScanners, args); + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); return scanner.shouldScan(args); } function getDependencyUpdatesForCommand(args) { - const scanner = findDependencyScannerForCommand(supportedScanners, args); + const scanner = findDependencyScannerForCommand( + commandScannerMapping, + args + ); return scanner.scan(args); } @@ -39,40 +32,12 @@ export function createNpmPackageManager(version) { }; } -const npm10_4AndAboveSupportedScanners = { - [npmInstallCommand]: dryRunScanner(), - [npmUpdateCommand]: dryRunScanner(), - [npmCiCommand]: dryRunScanner(), - [npmAuditCommand]: dryRunScanner({ - skipScanWhen: (args) => !args.includes("fix"), - }), - [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run - - // Running dry-run on install-test and install-ci-test will install & run tests. - // We only want to know if there are changes in the dependencies. - // So we run change the dry-run command to only check the install. - [npmInstallTestCommand]: dryRunScanner({ dryRunCommand: npmInstallCommand }), - [npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }), -}; - -const npm10_3AndBelowSupportedScanners = { +const commandScannerMapping = { [npmInstallCommand]: commandArgumentScanner(), [npmUpdateCommand]: commandArgumentScanner(), [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run }; -function isPriorToNpm10_4(version) { - try { - const [major, minor] = version.split(".").map(Number); - if (major < 10) return true; - if (major === 10 && minor < 4) return true; - return false; - } catch { - // Default to true: if version parsing fails, assume it's an older version - return true; - } -} - function findDependencyScannerForCommand(scanners, args) { const command = getNpmCommandForArgs(args); if (!command) { diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js deleted file mode 100644 index 6189b2f..0000000 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +++ /dev/null @@ -1,67 +0,0 @@ -import { parseDryRunOutput } from "../parsing/parseNpmInstallDryRunOutput.js"; -import { dryRunNpmCommandAndOutput } from "../runNpmCommand.js"; -import { hasDryRunArg } from "../utils/npmCommands.js"; - -export function dryRunScanner(scannerOptions) { - return { - scan: (args) => scanDependencies(scannerOptions, args), - shouldScan: (args) => shouldScanDependencies(scannerOptions, args), - }; -} - -function scanDependencies(scannerOptions, args) { - let dryRunArgs = args; - - if (scannerOptions?.dryRunCommand) { - // Replace the first argument with the dryRunCommand (eg: "install" instead of "install-test") - dryRunArgs = [scannerOptions.dryRunCommand, ...args.slice(1)]; - } - - return checkChangesWithDryRun(dryRunArgs); -} - -function shouldScanDependencies(scannerOptions, args) { - if (hasDryRunArg(args)) { - return false; - } - - if (scannerOptions?.skipScanWhen && scannerOptions.skipScanWhen(args)) { - return false; - } - - return true; -} - -async function checkChangesWithDryRun(args) { - const dryRunOutput = await dryRunNpmCommandAndOutput(args); - - // Dry-run can return a non-zero status code in some cases - // e.g., when running "npm audit fix --dry-run", it returns exit code 1 - // when there are vulnerabilities that can be fixed. - if (dryRunOutput.status !== 0 && !canCommandReturnNonZeroOnSuccess(args)) { - throw new Error( - `Dry-run command failed with exit code ${dryRunOutput.status} and output:\n${dryRunOutput.output}` - ); - } - - if (dryRunOutput.status !== 0 && !dryRunOutput.output) { - throw new Error( - `Dry-run command failed with exit code ${dryRunOutput.status} and produced no output.` - ); - } - - const parsedOutput = parseDryRunOutput(dryRunOutput.output); - - // reverse the array to have the top-level packages first - return parsedOutput.reverse(); -} - -function canCommandReturnNonZeroOnSuccess(args) { - if (args.length < 2) { - return false; - } - - // `npm audit fix --dry-run` can return exit code 1 when it succesfully ran and - // there were vulnerabilities that could be fixed - return args[0] === "audit" && args[1] === "fix"; -} diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js deleted file mode 100644 index 88d7681..0000000 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/dryRunScanner.spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, it, mock } from "node:test"; -import assert from "node:assert/strict"; - -describe("dryRunScanner", async () => { - const mockWriteError = mock.fn(); - const mockDryRunNpmCommandAndOutput = mock.fn(); - - // Mock ui module - mock.module("../../../environment/userInteraction.js", { - namedExports: { - ui: { - writeError: mockWriteError, - }, - }, - }); - - // Mock dryRunNpmCommandAndOutput function - mock.module("../runNpmCommand.js", { - namedExports: { - dryRunNpmCommandAndOutput: mockDryRunNpmCommandAndOutput, - }, - }); - - const { dryRunScanner } = await import("./dryRunScanner.js"); - - describe("doesCommandReturnNonZero", () => { - // We need to access the internal function for testing - // Since it's not exported, we'll test it indirectly through the main functionality - - it("should handle npm audit fix commands that return non-zero", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 1, - output: "found 5 vulnerabilities that can be fixed", - })); - - const scanner = dryRunScanner(); - const result = await scanner.scan(["audit", "fix"]); - - // Should not throw an error for audit fix commands - assert.ok(Array.isArray(result)); - assert.equal(mockWriteError.mock.callCount(), 0); - }); - - it("should throw error for unexpected non-zero exit codes", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 1, - output: "some error output", - })); - - const scanner = dryRunScanner(); - - await assert.rejects(async () => { - await scanner.scan(["install", "lodash"]); - }, /Dry-run command failed with exit code 1/); - }); - - it("should handle zero exit codes normally", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 0, - output: "added 1 package", - })); - - const scanner = dryRunScanner(); - const result = await scanner.scan(["install", "lodash"]); - - assert.ok(Array.isArray(result)); - assert.equal(mockWriteError.mock.callCount(), 0); - }); - - it("should throw error for non-zero exit with no output for audit fix", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 1, - output: "", - })); - - const scanner = dryRunScanner(); - - await assert.rejects(async () => { - await scanner.scan(["audit", "fix"]); - }, /Dry-run command failed with exit code 1/); - }); - }); - - describe("scanner functionality", () => { - it("should use dryRunCommand option when provided", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - mockDryRunNpmCommandAndOutput.mock.mockImplementationOnce(() => ({ - status: 0, - output: "no changes", - })); - - const scanner = dryRunScanner({ dryRunCommand: "install" }); - await scanner.scan(["install-test", "lodash"]); - - // Should call with "install" instead of "install-test" - assert.equal(mockDryRunNpmCommandAndOutput.mock.callCount(), 1); - const calledArgs = - mockDryRunNpmCommandAndOutput.mock.calls[0].arguments[0]; - assert.deepEqual(calledArgs, ["install", "lodash"]); - }); - - it("should skip scanning when hasDryRunArg returns true", async () => { - mockDryRunNpmCommandAndOutput.mock.resetCalls(); - mockWriteError.mock.resetCalls(); - - const scanner = dryRunScanner(); - const shouldScan = scanner.shouldScan(["install", "--dry-run"]); - - assert.equal(shouldScan, false); - // Should not call dryRunNpmCommandAndOutput since scanning is skipped - assert.equal(mockDryRunNpmCommandAndOutput.mock.callCount(), 0); - }); - - it("should skip scanning when skipScanWhen returns true", async () => { - const scanner = dryRunScanner({ - skipScanWhen: (args) => args.includes("--skip"), - }); - const shouldScan = scanner.shouldScan(["install", "--skip"]); - - assert.equal(shouldScan, false); - }); - - it("should scan when conditions are met", async () => { - const scanner = dryRunScanner(); - const shouldScan = scanner.shouldScan(["install", "lodash"]); - - assert.equal(shouldScan, true); - }); - }); -}); diff --git a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js b/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js deleted file mode 100644 index 3c1e673..0000000 --- a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js +++ /dev/null @@ -1,57 +0,0 @@ -export function parseDryRunOutput(output) { - const lines = output.split(/\r?\n/); - const packageChanges = []; - - for (const line of lines) { - if (line.startsWith("add ")) { - packageChanges.push(parseAdd(line)); - } else if (line.startsWith("remove ")) { - packageChanges.push(parseRemove(line)); - } else if (line.startsWith("change ")) { - packageChanges.push(parseChange(line)); - } - } - - return packageChanges; -} - -function parseAdd(line) { - const splitLine = getLineParts(line); - const packageName = splitLine[1]; - const packageVersion = splitLine[splitLine.length - 1]; - return addedPackage(packageName, packageVersion); -} - -function addedPackage(name, version) { - return { type: "add", name, version }; -} - -function parseRemove(line) { - const splitLine = getLineParts(line); - const packageName = splitLine[1]; - const packageVersion = splitLine[splitLine.length - 1]; - return removedPackage(packageName, packageVersion); -} - -function removedPackage(name, version) { - return { type: "remove", name, version }; -} - -function parseChange(line) { - const splitLine = getLineParts(line); - const packageName = splitLine[1]; - const packageVersion = splitLine[splitLine.length - 1]; - const oldVersion = splitLine[2]; - return changedPackage(packageName, packageVersion, oldVersion); -} - -function getLineParts(line) { - return line - .split(" ") - .map((part) => part.trim()) - .filter((part) => part !== ""); -} - -function changedPackage(name, version, oldVersion) { - return { type: "change", name, version, oldVersion }; -} diff --git a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js b/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js deleted file mode 100644 index cd7c2b1..0000000 --- a/packages/safe-chain/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { parseDryRunOutput } from "./parseNpmInstallDryRunOutput.js"; - -describe("parseNpmInstallDryRunOutput", () => { - it("should parse added packages", () => { - const output = ` -add @jest/transform 29.7.0 -add @jest/test-result 29.7.0 -add @jest/reporters 29.7.0 -add @jest/console 29.7.0 -add jest-cli 29.7.0 -add import-local 3.2.0 -add @jest/types 29.6.3 -add @jest/core 29.7.0 -add jest 29.7.0 - -added 267 packages in 831ms - -32 packages are looking for funding - run \`npm fund\` for details`; - - const expected = [ - { name: "@jest/transform", version: "29.7.0", type: "add" }, - { name: "@jest/test-result", version: "29.7.0", type: "add" }, - { name: "@jest/reporters", version: "29.7.0", type: "add" }, - { name: "@jest/console", version: "29.7.0", type: "add" }, - { name: "jest-cli", version: "29.7.0", type: "add" }, - { name: "import-local", version: "3.2.0", type: "add" }, - { name: "@jest/types", version: "29.6.3", type: "add" }, - { name: "@jest/core", version: "29.7.0", type: "add" }, - { name: "jest", version: "29.7.0", type: "add" }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should parse removed packages", () => { - const output = ` -remove react 19.1.0 - - removed 1 package in 115ms`; - - const expected = [{ name: "react", version: "19.1.0", type: "remove" }]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should parse changed packages", () => { - const output = ` -change react 19.0.0 => 19.1.0 - -changed 1 package in 204ms`; - - const expected = [ - { - name: "react", - version: "19.1.0", - oldVersion: "19.0.0", - type: "change", - }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should parse mixed package changes", () => { - const output = ` -add @jest/transform 29.7.0 -add @jest/test-result 29.7.0 -add @jest/reporters 29.7.0 -add @jest/console 29.7.0 -add jest-cli 29.7.0 -add import-local 3.2.0 -add @jest/types 29.6.3 -add @jest/core 29.7.0 -add jest 29.7.0 -remove react 19.1.0 -change lodash 4.17.0 => 4.18.0 - -removed 1 package in 115ms`; - - const expected = [ - { name: "@jest/transform", version: "29.7.0", type: "add" }, - { name: "@jest/test-result", version: "29.7.0", type: "add" }, - { name: "@jest/reporters", version: "29.7.0", type: "add" }, - { name: "@jest/console", version: "29.7.0", type: "add" }, - { name: "jest-cli", version: "29.7.0", type: "add" }, - { name: "import-local", version: "3.2.0", type: "add" }, - { name: "@jest/types", version: "29.6.3", type: "add" }, - { name: "@jest/core", version: "29.7.0", type: "add" }, - { name: "jest", version: "29.7.0", type: "add" }, - { name: "react", version: "19.1.0", type: "remove" }, - { - name: "lodash", - version: "4.18.0", - oldVersion: "4.17.0", - type: "change", - }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); - - it("should work with npm v22.0.0", () => { - const output = ` -add @jest/types 29.6.3 -add @jest/core 29.7.0 -add jest 29.7.0 - -added 257 packages in 791ms - -44 packages are looking for funding - run \`npm fund\` for details`; - - const expected = [ - { name: "@jest/types", version: "29.6.3", type: "add" }, - { name: "@jest/core", version: "29.7.0", type: "add" }, - { name: "jest", version: "29.7.0", type: "add" }, - ]; - - const result = parseDryRunOutput(output); - - assert.deepEqual(result, expected); - }); -}); diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index c744835..ba836e7 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -62,48 +62,24 @@ describe("E2E: npm coverage", () => { it(`safe-chain blocks download of malicious packages already in package.json`, async () => { const shell = await container.openShell("zsh"); - const npmVersion = (await shell.runCommand("npm --version")).output.trim(); - const majorVersion = parseInt(npmVersion.split(".")[0]); - const minorVersion = parseInt(npmVersion.split(".")[1]); - const isBelow10_4 = - majorVersion < 10 || (majorVersion === 10 && minorVersion < 4); await shell.runCommand( 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' ); var result = await shell.runCommand("npm install"); - if (isBelow10_4) { - 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-test"), - `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}` - ); - } else { - assert.ok( - result.output.includes("Malicious changes detected:"), - `Output did not include expected text. Output was:\n${result.output}` - ); - assert.ok( - result.output.includes("- safe-chain-test"), - `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}` - ); - } + 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-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); }); it("safe-chain blocks npx from executing malicious packages", async () => { From ea92ea0731faa010fcc028210f865ce5a91ce893 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 16:19:38 +0200 Subject: [PATCH 013/345] Remove abbrev package --- packages/safe-chain/package.json | 1 - .../src/packagemanager/npm/utils/cmd-list.js | 363 +++++++++++++++++- 2 files changed, 359 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 42bfb55..98ccd52 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -30,7 +30,6 @@ "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": { - "abbrev": "3.0.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/npm/utils/cmd-list.js b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js index 187204d..8467147 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js @@ -1,7 +1,5 @@ // Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js -import abbrev from "abbrev"; - const commands = [ "access", "adduser", @@ -72,6 +70,365 @@ const commands = [ "whoami", ]; +// This was ran with the abbrev package to generate the abbrevs object below +// console.log(abbrev(commands.concat(Object.keys(aliases)))); +const abbrevs = { + ac: "access", + acc: "access", + acce: "access", + acces: "access", + access: "access", + add: "add", + "add-": "add-user", + "add-u": "add-user", + "add-us": "add-user", + "add-use": "add-user", + "add-user": "add-user", + addu: "adduser", + addus: "adduser", + adduse: "adduser", + adduser: "adduser", + aud: "audit", + audi: "audit", + audit: "audit", + aut: "author", + auth: "author", + autho: "author", + author: "author", + b: "bugs", + bu: "bugs", + bug: "bugs", + bugs: "bugs", + c: "c", + ca: "cache", + cac: "cache", + cach: "cache", + cache: "cache", + ci: "ci", + cit: "cit", + "clean-install": "clean-install", + "clean-install-": "clean-install-test", + "clean-install-t": "clean-install-test", + "clean-install-te": "clean-install-test", + "clean-install-tes": "clean-install-test", + "clean-install-test": "clean-install-test", + com: "completion", + comp: "completion", + compl: "completion", + comple: "completion", + complet: "completion", + completi: "completion", + completio: "completion", + completion: "completion", + con: "config", + conf: "config", + confi: "config", + config: "config", + cr: "create", + cre: "create", + crea: "create", + creat: "create", + create: "create", + dd: "ddp", + ddp: "ddp", + ded: "dedupe", + dedu: "dedupe", + dedup: "dedupe", + dedupe: "dedupe", + dep: "deprecate", + depr: "deprecate", + depre: "deprecate", + deprec: "deprecate", + depreca: "deprecate", + deprecat: "deprecate", + deprecate: "deprecate", + dif: "diff", + diff: "diff", + "dist-tag": "dist-tag", + "dist-tags": "dist-tags", + docs: "docs", + doct: "doctor", + docto: "doctor", + doctor: "doctor", + ed: "edit", + edi: "edit", + edit: "edit", + exe: "exec", + exec: "exec", + expla: "explain", + explai: "explain", + explain: "explain", + explo: "explore", + explor: "explore", + explore: "explore", + find: "find", + "find-": "find-dupes", + "find-d": "find-dupes", + "find-du": "find-dupes", + "find-dup": "find-dupes", + "find-dupe": "find-dupes", + "find-dupes": "find-dupes", + fu: "fund", + fun: "fund", + fund: "fund", + g: "get", + ge: "get", + get: "get", + help: "help", + "help-": "help-search", + "help-s": "help-search", + "help-se": "help-search", + "help-sea": "help-search", + "help-sear": "help-search", + "help-searc": "help-search", + "help-search": "help-search", + hl: "hlep", + hle: "hlep", + hlep: "hlep", + ho: "home", + hom: "home", + home: "home", + i: "i", + ic: "ic", + in: "in", + inf: "info", + info: "info", + ini: "init", + init: "init", + inn: "innit", + inni: "innit", + innit: "innit", + ins: "ins", + inst: "inst", + insta: "insta", + instal: "instal", + install: "install", + "install-ci": "install-ci-test", + "install-ci-": "install-ci-test", + "install-ci-t": "install-ci-test", + "install-ci-te": "install-ci-test", + "install-ci-tes": "install-ci-test", + "install-ci-test": "install-ci-test", + "install-cl": "install-clean", + "install-cle": "install-clean", + "install-clea": "install-clean", + "install-clean": "install-clean", + "install-t": "install-test", + "install-te": "install-test", + "install-tes": "install-test", + "install-test": "install-test", + isnt: "isnt", + isnta: "isnta", + isntal: "isntal", + isntall: "isntall", + "isntall-": "isntall-clean", + "isntall-c": "isntall-clean", + "isntall-cl": "isntall-clean", + "isntall-cle": "isntall-clean", + "isntall-clea": "isntall-clean", + "isntall-clean": "isntall-clean", + iss: "issues", + issu: "issues", + issue: "issues", + issues: "issues", + it: "it", + la: "la", + lin: "link", + link: "link", + lis: "list", + list: "list", + ll: "ll", + ln: "ln", + logi: "login", + login: "login", + logo: "logout", + logou: "logout", + logout: "logout", + ls: "ls", + og: "ogr", + ogr: "ogr", + or: "org", + org: "org", + ou: "outdated", + out: "outdated", + outd: "outdated", + outda: "outdated", + outdat: "outdated", + outdate: "outdated", + outdated: "outdated", + ow: "owner", + own: "owner", + owne: "owner", + owner: "owner", + pa: "pack", + pac: "pack", + pack: "pack", + pi: "ping", + pin: "ping", + ping: "ping", + pk: "pkg", + pkg: "pkg", + pre: "prefix", + pref: "prefix", + prefi: "prefix", + prefix: "prefix", + pro: "profile", + prof: "profile", + profi: "profile", + profil: "profile", + profile: "profile", + pru: "prune", + prun: "prune", + prune: "prune", + pu: "publish", + pub: "publish", + publ: "publish", + publi: "publish", + publis: "publish", + publish: "publish", + q: "query", + qu: "query", + que: "query", + quer: "query", + query: "query", + r: "r", + rb: "rb", + reb: "rebuild", + rebu: "rebuild", + rebui: "rebuild", + rebuil: "rebuild", + rebuild: "rebuild", + rem: "remove", + remo: "remove", + remov: "remove", + remove: "remove", + rep: "repo", + repo: "repo", + res: "restart", + rest: "restart", + resta: "restart", + restar: "restart", + restart: "restart", + rm: "rm", + ro: "root", + roo: "root", + root: "root", + rum: "rum", + run: "run", + "run-": "run-script", + "run-s": "run-script", + "run-sc": "run-script", + "run-scr": "run-script", + "run-scri": "run-script", + "run-scrip": "run-script", + "run-script": "run-script", + s: "s", + sb: "sbom", + sbo: "sbom", + sbom: "sbom", + se: "se", + sea: "search", + sear: "search", + searc: "search", + search: "search", + set: "set", + sho: "show", + show: "show", + shr: "shrinkwrap", + shri: "shrinkwrap", + shrin: "shrinkwrap", + shrink: "shrinkwrap", + shrinkw: "shrinkwrap", + shrinkwr: "shrinkwrap", + shrinkwra: "shrinkwrap", + shrinkwrap: "shrinkwrap", + si: "sit", + sit: "sit", + star: "star", + stars: "stars", + start: "start", + sto: "stop", + stop: "stop", + t: "t", + tea: "team", + team: "team", + tes: "test", + test: "test", + to: "token", + tok: "token", + toke: "token", + token: "token", + ts: "tst", + tst: "tst", + ud: "udpate", + udp: "udpate", + udpa: "udpate", + udpat: "udpate", + udpate: "udpate", + un: "un", + und: "undeprecate", + unde: "undeprecate", + undep: "undeprecate", + undepr: "undeprecate", + undepre: "undeprecate", + undeprec: "undeprecate", + undepreca: "undeprecate", + undeprecat: "undeprecate", + undeprecate: "undeprecate", + uni: "uninstall", + unin: "uninstall", + unins: "uninstall", + uninst: "uninstall", + uninsta: "uninstall", + uninstal: "uninstall", + uninstall: "uninstall", + unl: "unlink", + unli: "unlink", + unlin: "unlink", + unlink: "unlink", + unp: "unpublish", + unpu: "unpublish", + unpub: "unpublish", + unpubl: "unpublish", + unpubli: "unpublish", + unpublis: "unpublish", + unpublish: "unpublish", + uns: "unstar", + unst: "unstar", + unsta: "unstar", + unstar: "unstar", + up: "up", + upd: "update", + upda: "update", + updat: "update", + update: "update", + upg: "upgrade", + upgr: "upgrade", + upgra: "upgrade", + upgrad: "upgrade", + upgrade: "upgrade", + ur: "urn", + urn: "urn", + v: "v", + veri: "verison", + veris: "verison", + veriso: "verison", + verison: "verison", + vers: "version", + versi: "version", + versio: "version", + version: "version", + vi: "view", + vie: "view", + view: "view", + who: "whoami", + whoa: "whoami", + whoam: "whoami", + whoami: "whoami", + why: "why", + x: "x", +}; + // These must resolve to an entry in commands const aliases = { // aliases @@ -158,8 +515,6 @@ export function deref(c) { return aliases[c]; } - const abbrevs = abbrev(commands.concat(Object.keys(aliases))); - // first deref the abbrev, if there is one // then resolve any aliases // so `npm install-cl` will resolve to `install-clean` then to `ci` From 4be412483e2f1c69cb3864e5173334391f490f09 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 10 Oct 2025 16:20:56 +0200 Subject: [PATCH 014/345] Also push new lockfile --- package-lock.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a8f765..88e9fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -411,15 +411,6 @@ "node": ">=14" } }, - "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -1731,7 +1722,6 @@ "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { - "abbrev": "3.0.1", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "make-fetch-happen": "14.0.3", From 8ed2330a3cf7747a6cf657fc5289cfc9bb52183e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 13 Oct 2025 15:49:42 +0200 Subject: [PATCH 015/345] Allow the safe-chain to act as a regular http proxy too (besides the CONNECT tunneling implementation) --- .../src/registryProxy/plainHttpProxy.js | 58 +++++++++++++++++++ .../src/registryProxy/registryProxy.js | 9 +-- 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/plainHttpProxy.js diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js new file mode 100644 index 0000000..68a2362 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -0,0 +1,58 @@ +import * as http from "http"; +import * as https from "https"; + +export function handleHttpProxyRequest(req, res) { + const url = new URL(req.url); + + let protocol; + if (url.protocol === "http:") { + protocol = http; + } else if (url.protocol === "https:") { + protocol = https; + } else { + res.writeHead(502); + res.end(`Bad Gateway: Unsupported protocol ${url.protocol}`); + return; + } + + const proxyRequest = protocol + .request( + req.url, + { method: req.method, headers: req.headers }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + + proxyRes.on("error", () => { + // Stream error while piping response + // Response headers already sent, can't send error status + }); + } + ) + .on("error", (err) => { + res.writeHead(502); + res.end(`Bad Gateway: ${err.message}`); + }); + + req.on("error", () => { + // Client request stream error + // Abort the proxy request + proxyRequest.destroy(); + }); + + res.on("error", () => { + // Client response stream error (client disconnected) + // Clean up proxy streams + proxyRequest.destroy(); + }); + + res.on("close", () => { + // Client disconnected + // Abort the proxy request to avoid unnecessary work + if (!res.writableEnded) { + proxyRequest.destroy(); + } + }); + + req.pipe(proxyRequest); +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3558673..2895753 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -1,6 +1,7 @@ import * as http from "http"; import { tunnelRequest } from "./tunnelRequestHandler.js"; 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"; @@ -54,13 +55,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { } function createProxyServer() { - const server = http.createServer((_, res) => { - res.writeHead(400, "Bad Request"); - res.write( - "Safe-chain proxy: Direct http not supported. Only CONNECT requests are allowed." - ); - res.end(); - }); + const server = http.createServer(handleHttpProxyRequest); return server; } From d2c155afeea4738370eacc8839a90108d16d4607 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 12:55:56 +0200 Subject: [PATCH 016/345] Add e2e test for registry over http --- test/e2e/DockerTestContainer.js | 20 +++++++++++++++++ test/e2e/safe-chain-proxy.e2e.spec.js | 31 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 483f03a..1a817eb 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -60,6 +60,26 @@ export class DockerTestContainer { } } + dockerExec(command, daemon = false) { + if (!this.isRunning) { + throw new Error("Container is not running"); + } + + try { + const dockerExecCommand = `docker exec ${daemon ? "-d " : " "}${ + this.containerName + } bash -c "${command}"`; + const output = execSync(dockerExecCommand, { + encoding: "utf-8", + stdio: "pipe", + timeout: 10000, + }); + return output; + } catch (error) { + throw new Error(`Failed to execute command: ${error.message}`); + } + } + async openShell(shell) { let ptyProcess = pty.spawn( "docker", diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 6abbb0f..3efd2aa 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -57,4 +57,35 @@ describe("E2E: Safe chain proxy", () => { "Proxy log does not contain expected entries" ); }); + + it(`safe-chain proxy allows to request through a local http registry`, async () => { + // Start a local npm registry (verdaccio) inside the container + container.dockerExec("npx -y verdaccio", true); + + // Wait for verdaccio to be ready (max 30 seconds) + for (let i = 0; i < 60; i++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const curlOutput = container.dockerExec( + "curl -I http://localhost:4873/" + ); + if (curlOutput.includes("200 OK")) { + break; + } + } catch { + // ignore, this means docker exec returned -1 and verdaccio is not yet ready + } + } + + const shell = await container.openShell("bash"); + const result = await shell.runCommand( + "npm --registry http://localhost:4873 install react" + ); + + // Check if the installation was successful + assert( + result.output.includes("added"), + "npm install did not complete successfully, output: " + result.output + ); + }); }); From f4933b08d00f82dc6089ce26d3c0c64aca937a8d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:15:14 +0200 Subject: [PATCH 017/345] Add log to diagnose e2e tests --- test/e2e/DockerTestContainer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 1a817eb..196afdb 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -116,6 +116,8 @@ export class DockerTestContainer { const timeout = setTimeout(() => { // Fallback in case the command doesn't finish in a reasonable time + // oxlint-disable-next-line no-console - having this log in CI helps diagnose issues + console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); }, 10000); From 2968960b41f691141e3b915443e794cb1086e902 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:22:58 +0200 Subject: [PATCH 018/345] Cleanup registryProxy, increase timeout on DockerTestContainer --- packages/safe-chain/src/registryProxy/registryProxy.js | 8 ++++++-- test/e2e/DockerTestContainer.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 2895753..d548999 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -16,7 +16,6 @@ const state = { export function createSafeChainProxy() { const server = createProxyServer(); - server.on("connect", handleConnect); return { startServer: () => startServer(server), @@ -55,7 +54,12 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { } function createProxyServer() { - const server = http.createServer(handleHttpProxyRequest); + const server = http.createServer( + handleHttpProxyRequest // This handles plain HTTP requests + ); + + // This handles HTTPS requests via the CONNECT method + server.on("connect", handleConnect); return server; } diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 196afdb..45b66d0 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -120,7 +120,7 @@ export class DockerTestContainer { console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); - }, 10000); + }, 20000); function handleInput(data) { allData.push(data); From b6c31e1a5a1168eff0f3a40ac479c61d7aaf0ca4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:30:06 +0200 Subject: [PATCH 019/345] Increase time to start verdaccio --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 3efd2aa..8a62052 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,8 +62,8 @@ describe("E2E: Safe chain proxy", () => { // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio", true); - // Wait for verdaccio to be ready (max 30 seconds) - for (let i = 0; i < 60; i++) { + // Wait for verdaccio to be ready (max 60 seconds) + for (let i = 0; i < 120; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( From c50eac977bbdfa371852e21f20674d1b50f52ce7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:34:47 +0200 Subject: [PATCH 020/345] Throw when verdaccio did not start --- test/e2e/safe-chain-proxy.e2e.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 8a62052..11d01f7 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,6 +62,7 @@ describe("E2E: Safe chain proxy", () => { // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio", true); + let verdaccioStarted = false; // Wait for verdaccio to be ready (max 60 seconds) for (let i = 0; i < 120; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); @@ -70,12 +71,16 @@ describe("E2E: Safe chain proxy", () => { "curl -I http://localhost:4873/" ); if (curlOutput.includes("200 OK")) { + verdaccioStarted = true; break; } } catch { // ignore, this means docker exec returned -1 and verdaccio is not yet ready } } + if (!verdaccioStarted) { + throw new Error("Verdaccio did not start in time"); + } const shell = await container.openShell("bash"); const result = await shell.runCommand( From 37585e80735f5c06192bb462cb9052936d087769 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:44:49 +0200 Subject: [PATCH 021/345] Add more logs, handle verdaccio not starting better --- test/e2e/safe-chain-proxy.e2e.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 11d01f7..12929df 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -72,6 +72,7 @@ describe("E2E: Safe chain proxy", () => { ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; + console.log("Verdaccio started, after " + i * 500 + "ms"); break; } } catch { @@ -79,10 +80,10 @@ describe("E2E: Safe chain proxy", () => { } } if (!verdaccioStarted) { - throw new Error("Verdaccio did not start in time"); + assert.fail("Verdaccio did not start in time"); } - const shell = await container.openShell("bash"); + const shell = await container.openShell("zsh"); const result = await shell.runCommand( "npm --registry http://localhost:4873 install react" ); From f655e8cfcb786754b81e4d1b5f4b022894e1b128 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 13:52:28 +0200 Subject: [PATCH 022/345] Change command to install through registry. --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 12929df..363787f 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -83,9 +83,9 @@ describe("E2E: Safe chain proxy", () => { assert.fail("Verdaccio did not start in time"); } - const shell = await container.openShell("zsh"); + const shell = await container.openShell("bash"); const result = await shell.runCommand( - "npm --registry http://localhost:4873 install react" + "npm install lodash --registry=http://localhost:4873" ); // Check if the installation was successful From 35beeb55b09ff9822a6bfccdd50ab73e2bd894c6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 14:10:23 +0200 Subject: [PATCH 023/345] Curl url with npm package --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 363787f..be0d6ea 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -68,7 +68,7 @@ describe("E2E: Safe chain proxy", () => { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/" + "curl -I http://localhost:4873/lodash" ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; @@ -88,6 +88,8 @@ describe("E2E: Safe chain proxy", () => { "npm install lodash --registry=http://localhost:4873" ); + console.log("NPM install output:", result.output); + // Check if the installation was successful assert( result.output.includes("added"), From a2d05b0cf057cedd93bde112a3f2ee082900274f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 14:18:33 +0200 Subject: [PATCH 024/345] More logs --- packages/safe-chain/src/registryProxy/plainHttpProxy.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 68a2362..507f518 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,8 +1,10 @@ import * as http from "http"; import * as https from "https"; +// oxlint-disable no-console - just for testing, remove afterwards export function handleHttpProxyRequest(req, res) { const url = new URL(req.url); + console.log(`Proxying request to: ${req.url}`); let protocol; if (url.protocol === "http:") { @@ -23,7 +25,8 @@ export function handleHttpProxyRequest(req, res) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); - proxyRes.on("error", () => { + proxyRes.on("error", (err) => { + console.log("Error in proxy response stream:", err); // Stream error while piping response // Response headers already sent, can't send error status }); @@ -35,18 +38,21 @@ export function handleHttpProxyRequest(req, res) { }); req.on("error", () => { + console.log("Error in client request stream"); // Client request stream error // Abort the proxy request proxyRequest.destroy(); }); res.on("error", () => { + console.log("Error in client response stream"); // Client response stream error (client disconnected) // Clean up proxy streams proxyRequest.destroy(); }); res.on("close", () => { + console.log("Client response stream closed"); // Client disconnected // Abort the proxy request to avoid unnecessary work if (!res.writableEnded) { From ee82134c19bad03b75c411929d38ebe052677c05 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 14:54:58 +0200 Subject: [PATCH 025/345] Proxyres on close and end --- .../src/registryProxy/plainHttpProxy.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 507f518..214ad0f 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -30,6 +30,22 @@ export function handleHttpProxyRequest(req, res) { // Stream error while piping response // Response headers already sent, can't send error status }); + + proxyRes.on("close", () => { + console.log("Proxy response stream closed"); + // Clean up if the proxy response stream closes + if (!res.writableEnded) { + res.end(); + } + }); + + proxyRes.on("end", () => { + console.log("Proxy response stream ended"); + // End of proxy response + if (!res.writableEnded) { + res.end(); + } + }); } ) .on("error", (err) => { From daf69964f2891ea878e6c255ae69e3ede6d3fd51 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:00:00 +0200 Subject: [PATCH 026/345] Test without safe-chain --- test/e2e/safe-chain-proxy.e2e.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index be0d6ea..0e34db0 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,6 +62,9 @@ describe("E2E: Safe chain proxy", () => { // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio", true); + const shell1 = await container.openShell("bash"); + await shell1.runCommand("safe-chain teardown"); + let verdaccioStarted = false; // Wait for verdaccio to be ready (max 60 seconds) for (let i = 0; i < 120; i++) { From bfe5820d0fbef2492ddac593acaa1352e748ea12 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:16:57 +0200 Subject: [PATCH 027/345] Log even more --- test/e2e/safe-chain-proxy.e2e.spec.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 0e34db0..8e602ca 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -59,14 +59,21 @@ describe("E2E: Safe chain proxy", () => { }); it(`safe-chain proxy allows to request through a local http registry`, async () => { + const configShell = await container.openShell("bash"); + await configShell.runCommand("touch ~/.verdaccio-config.yaml"); + await configShell.runCommand("echo 'log:' >> ~/.verdaccio-config.yaml"); + await configShell.runCommand( + "echo ' type: file' >> ~/.verdaccio-config.yaml" + ); + await configShell.runCommand( + "echo ' path: /verdaccio.log' >> ~/.verdaccio-config.yaml" + ); + // Start a local npm registry (verdaccio) inside the container - container.dockerExec("npx -y verdaccio", true); - - const shell1 = await container.openShell("bash"); - await shell1.runCommand("safe-chain teardown"); + container.dockerExec("npx -y verdaccio -c ~/.verdaccio-config.yaml", true); + // Polling until verdaccio is ready (max 60 seconds) let verdaccioStarted = false; - // Wait for verdaccio to be ready (max 60 seconds) for (let i = 0; i < 120; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { @@ -75,7 +82,7 @@ describe("E2E: Safe chain proxy", () => { ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; - console.log("Verdaccio started, after " + i * 500 + "ms"); + console.log("Verdaccio started, after " + i * 500 + "ms", curlOutput); break; } } catch { @@ -93,6 +100,13 @@ describe("E2E: Safe chain proxy", () => { console.log("NPM install output:", result.output); + const verdaccioLog = await container.openShell("bash"); + const { output: logOutput } = await verdaccioLog.runCommand( + "cat /verdaccio.log" + ); + + console.log("Verdaccio log output:", logOutput); + // Check if the installation was successful assert( result.output.includes("added"), From dfdce18c8ddcc2b04abf477f1bbd013aa00d0591 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:23:40 +0200 Subject: [PATCH 028/345] Fix config --- test/e2e/safe-chain-proxy.e2e.spec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 8e602ca..a6aadd8 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -61,12 +61,14 @@ describe("E2E: Safe chain proxy", () => { it(`safe-chain proxy allows to request through a local http registry`, async () => { const configShell = await container.openShell("bash"); await configShell.runCommand("touch ~/.verdaccio-config.yaml"); - await configShell.runCommand("echo 'log:' >> ~/.verdaccio-config.yaml"); + // verdaccio.yaml + // storage: ./storage + // log: { type: file, path: ./verdaccio.log, level: info } await configShell.runCommand( - "echo ' type: file' >> ~/.verdaccio-config.yaml" + "echo 'log: { type: file, path: /verdaccio.log, level: info }' >> ~/.verdaccio-config.yaml" ); await configShell.runCommand( - "echo ' path: /verdaccio.log' >> ~/.verdaccio-config.yaml" + "echo 'storage: ./storage' >> ~/.verdaccio-config.yaml" ); // Start a local npm registry (verdaccio) inside the container From 4c76242d443d3324c94b371deebefa423885c5e9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:25:10 +0200 Subject: [PATCH 029/345] More config --- test/e2e/safe-chain-proxy.e2e.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index a6aadd8..8b2d56b 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -65,7 +65,7 @@ describe("E2E: Safe chain proxy", () => { // storage: ./storage // log: { type: file, path: ./verdaccio.log, level: info } await configShell.runCommand( - "echo 'log: { type: file, path: /verdaccio.log, level: info }' >> ~/.verdaccio-config.yaml" + "echo 'log: { type: file, path: /verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml" ); await configShell.runCommand( "echo 'storage: ./storage' >> ~/.verdaccio-config.yaml" From b794b293d136d02f4ec908936edabfd705fc5c41 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:32:13 +0200 Subject: [PATCH 030/345] Fix config --- test/e2e/safe-chain-proxy.e2e.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 8b2d56b..51d5dd4 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -65,14 +65,14 @@ describe("E2E: Safe chain proxy", () => { // storage: ./storage // log: { type: file, path: ./verdaccio.log, level: info } await configShell.runCommand( - "echo 'log: { type: file, path: /verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml" + "echo 'storage: ./storage' >> ~/verdaccio-config.yaml" ); await configShell.runCommand( - "echo 'storage: ./storage' >> ~/.verdaccio-config.yaml" + "echo 'log: { type: file, path: /verdaccio.log, level: trace }' >> ~/verdaccio-config.yaml" ); // Start a local npm registry (verdaccio) inside the container - container.dockerExec("npx -y verdaccio -c ~/.verdaccio-config.yaml", true); + container.dockerExec("npx -y verdaccio -c ~/verdaccio-config.yaml", true); // Polling until verdaccio is ready (max 60 seconds) let verdaccioStarted = false; From 23bce71356fd68aa003e5032c76ba3626f6b9fbc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:40:08 +0200 Subject: [PATCH 031/345] Fix config 2 --- test/e2e/safe-chain-proxy.e2e.spec.js | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 51d5dd4..42eadfb 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -62,13 +62,37 @@ describe("E2E: Safe chain proxy", () => { const configShell = await container.openShell("bash"); await configShell.runCommand("touch ~/.verdaccio-config.yaml"); // verdaccio.yaml - // storage: ./storage - // log: { type: file, path: ./verdaccio.log, level: info } + /* +storage: ./storage +uplinks: + npmjs: + url: https://registry.npmjs.org/ +packages: + "**": + access: $all + proxy: npmjs +log: { type: file, path: ./verdaccio.log, level: trace, colors: false } + */ await configShell.runCommand( - "echo 'storage: ./storage' >> ~/verdaccio-config.yaml" + `echo 'storage: ./storage' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand(`echo 'uplinks:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand(`echo ' npmjs:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' url: https://registry.npmjs.org/' >> ~/.verdaccio-config.yaml` ); await configShell.runCommand( - "echo 'log: { type: file, path: /verdaccio.log, level: trace }' >> ~/verdaccio-config.yaml" + `echo 'packages:' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand(`echo ' "**":' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' access: $all' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo ' proxy: npmjs' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml` ); // Start a local npm registry (verdaccio) inside the container From 7ae4d3bc8d817253ea9b30c712c3c035d76b7ba6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 15:59:43 +0200 Subject: [PATCH 032/345] Try some more config --- test/e2e/safe-chain-proxy.e2e.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 42eadfb..a81224e 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -76,6 +76,16 @@ log: { type: file, path: ./verdaccio.log, level: trace, colors: false } await configShell.runCommand( `echo 'storage: ./storage' >> ~/.verdaccio-config.yaml` ); + await configShell.runCommand(`echo 'auth:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' htpasswd:' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo ' file: ./htpasswd' >> ~/.verdaccio-config.yaml` + ); + await configShell.runCommand( + `echo ' max_users: 100' >> ~/.verdaccio-config.yaml` + ); await configShell.runCommand(`echo 'uplinks:' >> ~/.verdaccio-config.yaml`); await configShell.runCommand(`echo ' npmjs:' >> ~/.verdaccio-config.yaml`); await configShell.runCommand( From 93223fe64012c1870574e60747d64284ec9a8e4d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 16:00:31 +0200 Subject: [PATCH 033/345] Try more config --- test/e2e/safe-chain-proxy.e2e.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index a81224e..dbc6522 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -106,7 +106,10 @@ log: { type: file, path: ./verdaccio.log, level: trace, colors: false } ); // Start a local npm registry (verdaccio) inside the container - container.dockerExec("npx -y verdaccio -c ~/verdaccio-config.yaml", true); + container.dockerExec( + "npx -y verdaccio --listen 4873 -c ~/verdaccio-config.yaml", + true + ); // Polling until verdaccio is ready (max 60 seconds) let verdaccioStarted = false; From d35a4ca3572cb20e7cf14103143fa7ca50f7ffc0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 16:05:39 +0200 Subject: [PATCH 034/345] Change config location --- test/e2e/safe-chain-proxy.e2e.spec.js | 36 +++++++++++++-------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index dbc6522..a1ffe53 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -60,7 +60,7 @@ describe("E2E: Safe chain proxy", () => { it(`safe-chain proxy allows to request through a local http registry`, async () => { const configShell = await container.openShell("bash"); - await configShell.runCommand("touch ~/.verdaccio-config.yaml"); + await configShell.runCommand("touch /.verdaccio-config.yaml"); // verdaccio.yaml /* storage: ./storage @@ -74,40 +74,38 @@ packages: log: { type: file, path: ./verdaccio.log, level: trace, colors: false } */ await configShell.runCommand( - `echo 'storage: ./storage' >> ~/.verdaccio-config.yaml` + `echo 'storage: ./storage' >> /.verdaccio-config.yaml` ); - await configShell.runCommand(`echo 'auth:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); await configShell.runCommand( - `echo ' htpasswd:' >> ~/.verdaccio-config.yaml` + `echo ' htpasswd:' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo ' file: ./htpasswd' >> ~/.verdaccio-config.yaml` + `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo ' max_users: 100' >> ~/.verdaccio-config.yaml` + `echo ' max_users: 100' >> /.verdaccio-config.yaml` ); - await configShell.runCommand(`echo 'uplinks:' >> ~/.verdaccio-config.yaml`); - await configShell.runCommand(`echo ' npmjs:' >> ~/.verdaccio-config.yaml`); + await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); + await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); await configShell.runCommand( - `echo ' url: https://registry.npmjs.org/' >> ~/.verdaccio-config.yaml` + `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` + ); + await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); + await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); + await configShell.runCommand( + `echo ' access: $all' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo 'packages:' >> ~/.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo ' "**":' >> ~/.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' access: $all' >> ~/.verdaccio-config.yaml` + `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` ); await configShell.runCommand( - `echo ' proxy: npmjs' >> ~/.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> ~/.verdaccio-config.yaml` + `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` ); // Start a local npm registry (verdaccio) inside the container container.dockerExec( - "npx -y verdaccio --listen 4873 -c ~/verdaccio-config.yaml", + "npx -y verdaccio --listen 4873 -c /verdaccio-config.yaml", true ); From b567016ddd0023260736a7edad5b2c4f5efdd11c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 14 Oct 2025 16:11:34 +0200 Subject: [PATCH 035/345] Simplify test --- test/e2e/safe-chain-proxy.e2e.spec.js | 103 +++++++++++++------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index a1ffe53..0c21f88 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -60,58 +60,55 @@ describe("E2E: Safe chain proxy", () => { it(`safe-chain proxy allows to request through a local http registry`, async () => { const configShell = await container.openShell("bash"); - await configShell.runCommand("touch /.verdaccio-config.yaml"); - // verdaccio.yaml - /* -storage: ./storage -uplinks: - npmjs: - url: https://registry.npmjs.org/ -packages: - "**": - access: $all - proxy: npmjs -log: { type: file, path: ./verdaccio.log, level: trace, colors: false } - */ - await configShell.runCommand( - `echo 'storage: ./storage' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' htpasswd:' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo ' max_users: 100' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); - await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); - await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); - await configShell.runCommand( - `echo ' access: $all' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` - ); - await configShell.runCommand( - `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` - ); + // await configShell.runCommand("touch /.verdaccio-config.yaml"); + // // verdaccio.yaml + // /* + // storage: ./storage + // uplinks: + // npmjs: + // url: https://registry.npmjs.org/ + // packages: + // "**": + // access: $all + // proxy: npmjs + // log: { type: file, path: ./verdaccio.log, level: trace, colors: false } + // */ + // await configShell.runCommand( + // `echo 'storage: ./storage' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand( + // `echo ' htpasswd:' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo ' max_users: 100' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand( + // `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); + // await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); + // await configShell.runCommand( + // `echo ' access: $all' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` + // ); + // await configShell.runCommand( + // `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` + // ); // Start a local npm registry (verdaccio) inside the container - container.dockerExec( - "npx -y verdaccio --listen 4873 -c /verdaccio-config.yaml", - true - ); + container.dockerExec("npx -y verdaccio --listen 4873", true); - // Polling until verdaccio is ready (max 60 seconds) + // Polling until verdaccio is ready (max 30 seconds) let verdaccioStarted = false; - for (let i = 0; i < 120; i++) { + for (let i = 0; i < 30; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( @@ -137,12 +134,12 @@ log: { type: file, path: ./verdaccio.log, level: trace, colors: false } console.log("NPM install output:", result.output); - const verdaccioLog = await container.openShell("bash"); - const { output: logOutput } = await verdaccioLog.runCommand( - "cat /verdaccio.log" - ); + // const verdaccioLog = await container.openShell("bash"); + // const { output: logOutput } = await verdaccioLog.runCommand( + // "cat /verdaccio.log" + // ); - console.log("Verdaccio log output:", logOutput); + // console.log("Verdaccio log output:", logOutput); // Check if the installation was successful assert( From 24bda852d0e168a5b24fe27b09a7cfbc4451bb8f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:42:16 +0200 Subject: [PATCH 036/345] Redo test - start simple --- test/e2e/safe-chain-proxy.e2e.spec.js | 79 ++++++--------------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 0c21f88..518390c 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -59,51 +59,6 @@ describe("E2E: Safe chain proxy", () => { }); it(`safe-chain proxy allows to request through a local http registry`, async () => { - const configShell = await container.openShell("bash"); - // await configShell.runCommand("touch /.verdaccio-config.yaml"); - // // verdaccio.yaml - // /* - // storage: ./storage - // uplinks: - // npmjs: - // url: https://registry.npmjs.org/ - // packages: - // "**": - // access: $all - // proxy: npmjs - // log: { type: file, path: ./verdaccio.log, level: trace, colors: false } - // */ - // await configShell.runCommand( - // `echo 'storage: ./storage' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand(`echo 'auth:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand( - // `echo ' htpasswd:' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo ' file: ./htpasswd' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo ' max_users: 100' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand(`echo 'uplinks:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand(`echo ' npmjs:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand( - // `echo ' url: https://registry.npmjs.org/' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand(`echo 'packages:' >> /.verdaccio-config.yaml`); - // await configShell.runCommand(`echo ' "**":' >> /.verdaccio-config.yaml`); - // await configShell.runCommand( - // `echo ' access: $all' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo ' proxy: npmjs' >> /.verdaccio-config.yaml` - // ); - // await configShell.runCommand( - // `echo 'log: { type: file, path: ./verdaccio.log, level: trace, colors: false }' >> /.verdaccio-config.yaml` - // ); - - // Start a local npm registry (verdaccio) inside the container container.dockerExec("npx -y verdaccio --listen 4873", true); // Polling until verdaccio is ready (max 30 seconds) @@ -112,7 +67,7 @@ describe("E2E: Safe chain proxy", () => { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/lodash" + "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; @@ -127,24 +82,24 @@ describe("E2E: Safe chain proxy", () => { assert.fail("Verdaccio did not start in time"); } - const shell = await container.openShell("bash"); - const result = await shell.runCommand( - "npm install lodash --registry=http://localhost:4873" - ); - - console.log("NPM install output:", result.output); - - // const verdaccioLog = await container.openShell("bash"); - // const { output: logOutput } = await verdaccioLog.runCommand( - // "cat /verdaccio.log" + // const shell = await container.openShell("bash"); + // const result = await shell.runCommand( + // "npm install lodash --registry=http://localhost:4873" // ); - // console.log("Verdaccio log output:", logOutput); + // console.log("NPM install output:", result.output); - // Check if the installation was successful - assert( - result.output.includes("added"), - "npm install did not complete successfully, output: " + result.output - ); + // // const verdaccioLog = await container.openShell("bash"); + // // const { output: logOutput } = await verdaccioLog.runCommand( + // // "cat /verdaccio.log" + // // ); + + // // console.log("Verdaccio log output:", logOutput); + + // // Check if the installation was successful + // assert( + // result.output.includes("added"), + // "npm install did not complete successfully, output: " + result.output + // ); }); }); From b4f7d845631e9b52c53b888827c0d70aaed07fdd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:50:13 +0200 Subject: [PATCH 037/345] Run npm install command --- test/e2e/package.json | 2 +- test/e2e/safe-chain-proxy.e2e.spec.js | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/test/e2e/package.json b/test/e2e/package.json index 9217808..b34fd0b 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "description": "End-to-end tests for the Aikido Safe Chain", "scripts": { - "test": "node --test --test-concurrency=1 **/*.spec.js" + "test": "node --test --test-concurrency=1 **/safe-chain-proxy.e2e.spec.js" }, "keywords": [], "author": "Aikido Security", diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 518390c..fb4b61d 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -63,7 +63,7 @@ describe("E2E: Safe chain proxy", () => { // Polling until verdaccio is ready (max 30 seconds) let verdaccioStarted = false; - for (let i = 0; i < 30; i++) { + for (let i = 0; i < 60; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); try { const curlOutput = container.dockerExec( @@ -71,7 +71,10 @@ describe("E2E: Safe chain proxy", () => { ); if (curlOutput.includes("200 OK")) { verdaccioStarted = true; - console.log("Verdaccio started, after " + i * 500 + "ms", curlOutput); + console.log( + "Verdaccio started, after " + i * 500 + "ms\n", + curlOutput + ); break; } } catch { @@ -82,19 +85,10 @@ describe("E2E: Safe chain proxy", () => { assert.fail("Verdaccio did not start in time"); } - // const shell = await container.openShell("bash"); - // const result = await shell.runCommand( - // "npm install lodash --registry=http://localhost:4873" - // ); + const shell = await container.openShell("bash"); + const result = await shell.runCommand("npm install lodash"); - // console.log("NPM install output:", result.output); - - // // const verdaccioLog = await container.openShell("bash"); - // // const { output: logOutput } = await verdaccioLog.runCommand( - // // "cat /verdaccio.log" - // // ); - - // // console.log("Verdaccio log output:", logOutput); + console.log("NPM install output:\n", result.output); // // Check if the installation was successful // assert( From 1a8d58889c410c82b4f9e51a1f28f2b221b10a9a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:50:56 +0200 Subject: [PATCH 038/345] Try again --- test/e2e/safe-chain-proxy.e2e.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index fb4b61d..c475fdf 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -90,6 +90,12 @@ describe("E2E: Safe chain proxy", () => { console.log("NPM install output:\n", result.output); + const curlOutput = container.dockerExec( + "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" + ); + + console.log("Curl output:\n", curlOutput); + // // Check if the installation was successful // assert( // result.output.includes("added"), From 1f2d4e86c7de83c5ca1b00e63b0467fa3c2dce4d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 07:54:35 +0200 Subject: [PATCH 039/345] Add registry to localhost again --- test/e2e/safe-chain-proxy.e2e.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index c475fdf..276d5e8 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -86,7 +86,9 @@ describe("E2E: Safe chain proxy", () => { } const shell = await container.openShell("bash"); - const result = await shell.runCommand("npm install lodash"); + const result = await shell.runCommand( + "npm install lodash --registry http://localhost:4873" + ); console.log("NPM install output:\n", result.output); From 3aec4737550a4c3e3cb6fce989173836c578f65b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 08:50:13 +0200 Subject: [PATCH 040/345] Without safe-chain --- test/e2e/safe-chain-proxy.e2e.spec.js | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 276d5e8..ffa1e79 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -14,8 +14,8 @@ describe("E2E: Safe chain proxy", () => { container = new DockerTestContainer(); await container.start(); - const installationShell = await container.openShell("zsh"); - await installationShell.runCommand("safe-chain setup"); + // const installationShell = await container.openShell("zsh"); + // await installationShell.runCommand("safe-chain setup"); }); afterEach(async () => { @@ -26,37 +26,37 @@ describe("E2E: Safe chain proxy", () => { } }); - it(`safe-chain proxy respects upstream proxy settings`, async () => { - // Configure and start a proxy inside the container - const proxy = await container.openShell("zsh"); - await proxy.runCommand( - `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` - ); - await proxy.runCommand("tinyproxy"); + // it(`safe-chain proxy respects upstream proxy settings`, async () => { + // // Configure and start a proxy inside the container + // const proxy = await container.openShell("zsh"); + // await proxy.runCommand( + // `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` + // ); + // await proxy.runCommand("tinyproxy"); - const shell = await container.openShell("zsh"); - await shell.runCommand( - 'export HTTPS_PROXY="http://user:password@localhost:8888"' - ); - const { output } = await shell.runCommand("npm install axios"); + // const shell = await container.openShell("zsh"); + // await shell.runCommand( + // 'export HTTPS_PROXY="http://user:password@localhost:8888"' + // ); + // const { output } = await shell.runCommand("npm install axios"); - // Check if the installation was successful - assert( - output.includes("added") || output.includes("up to date"), - "npm install did not complete successfully" - ); + // // Check if the installation was successful + // assert( + // output.includes("added") || output.includes("up to date"), + // "npm install did not complete successfully" + // ); - const proxyLog = await container.openShell("zsh"); - const { output: logOutput } = await proxyLog.runCommand( - "cat /var/log/tinyproxy/tinyproxy.log" - ); + // const proxyLog = await container.openShell("zsh"); + // const { output: logOutput } = await proxyLog.runCommand( + // "cat /var/log/tinyproxy/tinyproxy.log" + // ); - // Check if the proxy log contains entries for the npm install - assert( - logOutput.includes("CONNECT registry.npmjs.org:443"), - "Proxy log does not contain expected entries" - ); - }); + // // Check if the proxy log contains entries for the npm install + // assert( + // logOutput.includes("CONNECT registry.npmjs.org:443"), + // "Proxy log does not contain expected entries" + // ); + // }); it(`safe-chain proxy allows to request through a local http registry`, async () => { container.dockerExec("npx -y verdaccio --listen 4873", true); From 056a1963e3be19837e0b99d0e7f8b60670f323c5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 09:18:11 +0200 Subject: [PATCH 041/345] Remove test again --- test/e2e/safe-chain-proxy.e2e.spec.js | 95 +++++++-------------------- 1 file changed, 24 insertions(+), 71 deletions(-) diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index ffa1e79..22a7038 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -26,82 +26,35 @@ describe("E2E: Safe chain proxy", () => { } }); - // it(`safe-chain proxy respects upstream proxy settings`, async () => { - // // Configure and start a proxy inside the container - // const proxy = await container.openShell("zsh"); - // await proxy.runCommand( - // `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` - // ); - // await proxy.runCommand("tinyproxy"); + it(`safe-chain proxy respects upstream proxy settings`, async () => { + // Configure and start a proxy inside the container + const proxy = await container.openShell("zsh"); + await proxy.runCommand( + `echo 'BasicAuth user password' >> /etc/tinyproxy/tinyproxy.conf` + ); + await proxy.runCommand("tinyproxy"); - // const shell = await container.openShell("zsh"); - // await shell.runCommand( - // 'export HTTPS_PROXY="http://user:password@localhost:8888"' - // ); - // const { output } = await shell.runCommand("npm install axios"); + const shell = await container.openShell("zsh"); + await shell.runCommand( + 'export HTTPS_PROXY="http://user:password@localhost:8888"' + ); + const { output } = await shell.runCommand("npm install axios"); - // // Check if the installation was successful - // assert( - // output.includes("added") || output.includes("up to date"), - // "npm install did not complete successfully" - // ); - - // const proxyLog = await container.openShell("zsh"); - // const { output: logOutput } = await proxyLog.runCommand( - // "cat /var/log/tinyproxy/tinyproxy.log" - // ); - - // // Check if the proxy log contains entries for the npm install - // assert( - // logOutput.includes("CONNECT registry.npmjs.org:443"), - // "Proxy log does not contain expected entries" - // ); - // }); - - it(`safe-chain proxy allows to request through a local http registry`, async () => { - container.dockerExec("npx -y verdaccio --listen 4873", true); - - // Polling until verdaccio is ready (max 30 seconds) - let verdaccioStarted = false; - for (let i = 0; i < 60; i++) { - await new Promise((resolve) => setTimeout(resolve, 500)); - try { - const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" - ); - if (curlOutput.includes("200 OK")) { - verdaccioStarted = true; - console.log( - "Verdaccio started, after " + i * 500 + "ms\n", - curlOutput - ); - break; - } - } catch { - // ignore, this means docker exec returned -1 and verdaccio is not yet ready - } - } - if (!verdaccioStarted) { - assert.fail("Verdaccio did not start in time"); - } - - const shell = await container.openShell("bash"); - const result = await shell.runCommand( - "npm install lodash --registry http://localhost:4873" + // Check if the installation was successful + assert( + output.includes("added") || output.includes("up to date"), + "npm install did not complete successfully" ); - console.log("NPM install output:\n", result.output); - - const curlOutput = container.dockerExec( - "curl -I http://localhost:4873/lodash/-/lodash-4.17.21.tgz" + const proxyLog = await container.openShell("zsh"); + const { output: logOutput } = await proxyLog.runCommand( + "cat /var/log/tinyproxy/tinyproxy.log" ); - console.log("Curl output:\n", curlOutput); - - // // Check if the installation was successful - // assert( - // result.output.includes("added"), - // "npm install did not complete successfully, output: " + result.output - // ); + // Check if the proxy log contains entries for the npm install + assert( + logOutput.includes("CONNECT registry.npmjs.org:443"), + "Proxy log does not contain expected entries" + ); }); }); From fce7550609f217ad7c814e2a224854db93efea41 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 09:21:23 +0200 Subject: [PATCH 042/345] Cleanup debugging code from test again --- .../src/registryProxy/plainHttpProxy.js | 20 ------------------- test/e2e/DockerTestContainer.js | 2 +- test/e2e/package.json | 2 +- test/e2e/safe-chain-proxy.e2e.spec.js | 4 ++-- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 214ad0f..2cd5f24 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,10 +1,8 @@ import * as http from "http"; import * as https from "https"; -// oxlint-disable no-console - just for testing, remove afterwards export function handleHttpProxyRequest(req, res) { const url = new URL(req.url); - console.log(`Proxying request to: ${req.url}`); let protocol; if (url.protocol === "http:") { @@ -25,27 +23,12 @@ export function handleHttpProxyRequest(req, res) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); - proxyRes.on("error", (err) => { - console.log("Error in proxy response stream:", err); - // Stream error while piping response - // Response headers already sent, can't send error status - }); - proxyRes.on("close", () => { - console.log("Proxy response stream closed"); // Clean up if the proxy response stream closes if (!res.writableEnded) { res.end(); } }); - - proxyRes.on("end", () => { - console.log("Proxy response stream ended"); - // End of proxy response - if (!res.writableEnded) { - res.end(); - } - }); } ) .on("error", (err) => { @@ -54,21 +37,18 @@ export function handleHttpProxyRequest(req, res) { }); req.on("error", () => { - console.log("Error in client request stream"); // Client request stream error // Abort the proxy request proxyRequest.destroy(); }); res.on("error", () => { - console.log("Error in client response stream"); // Client response stream error (client disconnected) // Clean up proxy streams proxyRequest.destroy(); }); res.on("close", () => { - console.log("Client response stream closed"); // Client disconnected // Abort the proxy request to avoid unnecessary work if (!res.writableEnded) { diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 45b66d0..ec1af3c 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -120,7 +120,7 @@ export class DockerTestContainer { console.log("Command timeout reached"); resolve({ allData, output: parseShellOutput(allData), command }); ptyProcess.removeListener("data", handleInput); - }, 20000); + }, 15000); function handleInput(data) { allData.push(data); diff --git a/test/e2e/package.json b/test/e2e/package.json index b34fd0b..9217808 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "description": "End-to-end tests for the Aikido Safe Chain", "scripts": { - "test": "node --test --test-concurrency=1 **/safe-chain-proxy.e2e.spec.js" + "test": "node --test --test-concurrency=1 **/*.spec.js" }, "keywords": [], "author": "Aikido Security", diff --git a/test/e2e/safe-chain-proxy.e2e.spec.js b/test/e2e/safe-chain-proxy.e2e.spec.js index 22a7038..6abbb0f 100644 --- a/test/e2e/safe-chain-proxy.e2e.spec.js +++ b/test/e2e/safe-chain-proxy.e2e.spec.js @@ -14,8 +14,8 @@ describe("E2E: Safe chain proxy", () => { container = new DockerTestContainer(); await container.start(); - // const installationShell = await container.openShell("zsh"); - // await installationShell.runCommand("safe-chain setup"); + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); }); afterEach(async () => { From 37ef3e187b83a0b39b05160a73a15eaba582aba8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 09:25:24 +0200 Subject: [PATCH 043/345] Further cleanup --- .../safe-chain/src/registryProxy/plainHttpProxy.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 2cd5f24..29b7fe1 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -23,9 +23,17 @@ export function handleHttpProxyRequest(req, res) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); + proxyRes.on("error", () => { + // Proxy response stream error + // Clean up client response stream + if (res.writable) { + res.end(); + } + }); + proxyRes.on("close", () => { // Clean up if the proxy response stream closes - if (!res.writableEnded) { + if (res.writable) { res.end(); } }); @@ -51,9 +59,7 @@ export function handleHttpProxyRequest(req, res) { res.on("close", () => { // Client disconnected // Abort the proxy request to avoid unnecessary work - if (!res.writableEnded) { - proxyRequest.destroy(); - } + proxyRequest.destroy(); }); req.pipe(proxyRequest); From 3e8ce13db5e1d9d72e4c38a354f959c39e352456 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 11:51:56 +0200 Subject: [PATCH 044/345] Move generated abbrevs to a separate file --- .../npm/utils/abbrevs-generated.js | 358 +++++++++++++++++ .../src/packagemanager/npm/utils/cmd-list.js | 361 +----------------- 2 files changed, 360 insertions(+), 359 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js diff --git a/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js new file mode 100644 index 0000000..204ffa7 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js @@ -0,0 +1,358 @@ +// This was ran with the abbrev package to generate the abbrevs object below +// console.log(abbrev(commands.concat(Object.keys(aliases)))); +export const abbrevs = { + ac: "access", + acc: "access", + acce: "access", + acces: "access", + access: "access", + add: "add", + "add-": "add-user", + "add-u": "add-user", + "add-us": "add-user", + "add-use": "add-user", + "add-user": "add-user", + addu: "adduser", + addus: "adduser", + adduse: "adduser", + adduser: "adduser", + aud: "audit", + audi: "audit", + audit: "audit", + aut: "author", + auth: "author", + autho: "author", + author: "author", + b: "bugs", + bu: "bugs", + bug: "bugs", + bugs: "bugs", + c: "c", + ca: "cache", + cac: "cache", + cach: "cache", + cache: "cache", + ci: "ci", + cit: "cit", + "clean-install": "clean-install", + "clean-install-": "clean-install-test", + "clean-install-t": "clean-install-test", + "clean-install-te": "clean-install-test", + "clean-install-tes": "clean-install-test", + "clean-install-test": "clean-install-test", + com: "completion", + comp: "completion", + compl: "completion", + comple: "completion", + complet: "completion", + completi: "completion", + completio: "completion", + completion: "completion", + con: "config", + conf: "config", + confi: "config", + config: "config", + cr: "create", + cre: "create", + crea: "create", + creat: "create", + create: "create", + dd: "ddp", + ddp: "ddp", + ded: "dedupe", + dedu: "dedupe", + dedup: "dedupe", + dedupe: "dedupe", + dep: "deprecate", + depr: "deprecate", + depre: "deprecate", + deprec: "deprecate", + depreca: "deprecate", + deprecat: "deprecate", + deprecate: "deprecate", + dif: "diff", + diff: "diff", + "dist-tag": "dist-tag", + "dist-tags": "dist-tags", + docs: "docs", + doct: "doctor", + docto: "doctor", + doctor: "doctor", + ed: "edit", + edi: "edit", + edit: "edit", + exe: "exec", + exec: "exec", + expla: "explain", + explai: "explain", + explain: "explain", + explo: "explore", + explor: "explore", + explore: "explore", + find: "find", + "find-": "find-dupes", + "find-d": "find-dupes", + "find-du": "find-dupes", + "find-dup": "find-dupes", + "find-dupe": "find-dupes", + "find-dupes": "find-dupes", + fu: "fund", + fun: "fund", + fund: "fund", + g: "get", + ge: "get", + get: "get", + help: "help", + "help-": "help-search", + "help-s": "help-search", + "help-se": "help-search", + "help-sea": "help-search", + "help-sear": "help-search", + "help-searc": "help-search", + "help-search": "help-search", + hl: "hlep", + hle: "hlep", + hlep: "hlep", + ho: "home", + hom: "home", + home: "home", + i: "i", + ic: "ic", + in: "in", + inf: "info", + info: "info", + ini: "init", + init: "init", + inn: "innit", + inni: "innit", + innit: "innit", + ins: "ins", + inst: "inst", + insta: "insta", + instal: "instal", + install: "install", + "install-ci": "install-ci-test", + "install-ci-": "install-ci-test", + "install-ci-t": "install-ci-test", + "install-ci-te": "install-ci-test", + "install-ci-tes": "install-ci-test", + "install-ci-test": "install-ci-test", + "install-cl": "install-clean", + "install-cle": "install-clean", + "install-clea": "install-clean", + "install-clean": "install-clean", + "install-t": "install-test", + "install-te": "install-test", + "install-tes": "install-test", + "install-test": "install-test", + isnt: "isnt", + isnta: "isnta", + isntal: "isntal", + isntall: "isntall", + "isntall-": "isntall-clean", + "isntall-c": "isntall-clean", + "isntall-cl": "isntall-clean", + "isntall-cle": "isntall-clean", + "isntall-clea": "isntall-clean", + "isntall-clean": "isntall-clean", + iss: "issues", + issu: "issues", + issue: "issues", + issues: "issues", + it: "it", + la: "la", + lin: "link", + link: "link", + lis: "list", + list: "list", + ll: "ll", + ln: "ln", + logi: "login", + login: "login", + logo: "logout", + logou: "logout", + logout: "logout", + ls: "ls", + og: "ogr", + ogr: "ogr", + or: "org", + org: "org", + ou: "outdated", + out: "outdated", + outd: "outdated", + outda: "outdated", + outdat: "outdated", + outdate: "outdated", + outdated: "outdated", + ow: "owner", + own: "owner", + owne: "owner", + owner: "owner", + pa: "pack", + pac: "pack", + pack: "pack", + pi: "ping", + pin: "ping", + ping: "ping", + pk: "pkg", + pkg: "pkg", + pre: "prefix", + pref: "prefix", + prefi: "prefix", + prefix: "prefix", + pro: "profile", + prof: "profile", + profi: "profile", + profil: "profile", + profile: "profile", + pru: "prune", + prun: "prune", + prune: "prune", + pu: "publish", + pub: "publish", + publ: "publish", + publi: "publish", + publis: "publish", + publish: "publish", + q: "query", + qu: "query", + que: "query", + quer: "query", + query: "query", + r: "r", + rb: "rb", + reb: "rebuild", + rebu: "rebuild", + rebui: "rebuild", + rebuil: "rebuild", + rebuild: "rebuild", + rem: "remove", + remo: "remove", + remov: "remove", + remove: "remove", + rep: "repo", + repo: "repo", + res: "restart", + rest: "restart", + resta: "restart", + restar: "restart", + restart: "restart", + rm: "rm", + ro: "root", + roo: "root", + root: "root", + rum: "rum", + run: "run", + "run-": "run-script", + "run-s": "run-script", + "run-sc": "run-script", + "run-scr": "run-script", + "run-scri": "run-script", + "run-scrip": "run-script", + "run-script": "run-script", + s: "s", + sb: "sbom", + sbo: "sbom", + sbom: "sbom", + se: "se", + sea: "search", + sear: "search", + searc: "search", + search: "search", + set: "set", + sho: "show", + show: "show", + shr: "shrinkwrap", + shri: "shrinkwrap", + shrin: "shrinkwrap", + shrink: "shrinkwrap", + shrinkw: "shrinkwrap", + shrinkwr: "shrinkwrap", + shrinkwra: "shrinkwrap", + shrinkwrap: "shrinkwrap", + si: "sit", + sit: "sit", + star: "star", + stars: "stars", + start: "start", + sto: "stop", + stop: "stop", + t: "t", + tea: "team", + team: "team", + tes: "test", + test: "test", + to: "token", + tok: "token", + toke: "token", + token: "token", + ts: "tst", + tst: "tst", + ud: "udpate", + udp: "udpate", + udpa: "udpate", + udpat: "udpate", + udpate: "udpate", + un: "un", + und: "undeprecate", + unde: "undeprecate", + undep: "undeprecate", + undepr: "undeprecate", + undepre: "undeprecate", + undeprec: "undeprecate", + undepreca: "undeprecate", + undeprecat: "undeprecate", + undeprecate: "undeprecate", + uni: "uninstall", + unin: "uninstall", + unins: "uninstall", + uninst: "uninstall", + uninsta: "uninstall", + uninstal: "uninstall", + uninstall: "uninstall", + unl: "unlink", + unli: "unlink", + unlin: "unlink", + unlink: "unlink", + unp: "unpublish", + unpu: "unpublish", + unpub: "unpublish", + unpubl: "unpublish", + unpubli: "unpublish", + unpublis: "unpublish", + unpublish: "unpublish", + uns: "unstar", + unst: "unstar", + unsta: "unstar", + unstar: "unstar", + up: "up", + upd: "update", + upda: "update", + updat: "update", + update: "update", + upg: "upgrade", + upgr: "upgrade", + upgra: "upgrade", + upgrad: "upgrade", + upgrade: "upgrade", + ur: "urn", + urn: "urn", + v: "v", + veri: "verison", + veris: "verison", + veriso: "verison", + verison: "verison", + vers: "version", + versi: "version", + versio: "version", + version: "version", + vi: "view", + vie: "view", + view: "view", + who: "whoami", + whoa: "whoami", + whoam: "whoami", + whoami: "whoami", + why: "why", + x: "x", +}; diff --git a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js index 8467147..6e67520 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js @@ -1,5 +1,7 @@ // Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js +import { abbrevs } from "./abbrevs-generated.js"; + const commands = [ "access", "adduser", @@ -70,365 +72,6 @@ const commands = [ "whoami", ]; -// This was ran with the abbrev package to generate the abbrevs object below -// console.log(abbrev(commands.concat(Object.keys(aliases)))); -const abbrevs = { - ac: "access", - acc: "access", - acce: "access", - acces: "access", - access: "access", - add: "add", - "add-": "add-user", - "add-u": "add-user", - "add-us": "add-user", - "add-use": "add-user", - "add-user": "add-user", - addu: "adduser", - addus: "adduser", - adduse: "adduser", - adduser: "adduser", - aud: "audit", - audi: "audit", - audit: "audit", - aut: "author", - auth: "author", - autho: "author", - author: "author", - b: "bugs", - bu: "bugs", - bug: "bugs", - bugs: "bugs", - c: "c", - ca: "cache", - cac: "cache", - cach: "cache", - cache: "cache", - ci: "ci", - cit: "cit", - "clean-install": "clean-install", - "clean-install-": "clean-install-test", - "clean-install-t": "clean-install-test", - "clean-install-te": "clean-install-test", - "clean-install-tes": "clean-install-test", - "clean-install-test": "clean-install-test", - com: "completion", - comp: "completion", - compl: "completion", - comple: "completion", - complet: "completion", - completi: "completion", - completio: "completion", - completion: "completion", - con: "config", - conf: "config", - confi: "config", - config: "config", - cr: "create", - cre: "create", - crea: "create", - creat: "create", - create: "create", - dd: "ddp", - ddp: "ddp", - ded: "dedupe", - dedu: "dedupe", - dedup: "dedupe", - dedupe: "dedupe", - dep: "deprecate", - depr: "deprecate", - depre: "deprecate", - deprec: "deprecate", - depreca: "deprecate", - deprecat: "deprecate", - deprecate: "deprecate", - dif: "diff", - diff: "diff", - "dist-tag": "dist-tag", - "dist-tags": "dist-tags", - docs: "docs", - doct: "doctor", - docto: "doctor", - doctor: "doctor", - ed: "edit", - edi: "edit", - edit: "edit", - exe: "exec", - exec: "exec", - expla: "explain", - explai: "explain", - explain: "explain", - explo: "explore", - explor: "explore", - explore: "explore", - find: "find", - "find-": "find-dupes", - "find-d": "find-dupes", - "find-du": "find-dupes", - "find-dup": "find-dupes", - "find-dupe": "find-dupes", - "find-dupes": "find-dupes", - fu: "fund", - fun: "fund", - fund: "fund", - g: "get", - ge: "get", - get: "get", - help: "help", - "help-": "help-search", - "help-s": "help-search", - "help-se": "help-search", - "help-sea": "help-search", - "help-sear": "help-search", - "help-searc": "help-search", - "help-search": "help-search", - hl: "hlep", - hle: "hlep", - hlep: "hlep", - ho: "home", - hom: "home", - home: "home", - i: "i", - ic: "ic", - in: "in", - inf: "info", - info: "info", - ini: "init", - init: "init", - inn: "innit", - inni: "innit", - innit: "innit", - ins: "ins", - inst: "inst", - insta: "insta", - instal: "instal", - install: "install", - "install-ci": "install-ci-test", - "install-ci-": "install-ci-test", - "install-ci-t": "install-ci-test", - "install-ci-te": "install-ci-test", - "install-ci-tes": "install-ci-test", - "install-ci-test": "install-ci-test", - "install-cl": "install-clean", - "install-cle": "install-clean", - "install-clea": "install-clean", - "install-clean": "install-clean", - "install-t": "install-test", - "install-te": "install-test", - "install-tes": "install-test", - "install-test": "install-test", - isnt: "isnt", - isnta: "isnta", - isntal: "isntal", - isntall: "isntall", - "isntall-": "isntall-clean", - "isntall-c": "isntall-clean", - "isntall-cl": "isntall-clean", - "isntall-cle": "isntall-clean", - "isntall-clea": "isntall-clean", - "isntall-clean": "isntall-clean", - iss: "issues", - issu: "issues", - issue: "issues", - issues: "issues", - it: "it", - la: "la", - lin: "link", - link: "link", - lis: "list", - list: "list", - ll: "ll", - ln: "ln", - logi: "login", - login: "login", - logo: "logout", - logou: "logout", - logout: "logout", - ls: "ls", - og: "ogr", - ogr: "ogr", - or: "org", - org: "org", - ou: "outdated", - out: "outdated", - outd: "outdated", - outda: "outdated", - outdat: "outdated", - outdate: "outdated", - outdated: "outdated", - ow: "owner", - own: "owner", - owne: "owner", - owner: "owner", - pa: "pack", - pac: "pack", - pack: "pack", - pi: "ping", - pin: "ping", - ping: "ping", - pk: "pkg", - pkg: "pkg", - pre: "prefix", - pref: "prefix", - prefi: "prefix", - prefix: "prefix", - pro: "profile", - prof: "profile", - profi: "profile", - profil: "profile", - profile: "profile", - pru: "prune", - prun: "prune", - prune: "prune", - pu: "publish", - pub: "publish", - publ: "publish", - publi: "publish", - publis: "publish", - publish: "publish", - q: "query", - qu: "query", - que: "query", - quer: "query", - query: "query", - r: "r", - rb: "rb", - reb: "rebuild", - rebu: "rebuild", - rebui: "rebuild", - rebuil: "rebuild", - rebuild: "rebuild", - rem: "remove", - remo: "remove", - remov: "remove", - remove: "remove", - rep: "repo", - repo: "repo", - res: "restart", - rest: "restart", - resta: "restart", - restar: "restart", - restart: "restart", - rm: "rm", - ro: "root", - roo: "root", - root: "root", - rum: "rum", - run: "run", - "run-": "run-script", - "run-s": "run-script", - "run-sc": "run-script", - "run-scr": "run-script", - "run-scri": "run-script", - "run-scrip": "run-script", - "run-script": "run-script", - s: "s", - sb: "sbom", - sbo: "sbom", - sbom: "sbom", - se: "se", - sea: "search", - sear: "search", - searc: "search", - search: "search", - set: "set", - sho: "show", - show: "show", - shr: "shrinkwrap", - shri: "shrinkwrap", - shrin: "shrinkwrap", - shrink: "shrinkwrap", - shrinkw: "shrinkwrap", - shrinkwr: "shrinkwrap", - shrinkwra: "shrinkwrap", - shrinkwrap: "shrinkwrap", - si: "sit", - sit: "sit", - star: "star", - stars: "stars", - start: "start", - sto: "stop", - stop: "stop", - t: "t", - tea: "team", - team: "team", - tes: "test", - test: "test", - to: "token", - tok: "token", - toke: "token", - token: "token", - ts: "tst", - tst: "tst", - ud: "udpate", - udp: "udpate", - udpa: "udpate", - udpat: "udpate", - udpate: "udpate", - un: "un", - und: "undeprecate", - unde: "undeprecate", - undep: "undeprecate", - undepr: "undeprecate", - undepre: "undeprecate", - undeprec: "undeprecate", - undepreca: "undeprecate", - undeprecat: "undeprecate", - undeprecate: "undeprecate", - uni: "uninstall", - unin: "uninstall", - unins: "uninstall", - uninst: "uninstall", - uninsta: "uninstall", - uninstal: "uninstall", - uninstall: "uninstall", - unl: "unlink", - unli: "unlink", - unlin: "unlink", - unlink: "unlink", - unp: "unpublish", - unpu: "unpublish", - unpub: "unpublish", - unpubl: "unpublish", - unpubli: "unpublish", - unpublis: "unpublish", - unpublish: "unpublish", - uns: "unstar", - unst: "unstar", - unsta: "unstar", - unstar: "unstar", - up: "up", - upd: "update", - upda: "update", - updat: "update", - update: "update", - upg: "upgrade", - upgr: "upgrade", - upgra: "upgrade", - upgrad: "upgrade", - upgrade: "upgrade", - ur: "urn", - urn: "urn", - v: "v", - veri: "verison", - veris: "verison", - veriso: "verison", - verison: "verison", - vers: "version", - versi: "version", - versio: "version", - version: "version", - vi: "view", - vie: "view", - view: "view", - who: "whoami", - whoa: "whoami", - whoam: "whoami", - whoami: "whoami", - why: "why", - x: "x", -}; - // These must resolve to an entry in commands const aliases = { // aliases From 05354ba2f0be2f1eb1e4ac5ae37f418a2d67262a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 15 Oct 2025 11:56:03 +0200 Subject: [PATCH 045/345] Add some more comments on why http / https is handled in different code paths --- packages/safe-chain/src/registryProxy/plainHttpProxy.js | 3 +++ packages/safe-chain/src/registryProxy/registryProxy.js | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 29b7fe1..e337b44 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -4,6 +4,9 @@ import * as https from "https"; export function handleHttpProxyRequest(req, res) { const url = new URL(req.url); + // The protocol for the plainHttpProxy should usually only be http: + // but when the client for some reason sends an https: request directly + // instead of using the CONNECT method, we should handle it gracefully. let protocol; if (url.protocol === "http:") { protocol = http; diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index d548999..b0e8dd1 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -55,7 +55,10 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { function createProxyServer() { const server = http.createServer( - handleHttpProxyRequest // This handles plain HTTP requests + // This handles direct HTTP requests (non-CONNECT requests) + // This is normally http-only traffic, but we also handle + // https for clients that don't properly use CONNECT + handleHttpProxyRequest ); // This handles HTTPS requests via the CONNECT method From da865f855dd8ac248c07eda74071162cb1a2347d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 21 Oct 2025 14:29:17 +0200 Subject: [PATCH 046/345] Fix crash when a package does not contain a version (retracted packages) --- packages/safe-chain/src/api/npmApi.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/safe-chain/src/api/npmApi.js b/packages/safe-chain/src/api/npmApi.js index 96bacc2..deb917d 100644 --- a/packages/safe-chain/src/api/npmApi.js +++ b/packages/safe-chain/src/api/npmApi.js @@ -25,6 +25,10 @@ export async function resolvePackageVersion(packageName, versionRange) { return distTags[versionRange]; } + if (!packageInfo.versions) { + return null; + } + // If the version range is not a dist-tag, we need to resolve the highest version matching the range. // This is useful for ranges like "^1.0.0" or "~2.3.4". const availableVersions = Object.keys(packageInfo.versions); From 1ded3899b0307e3d37ae14ae41985b1e3f9f7f48 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 21 Oct 2025 14:56:46 +0200 Subject: [PATCH 047/345] Commit new tests --- packages/safe-chain/src/api/npmApi.spec.js | 211 +++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 packages/safe-chain/src/api/npmApi.spec.js diff --git a/packages/safe-chain/src/api/npmApi.spec.js b/packages/safe-chain/src/api/npmApi.spec.js new file mode 100644 index 0000000..0c7585d --- /dev/null +++ b/packages/safe-chain/src/api/npmApi.spec.js @@ -0,0 +1,211 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert"; + +describe("resolvePackageVersion", async () => { + const mockNpmFetchJson = mock.fn(); + + mock.module("npm-registry-fetch", { + namedExports: { + json: mockNpmFetchJson, + }, + }); + + const { resolvePackageVersion } = await import("./npmApi.js"); + + it("should return the version if it is already a fixed version", async () => { + const result = await resolvePackageVersion("express", "4.17.1"); + + assert.strictEqual(result, "4.17.1"); + }); + + it("should use 'latest' as default version range when not provided", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "4.18.2", + }, + versions: { + "4.18.2": {}, + }, + })); + + const result = await resolvePackageVersion("express"); + + assert.strictEqual(result, "4.18.2"); + }); + + it("should resolve dist-tag versions", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "4.18.2", + next: "5.0.0-beta.1", + }, + versions: { + "4.18.2": {}, + "5.0.0-beta.1": {}, + }, + })); + + const result = await resolvePackageVersion("express", "next"); + + assert.strictEqual(result, "5.0.0-beta.1"); + }); + + it("should resolve version ranges using semver", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "4.18.2", + }, + versions: { + "4.16.0": {}, + "4.17.0": {}, + "4.17.1": {}, + "4.18.0": {}, + "4.18.2": {}, + }, + })); + + const result = await resolvePackageVersion("express", "^4.17.0"); + + assert.strictEqual(result, "4.18.2"); + }); + + it("should resolve tilde ranges correctly", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "4.18.2", + }, + versions: { + "4.17.0": {}, + "4.17.1": {}, + "4.17.3": {}, + "4.18.0": {}, + }, + })); + + const result = await resolvePackageVersion("express", "~4.17.0"); + + assert.strictEqual(result, "4.17.3"); + }); + + it("should return null if package info cannot be fetched", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => { + throw new Error("Package not found"); + }); + + const result = await resolvePackageVersion("non-existent-package", "latest"); + + assert.strictEqual(result, null); + }); + + it("should return null if no versions match the range", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "1.0.0", + }, + versions: { + "1.0.0": {}, + "1.1.0": {}, + }, + })); + + const result = await resolvePackageVersion("express", "^5.0.0"); + + assert.strictEqual(result, null); + }); + + it("should return null if dist-tag does not exist", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "4.18.2", + }, + versions: { + "4.18.2": {}, + }, + })); + + const result = await resolvePackageVersion("express", "nonexistent-tag"); + + assert.strictEqual(result, null); + }); + + it("should return null if package info has no versions property (retracted package)", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + _id: "zenn", + name: "zenn", + time: { + modified: "2021-04-20T16:20:56.084Z", + created: "2017-07-10T19:48:07.891Z", + unpublished: { + time: "2021-04-20T16:20:56.084Z", + versions: [ + "0.9.0", + "0.9.1", + "0.9.2", + "0.9.3", + "0.9.4", + "0.9.5", + "0.9.6", + "0.9.8", + "0.9.9", + "0.9.10", + "0.9.11", + "0.9.12", + "0.9.13", + "0.9.14", + ], + }, + }, + })); + + const result = await resolvePackageVersion("zenn", "^0.9.0"); + + assert.strictEqual(result, null); + }); + + it("should return dist-tag version even if versions property is missing", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "4.18.2", + }, + })); + + const result = await resolvePackageVersion("express", "latest"); + + assert.strictEqual(result, "4.18.2"); + }); + + it("should handle scoped packages", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "1.2.3", + }, + versions: { + "1.2.3": {}, + }, + })); + + const result = await resolvePackageVersion("@scope/package", "latest"); + + assert.strictEqual(result, "1.2.3"); + }); + + it("should handle complex version ranges", async () => { + mockNpmFetchJson.mock.mockImplementationOnce(() => ({ + "dist-tags": { + latest: "2.5.0", + }, + versions: { + "1.0.0": {}, + "2.0.0": {}, + "2.3.0": {}, + "2.4.0": {}, + "2.5.0": {}, + "3.0.0": {}, + }, + })); + + const result = await resolvePackageVersion("express", ">=2.0.0 <3.0.0"); + + assert.strictEqual(result, "2.5.0"); + }); +}); From d0f2edec0a4079cb5285d1f5d13bc6797e2286f6 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 21 Oct 2025 15:25:12 -0700 Subject: [PATCH 048/345] 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 f4cdf91fc950aad8481722b5a25e1e0d6b187482 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 22 Oct 2025 15:41:33 +0200 Subject: [PATCH 049/345] Add tests for the proxy --- .../registryProxy.connect-tunnel.spec.js | 172 ++++++++++++ .../registryProxy.http-proxy.spec.js | 225 +++++++++++++++ .../registryProxy/registryProxy.mitm.spec.js | 261 ++++++++++++++++++ 3 files changed, 658 insertions(+) create mode 100644 packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js create mode 100644 packages/safe-chain/src/registryProxy/registryProxy.http-proxy.spec.js create mode 100644 packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js diff --git a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js new file mode 100644 index 0000000..e5f5902 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js @@ -0,0 +1,172 @@ +import { before, after, describe, it } from "node:test"; +import assert from "node:assert"; +import net from "net"; +import tls from "tls"; +import { + createSafeChainProxy, + mergeSafeChainProxyEnvironmentVariables, +} from "./registryProxy.js"; + +describe("registryProxy.connectTunnel", () => { + let proxy, proxyHost, proxyPort; + + before(async () => { + proxy = createSafeChainProxy(); + await proxy.startServer(); + const envVars = mergeSafeChainProxyEnvironmentVariables([]); + const proxyUrl = new URL(envVars.HTTPS_PROXY); + proxyHost = proxyUrl.hostname; + proxyPort = parseInt(proxyUrl.port, 10); + }); + + after(async () => { + await proxy.stopServer(); + }); + + it("should establish a tunnel for HTTP connect", async () => { + const socket = await connectToProxy(proxyHost, proxyPort); + const tunnelResponse = await establishHttpsTunnel( + socket, + "postman-echo.com", + 443 + ); + + assert.ok(tunnelResponse.includes("HTTP/1.1 200 Connection Established")); + socket.destroy(); + }); + + it("should send HTTPS request through the established tunnel", async () => { + const socket = await connectToProxy(proxyHost, proxyPort); + await establishHttpsTunnel(socket, "postman-echo.com", 443); + const httpsResponse = await sendHttpsRequestThroughTunnel( + socket, + "GET", + new URL("https://postman-echo.com/status/200") + ); + + assert.ok(httpsResponse.includes("HTTP/1.1 200 OK")); + + socket.destroy(); + }); + + describe("Error Handling", () => { + it("should return 502 Bad Gateway for invalid hostname", async () => { + const socket = await connectToProxy(proxyHost, proxyPort); + const connectRequest = `CONNECT invalid.hostname.that.does.not.exist:443 HTTP/1.1\r\nHost: invalid.hostname.that.does.not.exist:443\r\n\r\n`; + socket.write(connectRequest); + + let responseData = ""; + await new Promise((resolve) => { + socket.once("data", (data) => { + responseData += data.toString(); + resolve(); + }); + }); + + assert.ok(responseData.includes("HTTP/1.1 502 Bad Gateway")); + socket.destroy(); + }); + + it("should handle client disconnect during tunnel establishment", async () => { + const socket = await connectToProxy(proxyHost, proxyPort); + const connectRequest = `CONNECT postman-echo.com:443 HTTP/1.1\r\nHost: postman-echo.com:443\r\n\r\n`; + socket.write(connectRequest); + + // Immediately destroy the socket before tunnel is fully established + socket.destroy(); + + // If no crash occurs, the test passes + assert.ok(true); + }); + + + it("should handle socket errors without crashing", async () => { + const socket = await connectToProxy(proxyHost, proxyPort); + + socket.on("error", () => { + // Error handler is set to prevent crashes + }); + + const connectRequest = `CONNECT postman-echo.com:443 HTTP/1.1\r\nHost: postman-echo.com:443\r\n\r\n`; + socket.write(connectRequest); + + // Force an error by destroying the socket + socket.destroy(); + + // Wait a bit to ensure error handling completes + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Test passes if no unhandled error crashes the process + assert.ok(true); + }); + + }); +}); + +function connectToProxy(host, port) { + return new Promise((resolve, reject) => { + const socket = net.connect({ host, port }, () => { + resolve(socket); + }); + + socket.on("error", (err) => { + reject(err); + }); + }); +} + +function establishHttpsTunnel(socket, targetHost, targetPort) { + return new Promise((resolve, reject) => { + const connectRequest = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n\r\n`; + socket.write(connectRequest); + + let responseData = ""; + const onData = (data) => { + responseData += data.toString(); + if (responseData.includes("\r\n\r\n")) { + socket.removeListener("data", onData); + socket.removeListener("error", onError); + resolve(responseData); + } + }; + + const onError = (err) => { + socket.removeListener("data", onData); + socket.removeListener("error", onError); + reject(err); + }; + + socket.on("data", onData); + socket.on("error", onError); + }); +} + +function sendHttpsRequestThroughTunnel(socket, verb, url) { + return new Promise((resolve, reject) => { + const tlsSocket = tls.connect( + { + socket: socket, + servername: url.hostname, + }, + () => { + tlsSocket.write( + `${verb} ${url.pathname} HTTP/1.1\r\nHost: ${url.hostname}\r\nConnection: close\r\n\r\n` + ); + } + ); + + let tlsData = ""; + + tlsSocket.on("data", (data) => { + tlsData += data.toString(); + }); + + tlsSocket.on("end", () => { + resolve(tlsData); + }); + + tlsSocket.on("error", (err) => { + reject(err); + }); + }); +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.http-proxy.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.http-proxy.spec.js new file mode 100644 index 0000000..970543c --- /dev/null +++ b/packages/safe-chain/src/registryProxy/registryProxy.http-proxy.spec.js @@ -0,0 +1,225 @@ +import { before, after, describe, it } from "node:test"; +import assert from "node:assert"; +import http from "http"; +import { + createSafeChainProxy, + mergeSafeChainProxyEnvironmentVariables, +} from "./registryProxy.js"; + +describe("registryProxy.httpProxy", () => { + let proxy, proxyHost, proxyPort; + let testHttpServer, testHttpServerPort; + + before(async () => { + // Start safe-chain proxy + proxy = createSafeChainProxy(); + await proxy.startServer(); + const envVars = mergeSafeChainProxyEnvironmentVariables([]); + const proxyUrl = new URL(envVars.HTTPS_PROXY); + proxyHost = proxyUrl.hostname; + proxyPort = parseInt(proxyUrl.port, 10); + + // Start a test HTTP server to forward requests to + testHttpServer = http.createServer((req, res) => { + if (req.url === "/test") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("HTTP test response"); + } else if (req.url === "/echo-headers") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(req.headers)); + } else if (req.url === "/echo-method") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(req.method); + } else if (req.url === "/post-echo") { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", () => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(body); + }); + } else if (req.url === "/404") { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not Found"); + } else { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("OK"); + } + }); + + testHttpServerPort = await new Promise((resolve) => { + testHttpServer.listen(0, () => { + resolve(testHttpServer.address().port); + }); + }); + }); + + after(async () => { + await proxy.stopServer(); + await new Promise((resolve) => { + testHttpServer.close(() => resolve()); + setTimeout(resolve, 1000); + }); + }); + + it("should forward HTTP GET requests", async () => { + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + `http://localhost:${testHttpServerPort}/test`, + "GET" + ); + + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(response.body, "HTTP test response"); + }); + + it("should forward HTTP POST requests with body", async () => { + const postData = "test post data"; + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + `http://localhost:${testHttpServerPort}/post-echo`, + "POST", + postData + ); + + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(response.body, postData); + }); + + it("should preserve request headers", async () => { + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + `http://localhost:${testHttpServerPort}/echo-headers`, + "GET", + null, + { + "X-Custom-Header": "test-value", + "User-Agent": "test-agent/1.0", + } + ); + + assert.strictEqual(response.statusCode, 200); + const headers = JSON.parse(response.body); + assert.strictEqual(headers["x-custom-header"], "test-value"); + assert.strictEqual(headers["user-agent"], "test-agent/1.0"); + }); + + it("should preserve HTTP methods", async () => { + const methods = ["GET", "POST", "PUT", "DELETE"]; + + for (const method of methods) { + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + `http://localhost:${testHttpServerPort}/echo-method`, + method + ); + + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(response.body, method); + } + }); + + it("should forward 404 responses correctly", async () => { + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + `http://localhost:${testHttpServerPort}/404`, + "GET" + ); + + assert.strictEqual(response.statusCode, 404); + assert.strictEqual(response.body, "Not Found"); + }); + + it("should handle invalid host with 502 Bad Gateway", async () => { + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + "http://invalid-host-that-does-not-exist.test:9999/test", + "GET" + ); + + assert.strictEqual(response.statusCode, 502); + assert.ok(response.body.includes("Bad Gateway")); + }); + + it("should handle HTTPS URLs sent to HTTP proxy", async () => { + // Some clients incorrectly send https:// URLs to the HTTP proxy handler + // instead of using CONNECT. The proxy should handle this gracefully. + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + "https://registry.npmjs.org/lodash", + "GET" + ); + + // Should successfully forward the HTTPS request + assert.strictEqual(response.statusCode, 200); + assert.ok(response.body.includes("lodash")); + }); + + it("should handle unsupported protocols with 502", async () => { + const response = await makeHttpProxyRequest( + proxyHost, + proxyPort, + "ftp://example.com/file.txt", + "GET" + ); + + assert.strictEqual(response.statusCode, 502); + assert.ok(response.body.includes("Unsupported protocol")); + }); +}); + +function makeHttpProxyRequest( + proxyHost, + proxyPort, + targetUrl, + method = "GET", + body = null, + extraHeaders = {} +) { + return new Promise((resolve, reject) => { + const options = { + hostname: proxyHost, + port: proxyPort, + path: targetUrl, + method: method, + headers: { + Host: new URL(targetUrl).host, + ...extraHeaders, + }, + }; + + const req = http.request(options, (res) => { + let responseBody = ""; + + res.on("data", (chunk) => { + responseBody += chunk.toString(); + }); + + res.on("end", () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: responseBody, + }); + }); + }); + + req.on("error", (err) => { + reject(err); + }); + + if (body) { + req.write(body); + } + + req.end(); + }); +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js new file mode 100644 index 0000000..515284c --- /dev/null +++ b/packages/safe-chain/src/registryProxy/registryProxy.mitm.spec.js @@ -0,0 +1,261 @@ +import { before, after, describe, it } from "node:test"; +import assert from "node:assert"; +import net from "net"; +import tls from "tls"; +import { + createSafeChainProxy, + mergeSafeChainProxyEnvironmentVariables, +} from "./registryProxy.js"; +import { getCaCertPath } from "./certUtils.js"; +import fs from "fs"; + +describe("registryProxy.mitm", () => { + let proxy, proxyHost, proxyPort; + + before(async () => { + proxy = createSafeChainProxy(); + await proxy.startServer(); + const envVars = mergeSafeChainProxyEnvironmentVariables([]); + const proxyUrl = new URL(envVars.HTTPS_PROXY); + proxyHost = proxyUrl.hostname; + proxyPort = parseInt(proxyUrl.port, 10); + }); + + after(async () => { + await proxy.stopServer(); + }); + + it("should intercept HTTPS requests to npm registry", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/lodash" + ); + + assert.strictEqual(response.statusCode, 200); + assert.ok(response.body.includes("lodash")); + }); + + it("should allow non-malicious package downloads", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/lodash/-/lodash-4.17.21.tgz" + ); + + // Should get a response (200 or redirect, but not 403 blocked) + assert.notStrictEqual(response.statusCode, 403); + }); + + it("should handle 404 responses correctly", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/this-package-definitely-does-not-exist-12345" + ); + + assert.strictEqual(response.statusCode, 404); + }); + + it("should handle query parameters in URL", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/lodash?write=true" + ); + + assert.strictEqual(response.statusCode, 200); + }); + + it("should generate valid certificates for yarn registry", async () => { + const response = await makeRegistryRequest( + proxyHost, + proxyPort, + "registry.yarnpkg.com", + "/lodash" + ); + + assert.strictEqual(response.statusCode, 200); + }); + + it("should generate certificate with correct hostname in CN", async () => { + const { cert } = await makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/lodash" + ); + + // Check certificate common name matches the target hostname + assert.strictEqual(cert.subject.CN, "registry.npmjs.org"); + + // Check Subject Alternative Name includes the hostname + const san = cert.subjectaltname; + assert.ok(san.includes("registry.npmjs.org")); + + // Check certificate is issued by safe-chain CA + assert.strictEqual(cert.issuer.CN, "safe-chain proxy"); + }); + + it("should generate different certificates for different hostnames", async () => { + const { cert: cert1 } = await makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/lodash" + ); + + const { cert: cert2 } = await makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + "registry.yarnpkg.com", + "/lodash" + ); + + // Different hostnames should have different certificates + assert.notStrictEqual(cert1.fingerprint, cert2.fingerprint); + assert.strictEqual(cert1.subject.CN, "registry.npmjs.org"); + assert.strictEqual(cert2.subject.CN, "registry.yarnpkg.com"); + }); + + it("should cache generated certificates for same hostname", async () => { + const { cert: cert1 } = await makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/lodash" + ); + + const { cert: cert2 } = await makeRegistryRequestAndGetCert( + proxyHost, + proxyPort, + "registry.npmjs.org", + "/package/lodash" + ); + + // Same hostname should get the same certificate (fingerprint) + assert.strictEqual(cert1.fingerprint, cert2.fingerprint); + }); +}); + +async function makeRegistryRequest(proxyHost, proxyPort, targetHost, path) { + // Step 1: Connect to proxy + const socket = await new Promise((resolve, reject) => { + const sock = net.connect({ host: proxyHost, port: proxyPort }, () => { + resolve(sock); + }); + sock.on("error", reject); + }); + + // Step 2: Send CONNECT request + await new Promise((resolve) => { + const connectRequest = `CONNECT ${targetHost}:443 HTTP/1.1\r\nHost: ${targetHost}:443\r\n\r\n`; + socket.write(connectRequest); + socket.once("data", resolve); + }); + + // Step 3: Upgrade to TLS using the proxy's CA cert + const tlsSocket = tls.connect({ + socket: socket, + servername: targetHost, + ca: fs.readFileSync(getCaCertPath()), + rejectUnauthorized: true, + }); + + await new Promise((resolve) => { + tlsSocket.on("secureConnect", resolve); + }); + + // Step 4: Send HTTP request over TLS + const httpRequest = `GET ${path} HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\n\r\n`; + tlsSocket.write(httpRequest); + + // Step 5: Read response + return new Promise((resolve, reject) => { + let data = ""; + + tlsSocket.on("data", (chunk) => { + data += chunk.toString(); + }); + + tlsSocket.on("end", () => { + const lines = data.split("\r\n"); + const statusLine = lines[0]; + const statusCode = parseInt(statusLine.split(" ")[1]); + + // Find body after empty line + const emptyLineIndex = lines.findIndex(line => line === ""); + const body = lines.slice(emptyLineIndex + 1).join("\r\n"); + + resolve({ statusCode, body }); + }); + + tlsSocket.on("error", reject); + }); +} + +async function makeRegistryRequestAndGetCert(proxyHost, proxyPort, targetHost, path) { + // Step 1: Connect to proxy + const socket = await new Promise((resolve, reject) => { + const sock = net.connect({ host: proxyHost, port: proxyPort }, () => { + resolve(sock); + }); + sock.on("error", reject); + }); + + // Step 2: Send CONNECT request + await new Promise((resolve) => { + const connectRequest = `CONNECT ${targetHost}:443 HTTP/1.1\r\nHost: ${targetHost}:443\r\n\r\n`; + socket.write(connectRequest); + socket.once("data", resolve); + }); + + // Step 3: Upgrade to TLS and capture certificate + const tlsSocket = tls.connect({ + socket: socket, + servername: targetHost, + ca: fs.readFileSync(getCaCertPath()), + rejectUnauthorized: true, + }); + + let peerCert; + await new Promise((resolve) => { + tlsSocket.on("secureConnect", () => { + peerCert = tlsSocket.getPeerCertificate(); + resolve(); + }); + }); + + // Step 4: Send HTTP request over TLS + const httpRequest = `GET ${path} HTTP/1.1\r\nHost: ${targetHost}\r\nConnection: close\r\n\r\n`; + tlsSocket.write(httpRequest); + + // Step 5: Read response + const response = await new Promise((resolve, reject) => { + let data = ""; + + tlsSocket.on("data", (chunk) => { + data += chunk.toString(); + }); + + tlsSocket.on("end", () => { + const lines = data.split("\r\n"); + const statusLine = lines[0]; + const statusCode = parseInt(statusLine.split(" ")[1]); + + // Find body after empty line + const emptyLineIndex = lines.findIndex(line => line === ""); + const body = lines.slice(emptyLineIndex + 1).join("\r\n"); + + resolve({ statusCode, body }); + }); + + tlsSocket.on("error", reject); + }); + + return { cert: peerCert, response }; +} From f086aeb2be163d1887239f6e021fc3ed9e7a59fd Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 22 Oct 2025 06:59:32 -0700 Subject: [PATCH 050/345] 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 051/345] 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 052/345] 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 053/345] 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 054/345] 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 055/345] 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 7e72ae7d3d281a248afd5d926b9f2afaa41dd2e9 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Thu, 9 Oct 2025 16:33:40 +0200 Subject: [PATCH 056/345] On Unix/macOS, pass args to `spawn` to avoid escaping issues --- packages/safe-chain/src/utils/safeSpawn.js | 33 ++++++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index f45e4ff..410f455 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,4 +1,4 @@ -import { spawnSync, spawn } from "child_process"; +import { spawn, execSync } from "child_process"; function escapeArg(arg) { // Shell metacharacters that need escaping @@ -16,18 +16,39 @@ function escapeArg(arg) { function buildCommand(command, args) { const escapedArgs = args.map(escapeArg); + return `${command} ${escapedArgs.join(" ")}`; } -export function safeSpawnSync(command, args, options = {}) { - const fullCommand = buildCommand(command, args); - return spawnSync(fullCommand, { ...options, shell: true }); +function resolveCommandPath(command) { + // command will be "npm", "yarn", etc. + // Use 'command -v' to find the full path + const fullPath = execSync(`command -v ${command}`, { + encoding: "utf8", + shell: true, + }).trim(); + + if (!fullPath) { + throw new Error(`Command not found: ${command}`); + } + + return fullPath; } export async function safeSpawn(command, args, options = {}) { - const fullCommand = buildCommand(command, args); return new Promise((resolve, reject) => { - const child = spawn(fullCommand, { ...options, shell: true }); + // Windows requires shell: true because .bat and .cmd files are not executable + // without a terminal. On Unix/macOS, we resolve the full path first, then use + // array args (safer, no escaping needed). + // See: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options + let child; + if (process.platform === "win32") { + const fullCommand = buildCommand(command, args); + child = spawn(fullCommand, { ...options, shell: true }); + } else { + const fullPath = resolveCommandPath(command); + child = spawn(fullPath, args, options); + } child.on("close", (code) => { resolve({ From c74c23b0ffada047507409d4cd39535b30d8f040 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 23 Oct 2025 10:52:03 +0200 Subject: [PATCH 057/345] Fix unit tests --- packages/safe-chain/src/utils/safeSpawn.js | 3 ++- packages/safe-chain/src/utils/safeSpawn.spec.js | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 32669b3..96c0603 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,4 +1,5 @@ import { spawn, execSync } from "child_process"; +import os from "os"; function escapeArg(arg) { // Shell metacharacters that need escaping @@ -42,7 +43,7 @@ export async function safeSpawn(command, args, options = {}) { // array args (safer, no escaping needed). // See: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options let child; - if (process.platform === "win32") { + if (os.platform() === "win32") { const fullCommand = buildCommand(command, args); child = spawn(fullCommand, { ...options, shell: true }); } else { diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 6d8dd26..4ad005e 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -33,6 +33,12 @@ describe("safeSpawn", () => { }, }); + mock.module("os", { + namedExports: { + platform: () => "win32", + }, + }); + // Import after mocking const safeSpawnModule = await import("./safeSpawn.js"); safeSpawn = safeSpawnModule.safeSpawn; From 08c1328b521cb5fb0872e8107be62932e736c2e2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 23 Oct 2025 13:23:08 +0200 Subject: [PATCH 058/345] Cleanup code, add some tests --- packages/safe-chain/src/utils/safeSpawn.js | 27 ++++--- .../safe-chain/src/utils/safeSpawn.spec.js | 72 +++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 96c0603..c85b91e 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,22 +1,33 @@ import { spawn, execSync } from "child_process"; import os from "os"; -function escapeArg(arg) { - // Shell metacharacters that need escaping - // These characters have special meaning in shells and need to be quoted - const shellMetaChars = /[ "&'|;<>()$`\\!*?[\]{}~#]/; - +function sanitizeShellArgument(arg) { // If argument contains shell metacharacters, wrap in double quotes // and escape characters that are special even inside double quotes - if (shellMetaChars.test(arg)) { + if (hasShellMetaChars(arg)) { // Inside double quotes, we need to escape: " $ ` \ - return '"' + arg.replace(/(["`$\\])/g, "\\$1") + '"'; + return '"' + escapeDoubleQuoteContent(arg) + '"'; } return arg; } +function hasShellMetaChars(arg) { + // Shell metacharacters that need escaping + // These characters have special meaning in shells and need to be quoted + // Whenever one of these characters is present, we should quote the argument + // Characters: space, ", &, ', |, ;, <, >, (, ), $, `, \, !, *, ?, [, ], {, }, ~, # + const shellMetaChars = /[ "&'|;<>()$`\\!*?[\]{}~#]/; + return shellMetaChars.test(arg); +} + +function escapeDoubleQuoteContent(arg) { + // Escape special characters for shell safety + // This escapes ", $, `, and \ by prefixing them with a backslash + return arg.replace(/(["`$\\])/g, "\\$1"); +} + function buildCommand(command, args) { - const escapedArgs = args.map(escapeArg); + const escapedArgs = args.map(sanitizeShellArgument); return `${command} ${escapedArgs.join(" ")}`; } diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index 4ad005e..cf7bd41 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -112,4 +112,76 @@ describe("safeSpawn", () => { ); assert.strictEqual(spawnCalls[0].options.shell, true); }); + + it("should escape dollar signs to prevent variable expansion", async () => { + await safeSpawn("echo", ["$HOME/test"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'echo "\\$HOME/test"'); + }); + + it("should escape backticks to prevent command substitution", async () => { + await safeSpawn("echo", ["file`whoami`.txt"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'echo "file\\`whoami\\`.txt"'); + }); + + it("should escape backslashes properly", async () => { + await safeSpawn("echo", ["path\\with\\backslash"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual( + spawnCalls[0].command, + 'echo "path\\\\with\\\\backslash"' + ); + }); + + it("should handle multiple special characters in one argument", async () => { + await safeSpawn("cmd", ['test "quoted" $var `cmd` & more']); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual( + spawnCalls[0].command, + 'cmd "test \\"quoted\\" \\$var \\`cmd\\` & more"' + ); + }); + + it("should handle pipe character", async () => { + await safeSpawn("echo", ["foo|bar"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'echo "foo|bar"'); + }); + + it("should handle parentheses", async () => { + await safeSpawn("echo", ["(test)"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'echo "(test)"'); + }); + + it("should handle angle brackets for redirection", async () => { + await safeSpawn("echo", ["foo>output.txt"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'echo "foo>output.txt"'); + }); + + it("should handle wildcard characters", async () => { + await safeSpawn("echo", ["*.txt"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, 'echo "*.txt"'); + }); + + it("should handle multiple arguments with mixed escaping needs", async () => { + await safeSpawn("cmd", ["safe", "needs space", "$dangerous", "also-safe"]); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual( + spawnCalls[0].command, + 'cmd safe "needs space" "\\$dangerous" also-safe' + ); + }); }); From 7a55be49f4a35a9fca262c82e588996ebf4b46a0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 23 Oct 2025 13:29:14 +0200 Subject: [PATCH 059/345] Fix linting error --- packages/safe-chain/src/utils/safeSpawn.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index cf7bd41..cbdb7cb 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -22,7 +22,7 @@ describe("safeSpawn", () => { }, }; }, - execSync: (cmd, opts) => { + execSync: (cmd) => { // Simulate 'command -v' returning full path const match = cmd.match(/command -v (.+)/); if (match) { From 9a78cafbfdd1b71dd7ffac0319e84f359602cd3c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 23 Oct 2025 17:45:03 +0200 Subject: [PATCH 060/345] Introduce silent mode to disable logging --- README.md | 12 ++++ .../safe-chain/src/config/cliArguments.js | 17 +++++ .../src/config/cliArguments.spec.js | 69 ++++++++++++++++++- packages/safe-chain/src/config/settings.js | 13 ++++ .../src/environment/userInteraction.js | 31 ++++++++- .../src/registryProxy/registryProxy.js | 2 +- packages/safe-chain/src/scanning/index.js | 2 +- 7 files changed, 142 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1083c0e..7b3f038 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,18 @@ Example usage: npm install suspicious-package --safe-chain-malware-action=prompt ``` +## Logging + +You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag: + +- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit. + +Example usage: + +```shell +npm install express --safe-chain-logging=silent +``` + # Usage in CI/CD You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 87abb7b..70c56b1 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,5 +1,6 @@ const state = { malwareAction: undefined, + loggingLevel: undefined, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; @@ -7,6 +8,7 @@ const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; export function initializeCliArguments(args) { // Reset state on each call state.malwareAction = undefined; + state.loggingLevel = undefined; const safeChainArgs = []; const remainingArgs = []; @@ -20,6 +22,7 @@ export function initializeCliArguments(args) { } setMalwareAction(safeChainArgs); + setLoggingLevel(safeChainArgs); return remainingArgs; } @@ -48,3 +51,17 @@ function getLastArgEqualsValue(args, prefix) { export function getMalwareAction() { return state.malwareAction; } + +function setLoggingLevel(args) { + const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging="; + + const level = getLastArgEqualsValue(args, safeChainLoggingArg); + if (!level) { + return; + } + state.loggingLevel = level.toLowerCase(); +} + +export function getLoggingLevel() { + return state.loggingLevel; +} diff --git a/packages/safe-chain/src/config/cliArguments.spec.js b/packages/safe-chain/src/config/cliArguments.spec.js index 9d5c0ba..c7d9a84 100644 --- a/packages/safe-chain/src/config/cliArguments.spec.js +++ b/packages/safe-chain/src/config/cliArguments.spec.js @@ -1,6 +1,10 @@ import { describe, it } from "node:test"; import assert from "node:assert"; -import { initializeCliArguments, getMalwareAction } from "./cliArguments.js"; +import { + initializeCliArguments, + getMalwareAction, + getLoggingLevel, +} from "./cliArguments.js"; describe("initializeCliArguments", () => { it("should return all args when no safe-chain args are present", () => { @@ -105,4 +109,67 @@ describe("initializeCliArguments", () => { assert.deepEqual(result, ["install"]); assert.strictEqual(getMalwareAction(), "block"); }); + + it("should not set loggingLevel when no logging argument is passed", () => { + const args = ["install", "express", "--save"]; + initializeCliArguments(args); + + assert.strictEqual(getLoggingLevel(), undefined); + }); + + it("should parse logging=silent and set state", () => { + const args = ["--safe-chain-logging=silent", "install", "package"]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "package"]); + assert.strictEqual(getLoggingLevel(), "silent"); + }); + + it("should parse logging=normal and set state", () => { + const args = ["--safe-chain-logging=normal", "install", "package"]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "package"]); + assert.strictEqual(getLoggingLevel(), "normal"); + }); + + it("should handle multiple logging args, using the last one", () => { + const args = [ + "--safe-chain-logging=normal", + "--safe-chain-logging=silent", + "install", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install"]); + assert.strictEqual(getLoggingLevel(), "silent"); + }); + + it("should handle logging level case-insensitively", () => { + const args = ["--safe-chain-logging=SILENT", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getLoggingLevel(), "silent"); + }); + + it("should capture invalid logging level as-is (lowercased)", () => { + const args = ["--safe-chain-logging=invalid", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getLoggingLevel(), "invalid"); + }); + + it("should handle logging with other safe-chain args", () => { + const args = [ + "--safe-chain-debug", + "--safe-chain-logging=silent", + "--safe-chain-malware-action=block", + "install", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install"]); + assert.strictEqual(getLoggingLevel(), "silent"); + assert.strictEqual(getMalwareAction(), "block"); + }); }); diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ed2cae2..17c1cdb 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -10,5 +10,18 @@ export function getMalwareAction() { return MALWARE_ACTION_BLOCK; } +export function getLoggingLevel() { + const level = cliArguments.getLoggingLevel(); + + if (level === LOGGING_SILENT) { + return LOGGING_SILENT; + } + + return LOGGING_NORMAL; +} + export const MALWARE_ACTION_BLOCK = "block"; export const MALWARE_ACTION_PROMPT = "prompt"; + +export const LOGGING_SILENT = "silent"; +export const LOGGING_NORMAL = "normal"; diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 829afa1..99fe90f 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -3,16 +3,27 @@ import chalk from "chalk"; import ora from "ora"; import { createInterface } from "readline"; import { isCi } from "./environment.js"; +import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js"; + +function isSilentMode() { + return getLoggingLevel() === LOGGING_SILENT; +} function emptyLine() { + if (isSilentMode()) return; + writeInformation(""); } function writeInformation(message, ...optionalParams) { + if (isSilentMode()) return; + console.log(message, ...optionalParams); } function writeWarning(message, ...optionalParams) { + if (isSilentMode()) return; + if (!isCi()) { message = chalk.yellow(message); } @@ -26,7 +37,24 @@ function writeError(message, ...optionalParams) { console.error(message, ...optionalParams); } +function writeExitWithoutInstallingMaliciousPackages() { + let message = "Safe-chain: Exiting without installing malicious packages."; + if (!isCi()) { + message = chalk.red(message); + } + console.error(message); +} + function startProcess(message) { + if (isSilentMode()) { + return { + succeed: () => {}, + fail: () => {}, + stop: () => {}, + setText: () => {}, + }; + } + if (isCi()) { return { succeed: (message) => { @@ -60,7 +88,7 @@ function startProcess(message) { } async function confirm(config) { - if (isCi()) { + if (isCi() || isSilentMode()) { return Promise.resolve(config.default); } @@ -91,6 +119,7 @@ export const ui = { writeInformation, writeWarning, writeError, + writeExitWithoutInstallingMaliciousPackages, emptyLine, startProcess, confirm, diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index b0e8dd1..887fd47 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -153,7 +153,7 @@ function verifyNoMaliciousPackages() { } ui.emptyLine(); - ui.writeError("Exiting without installing malicious packages."); + ui.writeExitWithoutInstallingMaliciousPackages(); ui.emptyLine(); return false; diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 36f62ca..4b0f654 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -93,7 +93,7 @@ async function onMalwareFound() { } } - ui.writeError("Exiting without installing malicious packages."); + ui.writeExitWithoutInstallingMaliciousPackages(); ui.emptyLine(); return 1; } From 0f164d055ff16c9faeb79d661379cb797e622bf7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 23 Oct 2025 17:48:26 +0200 Subject: [PATCH 061/345] Fix mocking in tests --- packages/safe-chain/src/scanning/index.scanCommand.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/safe-chain/src/scanning/index.scanCommand.spec.js b/packages/safe-chain/src/scanning/index.scanCommand.spec.js index 1858d10..abcdc97 100644 --- a/packages/safe-chain/src/scanning/index.scanCommand.spec.js +++ b/packages/safe-chain/src/scanning/index.scanCommand.spec.js @@ -46,6 +46,7 @@ describe("scanCommand", async () => { writeError: () => {}, writeInformation: () => {}, writeWarning: () => {}, + writeExitWithoutInstallingMaliciousPackages: () => {}, emptyLine: () => {}, confirm: mockConfirm, }, From 1fdb15a39206231ae2acc242757d72675fb42ec1 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 23 Oct 2025 09:14:05 -0700 Subject: [PATCH 062/345] 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 063/345] 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 064/345] 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 065/345] 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 066/345] 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 d6dda73fb983b5f8aae30992c7c64925eddb0756 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 24 Oct 2025 16:21:14 +0200 Subject: [PATCH 067/345] WIP --- .../src/environment/userInteraction.js | 5 +++++ .../src/registryProxy/mitmRequestHandler.js | 18 ++++++++++++++++-- .../src/registryProxy/registryProxy.js | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 829afa1..1b4eae8 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -26,6 +26,10 @@ function writeError(message, ...optionalParams) { console.error(message, ...optionalParams); } +function writeVerboseInformation(message, ...optionalParams) { + writeInformation(message, ...optionalParams); +} + function startProcess(message) { if (isCi()) { return { @@ -89,6 +93,7 @@ async function confirm(config) { export const ui = { writeInformation, + writeVerboseInformation, writeWarning, writeError, emptyLine, diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 63a8168..b0c0af7 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -1,11 +1,16 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; +import { ui } from "../environment/userInteraction.js"; export function mitmConnect(req, clientSocket, isAllowed) { + ui.writeVerboseInformation(`Safe-chain: Set up MITM tunnel for ${req.url}`); const { hostname } = new URL(`http://${req.url}`); - clientSocket.on("error", () => { + clientSocket.on("error", (err) => { + ui.writeVerboseInformation( + `Safe-chain: Client socket error for ${req.url}: ${err.message}` + ); // NO-OP // This can happen if the client TCP socket sends RST instead of FIN. // Not subscribing to 'close' event will cause node to throw and crash. @@ -28,6 +33,9 @@ function createHttpsServer(hostname, isAllowed) { const targetUrl = `https://${hostname}${pathAndQuery}`; if (!(await isAllowed(targetUrl))) { + ui.writeVerboseInformation( + `Safe-chain: Blocking request to ${targetUrl}` + ); res.writeHead(403, "Forbidden - blocked by safe-chain"); res.end("Blocked by safe-chain"); return; @@ -57,7 +65,10 @@ function getRequestPathAndQuery(url) { function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); - proxyReq.on("error", () => { + proxyReq.on("error", (err) => { + ui.writeVerboseInformation( + `Safe-chain: Error occurred while proxying request: ${err.message}` + ); res.writeHead(502); res.end("Bad Gateway"); }); @@ -67,6 +78,9 @@ function forwardRequest(req, hostname, res) { }); req.on("end", () => { + ui.writeVerboseInformation( + `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` + ); proxyReq.end(); }); } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index b0e8dd1..b5227b4 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -109,6 +109,7 @@ function handleConnect(req, clientSocket, head) { mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket + ui.writeVerboseInformation(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } } From f5f3b91b40dc1bad0050ef1d4339672ecd2cfc5f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 24 Oct 2025 17:36:51 +0200 Subject: [PATCH 068/345] Test if command is safe to execute --- packages/safe-chain/src/utils/safeSpawn.js | 9 ++++++ .../safe-chain/src/utils/safeSpawn.spec.js | 29 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index c85b91e..8642b07 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -27,6 +27,10 @@ function escapeDoubleQuoteContent(arg) { } function buildCommand(command, args) { + if (args.length === 0) { + return command; + } + const escapedArgs = args.map(sanitizeShellArgument); return `${command} ${escapedArgs.join(" ")}`; @@ -48,6 +52,11 @@ function resolveCommandPath(command) { } export async function safeSpawn(command, args, options = {}) { + // command should always be alphanumeric or _ or - to avoid injection + if (!/^[a-zA-Z0-9_-]+$/.test(command)) { + throw new Error(`Invalid command name: ${command}`); + } + return new Promise((resolve, reject) => { // Windows requires shell: true because .bat and .cmd files are not executable // without a terminal. On Unix/macOS, we resolve the full path first, then use diff --git a/packages/safe-chain/src/utils/safeSpawn.spec.js b/packages/safe-chain/src/utils/safeSpawn.spec.js index cbdb7cb..d4180b7 100644 --- a/packages/safe-chain/src/utils/safeSpawn.spec.js +++ b/packages/safe-chain/src/utils/safeSpawn.spec.js @@ -4,9 +4,11 @@ import assert from "node:assert"; describe("safeSpawn", () => { let safeSpawn; let spawnCalls = []; + let os; beforeEach(async () => { spawnCalls = []; + os = "win32"; // Test Windows behavior by default // Mock child_process module to capture what command string gets built mock.module("child_process", { @@ -35,7 +37,7 @@ describe("safeSpawn", () => { mock.module("os", { namedExports: { - platform: () => "win32", + platform: () => os, }, }); @@ -184,4 +186,29 @@ describe("safeSpawn", () => { 'cmd safe "needs space" "\\$dangerous" also-safe' ); }); + + it("should reject command names with special characters", async () => { + await assert.rejects(async () => await safeSpawn("npm; echo hacked", []), { + message: "Invalid command name: npm; echo hacked", + }); + }); + + it("should reject command names with spaces", async () => { + await assert.rejects(async () => await safeSpawn("npm install", []), { + message: "Invalid command name: npm install", + }); + }); + + it("should reject command names with slashes", async () => { + await assert.rejects(async () => await safeSpawn("../../malicious", []), { + message: "Invalid command name: ../../malicious", + }); + }); + + it("should accept valid command names with letters, numbers, underscores and hyphens", async () => { + await safeSpawn("valid_command-123", []); + + assert.strictEqual(spawnCalls.length, 1); + assert.strictEqual(spawnCalls[0].command, "valid_command-123"); + }); }); From 15785fad73fb91f6bdb1873dd5ddc4ef730ad814 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 24 Oct 2025 09:59:53 -0700 Subject: [PATCH 069/345] 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 070/345] 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 071/345] 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 072/345] 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 073/345] 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 074/345] 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 075/345] 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 076/345] 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 0029a7e1c1fa8f9a16ce3ef3d913c46cea360c63 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 10:49:26 +0100 Subject: [PATCH 077/345] Add extra comments for regex clarification --- packages/safe-chain/src/utils/safeSpawn.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 8642b07..c398ac2 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -52,7 +52,9 @@ function resolveCommandPath(command) { } export async function safeSpawn(command, args, options = {}) { - // command should always be alphanumeric or _ or - to avoid injection + // 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}`); } From ab3319a3104dfe5c1ce015362986501f63fa95ac Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 11:51:19 +0100 Subject: [PATCH 078/345] Remove --safe-chain-malware-action flag --- .../safe-chain/src/config/cliArguments.js | 17 ------ .../src/config/cliArguments.spec.js | 56 +----------------- packages/safe-chain/src/config/settings.js | 13 ---- .../src/environment/userInteraction.js | 30 ---------- packages/safe-chain/src/scanning/index.js | 21 +------ .../src/scanning/index.scanCommand.spec.js | 59 ++----------------- 6 files changed, 8 insertions(+), 188 deletions(-) diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 70c56b1..f234bbb 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,5 +1,4 @@ const state = { - malwareAction: undefined, loggingLevel: undefined, }; @@ -7,7 +6,6 @@ const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; export function initializeCliArguments(args) { // Reset state on each call - state.malwareAction = undefined; state.loggingLevel = undefined; const safeChainArgs = []; @@ -21,22 +19,11 @@ export function initializeCliArguments(args) { } } - setMalwareAction(safeChainArgs); setLoggingLevel(safeChainArgs); return remainingArgs; } -function setMalwareAction(args) { - const safeChainMalwareActionArg = SAFE_CHAIN_ARG_PREFIX + "malware-action="; - - const action = getLastArgEqualsValue(args, safeChainMalwareActionArg); - if (!action) { - return; - } - state.malwareAction = action.toLowerCase(); -} - function getLastArgEqualsValue(args, prefix) { for (var i = args.length - 1; i >= 0; i--) { const arg = args[i]; @@ -48,10 +35,6 @@ function getLastArgEqualsValue(args, prefix) { return undefined; } -export function getMalwareAction() { - return state.malwareAction; -} - function setLoggingLevel(args) { const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging="; diff --git a/packages/safe-chain/src/config/cliArguments.spec.js b/packages/safe-chain/src/config/cliArguments.spec.js index c7d9a84..415d34a 100644 --- a/packages/safe-chain/src/config/cliArguments.spec.js +++ b/packages/safe-chain/src/config/cliArguments.spec.js @@ -1,10 +1,6 @@ import { describe, it } from "node:test"; import assert from "node:assert"; -import { - initializeCliArguments, - getMalwareAction, - getLoggingLevel, -} from "./cliArguments.js"; +import { initializeCliArguments, getLoggingLevel } from "./cliArguments.js"; describe("initializeCliArguments", () => { it("should return all args when no safe-chain args are present", () => { @@ -61,55 +57,6 @@ describe("initializeCliArguments", () => { assert.deepEqual(result, ["install", "my--safe-chain-package", "--save"]); }); - it("should not set malwareAction when no safe-chain arguments are passed", () => { - const args = ["install", "express", "--save"]; - const result = initializeCliArguments(args); - - assert.deepEqual(result, ["install", "express", "--save"]); - assert.strictEqual(getMalwareAction(), undefined); - }); - - it("should parse malware-action=block and set state", () => { - const args = ["--safe-chain-malware-action=block", "install", "package"]; - const result = initializeCliArguments(args); - - assert.deepEqual(result, ["install", "package"]); - assert.strictEqual(getMalwareAction(), "block"); - }); - - it("should parse malware-action=prompt and set state", () => { - const args = ["--safe-chain-malware-action=prompt", "install", "package"]; - const result = initializeCliArguments(args); - - assert.deepEqual(result, ["install", "package"]); - assert.strictEqual(getMalwareAction(), "prompt"); - }); - - it("should handle multiple malware-action args, using the last valid one", () => { - const args = [ - "--safe-chain-malware-action=block", - "--safe-chain-malware-action=prompt", - "install", - ]; - const result = initializeCliArguments(args); - - assert.deepEqual(result, ["install"]); - assert.strictEqual(getMalwareAction(), "prompt"); - }); - - it("should handle malware-action with other safe-chain args", () => { - const args = [ - "--safe-chain-debug", - "--safe-chain-malware-action=block", - "--safe-chain-verbose", - "install", - ]; - const result = initializeCliArguments(args); - - assert.deepEqual(result, ["install"]); - assert.strictEqual(getMalwareAction(), "block"); - }); - it("should not set loggingLevel when no logging argument is passed", () => { const args = ["install", "express", "--save"]; initializeCliArguments(args); @@ -170,6 +117,5 @@ describe("initializeCliArguments", () => { assert.deepEqual(result, ["install"]); assert.strictEqual(getLoggingLevel(), "silent"); - assert.strictEqual(getMalwareAction(), "block"); }); }); diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 17c1cdb..ad150b2 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -1,15 +1,5 @@ import * as cliArguments from "./cliArguments.js"; -export function getMalwareAction() { - const action = cliArguments.getMalwareAction(); - - if (action === MALWARE_ACTION_PROMPT) { - return MALWARE_ACTION_PROMPT; - } - - return MALWARE_ACTION_BLOCK; -} - export function getLoggingLevel() { const level = cliArguments.getLoggingLevel(); @@ -20,8 +10,5 @@ export function getLoggingLevel() { return LOGGING_NORMAL; } -export const MALWARE_ACTION_BLOCK = "block"; -export const MALWARE_ACTION_PROMPT = "prompt"; - export const LOGGING_SILENT = "silent"; export const LOGGING_NORMAL = "normal"; diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 99fe90f..e1a4f93 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -1,7 +1,6 @@ // oxlint-disable no-console import chalk from "chalk"; import ora from "ora"; -import { createInterface } from "readline"; import { isCi } from "./environment.js"; import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js"; @@ -87,34 +86,6 @@ function startProcess(message) { } } -async function confirm(config) { - if (isCi() || isSilentMode()) { - return Promise.resolve(config.default); - } - - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - const defaultText = config.default ? " (Y/n)" : " (y/N)"; - rl.question(`${config.message}${defaultText} `, (answer) => { - rl.close(); - - const normalizedAnswer = answer.trim().toLowerCase(); - - if (normalizedAnswer === "y" || normalizedAnswer === "yes") { - resolve(true); - } else if (normalizedAnswer === "n" || normalizedAnswer === "no") { - resolve(false); - } else { - resolve(config.default); - } - }); - }); -} - export const ui = { writeInformation, writeWarning, @@ -122,5 +93,4 @@ export const ui = { writeExitWithoutInstallingMaliciousPackages, emptyLine, startProcess, - confirm, }; diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 4b0f654..969c994 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -4,7 +4,6 @@ import { setTimeout } from "timers/promises"; import chalk from "chalk"; import { getPackageManager } from "../packagemanager/currentPackageManager.js"; import { ui } from "../environment/userInteraction.js"; -import { getMalwareAction, MALWARE_ACTION_PROMPT } from "../config/settings.js"; export function shouldScanCommand(args) { if (!args || args.length === 0) { @@ -65,7 +64,8 @@ export async function scanCommand(args) { return 0; } else { printMaliciousChanges(audit.disallowedChanges, spinner); - return await onMalwareFound(); + onMalwareFound(); + return 1; } } @@ -77,23 +77,8 @@ function printMaliciousChanges(changes, spinner) { } } -async function onMalwareFound() { +function onMalwareFound() { ui.emptyLine(); - - if (getMalwareAction() === MALWARE_ACTION_PROMPT) { - const continueInstall = await ui.confirm({ - message: - "Malicious packages were found. Do you want to continue with the installation?", - default: false, - }); - - if (continueInstall) { - ui.writeWarning("Continuing with the installation despite the risks..."); - return 0; - } - } - ui.writeExitWithoutInstallingMaliciousPackages(); ui.emptyLine(); - return 1; } diff --git a/packages/safe-chain/src/scanning/index.scanCommand.spec.js b/packages/safe-chain/src/scanning/index.scanCommand.spec.js index abcdc97..c47555f 100644 --- a/packages/safe-chain/src/scanning/index.scanCommand.spec.js +++ b/packages/safe-chain/src/scanning/index.scanCommand.spec.js @@ -1,10 +1,6 @@ import assert from "node:assert/strict"; -import { beforeEach, describe, it, mock } from "node:test"; +import { describe, it, mock } from "node:test"; import { setTimeout } from "node:timers/promises"; -import { - MALWARE_ACTION_PROMPT, - MALWARE_ACTION_BLOCK, -} from "../config/settings.js"; describe("scanCommand", async () => { const getScanTimeoutMock = mock.fn(() => 1000); @@ -15,8 +11,6 @@ describe("scanCommand", async () => { fail: () => {}, stop: () => {}, })); - const mockConfirm = mock.fn(() => true); - let malwareAction = MALWARE_ACTION_PROMPT; // import { getPackageManager } from "../packagemanager/currentPackageManager.js"; mock.module("../packagemanager/currentPackageManager.js", { @@ -48,19 +42,10 @@ describe("scanCommand", async () => { writeWarning: () => {}, writeExitWithoutInstallingMaliciousPackages: () => {}, emptyLine: () => {}, - confirm: mockConfirm, }, }, }); - mock.module("../config/settings.js", { - namedExports: { - getMalwareAction: () => malwareAction, - MALWARE_ACTION_PROMPT, - MALWARE_ACTION_BLOCK, - }, - }); - // import { auditChanges, MAX_LENGTH_EXCEEDED } from "./audit/index.js"; mock.module("./audit/index.js", { namedExports: { @@ -89,11 +74,6 @@ describe("scanCommand", async () => { const { scanCommand } = await import("./index.js"); - beforeEach(() => { - // Reset malware action back to prompt mode for other tests - malwareAction = MALWARE_ACTION_PROMPT; - }); - it("should succeed when there are no changes", async () => { let progressWasStopped = false; mockStartProcess.mock.mockImplementationOnce(() => ({ @@ -151,7 +131,7 @@ describe("scanCommand", async () => { assert.equal(failureMessageWasSet, true); }); - it("should fail and prompt the user when malicious changes are detected", async () => { + it("should fail and return 1 malicious changes are detected", async () => { let failureMessageWasSet = false; mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, @@ -164,16 +144,11 @@ describe("scanCommand", async () => { mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "malicious", version: "1.0.0" }, ]); - let userWasPrompted = false; - mockConfirm.mock.mockImplementationOnce(() => { - userWasPrompted = true; - return true; // Simulate user accepting the risk, otherwise the process would exit - }); - await scanCommand(["install", "malicious"]); + const result = await scanCommand(["install", "malicious"]); assert.equal(failureMessageWasSet, true); - assert.equal(userWasPrompted, true); + assert.equal(result, 1); }); it("should not report a timeout when the user takes a long time to respond (it should not affect the timeout)", async () => { @@ -190,10 +165,6 @@ describe("scanCommand", async () => { mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => { return [{ name: "malicious", version: "4.17.21" }]; }); - mockConfirm.mock.mockImplementationOnce(async () => { - await setTimeout(200); - return true; // Simulate user accepting the risk, otherwise the process would exit - }); await scanCommand(["install", "malicious"]); @@ -204,12 +175,6 @@ describe("scanCommand", async () => { }); it("should exit immediately when malicious changes are detected in block mode", async () => { - // Set malware action to block mode for this test - malwareAction = MALWARE_ACTION_BLOCK; - - // Reset mock call count - mockConfirm.mock.resetCalls(); - let failureMessageWasSet = false; mockStartProcess.mock.mockImplementationOnce(() => ({ @@ -229,19 +194,9 @@ describe("scanCommand", async () => { assert.equal(failureMessageWasSet, true); assert.equal(result, 1); - // Confirm should not have been called in block mode - assert.equal(mockConfirm.mock.callCount(), 0); }); it("should exit immediately when malicious changes are detected in block mode without prompting", async () => { - // Set malware action to block mode for this test - malwareAction = MALWARE_ACTION_BLOCK; - - // Reset mock call count - mockConfirm.mock.resetCalls(); - - let userWasPrompted = false; - mockStartProcess.mock.mockImplementationOnce(() => ({ setText: () => {}, succeed: () => {}, @@ -253,14 +208,8 @@ describe("scanCommand", async () => { { name: "malicious", version: "1.0.0" }, ]); - mockConfirm.mock.mockImplementationOnce(() => { - userWasPrompted = true; - return false; - }); - const result = await scanCommand(["install", "malicious"]); assert.equal(result, 1); - assert.equal(userWasPrompted, false); }); }); From ff724154fbfe176593955086e3a2638c734413e9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 13:49:29 +0100 Subject: [PATCH 079/345] Remove --safe-chain-malware-action documentation --- README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/README.md b/README.md index 7b3f038..e385ec4 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: 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. You can check the installed version by running: + ```shell safe-chain --version ``` @@ -75,19 +76,6 @@ To uninstall the Aikido Safe Chain, you can run the following command: # Configuration -## Malware Action - -You can control how Aikido Safe Chain responds when malware is detected using the `--safe-chain-malware-action` flag: - -- `--safe-chain-malware-action=block` (**default**) - Automatically blocks installation and exits with an error when malware is detected -- `--safe-chain-malware-action=prompt` - Prompts the user to decide whether to continue despite the malware detection - -Example usage: - -```shell -npm install suspicious-package --safe-chain-malware-action=prompt -``` - ## Logging You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag: From c5e25f4813d1a7c2952e0c8e52998dc88272ffce Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 17:09:28 +0100 Subject: [PATCH 080/345] Add verbose logging setting + setup buffering of logs to prevent interleaving logs with the package manager. --- packages/safe-chain/src/config/settings.js | 5 ++ .../src/environment/userInteraction.js | 51 ++++++++++++++++--- packages/safe-chain/src/main.js | 8 +++ .../safe-chain/src/scanning/audit/index.js | 7 +++ 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ad150b2..7f932a7 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -7,8 +7,13 @@ export function getLoggingLevel() { return LOGGING_SILENT; } + if (level === LOGGING_VERBOSE) { + return LOGGING_VERBOSE; + } + return LOGGING_NORMAL; } export const LOGGING_SILENT = "silent"; export const LOGGING_NORMAL = "normal"; +export const LOGGING_VERBOSE = "verbose"; diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index a6a2253..d81ebc9 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -2,12 +2,25 @@ import chalk from "chalk"; import ora from "ora"; import { isCi } from "./environment.js"; -import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js"; +import { + getLoggingLevel, + LOGGING_SILENT, + LOGGING_VERBOSE, +} from "../config/settings.js"; + +const state = { + bufferOutput: false, + bufferedMessages: [], +}; function isSilentMode() { return getLoggingLevel() === LOGGING_SILENT; } +function isVerboseMode() { + return getLoggingLevel() === LOGGING_VERBOSE; +} + function emptyLine() { if (isSilentMode()) return; @@ -17,7 +30,7 @@ function emptyLine() { function writeInformation(message, ...optionalParams) { if (isSilentMode()) return; - console.log(message, ...optionalParams); + writeOrBuffer(() => console.log(message, ...optionalParams)); } function writeWarning(message, ...optionalParams) { @@ -26,14 +39,14 @@ function writeWarning(message, ...optionalParams) { if (!isCi()) { message = chalk.yellow(message); } - console.warn(message, ...optionalParams); + writeOrBuffer(() => console.warn(message, ...optionalParams)); } function writeError(message, ...optionalParams) { if (!isCi()) { message = chalk.red(message); } - console.error(message, ...optionalParams); + writeOrBuffer(() => console.error(message, ...optionalParams)); } function writeExitWithoutInstallingMaliciousPackages() { @@ -41,12 +54,21 @@ function writeExitWithoutInstallingMaliciousPackages() { if (!isCi()) { message = chalk.red(message); } - console.error(message); + writeOrBuffer(() => console.error(message)); } function writeVerboseInformation(message, ...optionalParams) { - // TODO: Correctly implement verbose logging - writeInformation(message, ...optionalParams); + if (!isVerboseMode()) return; + + writeOrBuffer(() => console.log(message, ...optionalParams)); +} + +function writeOrBuffer(messageFunction) { + if (state.bufferOutput) { + state.bufferedMessages.push(messageFunction); + } else { + messageFunction(); + } } function startProcess(message) { @@ -91,6 +113,19 @@ function startProcess(message) { } } +function startBufferingLogs() { + state.bufferOutput = true; + state.bufferedMessages = []; +} + +function writeBufferedLogsAndStopBuffering() { + state.bufferOutput = false; + for (const log of state.bufferedMessages) { + log(); + } + state.bufferedMessages = []; +} + export const ui = { writeInformation, writeVerboseInformation, @@ -99,4 +134,6 @@ export const ui = { writeExitWithoutInstallingMaliciousPackages, emptyLine, startProcess, + startBufferingLogs, + writeBufferedLogsAndStopBuffering, }; diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index e106e83..3c7103a 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -25,8 +25,16 @@ export async function main(args) { } } + // Buffer logs during package manager execution, this avoids interleaving + // of logs from the package manager and safe-chain + // Not doing this could cause bugs to disappear when cursor movement codes + // are written by the package manager while safe-chain is writing logs + ui.startBufferingLogs(); const packageManagerResult = await getPackageManager().runCommand(args); + // Write all buffered logs + ui.writeBufferedLogsAndStopBuffering(); + if (!proxy.verifyNoMaliciousPackages()) { return 1; } diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 215bfa0..6bd1dec 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -1,3 +1,4 @@ +import { ui } from "../../environment/userInteraction.js"; import { MALWARE_STATUS_MALWARE, openMalwareDatabase, @@ -19,8 +20,14 @@ export async function auditChanges(changes) { ); if (malwarePackage) { + ui.writeVerboseInformation( + `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}` + ); disallowedChanges.push({ ...change, reason: malwarePackage.status }); } else { + ui.writeVerboseInformation( + `Safe-chain: Package ${change.name}@${change.version} is clean` + ); allowedChanges.push(change); } } From ddc8218a2d49291b9ca3dce11c3c87e3ca38485f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 17:14:45 +0100 Subject: [PATCH 081/345] Rename writeVerboseInformation to writeVerbose --- .../safe-chain/src/environment/userInteraction.js | 4 ++-- .../src/registryProxy/mitmRequestHandler.js | 12 +++++------- .../safe-chain/src/registryProxy/registryProxy.js | 2 +- packages/safe-chain/src/scanning/audit/index.js | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index d81ebc9..0a47959 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -57,7 +57,7 @@ function writeExitWithoutInstallingMaliciousPackages() { writeOrBuffer(() => console.error(message)); } -function writeVerboseInformation(message, ...optionalParams) { +function writeVerbose(message, ...optionalParams) { if (!isVerboseMode()) return; writeOrBuffer(() => console.log(message, ...optionalParams)); @@ -127,8 +127,8 @@ function writeBufferedLogsAndStopBuffering() { } export const ui = { + writeVerbose, writeInformation, - writeVerboseInformation, writeWarning, writeError, writeExitWithoutInstallingMaliciousPackages, diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index b0c0af7..eec59e8 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -4,11 +4,11 @@ import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; export function mitmConnect(req, clientSocket, isAllowed) { - ui.writeVerboseInformation(`Safe-chain: Set up MITM tunnel for ${req.url}`); + ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`); const { hostname } = new URL(`http://${req.url}`); clientSocket.on("error", (err) => { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Client socket error for ${req.url}: ${err.message}` ); // NO-OP @@ -33,9 +33,7 @@ function createHttpsServer(hostname, isAllowed) { const targetUrl = `https://${hostname}${pathAndQuery}`; if (!(await isAllowed(targetUrl))) { - ui.writeVerboseInformation( - `Safe-chain: Blocking request to ${targetUrl}` - ); + ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); res.writeHead(403, "Forbidden - blocked by safe-chain"); res.end("Blocked by safe-chain"); return; @@ -66,7 +64,7 @@ function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); proxyReq.on("error", (err) => { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Error occurred while proxying request: ${err.message}` ); res.writeHead(502); @@ -78,7 +76,7 @@ function forwardRequest(req, hostname, res) { }); req.on("end", () => { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Finished proxying request to ${req.url} for ${hostname}` ); proxyReq.end(); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3c8b902..3822639 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -109,7 +109,7 @@ function handleConnect(req, clientSocket, head) { mitmConnect(req, clientSocket, isAllowedUrl); } else { // For other hosts, just tunnel the request to the destination tcp socket - ui.writeVerboseInformation(`Safe-chain: Tunneling request to ${req.url}`); + ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } } diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 6bd1dec..16c54b6 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -20,12 +20,12 @@ export async function auditChanges(changes) { ); if (malwarePackage) { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}` ); disallowedChanges.push({ ...change, reason: malwarePackage.status }); } else { - ui.writeVerboseInformation( + ui.writeVerbose( `Safe-chain: Package ${change.name}@${change.version} is clean` ); allowedChanges.push(change); From 5eeb68e35501dd9de60e62a11ab001f309bffcad Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 27 Oct 2025 17:20:14 +0100 Subject: [PATCH 082/345] Add documentation for verbose log level --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e385ec4..05ea2a4 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,19 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin - `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit. -Example usage: + Example usage: -```shell -npm install express --safe-chain-logging=silent -``` + ```shell + npm install express --safe-chain-logging=silent + ``` + +- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes. + + Example usage: + + ```shell + npm install express --safe-chain-logging=verbose + ``` # Usage in CI/CD From 190607de9235727c1c06eb02af563b3be68ea136 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 27 Oct 2025 09:23:47 -0700 Subject: [PATCH 083/345] 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 084/345] 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 085/345] 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 086/345] 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 087/345] 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 088/345] 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 089/345] 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 090/345] 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 091/345] 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 092/345] 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 093/345] 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 094/345] 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 095/345] 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 096/345] 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 097/345] 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 098/345] 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 099/345] 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 100/345] 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 65c9ca62de57fc86dc66de120dcf252269542ffb Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 09:39:16 +0100 Subject: [PATCH 101/345] Subscribe to more error events to prevent the process from crashing --- .../src/registryProxy/mitmRequestHandler.js | 23 +++++++++++++++++- .../src/registryProxy/plainHttpProxy.js | 9 +++++-- .../src/registryProxy/tunnelRequestHandler.js | 24 ++++++++++++------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 63a8168..e95bb62 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -37,13 +37,19 @@ function createHttpsServer(hostname, isAllowed) { forwardRequest(req, hostname, res); } - return https.createServer( + const server = https.createServer( { key: cert.privateKey, cert: cert.certificate, }, handleRequest ); + + server.on("error", (err) => { + ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); + }); + + return server; } function getRequestPathAndQuery(url) { @@ -62,6 +68,11 @@ function forwardRequest(req, hostname, res) { res.end("Bad Gateway"); }); + req.on("error", (err) => { + ui.writeError(`Safe-chain: Error reading client request: ${err.message}`); + proxyReq.destroy(); + }); + req.on("data", (chunk) => { proxyReq.write(chunk); }); @@ -88,6 +99,16 @@ function createProxyRequest(hostname, req, res) { } const proxyReq = https.request(options, (proxyRes) => { + proxyRes.on("error", (err) => { + ui.writeError( + `Safe-chain: Error reading upstream response: ${err.message}` + ); + if (!res.headersSent) { + res.writeHead(502); + res.end("Bad Gateway"); + } + }); + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index e337b44..ac3dc69 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -43,8 +43,13 @@ export function handleHttpProxyRequest(req, res) { } ) .on("error", (err) => { - res.writeHead(502); - res.end(`Bad Gateway: ${err.message}`); + if (!res.headersSent) { + res.writeHead(502); + res.end(`Bad Gateway: ${err.message}`); + } else { + // Headers already sent, just destroy the response + res.destroy(); + } }); req.on("error", () => { diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index c28a022..91b1c40 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -24,12 +24,6 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); - clientSocket.on("error", () => { - // NO-OP - // This can happen if the client TCP socket sends RST instead of FIN. - // Not subscribing to 'close' event will cause node to throw and crash. - }); - const serverSocket = net.connect(port || 443, hostname, () => { clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); serverSocket.write(head); @@ -37,6 +31,14 @@ function tunnelRequestToDestination(req, clientSocket, head) { clientSocket.pipe(serverSocket); }); + clientSocket.on("error", () => { + // This can happen if the client TCP socket sends RST instead of FIN. + // Not subscribing to 'close' event will cause node to throw and crash. + if (serverSocket.writable) { + serverSocket.end(); + } + }); + serverSocket.on("error", (err) => { ui.writeError( `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` @@ -100,9 +102,13 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { proxy.port || 8080 } - ${err.message}` ); - if (clientSocket.writable) { - clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); - } + } else { + ui.writeError( + `Safe-chain: proxy socket error after connection - ${err.message}` + ); + } + if (clientSocket.writable) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } }); From efb0044419d747df02f2c063dd530ee3006aff2f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 10:26:56 +0100 Subject: [PATCH 102/345] Add global exception handlers --- packages/safe-chain/src/main.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index e106e83..c3af410 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -11,6 +11,21 @@ export async function main(args) { const proxy = createSafeChainProxy(); await proxy.startServer(); + // Global error handlers to log unhandled errors + process.on("uncaughtException", (error) => { + ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); + ui.writeVerbose(`Stack trace: ${error.stack}`); + process.exit(1); + }); + + process.on("unhandledRejection", (reason) => { + ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); + if (reason instanceof Error) { + ui.writeVerbose(`Stack trace: ${reason.stack}`); + } + process.exit(1); + }); + try { // This parses all the --safe-chain arguments and removes them from the args array args = initializeCliArguments(args); From 088c215569a8c9f02407043e0cdea623d33fc76d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 10:39:24 +0100 Subject: [PATCH 103/345] Write logs on SIGTERM and SIGINT --- packages/safe-chain/src/main.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 3c7103a..00a6e7f 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -8,6 +8,9 @@ import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; import chalk from "chalk"; export async function main(args) { + process.on("SIGINT", handleProcessTermination); + process.on("SIGTERM", handleProcessTermination); + const proxy = createSafeChainProxy(); await proxy.startServer(); @@ -59,3 +62,7 @@ export async function main(args) { await proxy.stopServer(); } } + +function handleProcessTermination() { + ui.writeBufferedLogsAndStopBuffering(); +} From bae43d0dcda6622c4e53eb0b9887a0755ce8a19c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 11:38:16 +0100 Subject: [PATCH 104/345] MITM handler: Close the response on server error --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index e95bb62..5967d1a 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -47,6 +47,12 @@ function createHttpsServer(hostname, isAllowed) { server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); + if (!res.headersSent) { + res.writeHead(502); + res.end("Bad Gateway"); + } else if (res.writable) { + res.destroy(); + } }); return server; From df5c424a42721b0aa0337f55869211270c2f939f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 11:38:39 +0100 Subject: [PATCH 105/345] Add missing import (ui) in mitmRequestHandler.js --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 5967d1a..101fdd8 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -1,6 +1,7 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; +import { ui } from "../environment/userInteraction.js"; export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); From 4dc14397ad45596b1afcac015362ca8c0dcdf5f9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 11:40:01 +0100 Subject: [PATCH 106/345] Use correct event name in comment (error) --- packages/safe-chain/src/registryProxy/tunnelRequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 91b1c40..ceaf91d 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -33,7 +33,7 @@ function tunnelRequestToDestination(req, clientSocket, head) { clientSocket.on("error", () => { // This can happen if the client TCP socket sends RST instead of FIN. - // Not subscribing to 'close' event will cause node to throw and crash. + // Not subscribing to 'error' event will cause node to throw and crash. if (serverSocket.writable) { serverSocket.end(); } From 78fd93b72a3adc71c3b16a979039b6f637750d3e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 11:41:39 +0100 Subject: [PATCH 107/345] End clientsocket without 502 in case of proxySocket error --- .../safe-chain/src/registryProxy/tunnelRequestHandler.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index ceaf91d..5c764f5 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -102,13 +102,16 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { proxy.port || 8080 } - ${err.message}` ); + if (clientSocket.writable) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + } } else { ui.writeError( `Safe-chain: proxy socket error after connection - ${err.message}` ); - } - if (clientSocket.writable) { - clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + if (clientSocket.writable) { + clientSocket.end(); + } } }); From 3721ca91131ee792a13972e6809048403a9b9d30 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 31 Oct 2025 13:56:35 +0100 Subject: [PATCH 108/345] Fix linter issues --- .oxlintrc.json | 3 ++- .../src/registryProxy/mitmRequestHandler.js | 19 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index b76f2ad..b9c483c 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -14,7 +14,8 @@ }, "rules": { "eslint/no-console": "error", - "eslint/no-empty": "error" + "eslint/no-empty": "error", + "eslint/no-undef": "error" }, "overrides": [ { diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 101fdd8..fe8998e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -14,6 +14,15 @@ export function mitmConnect(req, clientSocket, isAllowed) { const server = createHttpsServer(hostname, isAllowed); + server.on("error", (err) => { + ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); + if (!clientSocket.headersSent) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + } else if (clientSocket.writable) { + clientSocket.end(); + } + }); + // Establish the connection clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); @@ -46,16 +55,6 @@ function createHttpsServer(hostname, isAllowed) { handleRequest ); - server.on("error", (err) => { - ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); - if (!res.headersSent) { - res.writeHead(502); - res.end("Bad Gateway"); - } else if (res.writable) { - res.destroy(); - } - }); - return server; } From c2a9cc27337ac1ad46e9e0ac28f2114edb7ec0d4 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 31 Oct 2025 07:51:26 -0700 Subject: [PATCH 109/345] 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 110/345] 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 c88b1a624fc3832b15b884896b22375f976ee45e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:06:06 +0100 Subject: [PATCH 111/345] Type check safe-chain package --- package-lock.json | 357 ++++++++++++++++++ packages/safe-chain-bun/src/index.js | 2 +- packages/safe-chain/bin/aikido-bun.js | 1 + packages/safe-chain/bin/aikido-bunx.js | 1 + packages/safe-chain/bin/aikido-npm.js | 1 + packages/safe-chain/bin/aikido-npx.js | 1 + packages/safe-chain/bin/aikido-pnpm.js | 1 + packages/safe-chain/bin/aikido-pnpx.js | 1 + packages/safe-chain/bin/aikido-yarn.js | 1 + packages/safe-chain/package.json | 11 +- packages/safe-chain/src/api/aikido.js | 15 +- packages/safe-chain/src/api/npmApi.js | 25 +- .../safe-chain/src/config/cliArguments.js | 16 + packages/safe-chain/src/config/configFile.js | 51 ++- .../src/environment/userInteraction.js | 28 ++ packages/safe-chain/src/main.js | 8 +- .../packagemanager/_shared/matchesCommand.js | 5 + .../bun/createBunPackageManager.js | 18 +- .../packagemanager/currentPackageManager.js | 15 + .../npm/createPackageManager.js | 23 ++ .../commandArgumentScanner.js | 37 ++ .../npm/dependencyScanner/nullScanner.js | 5 +- .../parsing/parsePackagesFromInstallArgs.js | 37 +- .../src/packagemanager/npm/runNpmCommand.js | 15 +- .../npm/utils/abbrevs-generated.js | 1 + .../src/packagemanager/npm/utils/cmd-list.js | 5 + .../packagemanager/npm/utils/npmCommands.js | 8 + .../npx/createPackageManager.js | 3 + .../commandArgumentScanner.js | 14 +- .../npx/parsing/parsePackagesFromArguments.js | 22 ++ .../src/packagemanager/npx/runNpxCommand.js | 8 +- .../pnpm/createPackageManager.js | 11 + .../commandArgumentScanner.js | 7 + .../parsing/parsePackagesFromArguments.js | 21 ++ .../src/packagemanager/pnpm/runPnpmCommand.js | 9 +- .../yarn/createPackageManager.js | 8 + .../commandArgumentScanner.js | 7 + .../parsing/parsePackagesFromArguments.js | 24 ++ .../src/packagemanager/yarn/runYarnCommand.js | 13 +- .../safe-chain/src/registryProxy/certUtils.js | 4 + .../src/registryProxy/mitmRequestHandler.js | 40 +- .../src/registryProxy/parsePackageFromUrl.js | 4 + .../src/registryProxy/plainHttpProxy.js | 9 + .../src/registryProxy/registryProxy.js | 33 ++ .../src/registryProxy/tunnelRequestHandler.js | 22 ++ .../safe-chain/src/scanning/audit/index.js | 23 ++ packages/safe-chain/src/scanning/index.js | 19 +- .../src/scanning/malwareDatabase.js | 25 +- .../src/shell-integration/helpers.js | 41 +- .../src/shell-integration/setup-ci.js | 20 + .../safe-chain/src/shell-integration/setup.js | 5 +- .../src/shell-integration/shellDetection.js | 13 +- .../supported-shells/bash.js | 20 +- .../supported-shells/fish.js | 10 +- .../supported-shells/powershell.js | 10 +- .../supported-shells/windowsPowershell.js | 10 +- .../shell-integration/supported-shells/zsh.js | 7 +- .../src/shell-integration/teardown.js | 5 +- packages/safe-chain/src/utils/safeSpawn.js | 35 ++ packages/safe-chain/tsconfig.json | 21 ++ 60 files changed, 1179 insertions(+), 33 deletions(-) create mode 100644 packages/safe-chain/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 88e9fb5..4b0b5cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -411,6 +411,94 @@ "node": ">=14" } }, + "node_modules/@types/make-fetch-happen": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.4.tgz", + "integrity": "sha512-jKzweQaEMMAi55ehvR1z0JF6aSVQm/h1BXBhPLOJriaeQBctjw5YbpIGs7zAx9dN0Sa2OO5bcXwCkrlgenoPEA==", + "dev": true, + "dependencies": { + "@types/node-fetch": "*", + "@types/retry": "*", + "@types/ssri": "*" + } + }, + "node_modules/@types/node": { + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "dev": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/npm-package-arg": { + "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 + }, + "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, + "dependencies": { + "@types/node": "*", + "@types/node-fetch": "*", + "@types/npm-package-arg": "*", + "@types/npmlog": "*", + "@types/ssri": "*" + } + }, + "node_modules/@types/npmlog": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/npmlog/-/npmlog-7.0.0.tgz", + "integrity": "sha512-hJWbrKFvxKyWwSUXjZMYTINsSOY6IclhvGOZ97M8ac2tmR9hMwmTnYaMdpGhvju9ctWLTPhCS+eLfQNluiEjQQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "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 + }, + "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, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -444,6 +532,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -516,6 +610,19 @@ "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==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -581,6 +688,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -612,6 +731,29 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dev": true, + "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", @@ -653,6 +795,51 @@ "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -669,6 +856,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -681,6 +884,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==", + "dev": true, + "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", @@ -693,6 +905,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==", + "dev": true, + "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==", + "dev": true, + "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", @@ -728,6 +977,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==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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==", + "dev": true, + "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", @@ -919,6 +1219,36 @@ "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "dependencies": { + "mime-db": "1.52.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", @@ -1563,6 +1893,25 @@ "node": ">=18" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "dev": true + }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -1739,6 +2088,14 @@ "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", "safe-chain": "bin/safe-chain.js" + }, + "devDependencies": { + "@types/make-fetch-happen": "^10.0.4", + "@types/node": "^24.9.2", + "@types/node-forge": "^1.3.14", + "@types/npm-registry-fetch": "^8.0.9", + "@types/semver": "^7.7.1", + "typescript": "^5.9.3" } }, "packages/safe-chain-bun": { diff --git a/packages/safe-chain-bun/src/index.js b/packages/safe-chain-bun/src/index.js index 660e0bd..6e933c3 100644 --- a/packages/safe-chain-bun/src/index.js +++ b/packages/safe-chain-bun/src/index.js @@ -29,7 +29,7 @@ export const scanner = { }); } } - } catch (error) { + } catch (/** @type any */ error) { console.warn(`Safe-Chain security scan failed: ${error.message}`); } diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index 01e3972..2467512 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -7,4 +7,5 @@ const packageManagerName = "bun"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index fb378e5..6a84ff8 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -7,4 +7,5 @@ const packageManagerName = "bunx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 0e9f302..4a82c34 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -7,4 +7,5 @@ const packageManagerName = "npm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index d3dfdd6..40118d4 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -7,4 +7,5 @@ const packageManagerName = "npx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index 0a06217..e495568 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -7,4 +7,5 @@ const packageManagerName = "pnpm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index cdb6504..75f093d 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -7,4 +7,5 @@ const packageManagerName = "pnpx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index fd87606..3ca5b94 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -7,4 +7,5 @@ const packageManagerName = "yarn"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 98ccd52..4e55840 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -4,7 +4,8 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "oxlint --deny-warnings" + "lint": "oxlint --deny-warnings", + "typecheck": "tsc --noEmit" }, "bin": { "aikido-npm": "bin/aikido-npm.js", @@ -38,6 +39,14 @@ "ora": "8.2.0", "semver": "7.7.2" }, + "devDependencies": { + "@types/make-fetch-happen": "^10.0.4", + "@types/node": "^24.9.2", + "@types/npm-registry-fetch": "^8.0.9", + "@types/semver": "^7.7.1", + "@types/node-forge": "^1.3.14", + "typescript": "^5.9.3" + }, "main": "src/main.js", "bugs": { "url": "https://github.com/AikidoSec/safe-chain/issues" diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index c9eeea0..7e786d1 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -3,6 +3,16 @@ import fetch from "make-fetch-happen"; const malwareDatabaseUrl = "https://malware-list.aikido.dev/malware_predictions.json"; +/** + * @typedef MalwarePackage + * @property {string} package_name + * @property {string} version + * @property {string} reason + */ + +/** + * @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>} + */ export async function fetchMalwareDatabase() { const response = await fetch(malwareDatabaseUrl); if (!response.ok) { @@ -15,11 +25,14 @@ export async function fetchMalwareDatabase() { malwareDatabase: malwareDatabase, version: response.headers.get("etag") || undefined, }; - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error(`Error parsing malware database: ${error.message}`); } } +/** + * @returns {Promise} + */ export async function fetchMalwareDatabaseVersion() { const response = await fetch(malwareDatabaseUrl, { method: "HEAD", diff --git a/packages/safe-chain/src/api/npmApi.js b/packages/safe-chain/src/api/npmApi.js index deb917d..c8e2218 100644 --- a/packages/safe-chain/src/api/npmApi.js +++ b/packages/safe-chain/src/api/npmApi.js @@ -1,6 +1,11 @@ import * as semver from "semver"; import * as npmFetch from "npm-registry-fetch"; +/** + * @param {string} packageName + * @param {string | null} [versionRange] + * @returns {Promise} + */ export async function resolvePackageVersion(packageName, versionRange) { if (!versionRange) { versionRange = "latest"; @@ -11,7 +16,10 @@ export async function resolvePackageVersion(packageName, versionRange) { return versionRange; } - const packageInfo = await getPackageInfo(packageName); + const packageInfo = ( + /** @type {{"dist-tags"?: Record} | null} */ + await getPackageInfo(packageName) + ); if (!packageInfo) { // It is possible that no version is found (could be a private package, or a package that doesn't exist) // In this case, we return null to indicate that we couldn't resolve the version @@ -19,7 +27,7 @@ export async function resolvePackageVersion(packageName, versionRange) { } const distTags = packageInfo["dist-tags"]; - if (distTags && distTags[versionRange]) { + if (distTags && isDistTags(distTags)) { // If the version range is a dist-tag, return the version associated with that tag // e.g., "latest", "next", etc. return distTags[versionRange]; @@ -41,6 +49,19 @@ export async function resolvePackageVersion(packageName, versionRange) { return null; } +/** + * + * @param {unknown} distTags + * @returns {distTags is Record} + */ +function isDistTags(distTags) { + return typeof distTags === "object"; +} + +/** + * @param {string} packageName + * @returns {Promise | null>} + */ async function getPackageInfo(packageName) { try { return await npmFetch.json(packageName); diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index f234bbb..04645d8 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,9 +1,16 @@ +/** + * @type {{loggingLevel: string | undefined}} + */ const state = { loggingLevel: undefined, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; +/** + * @param {string[]} args + * @returns {string[]} + */ export function initializeCliArguments(args) { // Reset state on each call state.loggingLevel = undefined; @@ -24,6 +31,11 @@ export function initializeCliArguments(args) { return remainingArgs; } +/** + * @param {string[]} args + * @param {string} prefix + * @returns {string | undefined} + */ function getLastArgEqualsValue(args, prefix) { for (var i = args.length - 1; i >= 0; i--) { const arg = args[i]; @@ -35,6 +47,10 @@ function getLastArgEqualsValue(args, prefix) { return undefined; } +/** + * @param {string[]} args + * @returns {void} + */ function setLoggingLevel(args) { const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging="; diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 2feb307..83bc186 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,13 +3,46 @@ import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; +/** + * @returns {number} + */ export function getScanTimeout() { + if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { + const timeout = parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS); + if (!isNaN(timeout) && timeout >= 0) { + return timeout; + } + } + const config = readConfigFile(); + + if (hasScanTimeout(config) && config.scanTimeout >= 0) { + return config.scanTimeout; + } + + return 10000; // Default to 10 seconds +} + +/** + * @param {unknown} config + * + * @returns {config is {scanTimeout: number}} + */ +function hasScanTimeout(config) { return ( - parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds + typeof config === "object" && + config !== null && + "scanTimeout" in config && + typeof config.scanTimeout === "number" ); } +/** + * @param {import("../api/aikido.js").MalwarePackage[]} data + * @param {string | number} version + * + * @returns {void} + */ export function writeDatabaseToLocalCache(data, version) { try { const databasePath = getDatabasePath(); @@ -24,6 +57,9 @@ export function writeDatabaseToLocalCache(data, version) { } } +/** + * @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}} + */ export function readDatabaseFromLocalCache() { try { const databasePath = getDatabasePath(); @@ -55,6 +91,9 @@ export function readDatabaseFromLocalCache() { } } +/** + * @returns {unknown} + */ function readConfigFile() { const configFilePath = getConfigFilePath(); @@ -66,20 +105,30 @@ function readConfigFile() { return JSON.parse(data); } +/** + * @returns {string} + */ function getDatabasePath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "malwareDatabase.json"); } + function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "version.txt"); } +/** + * @returns {string} + */ function getConfigFilePath() { return path.join(getAikidoDirectory(), "config.json"); } +/** + * @returns {string} + */ function getAikidoDirectory() { const homeDir = os.homedir(); const aikidoDir = path.join(homeDir, ".aikido"); diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index e1a4f93..43f5312 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -14,12 +14,22 @@ function emptyLine() { writeInformation(""); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeInformation(message, ...optionalParams) { if (isSilentMode()) return; console.log(message, ...optionalParams); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeWarning(message, ...optionalParams) { if (isSilentMode()) return; @@ -29,6 +39,11 @@ function writeWarning(message, ...optionalParams) { console.warn(message, ...optionalParams); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeError(message, ...optionalParams) { if (!isCi()) { message = chalk.red(message); @@ -44,6 +59,19 @@ function writeExitWithoutInstallingMaliciousPackages() { console.error(message); } +/** + * @typedef Spinner + * @property {(message: string) => void} succeed + * @property {(message: string) => void} fail + * @property {() => void} stop + * @property {(message: string) => void} setText + */ + +/** + * @param {string} message + * + * @returns {Spinner} + */ function startProcess(message) { if (isSilentMode()) { return { diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index c3af410..fca1218 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -7,6 +7,10 @@ import { initializeCliArguments } from "./config/cliArguments.js"; import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; import chalk from "chalk"; +/** + * @param {string[]} args + * @returns {Promise} + */ export async function main(args) { const proxy = createSafeChainProxy(); await proxy.startServer(); @@ -14,6 +18,7 @@ export async function main(args) { // Global error handlers to log unhandled errors process.on("uncaughtException", (error) => { ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); + // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${error.stack}`); process.exit(1); }); @@ -21,6 +26,7 @@ export async function main(args) { process.on("unhandledRejection", (reason) => { ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); if (reason instanceof Error) { + // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${reason.stack}`); } process.exit(1); @@ -56,7 +62,7 @@ export async function main(args) { // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code return packageManagerResult.status; - } catch (error) { + } catch (/** @type any */ error) { ui.writeError("Failed to check for malicious packages:", error.message); // Returning the exit code back to the caller allows the promise diff --git a/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js b/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js index d72caca..f939352 100644 --- a/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js +++ b/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js @@ -1,3 +1,8 @@ +/** + * @param {string[]} args + * @param {...string} commandArgs + * @returns {boolean} + */ export function matchesCommand(args, ...commandArgs) { if (args.length < commandArgs.length) { return false; diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 14faa5f..88c84f5 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -2,6 +2,9 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createBunPackageManager() { return { runCommand: (args) => runBunCommand("bun", args), @@ -9,10 +12,13 @@ export function createBunPackageManager() { // For bun, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: () => [], + getDependencyUpdatesForCommand: async () => [], }; } +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createBunxPackageManager() { return { runCommand: (args) => runBunCommand("bunx", args), @@ -20,18 +26,24 @@ export function createBunxPackageManager() { // For bunx, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: () => [], + getDependencyUpdatesForCommand: async () => [], }; } +/** + * @param {string} command + * @param {string[]} args + * @returns {Promise<{status: number}>} + */ async function runBunCommand(command, args) { try { const result = await safeSpawn(command, args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2f019a1..aaf8aae 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -10,10 +10,25 @@ import { } from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; +/** + * @type {{packageManagerName: PackageManager | null}} + */ const state = { packageManagerName: null, }; +/** + * @typedef PackageManager + * @property {(args: string[]) => Promise<{ status: number }>} runCommand + * @property {(args: string[]) => boolean} isSupportedCommand + * @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand + */ + +/** + * @param {string} packageManagerName + * + * @return {PackageManager} + */ export function initializePackageManager(packageManagerName) { if (packageManagerName === "npm") { state.packageManagerName = createNpmPackageManager(); diff --git a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js index 731f406..465bf60 100644 --- a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js @@ -8,7 +8,15 @@ import { npmExecCommand, } from "./utils/npmCommands.js"; +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createNpmPackageManager() { + /** + * @param {string[]} args + * + * @returns {boolean} + */ function isSupportedCommand(args) { const scanner = findDependencyScannerForCommand( commandScannerMapping, @@ -17,6 +25,11 @@ export function createNpmPackageManager() { return scanner.shouldScan(args); } + /** + * @param {string[]} args + * + * @returns {Promise<{name: string, version: string, type: string}[]>} + */ function getDependencyUpdatesForCommand(args) { const scanner = findDependencyScannerForCommand( commandScannerMapping, @@ -32,12 +45,22 @@ export function createNpmPackageManager() { }; } +/** + * @type {Record} + */ const commandScannerMapping = { [npmInstallCommand]: commandArgumentScanner(), [npmUpdateCommand]: commandArgumentScanner(), [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run }; +/** + * + * @param {Record} scanners + * @param {string[]} args + * + * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ function findDependencyScannerForCommand(scanners, args) { const command = getNpmCommandForArgs(args); if (!command) { diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js index ae05f6d..51746c4 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js @@ -2,6 +2,29 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js"; import { hasDryRunArg } from "../utils/npmCommands.js"; +/** + * @typedef {Object} ScanResult + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef {Object} ScannerOptions + * @property {boolean} [ignoreDryRun] + */ + +/** + * @typedef CommandArgumentScanner + * @property {(args: string[]) => Promise} scan + * @property {(args: string[]) => boolean} shouldScan + */ + +/** + * @param {ScannerOptions} [opts] + * + * @returns {CommandArgumentScanner} + */ export function commandArgumentScanner(opts) { const ignoreDryRun = opts?.ignoreDryRun ?? false; @@ -10,14 +33,28 @@ export function commandArgumentScanner(opts) { shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun), }; } + +/** + * @param {string[]} args + * @returns {Promise} + */ function scanDependencies(args) { return checkChangesFromArgs(args); } +/** + * @param {string[]} args + * @param {boolean} ignoreDryRun + * @returns {boolean} + */ function shouldScanDependencies(args, ignoreDryRun) { return ignoreDryRun || !hasDryRunArg(args); } +/** + * @param {string[]} args + * @returns {Promise} + */ export async function checkChangesFromArgs(args) { const changes = []; const packageUpdates = parsePackagesFromInstallArgs(args); diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js index a7b2ffd..449fed4 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js @@ -1,6 +1,9 @@ +/** + * @returns {import("./commandArgumentScanner.js").CommandArgumentScanner} + */ export function nullScanner() { return { - scan: () => [], + scan: async () => [], shouldScan: () => false, }; } diff --git a/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js index e731240..b7277e7 100644 --- a/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js @@ -1,5 +1,22 @@ +/** + * @typedef {Object} PackageDetail + * @property {string} name + * @property {string} version + */ + +/** + * @typedef {Object} NpmOption + * @property {string} name + * @property {number} numberOfParameters + */ + +/** + * @param {string[]} args + * @returns {PackageDetail[]} + */ export function parsePackagesFromInstallArgs(args) { - const changes = []; + /** @type {{name: string, version: string | null}[]} */ + const changes = []; let defaultTag = "latest"; // Skip first argument (install command) @@ -32,9 +49,13 @@ export function parsePackagesFromInstallArgs(args) { } } - return changes; + return /** @type {PackageDetail[]} */ (changes); } +/** + * @param {string} arg + * @returns {NpmOption | undefined} + */ function getNpmOption(arg) { if (isNpmOptionWithParameter(arg)) { return { @@ -54,6 +75,10 @@ function getNpmOption(arg) { return undefined; } +/** + * @param {string} arg + * @returns {boolean} + */ function isNpmOptionWithParameter(arg) { const optionsWithParameters = [ "--access", @@ -81,6 +106,10 @@ function isNpmOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @returns {{name: string, version: string | null}} + */ function parsePackagename(arg) { arg = removeAlias(arg); const lastAtIndex = arg.lastIndexOf("@"); @@ -102,6 +131,10 @@ function parsePackagename(arg) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { const aliasIndex = arg.indexOf("@npm:"); if (aliasIndex !== -1) { diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index 26a4a9d..c068240 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -2,14 +2,20 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @param {string[]} args + * + * @returns {Promise<{status: number}>} + */ export async function runNpm(args) { try { const result = await safeSpawn("npm", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { @@ -19,6 +25,10 @@ export async function runNpm(args) { } } +/** + * @param {string[]} args + * @returns {Promise<{status: number, output?: string}>} + */ export async function dryRunNpmCommandAndOutput(args) { try { const result = await safeSpawn( @@ -26,6 +36,7 @@ export async function dryRunNpmCommandAndOutput(args) { [...args, "--ignore-scripts", "--dry-run"], { stdio: "pipe", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), } ); @@ -33,7 +44,7 @@ export async function dryRunNpmCommandAndOutput(args) { status: result.status, output: result.status === 0 ? result.stdout : result.stderr, }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { const output = error.stdout?.toString() ?? diff --git a/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js index 204ffa7..8e76ad1 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js @@ -1,5 +1,6 @@ // This was ran with the abbrev package to generate the abbrevs object below // console.log(abbrev(commands.concat(Object.keys(aliases)))); +/** @type {Record} */ export const abbrevs = { ac: "access", acc: "access", diff --git a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js index 6e67520..3bcdd0d 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js @@ -73,6 +73,7 @@ const commands = [ ]; // These must resolve to an entry in commands +/** @type {Record} */ const aliases = { // aliases author: "owner", @@ -138,6 +139,10 @@ const aliases = { "add-user": "adduser", }; +/** + * @param {string} c + * @returns {string | undefined} + */ export function deref(c) { if (!c) { return; diff --git a/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js b/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js index 3096144..b645369 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js @@ -1,5 +1,9 @@ import { deref } from "./cmd-list.js"; +/** + * @param {string[]} args + * @returns {string | null} + */ export function getNpmCommandForArgs(args) { if (args.length === 0) { return null; @@ -13,6 +17,10 @@ export function getNpmCommandForArgs(args) { return argCommand; } +/** + * @param {string[]} args + * @returns {boolean} + */ export function hasDryRunArg(args) { return args.some((arg) => arg === "--dry-run"); } diff --git a/packages/safe-chain/src/packagemanager/npx/createPackageManager.js b/packages/safe-chain/src/packagemanager/npx/createPackageManager.js index a3319fa..96d495b 100644 --- a/packages/safe-chain/src/packagemanager/npx/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npx/createPackageManager.js @@ -1,6 +1,9 @@ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; import { runNpx } from "./runNpxCommand.js"; +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createNpxPackageManager() { const scanner = commandArgumentScanner(); diff --git a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js index 16328cb..7f4da9f 100644 --- a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js @@ -1,16 +1,28 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; +/** + * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), - shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run + shouldScan: (args) => true, // all npx commands need to be scanned, npx doesn't have dry-run }; } + +/** + * @param {string[]} args + * @returns {Promise} + */ function scanDependencies(args) { return checkChangesFromArgs(args); } +/** + * @param {string[]} args + * @returns {Promise} + */ export async function checkChangesFromArgs(args) { const changes = []; const packageUpdates = parsePackagesFromArguments(args); diff --git a/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js b/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js index efc8d81..25fb249 100644 --- a/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +++ b/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js @@ -1,3 +1,8 @@ +/** + * @param {string[]} args + * + * @returns {{name: string, version: string}[]} + */ export function parsePackagesFromArguments(args) { let defaultTag = "latest"; @@ -21,6 +26,10 @@ export function parsePackagesFromArguments(args) { return []; } +/** + * @param {string} arg + * @returns {{name: string, numberOfParameters: number} | undefined} + */ function getOption(arg) { if (isOptionWithParameter(arg)) { return { @@ -41,6 +50,10 @@ function getOption(arg) { return undefined; } +/** + * @param {string} arg + * @returns {boolean} + */ function isOptionWithParameter(arg) { const optionsWithParameters = [ "--access", @@ -68,6 +81,11 @@ function isOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @param {string} defaultTag + * @returns {{name: string, version: string}} + */ function parsePackagename(arg, defaultTag) { // format can be --package=name@version // in that case, we need to remove the --package= part @@ -97,6 +115,10 @@ function parsePackagename(arg, defaultTag) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { // removes the alias. // Eg.: server@npm:http-server@latest becomes http-server@latest diff --git a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index b8896b7..61adcaa 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -2,14 +2,20 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @param {string[]} args + * + * @returns {Promise<{status: number}>} + */ export async function runNpx(args) { try { const result = await safeSpawn("npx", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { diff --git a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js index 15cb628..193470b 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js @@ -4,6 +4,9 @@ import { runPnpmCommand } from "./runPnpmCommand.js"; const scanner = commandArgumentScanner(); +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createPnpmPackageManager() { return { runCommand: (args) => runPnpmCommand(args, "pnpm"), @@ -23,6 +26,9 @@ export function createPnpmPackageManager() { }; } +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createPnpxPackageManager() { return { runCommand: (args) => runPnpmCommand(args, "pnpx"), @@ -32,6 +38,11 @@ export function createPnpxPackageManager() { }; } +/** + * @param {string[]} args + * @param {boolean} isPnpx + * @returns {Promise} + */ function getDependencyUpdatesForCommand(args, isPnpx) { if (isPnpx) { return scanner.scan(args); diff --git a/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js index c184b38..e46d2db 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js @@ -1,6 +1,9 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; +/** + * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), @@ -8,6 +11,10 @@ export function commandArgumentScanner() { }; } +/** + * @param {string[]} args + * @returns {Promise} + */ async function scanDependencies(args) { const changes = []; const packageUpdates = parsePackagesFromArguments(args); diff --git a/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js b/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js index d0383c2..b8a6f39 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +++ b/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js @@ -1,3 +1,7 @@ +/** + * @param {string[]} args + * @returns {{name: string, version: string}[]} + */ export function parsePackagesFromArguments(args) { const changes = []; let defaultTag = "latest"; @@ -22,6 +26,10 @@ export function parsePackagesFromArguments(args) { return changes; } +/** + * @param {string} arg + * @returns {{name: string, numberOfParameters: number} | undefined} + */ function getOption(arg) { if (isOptionWithParameter(arg)) { return { @@ -42,12 +50,21 @@ function getOption(arg) { return undefined; } +/** + * @param {string} arg + * @returns {boolean} + */ function isOptionWithParameter(arg) { const optionsWithParameters = ["--C", "--dir"]; return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @param {string} defaultTag + * @returns {{name: string, version: string}} + */ function parsePackagename(arg, defaultTag) { // format can be --package=name@version // in that case, we need to remove the --package= part @@ -77,6 +94,10 @@ function parsePackagename(arg, defaultTag) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { // removes the alias. // Eg.: server@npm:http-server@latest becomes http-server@latest diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index 794d6e3..db4ffa9 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -2,17 +2,24 @@ import { ui } from "../../environment/userInteraction.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; +/** + * @param {string[]} args + * @param {string} [toolName] + * @returns {Promise<{status: number}>} + */ export async function runPnpmCommand(args, toolName = "pnpm") { try { let result; if (toolName === "pnpm") { result = await safeSpawn("pnpm", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else if (toolName === "pnpx") { result = await safeSpawn("pnpx", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else { @@ -20,7 +27,7 @@ export async function runPnpmCommand(args, toolName = "pnpm") { } return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { diff --git a/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js b/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js index f49c763..f8a0c84 100644 --- a/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js @@ -3,6 +3,9 @@ import { runYarnCommand } from "./runYarnCommand.js"; const scanner = commandArgumentScanner(); +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createYarnPackageManager() { return { runCommand: runYarnCommand, @@ -18,6 +21,11 @@ export function createYarnPackageManager() { }; } +/** + * @param {string[]} args + * @param {...string} commandArgs + * @returns {boolean} + */ function matchesCommand(args, ...commandArgs) { if (args.length < commandArgs.length) { return false; diff --git a/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js index f5bdd9f..5141d54 100644 --- a/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js @@ -1,6 +1,9 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; +/** + * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), @@ -8,6 +11,10 @@ export function commandArgumentScanner() { }; } +/** + * @param {string[]} args + * @returns {Promise} + */ async function scanDependencies(args) { const changes = []; const packageUpdates = parsePackagesFromArguments(args); diff --git a/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js b/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js index 7b0255e..8f97de5 100644 --- a/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +++ b/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js @@ -1,3 +1,7 @@ +/** + * @param {string[]} args + * @returns {{name: string, version: string}[]} + */ export function parsePackagesFromArguments(args) { const changes = []; let defaultTag = "latest"; @@ -22,6 +26,11 @@ export function parsePackagesFromArguments(args) { return changes; } +/** + * @param {string} arg + * + * @returns {{name: string, numberOfParameters: number} | undefined} + */ function getOption(arg) { if (isOptionWithParameter(arg)) { return { @@ -42,6 +51,11 @@ function getOption(arg) { return undefined; } +/** + * @param {string} arg + * + * @returns {boolean} + */ function isOptionWithParameter(arg) { const optionsWithParameters = [ "--use-yarnrc", @@ -64,6 +78,12 @@ function isOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @param {string} defaultTag + * + * @returns {{name: string, version: string}} + */ function parsePackagename(arg, defaultTag) { // format can be --package=name@version // in that case, we need to remove the --package= part @@ -93,6 +113,10 @@ function parsePackagename(arg, defaultTag) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { // removes the alias. // Eg.: server@npm:http-server@latest becomes http-server@latest diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 65c27a0..1ba0f5c 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -2,8 +2,14 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @param {string[]} args + * + * @returns {Promise<{status: number}>} + */ export async function runYarnCommand(args) { try { + // @ts-expect-error values of process.env can be string | undefined const env = mergeSafeChainProxyEnvironmentVariables(process.env); await fixYarnProxyEnvironmentVariables(env); @@ -12,7 +18,7 @@ export async function runYarnCommand(args) { env, }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { @@ -22,6 +28,11 @@ export async function runYarnCommand(args) { } } +/** + * @param {Record} env + * + * @returns {Promise} + */ async function fixYarnProxyEnvironmentVariables(env) { // Yarn ignores standard proxy environment variable HTTPS_PROXY // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index d5d414c..a2fb7bb 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -12,6 +12,10 @@ export function getCaCertPath() { return path.join(certFolder, "ca-cert.pem"); } +/** + * @param {string} hostname + * @returns {{privateKey: string, certificate: string}} + */ export function generateCertForHost(hostname) { let existingCert = certCache.get(hostname); if (existingCert) { diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index fe8998e..be2b357 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -3,6 +3,11 @@ import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {(target: string) => Promise} isAllowed + */ export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); @@ -16,6 +21,7 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); + // @ts-expect-error Property 'headersSent' does not exist on type 'Socket' if (!clientSocket.headersSent) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } else if (clientSocket.writable) { @@ -30,10 +36,22 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.emit("connection", clientSocket); } +/** + * @param {string} hostname + * @param {(target: string) => Promise} isAllowed + * @returns {import("https").Server} + */ function createHttpsServer(hostname, isAllowed) { const cert = generateCertForHost(hostname); + /** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + * + * @returns {Promise} + */ async function handleRequest(req, res) { + // @ts-expect-error req.url might be undefined const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; @@ -58,6 +76,10 @@ function createHttpsServer(hostname, isAllowed) { return server; } +/** + * @param {string} url + * @returns {*|string} + */ function getRequestPathAndQuery(url) { if (url.startsWith("http://") || url.startsWith("https://")) { const parsedUrl = new URL(url); @@ -66,6 +88,11 @@ function getRequestPathAndQuery(url) { return url; } +/** + * @param {import("http").IncomingMessage} req + * @param {string} hostname + * @param {import("http").ServerResponse} res + */ function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); @@ -88,7 +115,15 @@ function forwardRequest(req, hostname, res) { }); } +/** + * @param {string} hostname + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + * + * @returns {import("http").ClientRequest} + */ function createProxyRequest(hostname, req, res) { + /** @type {import("http").RequestOptions} */ const options = { hostname: hostname, port: 443, @@ -97,7 +132,9 @@ function createProxyRequest(hostname, req, res) { headers: { ...req.headers }, }; - delete options.headers.host; + if (options.headers && "host" in options.headers) { + delete options.headers["host"]; + } const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; if (httpsProxy) { @@ -115,6 +152,7 @@ function createProxyRequest(hostname, req, res) { } }); + // @ts-expect-error statusCode might be undefined res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 7368b35..bffb182 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -1,5 +1,9 @@ export const knownRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; +/** + * @param {string} url + * @returns {{packageName: string | undefined, version: string | undefined}} + */ export function parsePackageFromUrl(url) { let packageName, version, registry; diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index ac3dc69..16b305a 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,7 +1,14 @@ import * as http from "http"; import * as https from "https"; +/** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + * + * @returns {void} + */ export function handleHttpProxyRequest(req, res) { + // @ts-expect-error req.url might be undefined const url = new URL(req.url); // The protocol for the plainHttpProxy should usually only be http: @@ -20,9 +27,11 @@ export function handleHttpProxyRequest(req, res) { const proxyRequest = protocol .request( + // @ts-expect-error req.url might be undefined req.url, { method: req.method, headers: req.headers }, (proxyRes) => { + // @ts-expect-error statusCode might be undefined res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 887fd47..bd98ff5 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -9,6 +9,9 @@ import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; const SERVER_STOP_TIMEOUT_MS = 1000; +/** + * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} + */ const state = { port: null, blockedRequests: [], @@ -24,6 +27,9 @@ export function createSafeChainProxy() { }; } +/** + * @returns {Record} + */ function getSafeChainProxyEnvironmentVariables() { if (!state.port) { return {}; @@ -36,6 +42,11 @@ function getSafeChainProxyEnvironmentVariables() { }; } +/** + * @param {Record} env + * + * @returns {Record} + */ export function mergeSafeChainProxyEnvironmentVariables(env) { const proxyEnv = getSafeChainProxyEnvironmentVariables(); @@ -67,6 +78,11 @@ function createProxyServer() { return server; } +/** + * @param {import("http").Server} server + * + * @returns {Promise} + */ function startServer(server) { return new Promise((resolve, reject) => { // Passing port 0 makes the OS assign an available port @@ -86,6 +102,11 @@ function startServer(server) { }); } +/** + * @param {import("http").Server} server + * + * @returns {Promise} + */ function stopServer(server) { return new Promise((resolve) => { try { @@ -99,10 +120,18 @@ function stopServer(server) { }); } +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL + // @ts-expect-error req.url might be undefined 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 @@ -113,6 +142,10 @@ function handleConnect(req, clientSocket, head) { } } +/** + * @param {string} url + * @returns {Promise} + */ async function isAllowedUrl(url) { const { packageName, version } = parsePackageFromUrl(url); diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 5c764f5..452ef15 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -1,6 +1,13 @@ import * as net from "net"; import { ui } from "../environment/userInteraction.js"; +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ export function tunnelRequest(req, clientSocket, head) { const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; @@ -21,9 +28,17 @@ export function tunnelRequest(req, clientSocket, head) { } } +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); + // @ts-expect-error port from URL is a string but net.connect accepts number const serverSocket = net.connect(port || 443, hostname, () => { clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); serverSocket.write(head); @@ -49,11 +64,18 @@ function tunnelRequestToDestination(req, clientSocket, head) { }); } +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * @param {string} proxyUrl + */ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { const { port, hostname } = new URL(`http://${req.url}`); const proxy = new URL(proxyUrl); // Connect to proxy server + // @ts-expect-error net.connect wants port as number but proxy.port is string const proxySocket = net.connect({ host: proxy.hostname, port: proxy.port, diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 215bfa0..8eea526 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -3,6 +3,25 @@ import { openMalwareDatabase, } from "../malwareDatabase.js"; +/** + * @typedef PackageChange + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef AuditResult + * @property {PackageChange[]} allowedChanges + * @property {(PackageChange & {reason: string})[]} disallowedChanges + * @property {boolean} isAllowed + */ + +/** + * @param {PackageChange[]} changes + * + * @returns {Promise} + */ export async function auditChanges(changes) { const allowedChanges = []; const disallowedChanges = []; @@ -34,6 +53,10 @@ export async function auditChanges(changes) { return auditResults; } +/** + * @param {{name: string, version: string, type: string}[]} changes + * @returns {Promise<{name: string, version: string, status: string}[]>} + */ async function getPackagesWithMalware(changes) { if (changes.length === 0) { return []; diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 969c994..d8e817e 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -5,6 +5,11 @@ import chalk from "chalk"; import { getPackageManager } from "../packagemanager/currentPackageManager.js"; import { ui } from "../environment/userInteraction.js"; +/** + * @param {string[]} args + * + * @returns {boolean} + */ export function shouldScanCommand(args) { if (!args || args.length === 0) { return false; @@ -13,6 +18,11 @@ export function shouldScanCommand(args) { return getPackageManager().isSupportedCommand(args); } +/** + * @param {string[]} args + * + * @returns {Promise} + */ export async function scanCommand(args) { if (!shouldScanCommand(args)) { return []; @@ -23,6 +33,7 @@ export async function scanCommand(args) { const spinner = ui.startProcess( "Safe-chain: Scanning for malicious packages..." ); + /** @type {import("./audit/index.js").AuditResult | undefined} */ let audit; await Promise.race([ @@ -44,7 +55,7 @@ export async function scanCommand(args) { } audit = await auditChanges(changes); - } catch (error) { + } catch (/** @type any */ error) { spinner.fail(`Safe-chain: Error while scanning.`); throw error; } @@ -69,6 +80,12 @@ export async function scanCommand(args) { } } +/** + * @param {import("./audit/index.js").PackageChange[]} changes + * @param spinner {import("../environment/userInteraction.js").Spinner} + * + * @return {void} + */ function printMaliciousChanges(changes, spinner) { spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:")); diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 1cb781b..481ff7d 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -8,6 +8,13 @@ import { } from "../config/configFile.js"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef MalwareDatabase + * @property {function(string, string): string} getPackageStatus + * @property {function(string, string): boolean} isMalware + */ + +/** @type {MalwareDatabase | null} */ let cachedMalwareDatabase = null; export async function openMalwareDatabase() { @@ -17,6 +24,11 @@ export async function openMalwareDatabase() { const malwareDatabase = await getMalwareDatabase(); + /** + * @param {string} name + * @param {string} version + * @returns {string} + */ function getPackageStatus(name, version) { const packageData = malwareDatabase.find( (pkg) => @@ -31,7 +43,7 @@ export async function openMalwareDatabase() { return packageData.reason; } - // This implicitely caches the malware database + // This implicitly caches the malware database // that's closed over by the getPackageStatus function cachedMalwareDatabase = { getPackageStatus, @@ -43,6 +55,9 @@ export async function openMalwareDatabase() { return cachedMalwareDatabase; } +/** + * @returns {Promise} + */ async function getMalwareDatabase() { const { malwareDatabase: cachedDatabase, version: cachedVersion } = readDatabaseFromLocalCache(); @@ -56,10 +71,11 @@ async function getMalwareDatabase() { } const { malwareDatabase, version } = await fetchMalwareDatabase(); + // @ts-expect-error version can be undefined writeDatabaseToLocalCache(malwareDatabase, version); return malwareDatabase; - } catch (error) { + } catch (/** @type any */ error) { if (cachedDatabase) { ui.writeWarning( "Failed to fetch the latest malware database. Using cached version." @@ -70,6 +86,11 @@ async function getMalwareDatabase() { } } +/** + * @param {string} status + * + * @returns {boolean} + */ function isMalwareStatus(status) { let malwareStatus = status.toUpperCase(); return malwareStatus === MALWARE_STATUS_MALWARE; diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 2345022..697f2cb 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,15 @@ import * as os from "os"; import fs from "fs"; import path from "path"; +/** + * @typedef AikidoTool + * @property {string} tool + * @property {string} aikidoCommand + */ + +/** + * @type {AikidoTool[]} + */ export const knownAikidoTools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, @@ -30,6 +39,11 @@ export function getPackageManagerList() { return `${tools.join(", ")}, and ${lastTool} commands`; } +/** + * @param {string} executableName + * + * @returns {boolean} + */ export function doesExecutableExistOnSystem(executableName) { if (os.platform() === "win32") { const result = spawnSync("where", [executableName], { stdio: "ignore" }); @@ -40,6 +54,13 @@ export function doesExecutableExistOnSystem(executableName) { } } +/** + * @param {string} filePath + * @param {RegExp} pattern + * @param {string} [eol] + * + * @returns {void} + */ export function removeLinesMatchingPattern(filePath, pattern, eol) { if (!fs.existsSync(filePath)) { return; @@ -54,6 +75,12 @@ export function removeLinesMatchingPattern(filePath, pattern, eol) { } const maxLineLength = 100; + +/** + * @param {string} line + * @param {RegExp} pattern + * @returns {boolean} + */ function shouldRemoveLine(line, pattern) { const isPatternMatch = pattern.test(line); @@ -82,7 +109,14 @@ function shouldRemoveLine(line, pattern) { return true; } -export function addLineToFile(filePath, line, eol) { +/** + * @param {string} filePath + * @param {string} line + * @param {string} [eol] + * + * @returns {void} + */ +export function addLineToFile(filePath, line, eol ) { createFileIfNotExists(filePath); eol = eol || os.EOL; @@ -92,6 +126,11 @@ export function addLineToFile(filePath, line, eol) { fs.writeFileSync(filePath, updatedContent, "utf-8"); } +/** + * @param {string} filePath + * + * @returns {void} + */ function createFileIfNotExists(filePath) { if (fs.existsSync(filePath)) { return; diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 0449ac4..8dd0e4d 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -28,6 +28,11 @@ export async function setupCi() { ui.writeInformation(`Added shims directory to PATH for CI environments.`); } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function createUnixShims(shimsDir) { // Read the template file const __filename = fileURLToPath(import.meta.url); @@ -64,6 +69,11 @@ function createUnixShims(shimsDir) { ); } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function createWindowsShims(shimsDir) { // Read the template file const __filename = fileURLToPath(import.meta.url); @@ -97,6 +107,11 @@ function createWindowsShims(shimsDir) { ); } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function createShims(shimsDir) { if (os.platform() === "win32") { createWindowsShims(shimsDir); @@ -105,6 +120,11 @@ function createShims(shimsDir) { } } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function modifyPathForCi(shimsDir) { if (process.env.GITHUB_PATH) { // In GitHub Actions, append the shims directory to GITHUB_PATH diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index afa96e8..7185c5a 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -43,7 +43,7 @@ export async function setup() { ui.emptyLine(); ui.writeInformation(`Please restart your terminal to apply the changes.`); } - } catch (error) { + } catch (/** @type {any} */ error) { ui.writeError( `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.` ); @@ -53,6 +53,7 @@ export async function setup() { /** * Calls the setup function for the given shell and reports the result. + * @param {import("./shellDetection.js").Shell} shell */ function setupShell(shell) { let success = false; @@ -60,7 +61,7 @@ function setupShell(shell) { try { shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases success = shell.setup(knownAikidoTools); - } catch (err) { + } catch (/** @type {any} */ err) { success = false; error = err; } diff --git a/packages/safe-chain/src/shell-integration/shellDetection.js b/packages/safe-chain/src/shell-integration/shellDetection.js index d868f6f..701570a 100644 --- a/packages/safe-chain/src/shell-integration/shellDetection.js +++ b/packages/safe-chain/src/shell-integration/shellDetection.js @@ -5,6 +5,17 @@ import windowsPowershell from "./supported-shells/windowsPowershell.js"; import fish from "./supported-shells/fish.js"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef Shell + * @property {string} name + * @property {() => boolean} isInstalled + * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup + * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown + */ + +/** + * @returns {Shell[]} + */ export function detectShells() { let possibleShells = [zsh, bash, powershell, windowsPowershell, fish]; let availableShells = []; @@ -15,7 +26,7 @@ export function detectShells() { availableShells.push(shell); } } - } catch (error) { + } catch (/** @type {any} */ error) { ui.writeError( `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}` ); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.js index 6038f95..a2a3739 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -15,6 +15,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -57,13 +62,18 @@ function getStartupFile() { }).trim(); return windowsFixPath(path); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @param {string} path + * + * @returns {string} + */ function windowsFixPath(path) { try { if (os.platform() !== "win32") { @@ -93,6 +103,11 @@ function hasCygpath() { } } +/** + * @param {string} path + * + * @returns {string} + */ function cygpathw(path) { try { var result = spawnSync("cygpath", ["-w", path], { @@ -108,6 +123,9 @@ function cygpathw(path) { } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.js index 4c39ba6..0af6ae3 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.js @@ -14,6 +14,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -54,13 +59,16 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js index 47524c2..8cec258 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -13,6 +13,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -50,13 +55,16 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js index 03ff7f8..e554a32 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -13,6 +13,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -50,13 +55,16 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js index b90f769..fc2b807 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -14,6 +14,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -54,7 +59,7 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); diff --git a/packages/safe-chain/src/shell-integration/teardown.js b/packages/safe-chain/src/shell-integration/teardown.js index d6b1277..bc83b48 100644 --- a/packages/safe-chain/src/shell-integration/teardown.js +++ b/packages/safe-chain/src/shell-integration/teardown.js @@ -3,6 +3,9 @@ import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +/** + * @returns {Promise} + */ export async function teardown() { ui.writeInformation( chalk.bold("Removing shell aliases.") + @@ -52,7 +55,7 @@ export async function teardown() { ui.emptyLine(); ui.writeInformation(`Please restart your terminal to apply the changes.`); } - } catch (error) { + } catch (/** @type {any} */ error) { ui.writeError( `Failed to remove shell aliases: ${error.message}. Please check your shell configuration.` ); diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index c398ac2..489d070 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,6 +1,11 @@ import { spawn, execSync } from "child_process"; import os from "os"; +/** + * @param {string} arg + * + * @returns {string} + */ function sanitizeShellArgument(arg) { // If argument contains shell metacharacters, wrap in double quotes // and escape characters that are special even inside double quotes @@ -11,6 +16,11 @@ function sanitizeShellArgument(arg) { return arg; } +/** + * @param {string} arg + * + * @returns {boolean} + */ function hasShellMetaChars(arg) { // Shell metacharacters that need escaping // These characters have special meaning in shells and need to be quoted @@ -20,12 +30,23 @@ function hasShellMetaChars(arg) { return shellMetaChars.test(arg); } +/** + * @param {string} arg + * + * @returns {string} + */ function escapeDoubleQuoteContent(arg) { // Escape special characters for shell safety // This escapes ", $, `, and \ by prefixing them with a backslash return arg.replace(/(["`$\\])/g, "\\$1"); } +/** + * @param {string} command + * @param {string[]} args + * + * @returns {string} + */ function buildCommand(command, args) { if (args.length === 0) { return command; @@ -36,11 +57,17 @@ function buildCommand(command, args) { return `${command} ${escapedArgs.join(" ")}`; } +/** + * @param {string} command + * + * @returns {string} + */ function resolveCommandPath(command) { // command will be "npm", "yarn", etc. // Use 'command -v' to find the full path const fullPath = execSync(`command -v ${command}`, { encoding: "utf8", + // @ts-expect-error shell is a string option shell: true, }).trim(); @@ -51,6 +78,13 @@ function resolveCommandPath(command) { return fullPath; } +/** + * @param {string} command + * @param {string[]} args + * @param {import("child_process").SpawnOptions} options + * + * @returns {Promise<{status: number, stdout: string, stderr: string}>} + */ export async function safeSpawn(command, args, options = {}) { // The command is always one of our supported package managers. // It should always be alphanumeric or _ or - @@ -87,6 +121,7 @@ export async function safeSpawn(command, args, options = {}) { child.on("close", (code) => { resolve({ + // @ts-expect-error code can be null status: code, stdout: stdout, stderr: stderr, diff --git a/packages/safe-chain/tsconfig.json b/packages/safe-chain/tsconfig.json new file mode 100644 index 0000000..c357bb1 --- /dev/null +++ b/packages/safe-chain/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "strict": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "allowJs": true, + "checkJs": true, + "noEmit": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*.js", + "bin/**/*.js" + ], + "exclude": [ + "node_modules", + "src/**/*.spec.js" + ] +} From 5adfb36629f22550475a3bbe746dc17cb6c781d3 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:07:31 +0100 Subject: [PATCH 112/345] Run typecheck as part of CI --- .github/workflows/test-on-pr.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 85d6aba..f8087ef 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -31,6 +31,9 @@ jobs: - name: Run linting run: npm run lint + - name: Type check + run: npm run typecheck --workspace=packages/safe-chain + - name: Create package tarball run: npm pack --workspace=packages/safe-chain From 6f962a9299306da6195da4c2365dd12fac9fbdb2 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:09:08 +0100 Subject: [PATCH 113/345] Use Node.js 18 types --- package-lock.json | 17 ++++++++++++++++- packages/safe-chain/package.json | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b0b5cf..f258df3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2091,7 +2091,7 @@ }, "devDependencies": { "@types/make-fetch-happen": "^10.0.4", - "@types/node": "^24.9.2", + "@types/node": "^18.19.130", "@types/node-forge": "^1.3.14", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", @@ -2109,6 +2109,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", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 4e55840..e201b0f 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@types/make-fetch-happen": "^10.0.4", - "@types/node": "^24.9.2", + "@types/node": "^18.19.130", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", "@types/node-forge": "^1.3.14", From 4f148593516e3d8ad94385818de2f016e1216c56 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:24:57 +0100 Subject: [PATCH 114/345] Fix check --- packages/safe-chain/src/api/npmApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/api/npmApi.js b/packages/safe-chain/src/api/npmApi.js index c8e2218..c03abef 100644 --- a/packages/safe-chain/src/api/npmApi.js +++ b/packages/safe-chain/src/api/npmApi.js @@ -17,7 +17,7 @@ export async function resolvePackageVersion(packageName, versionRange) { } const packageInfo = ( - /** @type {{"dist-tags"?: Record} | null} */ + /** @type {{"dist-tags"?: Record, versions?: Record} | null} */ await getPackageInfo(packageName) ); if (!packageInfo) { @@ -27,7 +27,7 @@ export async function resolvePackageVersion(packageName, versionRange) { } const distTags = packageInfo["dist-tags"]; - if (distTags && isDistTags(distTags)) { + if (distTags && isDistTags(distTags) && distTags[versionRange]) { // If the version range is a dist-tag, return the version associated with that tag // e.g., "latest", "next", etc. return distTags[versionRange]; From 29dd63d1ebdfab45c0a276a0e4373976d341aa8e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:26:15 +0100 Subject: [PATCH 115/345] Reduce diff --- packages/safe-chain/src/config/configFile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 83bc186..20dabdc 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -113,7 +113,6 @@ function getDatabasePath() { return path.join(aikidoDir, "malwareDatabase.json"); } - function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "version.txt"); From 484cbcd96058eeed382c597c606d371a79a95b4d Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:28:11 +0100 Subject: [PATCH 116/345] Use @typedef {Object} X MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When you write @typedef {Object} ScanResult, you’re telling both JSDoc and TypeScript’s parser that this typedef represents an object type, not just an abstract name. This is important because it makes tools like IDEs, linters, and TypeScript’s JSDoc inference more reliable. It avoids ambiguity, especially in cases where the typedef might later be confused with something like a primitive, union, or function type. The official TypeScript documentation and the JSDoc spec both show this form as the canonical one for object shapes. --- packages/safe-chain/src/api/aikido.js | 2 +- packages/safe-chain/src/environment/userInteraction.js | 2 +- .../safe-chain/src/packagemanager/currentPackageManager.js | 2 +- .../npm/dependencyScanner/commandArgumentScanner.js | 2 +- packages/safe-chain/src/scanning/audit/index.js | 4 ++-- packages/safe-chain/src/scanning/malwareDatabase.js | 2 +- packages/safe-chain/src/shell-integration/helpers.js | 2 +- packages/safe-chain/src/shell-integration/shellDetection.js | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 7e786d1..901131c 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -4,7 +4,7 @@ const malwareDatabaseUrl = "https://malware-list.aikido.dev/malware_predictions.json"; /** - * @typedef MalwarePackage + * @typedef {Object} MalwarePackage * @property {string} package_name * @property {string} version * @property {string} reason diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 43f5312..3f690fb 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -60,7 +60,7 @@ function writeExitWithoutInstallingMaliciousPackages() { } /** - * @typedef Spinner + * @typedef {Object} Spinner * @property {(message: string) => void} succeed * @property {(message: string) => void} fail * @property {() => void} stop diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index aaf8aae..ef7e9aa 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -18,7 +18,7 @@ const state = { }; /** - * @typedef PackageManager + * @typedef {Object} PackageManager * @property {(args: string[]) => Promise<{ status: number }>} runCommand * @property {(args: string[]) => boolean} isSupportedCommand * @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js index 51746c4..f19449b 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js @@ -15,7 +15,7 @@ import { hasDryRunArg } from "../utils/npmCommands.js"; */ /** - * @typedef CommandArgumentScanner + * @typedef {Object} CommandArgumentScanner * @property {(args: string[]) => Promise} scan * @property {(args: string[]) => boolean} shouldScan */ diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 8eea526..b6dfcc5 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -4,14 +4,14 @@ import { } from "../malwareDatabase.js"; /** - * @typedef PackageChange + * @typedef {Object} PackageChange * @property {string} name * @property {string} version * @property {string} type */ /** - * @typedef AuditResult + * @typedef {Object} AuditResult * @property {PackageChange[]} allowedChanges * @property {(PackageChange & {reason: string})[]} disallowedChanges * @property {boolean} isAllowed diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 481ff7d..b54846e 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -9,7 +9,7 @@ import { import { ui } from "../environment/userInteraction.js"; /** - * @typedef MalwareDatabase + * @typedef {Object} MalwareDatabase * @property {function(string, string): string} getPackageStatus * @property {function(string, string): boolean} isMalware */ diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 697f2cb..1732228 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -4,7 +4,7 @@ import fs from "fs"; import path from "path"; /** - * @typedef AikidoTool + * @typedef {Object} AikidoTool * @property {string} tool * @property {string} aikidoCommand */ diff --git a/packages/safe-chain/src/shell-integration/shellDetection.js b/packages/safe-chain/src/shell-integration/shellDetection.js index 701570a..9e0f110 100644 --- a/packages/safe-chain/src/shell-integration/shellDetection.js +++ b/packages/safe-chain/src/shell-integration/shellDetection.js @@ -6,7 +6,7 @@ import fish from "./supported-shells/fish.js"; import { ui } from "../environment/userInteraction.js"; /** - * @typedef Shell + * @typedef {Object} Shell * @property {string} name * @property {() => boolean} isInstalled * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup From 86a2b8c2a715f7a353640a9b390e00cafb46d19f Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:44:48 +0100 Subject: [PATCH 117/345] Fix lint --- .../npx/dependencyScanner/commandArgumentScanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js index 7f4da9f..689e3f8 100644 --- a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js @@ -7,7 +7,7 @@ import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArgument export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), - shouldScan: (args) => true, // all npx commands need to be scanned, npx doesn't have dry-run + shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run }; } From e164eb8b959c0064ad338f2e36fccd8de6123bc4 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:47:13 +0100 Subject: [PATCH 118/345] Reduce diff --- packages/safe-chain/src/shell-integration/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 1732228..89321b1 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -116,7 +116,7 @@ function shouldRemoveLine(line, pattern) { * * @returns {void} */ -export function addLineToFile(filePath, line, eol ) { +export function addLineToFile(filePath, line, eol) { createFileIfNotExists(filePath); eol = eol || os.EOL; From b489fe822cf67a188485f1ffc7767c1282d7b5b2 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:29:23 +0100 Subject: [PATCH 119/345] Example of mistake --- packages/safe-chain/src/shell-integration/helpers.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 89321b1..e0955ff 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,12 @@ import * as os from "os"; import fs from "fs"; import path from "path"; +function mistakeHere() { + os.EOLL; +} + +mistakeHere(); + /** * @typedef {Object} AikidoTool * @property {string} tool From 0cfce2d436b28f8f83d4139bb18faa8372460f9a Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:29:36 +0100 Subject: [PATCH 120/345] Revert "Example of mistake" This reverts commit b489fe822cf67a188485f1ffc7767c1282d7b5b2. --- packages/safe-chain/src/shell-integration/helpers.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index e0955ff..89321b1 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,12 +3,6 @@ import * as os from "os"; import fs from "fs"; import path from "path"; -function mistakeHere() { - os.EOLL; -} - -mistakeHere(); - /** * @typedef {Object} AikidoTool * @property {string} tool From 1724e0b1990b157d1d2a847af94f9d3df408bf22 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:31:02 +0100 Subject: [PATCH 121/345] Introduce mistake that passes linter --- packages/safe-chain/src/shell-integration/helpers.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 89321b1..3949886 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,13 @@ import * as os from "os"; import fs from "fs"; import path from "path"; +function mistakeHere() { + return os.EOLL; +} + +const abc = mistakeHere(); +console.log(abc); + /** * @typedef {Object} AikidoTool * @property {string} tool From e8e7c85c62b4eaaceb5cf1a71debac49096c784b Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:31:23 +0100 Subject: [PATCH 122/345] Revert "Introduce mistake that passes linter" This reverts commit 1724e0b1990b157d1d2a847af94f9d3df408bf22. --- packages/safe-chain/src/shell-integration/helpers.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 3949886..89321b1 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,13 +3,6 @@ import * as os from "os"; import fs from "fs"; import path from "path"; -function mistakeHere() { - return os.EOLL; -} - -const abc = mistakeHere(); -console.log(abc); - /** * @typedef {Object} AikidoTool * @property {string} tool From 49d31049ac2ca3e3132b7b36187282cc891e9c49 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:04:12 +0100 Subject: [PATCH 123/345] Revert code Let's do it in a separate PR --- packages/safe-chain/src/config/configFile.js | 30 ++------------------ 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 20dabdc..a11acc2 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -7,34 +7,10 @@ import { ui } from "../environment/userInteraction.js"; * @returns {number} */ export function getScanTimeout() { - if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { - const timeout = parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS); - if (!isNaN(timeout) && timeout >= 0) { - return timeout; - } - } + const config = /** @type {{scanTimeout: number}} */ (readConfigFile()); - const config = readConfigFile(); - - if (hasScanTimeout(config) && config.scanTimeout >= 0) { - return config.scanTimeout; - } - - return 10000; // Default to 10 seconds -} - -/** - * @param {unknown} config - * - * @returns {config is {scanTimeout: number}} - */ -function hasScanTimeout(config) { - return ( - typeof config === "object" && - config !== null && - "scanTimeout" in config && - typeof config.scanTimeout === "number" - ); + // @ts-expect-error values of process.env can be string | undefined + return parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds } /** From ad9551ca6dc70cd206816905822b1cfdd4301112 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:26:10 +0100 Subject: [PATCH 124/345] Improve types and remove `async` --- .../src/packagemanager/bun/createBunPackageManager.js | 4 ++-- .../src/packagemanager/currentPackageManager.js | 9 ++++++++- .../src/packagemanager/npm/createPackageManager.js | 2 +- .../npm/dependencyScanner/commandArgumentScanner.js | 2 +- .../packagemanager/npm/dependencyScanner/nullScanner.js | 2 +- .../src/packagemanager/pnpm/createPackageManager.js | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 88c84f5..9716261 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -12,7 +12,7 @@ export function createBunPackageManager() { // For bun, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: async () => [], + getDependencyUpdatesForCommand: () => [], }; } @@ -26,7 +26,7 @@ export function createBunxPackageManager() { // For bunx, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: async () => [], + getDependencyUpdatesForCommand: () => [], }; } diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index ef7e9aa..390d4d1 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -17,11 +17,18 @@ const state = { packageManagerName: null, }; +/** + * @typedef {Object} GetDependencyUpdatesResult + * @property {string} name + * @property {string} version + * @property {string} type + */ + /** * @typedef {Object} PackageManager * @property {(args: string[]) => Promise<{ status: number }>} runCommand * @property {(args: string[]) => boolean} isSupportedCommand - * @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand + * @property {(args: string[]) => Promise | GetDependencyUpdatesResult[]} getDependencyUpdatesForCommand */ /** diff --git a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js index 465bf60..fa72276 100644 --- a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js @@ -28,7 +28,7 @@ export function createNpmPackageManager() { /** * @param {string[]} args * - * @returns {Promise<{name: string, version: string, type: string}[]>} + * @returns {ReturnType} */ function getDependencyUpdatesForCommand(args) { const scanner = findDependencyScannerForCommand( diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js index f19449b..c4f6bb6 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js @@ -16,7 +16,7 @@ import { hasDryRunArg } from "../utils/npmCommands.js"; /** * @typedef {Object} CommandArgumentScanner - * @property {(args: string[]) => Promise} scan + * @property {(args: string[]) => Promise | ScanResult[]} scan * @property {(args: string[]) => boolean} shouldScan */ diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js index 449fed4..5c1d3bd 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js @@ -3,7 +3,7 @@ */ export function nullScanner() { return { - scan: async () => [], + scan: () => [], shouldScan: () => false, }; } diff --git a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js index 193470b..c3046c8 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js @@ -41,7 +41,7 @@ export function createPnpxPackageManager() { /** * @param {string[]} args * @param {boolean} isPnpx - * @returns {Promise} + * @returns {ReturnType} */ function getDependencyUpdatesForCommand(args, isPnpx) { if (isPnpx) { From c3a62826d40b11974a229f5a86d3dfe3e7e12f74 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:28:24 +0100 Subject: [PATCH 125/345] Make prop optional --- packages/safe-chain/src/config/configFile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index a11acc2..7273f7a 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -7,7 +7,7 @@ import { ui } from "../environment/userInteraction.js"; * @returns {number} */ export function getScanTimeout() { - const config = /** @type {{scanTimeout: number}} */ (readConfigFile()); + const config = /** @type {{scanTimeout?: number}} */ (readConfigFile()); // @ts-expect-error values of process.env can be string | undefined return parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds From 910276deebd217a123cf5edcf887d173c89007e6 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:30:21 +0100 Subject: [PATCH 126/345] Fix type --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index be2b357..ff80087 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -78,7 +78,7 @@ function createHttpsServer(hostname, isAllowed) { /** * @param {string} url - * @returns {*|string} + * @returns {string} */ function getRequestPathAndQuery(url) { if (url.startsWith("http://") || url.startsWith("https://")) { From 855f6a417f304fa63cb538629e160b7060cbb27b Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:31:04 +0100 Subject: [PATCH 127/345] Use original notation --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index ff80087..5bbf38a 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -133,7 +133,7 @@ function createProxyRequest(hostname, req, res) { }; if (options.headers && "host" in options.headers) { - delete options.headers["host"]; + delete options.headers.host; } const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; From 932ea6b8f9306be03f0c0a1f7c3a7aa5dc499754 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 11:47:59 +0100 Subject: [PATCH 128/345] Add type information for new functions. --- package.json | 3 ++- .../safe-chain/src/environment/userInteraction.js | 12 ++++++++++++ packages/safe-chain/src/main.js | 2 -- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0193a82..6a5dec3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "scripts": { "test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun", "test:e2e": "npm run test --workspace=test/e2e", - "lint": "npm run lint --workspace=packages/safe-chain" + "lint": "npm run lint --workspace=packages/safe-chain", + "typecheck": "npm run typecheck --workspace=packages/safe-chain" }, "repository": { "type": "git", diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 14fbc4a..3222874 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -8,6 +8,9 @@ import { LOGGING_VERBOSE, } from "../config/settings.js"; +/** + * @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}} + */ const state = { bufferOutput: false, bufferedMessages: [], @@ -72,12 +75,21 @@ function writeExitWithoutInstallingMaliciousPackages() { writeOrBuffer(() => console.error(message)); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeVerbose(message, ...optionalParams) { if (!isVerboseMode()) return; writeOrBuffer(() => console.log(message, ...optionalParams)); } +/** + * + * @param {() => void} messageFunction + */ function writeOrBuffer(messageFunction) { if (state.bufferOutput) { state.bufferedMessages.push(messageFunction); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index feb5156..eea9257 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -21,7 +21,6 @@ export async function main(args) { // Global error handlers to log unhandled errors process.on("uncaughtException", (error) => { ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); - // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${error.stack}`); process.exit(1); }); @@ -29,7 +28,6 @@ export async function main(args) { process.on("unhandledRejection", (reason) => { ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); if (reason instanceof Error) { - // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${reason.stack}`); } process.exit(1); From 14c4c4997edbf5b75b81a82e327545a6e4a9dcd1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 13:57:29 +0100 Subject: [PATCH 129/345] Remove @ts-expect-error suppressions --- packages/safe-chain/bin/aikido-bun.js | 1 - packages/safe-chain/bin/aikido-bunx.js | 1 - packages/safe-chain/bin/aikido-npm.js | 1 - packages/safe-chain/bin/aikido-npx.js | 1 - packages/safe-chain/bin/aikido-pnpm.js | 1 - packages/safe-chain/bin/aikido-pnpx.js | 1 - packages/safe-chain/bin/aikido-yarn.js | 1 - packages/safe-chain/src/main.js | 2 +- .../bun/createBunPackageManager.js | 1 - .../src/packagemanager/npm/runNpmCommand.js | 35 ------------------- .../src/packagemanager/npx/runNpxCommand.js | 1 - .../src/packagemanager/pnpm/runPnpmCommand.js | 2 -- .../src/packagemanager/yarn/runYarnCommand.js | 1 - .../src/registryProxy/mitmRequestHandler.js | 19 +++++++--- .../src/registryProxy/plainHttpProxy.js | 18 ++++++++-- .../src/registryProxy/registryProxy.js | 10 +++--- .../src/registryProxy/tunnelRequestHandler.js | 26 +++++++------- packages/safe-chain/src/scanning/index.js | 4 +-- .../src/scanning/malwareDatabase.js | 7 ++-- packages/safe-chain/src/utils/safeSpawn.js | 8 +++-- 20 files changed, 62 insertions(+), 79 deletions(-) diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index 2467512..01e3972 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -7,5 +7,4 @@ const packageManagerName = "bun"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index 6a84ff8..fb378e5 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -7,5 +7,4 @@ const packageManagerName = "bunx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 4a82c34..0e9f302 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -7,5 +7,4 @@ const packageManagerName = "npm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index 40118d4..d3dfdd6 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -7,5 +7,4 @@ const packageManagerName = "npx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index e495568..0a06217 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -7,5 +7,4 @@ const packageManagerName = "pnpm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 75f093d..cdb6504 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -7,5 +7,4 @@ const packageManagerName = "pnpx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index 3ca5b94..fd87606 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -7,5 +7,4 @@ const packageManagerName = "yarn"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); -// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index eea9257..3fba24f 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -9,7 +9,7 @@ import chalk from "chalk"; /** * @param {string[]} args - * @returns {Promise} + * @returns {Promise} */ export async function main(args) { process.on("SIGINT", handleProcessTermination); diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 9716261..037a512 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -39,7 +39,6 @@ async function runBunCommand(command, args) { try { const result = await safeSpawn(command, args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index c068240..af57fad 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -11,7 +11,6 @@ export async function runNpm(args) { try { const result = await safeSpawn("npm", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; @@ -24,37 +23,3 @@ export async function runNpm(args) { } } } - -/** - * @param {string[]} args - * @returns {Promise<{status: number, output?: string}>} - */ -export async function dryRunNpmCommandAndOutput(args) { - try { - const result = await safeSpawn( - "npm", - [...args, "--ignore-scripts", "--dry-run"], - { - stdio: "pipe", - // @ts-expect-error values of process.env can be string | undefined - env: mergeSafeChainProxyEnvironmentVariables(process.env), - } - ); - return { - status: result.status, - output: result.status === 0 ? result.stdout : result.stderr, - }; - } catch (/** @type any */ 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/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index 61adcaa..2501b79 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -11,7 +11,6 @@ export async function runNpx(args) { try { const result = await safeSpawn("npx", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index db4ffa9..d958fb8 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -13,13 +13,11 @@ export async function runPnpmCommand(args, toolName = "pnpm") { if (toolName === "pnpm") { result = await safeSpawn("pnpm", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else if (toolName === "pnpx") { result = await safeSpawn("pnpx", args, { stdio: "inherit", - // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else { diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 1ba0f5c..04650f7 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -9,7 +9,6 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg */ export async function runYarnCommand(args) { try { - // @ts-expect-error values of process.env can be string | undefined const env = mergeSafeChainProxyEnvironmentVariables(process.env); await fixYarnProxyEnvironmentVariables(env); diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index c990a98..6f7b20e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,7 +5,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {(target: string) => Promise} isAllowed */ export function mitmConnect(req, clientSocket, isAllowed) { @@ -25,7 +25,6 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); - // @ts-expect-error Property 'headersSent' does not exist on type 'Socket' if (!clientSocket.headersSent) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } else if (clientSocket.writable) { @@ -55,7 +54,13 @@ function createHttpsServer(hostname, isAllowed) { * @returns {Promise} */ async function handleRequest(req, res) { - // @ts-expect-error req.url might be undefined + if (!req.url) { + ui.writeError("Safe-chain: Request missing URL"); + res.writeHead(400, "Bad Request"); + res.end("Bad Request: Missing URL"); + return; + } + const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; @@ -163,7 +168,13 @@ function createProxyRequest(hostname, req, res) { } }); - // @ts-expect-error statusCode might be undefined + if (!proxyRes.statusCode) { + ui.writeError("Safe-chain: Proxy response missing status code"); + res.writeHead(500); + res.end("Internal Server Error"); + return; + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index 16b305a..75b9d77 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,5 +1,6 @@ import * as http from "http"; import * as https from "https"; +import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req @@ -8,7 +9,13 @@ import * as https from "https"; * @returns {void} */ export function handleHttpProxyRequest(req, res) { - // @ts-expect-error req.url might be undefined + if (!req.url) { + ui.writeError("Safe-chain: Request missing URL"); + res.writeHead(400, "Bad Request"); + res.end("Bad Request: Missing URL"); + return; + } + const url = new URL(req.url); // The protocol for the plainHttpProxy should usually only be http: @@ -27,11 +34,16 @@ export function handleHttpProxyRequest(req, res) { const proxyRequest = protocol .request( - // @ts-expect-error req.url might be undefined req.url, { method: req.method, headers: req.headers }, (proxyRes) => { - // @ts-expect-error statusCode might be undefined + if (!proxyRes.statusCode) { + ui.writeError("Safe-chain: Proxy response missing status code"); + res.writeHead(500); + res.end("Internal Server Error"); + return; + } + res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index ed103b2..9e7bb7b 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -43,7 +43,7 @@ function getSafeChainProxyEnvironmentVariables() { } /** - * @param {Record} env + * @param {Record} env * * @returns {Record} */ @@ -56,7 +56,7 @@ export function mergeSafeChainProxyEnvironmentVariables(env) { // So we only copy the variable if it's not already set in a different case const upperKey = key.toUpperCase(); - if (!proxyEnv[upperKey]) { + if (!proxyEnv[upperKey] && env[key]) { proxyEnv[key] = env[key]; } } @@ -122,7 +122,7 @@ function stopServer(server) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -130,9 +130,9 @@ function stopServer(server) { function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL + const url = req.url; - // @ts-expect-error req.url might be undefined - if (knownRegistries.some((reg) => req.url.includes(reg))) { + if (url && knownRegistries.some((reg) => 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); diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 452ef15..4b756d7 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -3,7 +3,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -30,7 +30,7 @@ export function tunnelRequest(req, clientSocket, head) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * * @returns {void} @@ -38,13 +38,16 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); - // @ts-expect-error port from URL is a string but net.connect accepts number - const serverSocket = net.connect(port || 443, hostname, () => { - clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); - serverSocket.write(head); - serverSocket.pipe(clientSocket); - clientSocket.pipe(serverSocket); - }); + const serverSocket = net.connect( + Number.parseInt(port) || 443, + hostname, + () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + } + ); clientSocket.on("error", () => { // This can happen if the client TCP socket sends RST instead of FIN. @@ -66,7 +69,7 @@ function tunnelRequestToDestination(req, clientSocket, head) { /** * @param {import("http").IncomingMessage} req - * @param {import("net").Socket} clientSocket + * @param {import("http").ServerResponse} clientSocket * @param {Buffer} head * @param {string} proxyUrl */ @@ -75,10 +78,9 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { const proxy = new URL(proxyUrl); // Connect to proxy server - // @ts-expect-error net.connect wants port as number but proxy.port is string const proxySocket = net.connect({ host: proxy.hostname, - port: proxy.port, + port: Number.parseInt(proxy.port) || 80, }); proxySocket.on("connect", () => { diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index d8e817e..44ff57c 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -21,11 +21,11 @@ export function shouldScanCommand(args) { /** * @param {string[]} args * - * @returns {Promise} + * @returns {Promise} */ export async function scanCommand(args) { if (!shouldScanCommand(args)) { - return []; + return 0; } let timedOut = false; diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index b54846e..03c7081 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -71,8 +71,11 @@ async function getMalwareDatabase() { } const { malwareDatabase, version } = await fetchMalwareDatabase(); - // @ts-expect-error version can be undefined - writeDatabaseToLocalCache(malwareDatabase, version); + + if (version) { + // Only cache the malware database when we have a version. + writeDatabaseToLocalCache(malwareDatabase, version); + } return malwareDatabase; } catch (/** @type any */ error) { diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index 489d070..e17bdb5 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -67,8 +67,6 @@ function resolveCommandPath(command) { // Use 'command -v' to find the full path const fullPath = execSync(`command -v ${command}`, { encoding: "utf8", - // @ts-expect-error shell is a string option - shell: true, }).trim(); if (!fullPath) { @@ -120,8 +118,12 @@ export async function safeSpawn(command, args, options = {}) { }); child.on("close", (code) => { + // Code is null if it terminated by a signal. This should never + // happen in our code. If this happens, return 1 error code. + + code = code ?? 1; + resolve({ - // @ts-expect-error code can be null status: code, stdout: stdout, stderr: stderr, From 5304a7744a67da875ed73a1ce744f03fccbfc63a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 14:41:29 +0100 Subject: [PATCH 130/345] Add better error handling, tests and type checks for configFile.js --- packages/safe-chain/src/config/configFile.js | 30 +++- .../safe-chain/src/config/configFile.spec.js | 140 ++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 packages/safe-chain/src/config/configFile.spec.js diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 7273f7a..ab96bb1 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,14 +3,32 @@ import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef {Object} SafeChainConfig + * @property {any} scanTimeout // This should be a number + */ + /** * @returns {number} */ export function getScanTimeout() { - const config = /** @type {{scanTimeout?: number}} */ (readConfigFile()); + const config = readConfigFile(); - // @ts-expect-error values of process.env can be string | undefined - return parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds + if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { + const scanTimeout = Number(process.env.AIKIDO_SCAN_TIMEOUT_MS); + if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + return scanTimeout; + } + } + + if (config.scanTimeout) { + const scanTimeout = Number(config.scanTimeout); + if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + return scanTimeout; + } + } + + return 10000; // Default to 10 seconds } /** @@ -68,13 +86,15 @@ export function readDatabaseFromLocalCache() { } /** - * @returns {unknown} + * @returns {SafeChainConfig} */ function readConfigFile() { const configFilePath = getConfigFilePath(); if (!fs.existsSync(configFilePath)) { - return {}; + return { + scanTimeout: undefined, + }; } const data = fs.readFileSync(configFilePath, "utf8"); diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js new file mode 100644 index 0000000..f52d20b --- /dev/null +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -0,0 +1,140 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert"; +import { getScanTimeout } from "./configFile.js"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +describe("getScanTimeout", () => { + let originalEnv; + let aikidoDir; + let configPath; + let configBackupPath; + + beforeEach(() => { + // Save original environment + originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS; + + // Use the actual .aikido directory + aikidoDir = path.join(os.homedir(), ".aikido"); + configPath = path.join(aikidoDir, "config.json"); + configBackupPath = path.join(aikidoDir, "config.json.backup"); + + // Backup existing config if it exists + if (fs.existsSync(configPath)) { + fs.copyFileSync(configPath, configBackupPath); + } + }); + + afterEach(() => { + // Restore original environment + if (originalEnv !== undefined) { + process.env.AIKIDO_SCAN_TIMEOUT_MS = originalEnv; + } else { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + } + + // Restore original config file + if (fs.existsSync(configBackupPath)) { + fs.copyFileSync(configBackupPath, configPath); + fs.unlinkSync(configBackupPath); + } else if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + } + }); + + it("should return default timeout of 10000ms when no config or env var is set", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + } + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); + + it("should return timeout from config file when set", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 5000); + }); + + it("should prioritize environment variable over config file", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 20000); + }); + + it("should handle invalid environment variable and fall back to config", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 7000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 7000); + }); + + it("should ignore zero and negative values and fall back to default", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; + + let timeout = getScanTimeout(); + assert.strictEqual(timeout, 10000); + + process.env.AIKIDO_SCAN_TIMEOUT_MS = "-5000"; + + timeout = getScanTimeout(); + assert.strictEqual(timeout, 10000); + }); + + it("should ignore textual non-numeric values in environment variable and fall back to config", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 8000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 8000); + }); + + it("should ignore textual non-numeric values in config file and fall back to default", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "slow" })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); + + it("should ignore textual non-numeric values in both env and config, fall back to default", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "medium" })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); + + it("should ignore mixed alphanumeric strings in environment variable", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 6000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 6000); + }); + + it("should ignore mixed alphanumeric strings in config file", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "3000ms" })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); +}); From 1e7cd74364b2c80336d659eca26c99c8db169841 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 14:49:44 +0100 Subject: [PATCH 131/345] Mock filesystem in configFile.spec.js --- packages/safe-chain/src/config/configFile.js | 5 +- .../safe-chain/src/config/configFile.spec.js | 102 ++++++++++++------ 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index ab96bb1..cf36b12 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -5,7 +5,10 @@ import { ui } from "../environment/userInteraction.js"; /** * @typedef {Object} SafeChainConfig - * @property {any} scanTimeout // This should be a number + * + * This should be a number, but can be anything because it is user-input. + * We cannot trust the input and should add the necessary validations. + * @property {any} scanTimeout */ /** diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index f52d20b..8ec980c 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -1,29 +1,32 @@ -import { describe, it, beforeEach, afterEach } from "node:test"; +import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; -import { getScanTimeout } from "./configFile.js"; -import fs from "fs"; -import path from "path"; -import os from "os"; describe("getScanTimeout", () => { let originalEnv; - let aikidoDir; - let configPath; - let configBackupPath; + let fsMock; + let getScanTimeout; - beforeEach(() => { + beforeEach(async () => { // Save original environment originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS; - // Use the actual .aikido directory - aikidoDir = path.join(os.homedir(), ".aikido"); - configPath = path.join(aikidoDir, "config.json"); - configBackupPath = path.join(aikidoDir, "config.json.backup"); + // Mock fs module + fsMock = { + existsSync: mock.fn(() => false), + readFileSync: mock.fn(() => "{}"), + writeFileSync: mock.fn(), + mkdirSync: mock.fn(), + }; - // Backup existing config if it exists - if (fs.existsSync(configPath)) { - fs.copyFileSync(configPath, configBackupPath); - } + mock.module("fs", { + namedExports: fsMock, + }); + + // Re-import the module to get the mocked version + const configFileModule = await import( + `./configFile.js?update=${Date.now()}` + ); + getScanTimeout = configFileModule.getScanTimeout; }); afterEach(() => { @@ -34,20 +37,14 @@ describe("getScanTimeout", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; } - // Restore original config file - if (fs.existsSync(configBackupPath)) { - fs.copyFileSync(configBackupPath, configPath); - fs.unlinkSync(configBackupPath); - } else if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - } + // Reset all mocks + mock.restoreAll(); }); it("should return default timeout of 10000ms when no config or env var is set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - } + // Mock: config file doesn't exist + fsMock.existsSync.mock.mockImplementation(() => false); const timeout = getScanTimeout(); @@ -56,7 +53,11 @@ describe("getScanTimeout", () => { it("should return timeout from config file when set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + // Mock: config file exists with scanTimeout: 5000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 5000 }) + ); const timeout = getScanTimeout(); @@ -65,7 +66,11 @@ describe("getScanTimeout", () => { it("should prioritize environment variable over config file", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + // Mock: config file exists with scanTimeout: 5000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 5000 }) + ); const timeout = getScanTimeout(); @@ -74,7 +79,11 @@ describe("getScanTimeout", () => { it("should handle invalid environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 7000 })); + // Mock: config file exists with scanTimeout: 7000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 7000 }) + ); const timeout = getScanTimeout(); @@ -82,6 +91,9 @@ describe("getScanTimeout", () => { }); it("should ignore zero and negative values and fall back to default", () => { + // Mock: config file doesn't exist + fsMock.existsSync.mock.mockImplementation(() => false); + process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; let timeout = getScanTimeout(); @@ -95,7 +107,11 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 8000 })); + // Mock: config file exists with scanTimeout: 8000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 8000 }) + ); const timeout = getScanTimeout(); @@ -104,7 +120,11 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in config file and fall back to default", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "slow" })); + // Mock: config file exists with scanTimeout: "slow" + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: "slow" }) + ); const timeout = getScanTimeout(); @@ -113,7 +133,11 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in both env and config, fall back to default", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "medium" })); + // Mock: config file exists with scanTimeout: "medium" + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: "medium" }) + ); const timeout = getScanTimeout(); @@ -122,7 +146,11 @@ describe("getScanTimeout", () => { it("should ignore mixed alphanumeric strings in environment variable", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 6000 })); + // Mock: config file exists with scanTimeout: 6000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 6000 }) + ); const timeout = getScanTimeout(); @@ -131,7 +159,11 @@ describe("getScanTimeout", () => { it("should ignore mixed alphanumeric strings in config file", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "3000ms" })); + // Mock: config file exists with scanTimeout: "3000ms" + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: "3000ms" }) + ); const timeout = getScanTimeout(); From 8c872b3861d8dab2ef8fac63b1ac97a5835a8f4c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 14:54:42 +0100 Subject: [PATCH 132/345] Better error handling and extract validation logic to a re-usable function. --- packages/safe-chain/src/config/configFile.js | 31 ++++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index cf36b12..901e7d7 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -18,15 +18,15 @@ export function getScanTimeout() { const config = readConfigFile(); if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { - const scanTimeout = Number(process.env.AIKIDO_SCAN_TIMEOUT_MS); - if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + const scanTimeout = validateTimeout(process.env.AIKIDO_SCAN_TIMEOUT_MS); + if (scanTimeout != null) { return scanTimeout; } } if (config.scanTimeout) { - const scanTimeout = Number(config.scanTimeout); - if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + const scanTimeout = validateTimeout(config.scanTimeout); + if (scanTimeout != null) { return scanTimeout; } } @@ -34,6 +34,19 @@ export function getScanTimeout() { return 10000; // Default to 10 seconds } +/** + * + * @param {any} value + * @returns {number?} + */ +function validateTimeout(value) { + const timeout = Number(value); + if (!Number.isNaN(timeout) && timeout > 0) { + return timeout; + } + return null; +} + /** * @param {import("../api/aikido.js").MalwarePackage[]} data * @param {string | number} version @@ -100,8 +113,14 @@ function readConfigFile() { }; } - const data = fs.readFileSync(configFilePath, "utf8"); - return JSON.parse(data); + try { + const data = fs.readFileSync(configFilePath, "utf8"); + return JSON.parse(data); + } catch { + return { + scanTimeout: undefined, + }; + } } /** From 27ca2153b0eaa567a190562145e6f036bc499243 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 3 Nov 2025 06:51:14 -0800 Subject: [PATCH 133/345] 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 134/345] 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 135/345] 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 136/345] 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 137/345] 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 138/345] 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 139/345] 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 140/345] 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 141/345] 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 142/345] 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 143/345] 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 3ea4e82acb6b058f2c504298027e55616573fccc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 4 Nov 2025 11:26:07 +0100 Subject: [PATCH 144/345] Write a warning if no version was returned from the malware download, causing the malware db not to be cached. --- packages/safe-chain/src/scanning/malwareDatabase.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 03c7081..539044b 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -75,9 +75,15 @@ async function getMalwareDatabase() { if (version) { // Only cache the malware database when we have a version. writeDatabaseToLocalCache(malwareDatabase, version); + return malwareDatabase; + } else { + // We received a valid malware database, but the response + // did not contain an etag header with the version + ui.writeWarning( + "The malware database was downloaded, but could not be cached due to a missing version." + ); + return malwareDatabase; } - - return malwareDatabase; } catch (/** @type any */ error) { if (cachedDatabase) { ui.writeWarning( From 497401e8e053c440ea39817bf69e6405437024c8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 4 Nov 2025 13:18:36 +0100 Subject: [PATCH 145/345] Remove yarn version check --- .../src/packagemanager/yarn/runYarnCommand.js | 26 ++----------------- .../yarn/runYarnCommand.spec.js | 4 +-- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 04650f7..2089551 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -35,29 +35,7 @@ export async function runYarnCommand(args) { async function fixYarnProxyEnvironmentVariables(env) { // Yarn ignores standard proxy environment variable HTTPS_PROXY // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. - // Don't use YARN_HTTPS_CA_FILE_PATH though, as it causes to ignore all system CAs + // Don't use YARN_HTTPS_CA_FILE_PATH or YARN_CA_FILE_PATH though, it causes yarn to ignore all system CAs - // Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs - // When setting all variables, yarn returns an error about conflicting variables - // - v2/v3: "Usage Error: Unrecognized or legacy configuration settings found: httpsCaFilePath" - // - v4+: "Usage Error: Unrecognized or legacy configuration settings found: caFilePath" - - const version = await yarnVersion(); - const majorVersion = parseInt(version.split(".")[0]); - - if (majorVersion >= 4) { - env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - } else if (majorVersion === 2 || majorVersion === 3) { - env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; - } -} - -async function yarnVersion() { - const result = await safeSpawn("yarn", ["--version"], { - stdio: "pipe", - }); - if (result.status !== 0) { - throw new Error("Failed to get yarn version"); - } - return result.stdout.trim(); + env.YARN_HTTPS_PROXY = env.HTTPS_PROXY; } diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js index bd3d04d..21475f9 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.spec.js @@ -103,13 +103,13 @@ describe("runYarnCommand", () => { ); }); - it("should not set Yarn-specific proxy vars for Yarn v1", async () => { + it("should set YARN_HTTPS_PROXY for Yarn v1", async () => { yarnVersion = "1.22.19"; await runYarnCommand(["add", "lodash"]); assert.strictEqual( capturedEnv.YARN_HTTPS_PROXY, - undefined, + "http://localhost:8080", "YARN_HTTPS_PROXY should not be set for Yarn v1" ); assert.strictEqual( From 2b6b9b6737eaf6fcbb52721752458d5adf6941c5 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 4 Nov 2025 06:59:45 -0800 Subject: [PATCH 146/345] 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) From 6241c56fdaaf2a35fd2025115a3dbab95c98c1e1 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 4 Nov 2025 13:29:31 -0800 Subject: [PATCH 147/345] Skeleton for CI support --- README.md | 19 +++ .../templates/unix-python-wrapper.template.sh | 31 ++++ .../windows-python-wrapper.template.cmd | 39 +++++ .../src/shell-integration/setup-ci.js | 99 +++++++++++-- test/e2e/pip-ci.e2e.spec.js | 139 ++++++++++++++++++ 5 files changed, 316 insertions(+), 11 deletions(-) create mode 100644 packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh create mode 100644 packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd create mode 100644 test/e2e/pip-ci.e2e.spec.js diff --git a/README.md b/README.md index acea710..0f69d7f 100644 --- a/README.md +++ b/README.md @@ -165,3 +165,22 @@ This automatically configures your CI environment to use Aikido Safe Chain for a ``` After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. + +### Python (pip/pip3) example + +```yaml +- name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + +- name: Setup safe-chain + run: | + npm i -g @aikidosec/safe-chain + safe-chain setup-ci + +- name: Install Python dependencies + run: | + pip3 install --upgrade pip + pip3 install -r requirements.txt +``` diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh new file mode 100644 index 0000000..c4edf2a --- /dev/null +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Generated wrapper for python/python3 by safe-chain +# Intercepts `python[3] -m pip[...]` in CI environments + +# Function to remove shim from PATH (POSIX-compliant) +remove_shim_from_path() { + echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g" +} + +# Determine which python variant we were invoked as based on the script name +invoked=$(basename "$0") + +# If invoked as `python -m pip[...]` or `python3 -m pip[...]`, route to aikido +if [ "$1" = "-m" ] && [ -n "$2" ] && echo "$2" | grep -Eq '^pip(3)?$'; then + mod="$2" + shift 2 + if [ "$invoked" = "python3" ] || [ "$mod" = "pip3" ]; then + PATH=$(remove_shim_from_path) exec aikido-pip3 "$@" + else + PATH=$(remove_shim_from_path) exec aikido-pip "$@" + fi +fi + +# Otherwise, find and exec the real python/python3 matching the invoked name +original_cmd=$(PATH=$(remove_shim_from_path) command -v "$invoked") +if [ -n "$original_cmd" ]; then + exec "$original_cmd" "$@" +else + echo "Error: Could not find original $invoked" >&2 + exit 1 +fi diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd new file mode 100644 index 0000000..c9f1eda --- /dev/null +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd @@ -0,0 +1,39 @@ +@echo off +REM Generated wrapper for python/python3 by safe-chain +REM Intercepts `python[3] -m pip[...]` in CI environments + +REM Remove shim directory from PATH to prevent infinite loops +set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims" +call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%" + +REM Determine invoked name (python or python3) from the script name +set "INVOKED=%~n0" + +REM Check for -m pip or -m pip3 +if "%1"=="-m" ( + if /I "%2"=="pip3" ( + shift + shift + set "PATH=%CLEAN_PATH%" & aikido-pip3 %* + goto :eof + ) + if /I "%2"=="pip" ( + shift + shift + if /I "%INVOKED%"=="python3" ( + set "PATH=%CLEAN_PATH%" & aikido-pip3 %* + ) else ( + set "PATH=%CLEAN_PATH%" & aikido-pip %* + ) + goto :eof + ) +) + +REM Fallback to real python/python3 matching the invoked name +for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where %INVOKED% 2^>nul') do ( + "%%i" %* + goto :eof +) + +echo Error: Could not find original %INVOKED% 1>&2 +exit /b 1 diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 64fff16..b061bb6 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -51,13 +51,9 @@ function createUnixShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool except pip (CI support not yet implemented) + // Create a shim for each tool 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); @@ -70,11 +66,54 @@ function createUnixShims(shimsDir) { created++; } + // Also create python and python3 shims to support `python[3] -m pip[3]` in CI + createUnixPythonShims(shimsDir); + ui.writeInformation( `Created ${created} Unix shim(s) in ${shimsDir}` ); } +/** + * @param {string} shimsDir + */ +function createUnixPythonShims(shimsDir) { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const entries = [ + { + name: "python", + template: path.resolve( + __dirname, + "path-wrappers", + "templates", + "unix-python-wrapper.template.sh" + ), + }, + { + name: "python3", + template: path.resolve( + __dirname, + "path-wrappers", + "templates", + "unix-python-wrapper.template.sh" + ), + }, + ]; + + for (const entry of entries) { + if (!fs.existsSync(entry.template)) { + ui.writeError(`Template file not found: ${entry.template}`); + continue; + } + const shimContent = fs.readFileSync(entry.template, "utf-8"); + const shimPath = `${shimsDir}/${entry.name}`; + fs.writeFileSync(shimPath, shimContent, "utf-8"); + fs.chmodSync(shimPath, 0o755); + } +} + /** * @param {string} shimsDir * @@ -98,27 +137,65 @@ function createWindowsShims(shimsDir) { const template = fs.readFileSync(templatePath, "utf-8"); - // Create a shim for each tool except pip (CI support not yet implemented) + // Create a shim for each tool 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`); + const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`; fs.writeFileSync(shimPath, shimContent, "utf-8"); created++; } + // Also create python and python3 shims for Windows to support `python[3] -m pip[3]` in CI + createWindowsPythonShims(shimsDir); + ui.writeInformation( `Created ${created} Windows shim(s) in ${shimsDir}` ); } +/** + * @param {string} shimsDir + */ +function createWindowsPythonShims(shimsDir) { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const entries = [ + { + name: "python.cmd", + template: path.resolve( + __dirname, + "path-wrappers", + "templates", + "windows-python-wrapper.template.cmd" + ), + }, + { + name: "python3.cmd", + template: path.resolve( + __dirname, + "path-wrappers", + "templates", + "windows-python-wrapper.template.cmd" + ), + }, + ]; + + for (const entry of entries) { + if (!fs.existsSync(entry.template)) { + ui.writeError(`Windows template file not found: ${entry.template}`); + continue; + } + const shimContent = fs.readFileSync(entry.template, "utf-8"); + const shimPath = `${shimsDir}/${entry.name}`; + fs.writeFileSync(shimPath, shimContent, "utf-8"); + } +} + /** * @param {string} shimsDir * diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js new file mode 100644 index 0000000..7b2bfa0 --- /dev/null +++ b/test/e2e/pip-ci.e2e.spec.js @@ -0,0 +1,139 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: safe-chain setup-ci command for pip/pip3", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + container = new DockerTestContainer(); + await container.start(); + }); + + afterEach(async () => { + if (container) { + await container.stop(); + container = null; + } + }); + + for (let shell of ["bash", "zsh"]) { + it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => { + // Setup safe-chain CI shims + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + + // Add $HOME/.safe-chain/shims to PATH for subsequent shells + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + // Use --break-system-packages to avoid Debian/Ubuntu external management restrictions + const result = await projectShell.runCommand( + "pip3 install --break-system-packages certifi" + ); + + const hasExpectedOutput = result.output.includes( + "Scanning for malicious packages..." + ); + assert.ok( + hasExpectedOutput, + hasExpectedOutput + ? "Expected pip3 command to be wrapped by safe-chain" + : `Output did not contain \"Scanning for malicious packages...\": \n${result.output}` + ); + }); + + it(`setup-ci routes python -m pip through safe-chain for ${shell}`, async () => { + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + const result = await projectShell.runCommand( + "python -m pip install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("Scanning for malicious packages..."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + + it(`setup-ci routes python -m pip3 through safe-chain for ${shell}`, async () => { + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + const result = await projectShell.runCommand( + "python -m pip3 install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("Scanning for malicious packages..."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + + it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => { + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + const result = await projectShell.runCommand( + "python3 -m pip install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("Scanning for malicious packages..."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + + it(`setup-ci routes python3 -m pip3 through safe-chain for ${shell}`, async () => { + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + const result = await projectShell.runCommand( + "python3 -m pip3 install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("Scanning for malicious packages..."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + } +}); From 58a5e837f79b97b91572298fb5178a99bc703e9f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 4 Nov 2025 13:32:07 -0800 Subject: [PATCH 148/345] Add unit tests --- .../src/shell-integration/setup-ci.spec.js | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/safe-chain/src/shell-integration/setup-ci.spec.js b/packages/safe-chain/src/shell-integration/setup-ci.spec.js index 0a26124..5471f36 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.spec.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.spec.js @@ -146,5 +146,66 @@ describe("Setup CI shell integration", () => { const unixNpmShim = path.join(mockShimsDir, "npm"); assert.ok(!fs.existsSync(unixNpmShim), "Unix npm shim should not exist on Windows"); }); + + it("should create python and python3 shims from unix-python wrapper template", async () => { + // Add unix-python wrapper template to mock templates + const unixPythonTemplatePath = path.join( + mockTemplateDir, + "path-wrappers", + "templates", + "unix-python-wrapper.template.sh" + ); + fs.writeFileSync( + unixPythonTemplatePath, + "#!/bin/bash\n# Python wrapper\nexec aikido-pip \"$@\"\n", + "utf-8" + ); + + await setupCi(); + + // Check if python shim was created + const pythonShimPath = path.join(mockShimsDir, "python"); + assert.ok(fs.existsSync(pythonShimPath), "python shim should exist"); + // Check if python3 shim was created + const python3ShimPath = path.join(mockShimsDir, "python3"); + assert.ok(fs.existsSync(python3ShimPath), "python3 shim should exist"); + // Check content of python shim + const pythonShimContent = fs.readFileSync(pythonShimPath, "utf-8"); + assert.ok(pythonShimContent.includes("Python wrapper"), "python shim should use unix-python wrapper template"); + // Check content of python3 shim + const python3ShimContent = fs.readFileSync(python3ShimPath, "utf-8"); + assert.ok(python3ShimContent.includes("Python wrapper"), "python3 shim should use unix-python wrapper template"); + }); + + it("should create python.cmd and python3.cmd shims from windows-python wrapper template on win32 platform", async () => { + mockPlatform = "win32"; + // Add windows-python wrapper template to mock templates + const windowsPythonTemplatePath = path.join( + mockTemplateDir, + "path-wrappers", + "templates", + "windows-python-wrapper.template.cmd" + ); + fs.writeFileSync( + windowsPythonTemplatePath, + "@echo off\nREM Python wrapper\n{{AIKIDO_COMMAND}} %*\n", + "utf-8" + ); + + await setupCi(); + + // Check if python.cmd shim was created + const pythonCmdShimPath = path.join(mockShimsDir, "python.cmd"); + assert.ok(fs.existsSync(pythonCmdShimPath), "python.cmd shim should exist"); + // Check if python3.cmd shim was created + const python3CmdShimPath = path.join(mockShimsDir, "python3.cmd"); + assert.ok(fs.existsSync(python3CmdShimPath), "python3.cmd shim should exist"); + // Check content of python.cmd shim + const pythonCmdShimContent = fs.readFileSync(pythonCmdShimPath, "utf-8"); + assert.ok(pythonCmdShimContent.includes("Python wrapper"), "python.cmd should use windows-python wrapper template"); + // Check content of python3.cmd shim + const python3CmdShimContent = fs.readFileSync(python3CmdShimPath, "utf-8"); + assert.ok(python3CmdShimContent.includes("Python wrapper"), "python3.cmd should use windows-python wrapper template"); + }); }); }); \ No newline at end of file From 03312cd7077e97de5d18ab9cda7a4273aa9e7fac Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 4 Nov 2025 14:34:26 -0800 Subject: [PATCH 149/345] Clean up logging --- packages/safe-chain/src/scanning/audit/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 2d215cb..784b95f 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -34,8 +34,6 @@ export async function auditChanges(changes) { ); for (const change of changes) { - //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 ); From e4c40330f70e460ac3fe8ba8636d4e08df2f2732 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 5 Nov 2025 12:01:08 +0100 Subject: [PATCH 150/345] Only write to stdout when safe-chain audited packages --- packages/safe-chain/src/main.js | 16 +- .../safe-chain/src/scanning/audit/index.js | 27 +++ .../src/scanning/audit/index.spec.js | 188 ++++++++++++++++++ 3 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 packages/safe-chain/src/scanning/audit/index.spec.js diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 3fba24f..f4d5866 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -6,6 +6,7 @@ import { getPackageManager } from "./packagemanager/currentPackageManager.js"; import { initializeCliArguments } from "./config/cliArguments.js"; import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; import chalk from "chalk"; +import { getAuditStats } from "./scanning/audit/index.js"; /** * @param {string[]} args @@ -61,12 +62,15 @@ export async function main(args) { return 1; } - ui.emptyLine(); - ui.writeInformation( - `${chalk.green( - "✔" - )} Safe-chain: Command completed, no malicious packages found.` - ); + const auditStats = getAuditStats(); + if (auditStats.verifiedPackages > 0) { + ui.emptyLine(); + ui.writeInformation( + `${chalk.green("✔")} Safe-chain: Scanned ${ + auditStats.verifiedPackages + } packages, no malware found.` + ); + } // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 2d215cb..5b307fb 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -18,6 +18,29 @@ import { * @property {boolean} isAllowed */ +/** + * @typedef {Object} AuditStats + * @property {number} verifiedPackages + * @property {number} safePackages + * @property {number} malwarePackages + */ + +/** + * @type AuditStats + */ +const auditStats = { + verifiedPackages: 0, + safePackages: 0, + malwarePackages: 0, +}; + +/** + * @returns {AuditStats} + */ +export function getAuditStats() { + return auditStats; +} + /** * @param {PackageChange[]} changes * @@ -41,16 +64,20 @@ export async function auditChanges(changes) { ); if (malwarePackage) { + auditStats.malwarePackages += 1; ui.writeVerbose( `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}` ); disallowedChanges.push({ ...change, reason: malwarePackage.status }); } else { + auditStats.safePackages += 1; ui.writeVerbose( `Safe-chain: Package ${change.name}@${change.version} is clean` ); allowedChanges.push(change); } + + auditStats.verifiedPackages += 1; } const auditResults = { diff --git a/packages/safe-chain/src/scanning/audit/index.spec.js b/packages/safe-chain/src/scanning/audit/index.spec.js new file mode 100644 index 0000000..51c9d23 --- /dev/null +++ b/packages/safe-chain/src/scanning/audit/index.spec.js @@ -0,0 +1,188 @@ +import assert from "node:assert/strict"; +import { describe, it, mock, beforeEach } from "node:test"; + +describe("audit/index", async () => { + const mockWriteVerbose = mock.fn(); + + // Mock UI module + mock.module("../../environment/userInteraction.js", { + namedExports: { + ui: { + writeVerbose: mockWriteVerbose, + }, + }, + }); + + // Mock malware database + const mockIsMalware = mock.fn(); + mock.module("../malwareDatabase.js", { + namedExports: { + MALWARE_STATUS_MALWARE: "malware", + openMalwareDatabase: async () => ({ + isMalware: mockIsMalware, + }), + }, + }); + + const { auditChanges, getAuditStats } = await import("./index.js"); + + beforeEach(() => { + mockWriteVerbose.mock.resetCalls(); + mockIsMalware.mock.resetCalls(); + }); + + describe("getAuditStats", () => { + it("should return audit stats object with correct structure", () => { + const stats = getAuditStats(); + + assert.ok(stats.hasOwnProperty("verifiedPackages")); + assert.ok(stats.hasOwnProperty("safePackages")); + assert.ok(stats.hasOwnProperty("malwarePackages")); + assert.equal(typeof stats.verifiedPackages, "number"); + assert.equal(typeof stats.safePackages, "number"); + assert.equal(typeof stats.malwarePackages, "number"); + }); + + it("should return the same object reference on multiple calls", () => { + const stats1 = getAuditStats(); + const stats2 = getAuditStats(); + + assert.equal(stats1, stats2); + }); + }); + + describe("auditChanges", () => { + it("should return empty allowed and disallowed arrays when no changes provided", async () => { + const result = await auditChanges([]); + + assert.deepEqual(result.allowedChanges, []); + assert.deepEqual(result.disallowedChanges, []); + assert.equal(result.isAllowed, true); + }); + + it("should mark package as allowed when not malware", async () => { + mockIsMalware.mock.mockImplementation(() => false); + + const changes = [{ name: "lodash", version: "4.17.21", type: "add" }]; + const result = await auditChanges(changes); + + assert.equal(result.allowedChanges.length, 1); + assert.equal(result.disallowedChanges.length, 0); + assert.equal(result.isAllowed, true); + assert.deepEqual(result.allowedChanges[0], changes[0]); + }); + + it("should mark package as disallowed when malware detected", async () => { + mockIsMalware.mock.mockImplementation(() => true); + + const changes = [ + { name: "malicious-pkg", version: "1.0.0", type: "add" }, + ]; + const result = await auditChanges(changes); + + assert.equal(result.allowedChanges.length, 0); + assert.equal(result.disallowedChanges.length, 1); + assert.equal(result.isAllowed, false); + assert.equal(result.disallowedChanges[0].name, "malicious-pkg"); + assert.equal(result.disallowedChanges[0].version, "1.0.0"); + assert.equal(result.disallowedChanges[0].reason, "malware"); + }); + + it("should handle mixed safe and malware packages", async () => { + mockIsMalware.mock.mockImplementation((name) => { + return name === "malicious-pkg"; + }); + + const changes = [ + { name: "lodash", version: "4.17.21", type: "add" }, + { name: "malicious-pkg", version: "1.0.0", type: "add" }, + { name: "express", version: "4.18.0", type: "add" }, + ]; + const result = await auditChanges(changes); + + assert.equal(result.allowedChanges.length, 2); + assert.equal(result.disallowedChanges.length, 1); + assert.equal(result.isAllowed, false); + assert.equal(result.disallowedChanges[0].name, "malicious-pkg"); + }); + + it("should only check malware for add and change types", async () => { + mockIsMalware.mock.mockImplementation(() => false); + + const changes = [ + { name: "pkg1", version: "1.0.0", type: "add" }, + { name: "pkg2", version: "2.0.0", type: "change" }, + { name: "pkg3", version: "3.0.0", type: "remove" }, + ]; + await auditChanges(changes); + + // Should only check pkg1 and pkg2, not pkg3 (remove type) + assert.equal(mockIsMalware.mock.calls.length, 2); + }); + + it("should increment verifiedPackages counter for each package", async () => { + mockIsMalware.mock.mockImplementation(() => false); + + const statsBefore = getAuditStats(); + const initialCount = statsBefore.verifiedPackages; + + const changes = [ + { name: "pkg1", version: "1.0.0", type: "add" }, + { name: "pkg2", version: "2.0.0", type: "add" }, + { name: "pkg3", version: "3.0.0", type: "add" }, + ]; + await auditChanges(changes); + + const statsAfter = getAuditStats(); + assert.equal(statsAfter.verifiedPackages, initialCount + 3); + }); + + it("should increment safePackages counter for safe packages", async () => { + mockIsMalware.mock.mockImplementation(() => false); + + const statsBefore = getAuditStats(); + const initialCount = statsBefore.safePackages; + + const changes = [ + { name: "lodash", version: "4.17.21", type: "add" }, + { name: "express", version: "4.18.0", type: "add" }, + ]; + await auditChanges(changes); + + const statsAfter = getAuditStats(); + assert.equal(statsAfter.safePackages, initialCount + 2); + }); + + it("should increment malwarePackages counter for malware packages", async () => { + mockIsMalware.mock.mockImplementation(() => true); + + const statsBefore = getAuditStats(); + const initialCount = statsBefore.malwarePackages; + + const changes = [ + { name: "malicious-1", version: "1.0.0", type: "add" }, + { name: "malicious-2", version: "2.0.0", type: "add" }, + ]; + await auditChanges(changes); + + const statsAfter = getAuditStats(); + assert.equal(statsAfter.malwarePackages, initialCount + 2); + }); + + it("should accumulate stats across multiple auditChanges calls", async () => { + mockIsMalware.mock.mockImplementation(() => false); + + const statsBefore = getAuditStats(); + const initialVerified = statsBefore.verifiedPackages; + + // First call + await auditChanges([{ name: "pkg1", version: "1.0.0", type: "add" }]); + + // Second call + await auditChanges([{ name: "pkg2", version: "2.0.0", type: "add" }]); + + const statsAfter = getAuditStats(); + assert.equal(statsAfter.verifiedPackages, initialVerified + 2); + }); + }); +}); From 378b0ac7c92da7b0fee77b16ae94f4a1f73d4f6b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 5 Nov 2025 12:19:47 +0100 Subject: [PATCH 151/345] Rename verifiedPackages to totalPackages, fix e2e tests --- packages/safe-chain/src/main.js | 4 +- .../safe-chain/src/scanning/audit/index.js | 6 +- .../src/scanning/audit/index.spec.js | 14 +-- test/e2e/bun.e2e.spec.js | 2 +- test/e2e/npm-ci.e2e.spec.js | 2 +- test/e2e/npm.e2e.spec.js | 2 +- test/e2e/pip.e2e.spec.js | 110 +++++++++++------- test/e2e/pnpm-ci.e2e.spec.js | 2 +- test/e2e/pnpm.e2e.spec.js | 2 +- test/e2e/yarn-ci.e2e.spec.js | 2 +- test/e2e/yarn.e2e.spec.js | 2 +- 11 files changed, 89 insertions(+), 59 deletions(-) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index f4d5866..ea4fe0e 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -63,11 +63,11 @@ export async function main(args) { } const auditStats = getAuditStats(); - if (auditStats.verifiedPackages > 0) { + if (auditStats.totalPackages > 0) { ui.emptyLine(); ui.writeInformation( `${chalk.green("✔")} Safe-chain: Scanned ${ - auditStats.verifiedPackages + auditStats.totalPackages } packages, no malware found.` ); } diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 5b307fb..803051a 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -20,7 +20,7 @@ import { /** * @typedef {Object} AuditStats - * @property {number} verifiedPackages + * @property {number} totalPackages * @property {number} safePackages * @property {number} malwarePackages */ @@ -29,7 +29,7 @@ import { * @type AuditStats */ const auditStats = { - verifiedPackages: 0, + totalPackages: 0, safePackages: 0, malwarePackages: 0, }; @@ -77,7 +77,7 @@ export async function auditChanges(changes) { allowedChanges.push(change); } - auditStats.verifiedPackages += 1; + auditStats.totalPackages += 1; } const auditResults = { diff --git a/packages/safe-chain/src/scanning/audit/index.spec.js b/packages/safe-chain/src/scanning/audit/index.spec.js index 51c9d23..33ca9e3 100644 --- a/packages/safe-chain/src/scanning/audit/index.spec.js +++ b/packages/safe-chain/src/scanning/audit/index.spec.js @@ -35,10 +35,10 @@ describe("audit/index", async () => { it("should return audit stats object with correct structure", () => { const stats = getAuditStats(); - assert.ok(stats.hasOwnProperty("verifiedPackages")); + assert.ok(stats.hasOwnProperty("totalPackages")); assert.ok(stats.hasOwnProperty("safePackages")); assert.ok(stats.hasOwnProperty("malwarePackages")); - assert.equal(typeof stats.verifiedPackages, "number"); + assert.equal(typeof stats.totalPackages, "number"); assert.equal(typeof stats.safePackages, "number"); assert.equal(typeof stats.malwarePackages, "number"); }); @@ -120,11 +120,11 @@ describe("audit/index", async () => { assert.equal(mockIsMalware.mock.calls.length, 2); }); - it("should increment verifiedPackages counter for each package", async () => { + it("should increment totalPackages counter for each package", async () => { mockIsMalware.mock.mockImplementation(() => false); const statsBefore = getAuditStats(); - const initialCount = statsBefore.verifiedPackages; + const initialCount = statsBefore.totalPackages; const changes = [ { name: "pkg1", version: "1.0.0", type: "add" }, @@ -134,7 +134,7 @@ describe("audit/index", async () => { await auditChanges(changes); const statsAfter = getAuditStats(); - assert.equal(statsAfter.verifiedPackages, initialCount + 3); + assert.equal(statsAfter.totalPackages, initialCount + 3); }); it("should increment safePackages counter for safe packages", async () => { @@ -173,7 +173,7 @@ describe("audit/index", async () => { mockIsMalware.mock.mockImplementation(() => false); const statsBefore = getAuditStats(); - const initialVerified = statsBefore.verifiedPackages; + const initialCount = statsBefore.totalPackages; // First call await auditChanges([{ name: "pkg1", version: "1.0.0", type: "add" }]); @@ -182,7 +182,7 @@ describe("audit/index", async () => { await auditChanges([{ name: "pkg2", version: "2.0.0", type: "add" }]); const statsAfter = getAuditStats(); - assert.equal(statsAfter.verifiedPackages, initialVerified + 2); + assert.equal(statsAfter.totalPackages, initialCount + 2); }); }); }); diff --git a/test/e2e/bun.e2e.spec.js b/test/e2e/bun.e2e.spec.js index 8dea93b..4f24b7d 100644 --- a/test/e2e/bun.e2e.spec.js +++ b/test/e2e/bun.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: bun coverage", () => { const result = await shell.runCommand("bun i axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/npm-ci.e2e.spec.js b/test/e2e/npm-ci.e2e.spec.js index dc1c23f..18ee789 100644 --- a/test/e2e/npm-ci.e2e.spec.js +++ b/test/e2e/npm-ci.e2e.spec.js @@ -36,7 +36,7 @@ describe("E2E: npm coverage using PATH", () => { const result = await shell.runCommand("npm i axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/npm.e2e.spec.js b/test/e2e/npm.e2e.spec.js index ba836e7..b2b7211 100644 --- a/test/e2e/npm.e2e.spec.js +++ b/test/e2e/npm.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: npm coverage", () => { const result = await shell.runCommand("npm i axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index c647d30..ec2cdc5 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: pip coverage", () => { const result = await shell.runCommand("pip3 install requests"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); @@ -41,7 +41,7 @@ describe("E2E: pip coverage", () => { const result = await shell.runCommand("pip3 download requests"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); @@ -51,7 +51,7 @@ describe("E2E: pip coverage", () => { const result = await shell.runCommand("pip3 wheel requests"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); @@ -61,17 +61,19 @@ describe("E2E: pip coverage", () => { const result = await shell.runCommand("pip3 install --dry-run requests"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `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"'); + const result = await shell.runCommand( + 'pip3 install "requests[socks]==2.32.3"' + ); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); @@ -81,27 +83,27 @@ describe("E2E: pip coverage", () => { const result = await shell.runCommand('pip3 install "Jinja2>=3.1,<3.2"'); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); 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'); + const result = await shell.runCommand("python3 -m pip install requests"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware 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'); + const result = await shell.runCommand("python3 -m pip download requests"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); @@ -111,7 +113,9 @@ describe("E2E: pip coverage", () => { // 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"); + const result = await shell.runCommand( + "pip3 install --break-system-packages safe-chain-pi-test" + ); assert.ok( result.output.includes("blocked 1 malicious package downloads:"), @@ -135,60 +139,72 @@ describe("E2E: pip coverage", () => { 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'); + const result = await shell.runCommand( + "python -m pip install --break-system-packages requests" + ); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware 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"), + 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'); + const result = await shell.runCommand( + "python -m pip3 install --break-system-packages requests" + ); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware 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"), + 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'); + const result = await shell.runCommand( + "python3 -m pip install --break-system-packages requests" + ); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware 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"), + 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'); + const result = await shell.runCommand( + "python3 -m pip3 install --break-system-packages requests" + ); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware 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"), + result.output.includes("Successfully installed") || + result.output.includes("Requirement already satisfied"), `Installation did not succeed. Output was:\n${result.output}` ); }); @@ -197,17 +213,20 @@ describe("E2E: pip coverage", () => { 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'); + 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."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); - // Verify installation succeeded (would fail if certificate validation via env CA bundle broke) + // 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 - CA bundle may not be working. Output was:\n${result.output}` + result.output.includes("Successfully installed") || + result.output.includes("Requirement already satisfied"), + `Installation from GitHub failed - CA bundle may not be working. Output was:\n${result.output}` ); // Verify package was actually installed @@ -223,10 +242,12 @@ describe("E2E: pip coverage", () => { // 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'); + const result = await shell.runCommand( + "pip3 install --break-system-packages certifi" + ); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); @@ -238,7 +259,9 @@ describe("E2E: pip coverage", () => { // Should NOT contain SSL or certificate errors assert.ok( - !result.output.match(/SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i), + !result.output.match( + /SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i + ), `Should not have SSL/certificate errors. Output was:\n${result.output}` ); }); @@ -247,17 +270,20 @@ describe("E2E: pip coverage", () => { 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 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'); + 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."), + result.output.includes("no malware 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"), + result.output.includes("Successfully installed") || + result.output.includes("Requirement already satisfied"), `Installation from direct HTTPS URL failed. Output was:\n${result.output}` ); }); @@ -267,24 +293,28 @@ describe("E2E: pip coverage", () => { // 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://test.pypi.org/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."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); // Should succeed if CA bundle properly handles tunneled hosts assert.ok( - result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + result.output.includes("Successfully installed") || + result.output.includes("Requirement already satisfied"), `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 assert.ok( - !result.output.match(/SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i), + !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}` ); }); - }); diff --git a/test/e2e/pnpm-ci.e2e.spec.js b/test/e2e/pnpm-ci.e2e.spec.js index 339a5e0..6b92399 100644 --- a/test/e2e/pnpm-ci.e2e.spec.js +++ b/test/e2e/pnpm-ci.e2e.spec.js @@ -36,7 +36,7 @@ describe("E2E: pnpm coverage", () => { const result = await shell.runCommand("pnpm add axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/pnpm.e2e.spec.js b/test/e2e/pnpm.e2e.spec.js index c0187d7..944530c 100644 --- a/test/e2e/pnpm.e2e.spec.js +++ b/test/e2e/pnpm.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: pnpm coverage", () => { const result = await shell.runCommand("pnpm add axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/yarn-ci.e2e.spec.js b/test/e2e/yarn-ci.e2e.spec.js index 33ef4f2..8aac426 100644 --- a/test/e2e/yarn-ci.e2e.spec.js +++ b/test/e2e/yarn-ci.e2e.spec.js @@ -36,7 +36,7 @@ describe("E2E: yarn coverage", () => { const result = await shell.runCommand("yarn add axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); diff --git a/test/e2e/yarn.e2e.spec.js b/test/e2e/yarn.e2e.spec.js index 3909318..32a8114 100644 --- a/test/e2e/yarn.e2e.spec.js +++ b/test/e2e/yarn.e2e.spec.js @@ -31,7 +31,7 @@ describe("E2E: yarn coverage", () => { const result = await shell.runCommand("yarn add axios"); assert.ok( - result.output.includes("no malicious packages found."), + result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}` ); }); From 9c23345f1ca79fdf1df232825f25b888dd894c3f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 07:29:57 -0800 Subject: [PATCH 152/345] Add flags to prevent errors in Docker image --- test/e2e/pip.e2e.spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index c647d30..b61c602 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -28,7 +28,7 @@ describe("E2E: pip coverage", () => { it(`successfully installs known safe packages with pip3`, async () => { const shell = await container.openShell("zsh"); - const result = await shell.runCommand("pip3 install requests"); + const result = await shell.runCommand("pip3 install --break-system-packages requests"); assert.ok( result.output.includes("no malicious packages found."), @@ -38,7 +38,7 @@ describe("E2E: pip coverage", () => { it(`pip3 download`, async () => { const shell = await container.openShell("zsh"); - const result = await shell.runCommand("pip3 download requests"); + const result = await shell.runCommand("pip3 download --break-system-packages requests"); assert.ok( result.output.includes("no malicious packages found."), @@ -48,7 +48,7 @@ describe("E2E: pip coverage", () => { it(`pip3 .whl`, async () => { const shell = await container.openShell("zsh"); - const result = await shell.runCommand("pip3 wheel requests"); + const result = await shell.runCommand("pip3 wheel --break-system-packagesrequests"); assert.ok( result.output.includes("no malicious packages found."), @@ -58,7 +58,7 @@ describe("E2E: pip coverage", () => { 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"); + const result = await shell.runCommand("pip3 install --dry-run --break-system-packages requests"); assert.ok( result.output.includes("no malicious packages found."), @@ -68,7 +68,7 @@ describe("E2E: pip coverage", () => { 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"'); + const result = await shell.runCommand('pip3 install --break-system-packages "requests[socks]==2.32.3"'); assert.ok( result.output.includes("no malicious packages found."), @@ -78,7 +78,7 @@ describe("E2E: pip coverage", () => { 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"'); + const result = await shell.runCommand('pip3 install --break-system-packages "Jinja2>=3.1,<3.2"'); assert.ok( result.output.includes("no malicious packages found."), @@ -88,7 +88,7 @@ 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'); + const result = await shell.runCommand('python3 -m pip install --break-system-packages requests'); assert.ok( result.output.includes("no malicious packages found."), @@ -98,7 +98,7 @@ describe("E2E: pip coverage", () => { 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'); + const result = await shell.runCommand('python3 -m pip download --break-system-packagesrequests'); assert.ok( result.output.includes("no malicious packages found."), From 9f0f50eb15ae39628e2e7cedf3c7718855035b86 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 07:57:29 -0800 Subject: [PATCH 153/345] Small fix --- 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 b61c602..03a6e3b 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -38,7 +38,7 @@ describe("E2E: pip coverage", () => { it(`pip3 download`, async () => { const shell = await container.openShell("zsh"); - const result = await shell.runCommand("pip3 download --break-system-packages requests"); + const result = await shell.runCommand("pip3 download requests"); assert.ok( result.output.includes("no malicious packages found."), @@ -48,7 +48,7 @@ describe("E2E: pip coverage", () => { it(`pip3 .whl`, async () => { const shell = await container.openShell("zsh"); - const result = await shell.runCommand("pip3 wheel --break-system-packagesrequests"); + const result = await shell.runCommand("pip3 wheel requests"); assert.ok( result.output.includes("no malicious packages found."), @@ -98,7 +98,7 @@ describe("E2E: pip coverage", () => { 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 --break-system-packagesrequests'); + const result = await shell.runCommand('python3 -m pip download requests'); assert.ok( result.output.includes("no malicious packages found."), From f0a3ae51dba9a6586fd49970c0cd0bde05c9b694 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 08:34:40 -0800 Subject: [PATCH 154/345] Only use mitm for pip packages --- .../pip/createPackageManager.js | 70 +------ .../pip/createPackageManager.spec.js | 14 +- .../commandArgumentScanner.js | 77 -------- .../commandArgumentScanner.spec.js | 144 -------------- .../parsing/parsePackagesFromInstallArgs.js | 179 ------------------ .../parsePackagesFromInstallArgs.spec.js | 110 ----------- .../packagemanager/pip/utils/pipCommands.js | 30 --- .../pip/utils/pipCommands.spec.js | 83 -------- 8 files changed, 9 insertions(+), 698 deletions(-) delete mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js delete mode 100644 packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js delete mode 100644 packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js delete mode 100644 packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js delete mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js delete mode 100644 packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index af3036f..cb5484d 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -1,79 +1,15 @@ -import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; import { runPip } from "./runPipCommand.js"; -import { - getPipCommandForArgs, - pipInstallCommand, - pipDownloadCommand, - pipWheelCommand, -} from "./utils/pipCommands.js"; /** * @param {string} [command] * @returns {import("../currentPackageManager.js").PackageManager} */ export function createPipPackageManager(command = "pip") { - /** - * @param {string[]} args - * @returns {boolean} - */ - function isSupportedCommand(args) { - const scanner = findDependencyScannerForCommand( - commandScannerMapping, - args - ); - return scanner.shouldScan(args); - } - - /** - * @param {string[]} args - * @returns {ReturnType} - */ - function getDependencyUpdatesForCommand(args) { - const scanner = findDependencyScannerForCommand( - commandScannerMapping, - args - ); - return scanner.scan(args); - } - return { runCommand: /** @param {string[]} args */ (args) => runPip(command, args), - isSupportedCommand, - getDependencyUpdatesForCommand, + // For pip, rely solely on MITM proxy to detect/deny downloads from known registries. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], }; } -/** - * @type {Record} - */ -const commandScannerMapping = { - [pipInstallCommand]: commandArgumentScanner(), - [pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI - [pipWheelCommand]: commandArgumentScanner(), // wheel downloads and builds packages - // Other commands return null scanner by default -}; - -/** - * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} - */ -function nullScanner() { - return { - shouldScan: () => false, - scan: () => [], - }; -} - -/** - * @param {Record} scanners - * @param {string[]} args - * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} - */ -function findDependencyScannerForCommand(scanners, args) { - const command = getPipCommandForArgs(args); - if (!command) { - return nullScanner(); - } - - const scanner = scanners[command]; - return scanner || nullScanner(); -} diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js index 2d38b0d..69fc242 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js @@ -19,10 +19,10 @@ test("createPipPackageManager", async (t) => { await t.test("should support install, download, and wheel commands", () => { const pm = createPipPackageManager(); - - assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), true); - assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), true); - assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), true); + // With MITM-only approach, pip does not pre-scan by args + assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), false); + assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), false); + assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), false); }); await t.test("should not support uninstall and info commands", () => { @@ -35,12 +35,10 @@ test("createPipPackageManager", async (t) => { await t.test("should extract packages from install command", () => { const pm = createPipPackageManager(); - + // MITM-only: no dependency extraction from args const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]); assert.ok(Array.isArray(result)); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].name, "requests"); - assert.strictEqual(result[0].version, "2.28.0"); + assert.strictEqual(result.length, 0); }); await t.test("should return empty array for unsupported commands", () => { diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js deleted file mode 100644 index 27a07c2..0000000 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ /dev/null @@ -1,77 +0,0 @@ -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 {ScannerOptions} [options] - * - * @returns {CommandArgumentScanner} - */ -export function commandArgumentScanner(options = {}) { - const { ignoreDryRun = false } = options; - - /** - * @param {string[]} args - */ - function shouldScan(args) { - return shouldScanDependencies(args, ignoreDryRun); - } - - /** - * @param {string[]} args - * @returns {Promise | ScanResult[]} - */ - function scan(args) { - return scanDependencies(args); - } - - return { - shouldScan, - scan, - }; -} - -/** - * @param {string[]} args - * @param {boolean} ignoreDryRun - */ -function shouldScanDependencies(args, ignoreDryRun) { - return ignoreDryRun || !hasDryRunArg(args); -} - -/** - * @param {string[]} args - * @returns {Promise | ScanResult[]} - */ -function scanDependencies(args) { - return checkChangesFromArgs(args); -} - -/** - * @param {string[]} args - * @returns {Promise | ScanResult[]} - */ -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 deleted file mode 100644 index 9570756..0000000 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { commandArgumentScanner, checkChangesFromArgs } from "./commandArgumentScanner.js"; - -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"); - }); -}); - -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); - }); -}); - -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 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 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); - 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 handle empty args", () => { - const result = checkChangesFromArgs([]); - assert.deepStrictEqual(result, []); - }); -}); diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js deleted file mode 100644 index ac3d99f..0000000 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @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 rely on the MITM scanner: - * - package_name>=version - * - package_name<=version - * - package_name>version - * - package_name= 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 deleted file mode 100644 index 8a653c9..0000000 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -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", version: "latest", type: "add" }, - ]); - }); - - it("should parse package with version specifier", () => { - const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]); - assert.deepEqual(result, [ - { name: "requests", version: "2.28.0", type: "add" }, - ]); - }); - - it("should skip flags", () => { - const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]); - assert.deepEqual(result, [ - { 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", 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 skip ranges", () => { - const result = parsePackagesFromInstallArgs(["install", "requests>=2,<3"]); - 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 skip VCS/URL/path)", () => { - 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, []); - }); - - it("should return empty array for no packages", () => { - 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 deleted file mode 100644 index 92699ac..0000000 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ /dev/null @@ -1,30 +0,0 @@ -export const pipInstallCommand = "install"; -export const pipDownloadCommand = "download"; -export const pipWheelCommand = "wheel"; - -/** - * @param {string[]} args - * @returns {string | null} - */ -export function getPipCommandForArgs(args) { - if (!args || args.length === 0) { - return null; - } - - // The first non-flag argument is the command - for (const arg of args) { - if (!arg.startsWith("-")) { - return arg; - } - } - - return null; -} - -/** - * @param {string[]} args - * @returns {boolean} - */ -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 deleted file mode 100644 index 346ad8f..0000000 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { - getPipCommandForArgs, - hasDryRunArg, - pipInstallCommand, - pipDownloadCommand, - pipWheelCommand, -} 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"); - }); -}); From 87606def48cd00749e27a710fbd54cad31379c8e Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 09:18:18 -0800 Subject: [PATCH 155/345] Fix comments --- .../src/packagemanager/pip/createPackageManager.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js index 69fc242..d2668c0 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js @@ -19,7 +19,7 @@ test("createPipPackageManager", async (t) => { await t.test("should support install, download, and wheel commands", () => { const pm = createPipPackageManager(); - // With MITM-only approach, pip does not pre-scan by args + // MITM-only approach, pip does not scan args assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), false); assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), false); assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), false); @@ -35,7 +35,6 @@ test("createPipPackageManager", async (t) => { await t.test("should extract packages from install command", () => { const pm = createPipPackageManager(); - // MITM-only: no dependency extraction from args const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]); assert.ok(Array.isArray(result)); assert.strictEqual(result.length, 0); From bded1fe6607dcac5a1b0c8fe272cdef7fb1375a9 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 09:28:57 -0800 Subject: [PATCH 156/345] Fix test --- test/e2e/pip.e2e.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index adabe9f..5d046a7 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -96,7 +96,7 @@ 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"); + const result = await shell.runCommand("python3 -m pip install --break-system-packages requests"); assert.ok( result.output.includes("no malware found."), From 3b56a0181f43cb0dd3d5096bc8ed38ba11de14d4 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 09:55:09 -0800 Subject: [PATCH 157/345] Update comment --- packages/safe-chain/bin/safe-chain.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index b416f43..30f4086 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, bunx and pip.` + )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` ); ui.writeInformation( `- ${chalk.cyan( From 216e16cfb1406b8fdeffbfeb0a5edb4448ca68b8 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 11:13:24 -0800 Subject: [PATCH 158/345] Fix e2e test --- test/e2e/pip-ci.e2e.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index 7b2bfa0..a32a677 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -42,13 +42,13 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); const hasExpectedOutput = result.output.includes( - "Scanning for malicious packages..." + "no malware found." ); assert.ok( hasExpectedOutput, hasExpectedOutput ? "Expected pip3 command to be wrapped by safe-chain" - : `Output did not contain \"Scanning for malicious packages...\": \n${result.output}` + : `Output did not contain \"no malware found.\": \n${result.output}` ); }); @@ -68,7 +68,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); assert.ok( - result.output.includes("Scanning for malicious packages..."), + result.output.includes("no malware found."), `Output did not contain scan message. Output was:\n${result.output}` ); }); @@ -89,7 +89,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); assert.ok( - result.output.includes("Scanning for malicious packages..."), + result.output.includes("no malware found."), `Output did not contain scan message. Output was:\n${result.output}` ); }); @@ -110,7 +110,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); assert.ok( - result.output.includes("Scanning for malicious packages..."), + result.output.includes("no malware found."), `Output did not contain scan message. Output was:\n${result.output}` ); }); @@ -131,7 +131,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); assert.ok( - result.output.includes("Scanning for malicious packages..."), + result.output.includes("no malware found."), `Output did not contain scan message. Output was:\n${result.output}` ); }); From ec4228edc148676a5a4fa14f16e897515c0d0bbc Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 11:23:37 -0800 Subject: [PATCH 159/345] Add more test cases --- test/e2e/pip-ci.e2e.spec.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index a32a677..bcca90a 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -21,6 +21,22 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { } }); + describe("E2E: pip CI support", () => { + it("does not intercept python3 --version", async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python3 --version"); + assert.ok(result.output.match(/Python \d+\.\d+\.\d+/), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 command"); + }); + + it("does not intercept python3 -c 'print(\"hello\")'", async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python3 -c 'print(\"hello\")'"); + assert.ok(result.output.includes("hello"), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command"); + }); + }); + for (let shell of ["bash", "zsh"]) { it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => { // Setup safe-chain CI shims From 7cff2818e421f61069d7a86638092a85dc7c153e Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 15:40:54 -0800 Subject: [PATCH 160/345] Fix Windows template --- .../windows-python-wrapper.template.cmd | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd index c9f1eda..5b4ddd9 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd @@ -9,27 +9,32 @@ call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%" REM Determine invoked name (python or python3) from the script name set "INVOKED=%~n0" -REM Check for -m pip or -m pip3 -if "%1"=="-m" ( - if /I "%2"=="pip3" ( - shift - shift - set "PATH=%CLEAN_PATH%" & aikido-pip3 %* - goto :eof - ) - if /I "%2"=="pip" ( - shift - shift - if /I "%INVOKED%"=="python3" ( - set "PATH=%CLEAN_PATH%" & aikido-pip3 %* - ) else ( - set "PATH=%CLEAN_PATH%" & aikido-pip %* - ) - goto :eof - ) +REM Check for -m pip or -m pip3 without parentheses to avoid parser issues +if /I "%1" NEQ "-m" goto FALLBACK + +set "SECOND=%2" +if /I "%SECOND%"=="pip3" goto CALL_PIP3 +if /I "%SECOND%"=="pip" goto CALL_PIP +goto FALLBACK + +:CALL_PIP3 +shift +shift +set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 +goto :eof + +:CALL_PIP +shift +shift +if /I "%INVOKED%"=="python3" ( + set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 +) else ( + set "PATH=%CLEAN_PATH%" & aikido-pip %1 %2 %3 %4 %5 %6 %7 %8 %9 ) +goto :eof REM Fallback to real python/python3 matching the invoked name +:FALLBACK for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where %INVOKED% 2^>nul') do ( "%%i" %* goto :eof From fa4c46c23dd2874e1540365ff3c9f5aa83144f05 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 15:47:41 -0800 Subject: [PATCH 161/345] Cleanup readme --- README.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/README.md b/README.md index 0f69d7f..acea710 100644 --- a/README.md +++ b/README.md @@ -165,22 +165,3 @@ This automatically configures your CI environment to use Aikido Safe Chain for a ``` After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. - -### Python (pip/pip3) example - -```yaml -- name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - -- name: Setup safe-chain - run: | - npm i -g @aikidosec/safe-chain - safe-chain setup-ci - -- name: Install Python dependencies - run: | - pip3 install --upgrade pip - pip3 install -r requirements.txt -``` From 84cf485b31541f054ac5159cc9d7af1b072bea95 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 16:24:57 -0800 Subject: [PATCH 162/345] Add comment explaining forwarding --- .../templates/windows-python-wrapper.template.cmd | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd index 5b4ddd9..2974ee5 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd @@ -20,12 +20,18 @@ goto FALLBACK :CALL_PIP3 shift shift -set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 +REM Note on argument forwarding: +REM - We cannot use %* here because SHIFT does not update %* (it still contains the original argv). +REM - CMD only exposes nine positional parameters at a time: %1 .. %9. %10 is parsed as %1 followed by '0'. +\set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 goto :eof :CALL_PIP shift shift +REM Note on argument forwarding: +REM - We cannot use %* here because SHIFT does not update %* (it still contains the original argv). +REM - CMD only exposes nine positional parameters at a time: %1 .. %9. %10 is parsed as %1 followed by '0'. if /I "%INVOKED%"=="python3" ( set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 ) else ( From 0a3028329f83e718d7590a51b15cd0d7892c3cc9 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 5 Nov 2025 16:32:57 -0800 Subject: [PATCH 163/345] Fix template --- .../path-wrappers/templates/windows-python-wrapper.template.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd index 2974ee5..abeee5f 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd @@ -23,7 +23,7 @@ shift REM Note on argument forwarding: REM - We cannot use %* here because SHIFT does not update %* (it still contains the original argv). REM - CMD only exposes nine positional parameters at a time: %1 .. %9. %10 is parsed as %1 followed by '0'. -\set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 +set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 goto :eof :CALL_PIP From f400c5576a66c7e3840138407461c0aa9ccdb65f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 08:32:25 -0800 Subject: [PATCH 164/345] WIP --- package-lock.json | 2 + packages/safe-chain/bin/aikido-python.js | 22 +++++ packages/safe-chain/bin/aikido-python3.js | 22 +++++ packages/safe-chain/package.json | 2 + .../src/shell-integration/helpers.js | 2 + .../templates/unix-python-wrapper.template.sh | 31 ------- .../templates/unix-wrapper.template.sh | 4 +- .../windows-python-wrapper.template.cmd | 44 ---------- .../src/shell-integration/setup-ci.js | 83 ------------------- .../startup-scripts/init-fish.fish | 20 +---- .../startup-scripts/init-posix.sh | 20 +---- .../startup-scripts/init-pwsh.ps1 | 20 +---- 12 files changed, 59 insertions(+), 213 deletions(-) create mode 100644 packages/safe-chain/bin/aikido-python.js create mode 100644 packages/safe-chain/bin/aikido-python3.js delete mode 100644 packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh delete mode 100644 packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd diff --git a/package-lock.json b/package-lock.json index ee38fa8..a9c32df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2096,6 +2096,8 @@ "aikido-npx": "bin/aikido-npx.js", "aikido-pip": "bin/aikido-pip.js", "aikido-pip3": "bin/aikido-pip3.js", + "aikido-python": "bin/aikido-python.js", + "aikido-python3": "bin/aikido-python3.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-python.js b/packages/safe-chain/bin/aikido-python.js new file mode 100644 index 0000000..c22c601 --- /dev/null +++ b/packages/safe-chain/bin/aikido-python.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + + +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; +import { main } from "../src/main.js"; + +const argv = process.argv.slice(2); + +const supportedArgs = ["pip", "pip3"]; + +if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) { + setEcoSystem(ECOSYSTEM_PY); + + initializePackageManager(argv[1]); + var exitCode = await main(argv.slice(2)); + process.exit(exitCode); +} else { + // Fallback: run the real python + const { spawn } = await import("child_process"); + spawn("python", argv, { stdio: "inherit" }); +} diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js new file mode 100644 index 0000000..48659e5 --- /dev/null +++ b/packages/safe-chain/bin/aikido-python3.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + + +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; +import { main } from "../src/main.js"; + +const argv = process.argv.slice(2); + +const supportedArgs = ["pip", "pip3"]; + +if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) { + setEcoSystem(ECOSYSTEM_PY); + // python3 -m pip or python3 -m pip3: always use pip3 package manager + initializePackageManager("pip3"); + var exitCode = await main(argv.slice(2)); + process.exit(exitCode); +} else { + // Fallback: run the real python3 + const { spawn } = await import("child_process"); + spawn("python3", argv, { stdio: "inherit" }); +} diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index d93a058..f21a372 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -17,6 +17,8 @@ "aikido-bunx": "bin/aikido-bunx.js", "aikido-pip": "bin/aikido-pip.js", "aikido-pip3": "bin/aikido-pip3.js", + "aikido-python": "bin/aikido-python.js", + "aikido-python3": "bin/aikido-python3.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 4ba7c24..c405c54 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -22,6 +22,8 @@ export const knownAikidoTools = [ { tool: "bunx", aikidoCommand: "aikido-bunx" }, { tool: "pip", aikidoCommand: "aikido-pip" }, { tool: "pip3", aikidoCommand: "aikido-pip3" }, + { tool: "python", aikidoCommand: "aikido-python" }, + { tool: "python3", aikidoCommand: "aikido-python3" }, // When adding a new tool here, also update the documentation for the new tool in the README.md ]; diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh deleted file mode 100644 index c4edf2a..0000000 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-python-wrapper.template.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh -# Generated wrapper for python/python3 by safe-chain -# Intercepts `python[3] -m pip[...]` in CI environments - -# Function to remove shim from PATH (POSIX-compliant) -remove_shim_from_path() { - echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g" -} - -# Determine which python variant we were invoked as based on the script name -invoked=$(basename "$0") - -# If invoked as `python -m pip[...]` or `python3 -m pip[...]`, route to aikido -if [ "$1" = "-m" ] && [ -n "$2" ] && echo "$2" | grep -Eq '^pip(3)?$'; then - mod="$2" - shift 2 - if [ "$invoked" = "python3" ] || [ "$mod" = "pip3" ]; then - PATH=$(remove_shim_from_path) exec aikido-pip3 "$@" - else - PATH=$(remove_shim_from_path) exec aikido-pip "$@" - fi -fi - -# Otherwise, find and exec the real python/python3 matching the invoked name -original_cmd=$(PATH=$(remove_shim_from_path) command -v "$invoked") -if [ -n "$original_cmd" ]; then - exec "$original_cmd" "$@" -else - echo "Error: Could not find original $invoked" >&2 - exit 1 -fi diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh index 6e6d826..7663006 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -7,6 +7,8 @@ remove_shim_from_path() { echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g" } +echo "[safe-chain debug] command -v {{AIKIDO_COMMAND}} (raw PATH): $(command -v {{AIKIDO_COMMAND}} 2>/dev/null || echo notfound)" >&2 +echo "[safe-chain debug] PATH (raw): $PATH" >&2 if command -v {{AIKIDO_COMMAND}} >/dev/null 2>&1; then # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@" @@ -19,4 +21,4 @@ else echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2 exit 1 fi -fi \ No newline at end of file +fi diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd deleted file mode 100644 index 5b4ddd9..0000000 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-python-wrapper.template.cmd +++ /dev/null @@ -1,44 +0,0 @@ -@echo off -REM Generated wrapper for python/python3 by safe-chain -REM Intercepts `python[3] -m pip[...]` in CI environments - -REM Remove shim directory from PATH to prevent infinite loops -set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims" -call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%" - -REM Determine invoked name (python or python3) from the script name -set "INVOKED=%~n0" - -REM Check for -m pip or -m pip3 without parentheses to avoid parser issues -if /I "%1" NEQ "-m" goto FALLBACK - -set "SECOND=%2" -if /I "%SECOND%"=="pip3" goto CALL_PIP3 -if /I "%SECOND%"=="pip" goto CALL_PIP -goto FALLBACK - -:CALL_PIP3 -shift -shift -set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 -goto :eof - -:CALL_PIP -shift -shift -if /I "%INVOKED%"=="python3" ( - set "PATH=%CLEAN_PATH%" & aikido-pip3 %1 %2 %3 %4 %5 %6 %7 %8 %9 -) else ( - set "PATH=%CLEAN_PATH%" & aikido-pip %1 %2 %3 %4 %5 %6 %7 %8 %9 -) -goto :eof - -REM Fallback to real python/python3 matching the invoked name -:FALLBACK -for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where %INVOKED% 2^>nul') do ( - "%%i" %* - goto :eof -) - -echo Error: Could not find original %INVOKED% 1>&2 -exit /b 1 diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index b061bb6..926386d 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -66,53 +66,12 @@ function createUnixShims(shimsDir) { created++; } - // Also create python and python3 shims to support `python[3] -m pip[3]` in CI - createUnixPythonShims(shimsDir); - ui.writeInformation( `Created ${created} Unix shim(s) in ${shimsDir}` ); } -/** - * @param {string} shimsDir - */ -function createUnixPythonShims(shimsDir) { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const entries = [ - { - name: "python", - template: path.resolve( - __dirname, - "path-wrappers", - "templates", - "unix-python-wrapper.template.sh" - ), - }, - { - name: "python3", - template: path.resolve( - __dirname, - "path-wrappers", - "templates", - "unix-python-wrapper.template.sh" - ), - }, - ]; - - for (const entry of entries) { - if (!fs.existsSync(entry.template)) { - ui.writeError(`Template file not found: ${entry.template}`); - continue; - } - const shimContent = fs.readFileSync(entry.template, "utf-8"); - const shimPath = `${shimsDir}/${entry.name}`; - fs.writeFileSync(shimPath, shimContent, "utf-8"); - fs.chmodSync(shimPath, 0o755); - } -} /** * @param {string} shimsDir @@ -149,53 +108,11 @@ function createWindowsShims(shimsDir) { created++; } - // Also create python and python3 shims for Windows to support `python[3] -m pip[3]` in CI - createWindowsPythonShims(shimsDir); - ui.writeInformation( `Created ${created} Windows shim(s) in ${shimsDir}` ); } -/** - * @param {string} shimsDir - */ -function createWindowsPythonShims(shimsDir) { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - const entries = [ - { - name: "python.cmd", - template: path.resolve( - __dirname, - "path-wrappers", - "templates", - "windows-python-wrapper.template.cmd" - ), - }, - { - name: "python3.cmd", - template: path.resolve( - __dirname, - "path-wrappers", - "templates", - "windows-python-wrapper.template.cmd" - ), - }, - ]; - - for (const entry of entries) { - if (!fs.existsSync(entry.template)) { - ui.writeError(`Windows template file not found: ${entry.template}`); - continue; - } - const shimContent = fs.readFileSync(entry.template, "utf-8"); - const shimPath = `${shimsDir}/${entry.name}`; - fs.writeFileSync(shimPath, shimContent, "utf-8"); - } -} - /** * @param {string} 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 13494d1..ebf89ff 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 @@ -79,26 +79,10 @@ 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 + wrapSafeChainCommand "python" "aikido-python" $argv 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 args $argv[3..-1] - # python3 always uses pip3, regardless of whether user types `pip` or `pip3` - wrapSafeChainCommand "pip3" "aikido-pip3" $args - else - command python3 $argv - end + wrapSafeChainCommand "python3" "aikido-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 05b8b81..278b31a 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 @@ -71,26 +71,10 @@ function pip3() { # `python -m pip`, `python -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 + wrapSafeChainCommand "python" "aikido-python" "$@" } # `python3 -m pip`, `python3 -m pip3'. function python3() { - if [[ "$1" == "-m" && "$2" == pip* ]]; then - shift 2 - # python3 always uses pip3, regardless of whether user types `pip` or `pip3` - wrapSafeChainCommand "pip3" "aikido-pip3" "$@" - else - command python3 "$@" - fi + wrapSafeChainCommand "python3" "aikido-python3" "$@" } 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 6425f2f..b692107 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 @@ -97,27 +97,11 @@ function pip3 { # `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)?$') { - $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 - } + Invoke-WrappedCommand 'python' 'aikido-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)?$') { - # python3 always uses pip3, regardless of whether user types `pip` or `pip3` - $pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() } - Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs - } - else { - Invoke-RealCommand 'python3' $Args - } + Invoke-WrappedCommand 'python3' 'aikido-python3' $args } From e251908cb306ae5b97f27177afa04bd6f0bbb5ec Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 6 Nov 2025 18:00:11 +0100 Subject: [PATCH 165/345] Add interceptors for MITM --- .../interceptors/interceptorBuilder.js | 50 +++++++++++++++++++ .../interceptors/requestInterceptorBuilder.js | 30 +++++++++++ .../src/registryProxy/mitmRequestHandler.js | 23 ++++++--- .../src/registryProxy/registryProxy.js | 31 ++++++++++-- 4 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js create mode 100644 packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js new file mode 100644 index 0000000..574abb9 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -0,0 +1,50 @@ +/** + * @typedef {import('./requestInterceptorBuilder.js').RequestInterceptorBuilder} RequestInterceptorBuilder + * @typedef {import('./requestInterceptorBuilder.js').RequestInterceptor} RequestInterceptor + * + * @typedef {Object} InterceptorBuilder + * @property {(requestFunc: (requestHandlerBuilder: RequestInterceptorBuilder) => Promise) => void} onRequest + * @property {() => Interceptor} build + * + * @typedef {Object} Interceptor + * @property {(targetUrl: string) => Promise} handleRequest + */ + +import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js"; + +/** + * @returns {InterceptorBuilder} + */ +export function createInterceptorBuilder() { + /** + * @type {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise>} + */ + const requestHandlers = []; + + return { + onRequest(requestFunc) { + requestHandlers.push(requestFunc); + }, + build() { + return buildInterceptor(requestHandlers); + }, + }; +} + +/** + * @param {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise>} requestHandlers + * @returns {Interceptor} + */ +function buildInterceptor(requestHandlers) { + return { + async handleRequest(targetUrl) { + const reqInterceptorBuilder = createRequestInterceptorBuilder(targetUrl); + + for (const handler of requestHandlers) { + await handler(reqInterceptorBuilder); + } + + return reqInterceptorBuilder.build(); + }, + }; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js new file mode 100644 index 0000000..e0d560a --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -0,0 +1,30 @@ +/** + * @typedef {Object} RequestInterceptorBuilder + * @property {string} targetUrl + * @property {(statusCode: number, message: string) => void} blockRequest + * @property {() => RequestInterceptor} build + * + * @typedef {Object} RequestInterceptor + * @property {{statusCode: number, message: string} | undefined} blockResponse + */ + +/** + * @param {string} targetUrl + * @returns {RequestInterceptorBuilder} + */ +export function createRequestInterceptorBuilder(targetUrl) { + /** @type {{statusCode: number, message: string} | undefined} */ + let blockResponse = undefined; + + return { + targetUrl, + blockRequest(statusCode, message) { + blockResponse = { statusCode, message }; + }, + build() { + return { + blockResponse, + }; + }, + }; +} diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 6f7b20e..58f220c 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -3,12 +3,16 @@ import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor + */ + /** * @param {import("http").IncomingMessage} req * @param {import("http").ServerResponse} clientSocket - * @param {(target: string) => Promise} isAllowed + * @param {Interceptor} interceptor */ -export function mitmConnect(req, clientSocket, isAllowed) { +export function mitmConnect(req, clientSocket, interceptor) { ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`); const { hostname } = new URL(`http://${req.url}`); @@ -21,7 +25,7 @@ export function mitmConnect(req, clientSocket, isAllowed) { // Not subscribing to 'close' event will cause node to throw and crash. }); - const server = createHttpsServer(hostname, isAllowed); + const server = createHttpsServer(hostname, interceptor); server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); @@ -41,10 +45,10 @@ export function mitmConnect(req, clientSocket, isAllowed) { /** * @param {string} hostname - * @param {(target: string) => Promise} isAllowed + * @param {Interceptor} interceptor * @returns {import("https").Server} */ -function createHttpsServer(hostname, isAllowed) { +function createHttpsServer(hostname, interceptor) { const cert = generateCertForHost(hostname); /** @@ -64,10 +68,13 @@ function createHttpsServer(hostname, isAllowed) { const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; - if (!(await isAllowed(targetUrl))) { + const interceptorResult = await interceptor.handleRequest(targetUrl); + const blockResponse = interceptorResult?.blockResponse; + + if (blockResponse) { ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); - res.writeHead(403, "Forbidden - blocked by safe-chain"); - res.end("Blocked by safe-chain"); + res.writeHead(blockResponse.statusCode, blockResponse.message); + res.end(blockResponse.message); return; } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index c5e272b..d66f397 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -4,10 +4,19 @@ import { mitmConnect } from "./mitmRequestHandler.js"; 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 { + 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"; +import { createInterceptorBuilder } from "./interceptors/interceptorBuilder.js"; const SERVER_STOP_TIMEOUT_MS = 1000; /** @@ -143,7 +152,7 @@ function handleConnect(req, clientSocket, head) { } if (isKnownRegistry) { - mitmConnect(req, clientSocket, isAllowedUrl); + mitmConnect(req, clientSocket, createMitmInterceptor()); } else { // For other hosts, just tunnel the request to the destination tcp socket ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); @@ -151,6 +160,22 @@ function handleConnect(req, clientSocket, head) { } } +/** + * + * @returns {import("./interceptors/interceptorBuilder.js").Interceptor} + */ +function createMitmInterceptor() { + const builder = createInterceptorBuilder(); + + builder.onRequest(async (req) => { + if (!(await isAllowedUrl(req.targetUrl))) { + req.blockRequest(403, "Forbidden - blocked by safe-chain"); + } + }); + + return builder.build(); +} + /** * @param {string} url * @returns {Promise} From 28d24bb6eaabd66253d159a111a75da3ec0d3b3b Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 10:26:26 -0800 Subject: [PATCH 166/345] Another iteration --- packages/safe-chain/bin/aikido-pip.js | 16 ++--- packages/safe-chain/bin/aikido-pip3.js | 18 +++--- packages/safe-chain/bin/aikido-python.js | 25 ++++---- packages/safe-chain/bin/aikido-python3.js | 27 ++++---- .../packagemanager/currentPackageManager.js | 4 +- .../pip/createPackageManager.js | 15 +++-- .../src/packagemanager/pip/pipSettings.js | 31 ++++++++++ .../src/packagemanager/pip/runPipCommand.js | 2 +- .../src/registryProxy/registryProxy.js | 4 +- test/e2e/Dockerfile | 1 + test/e2e/pip-ci.e2e.spec.js | 62 ++++++++++++------- test/e2e/pip.e2e.spec.js | 36 ----------- 12 files changed, 134 insertions(+), 107 deletions(-) create mode 100644 packages/safe-chain/src/packagemanager/pip/pipSettings.js diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 92ba4e3..59951ed 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -1,19 +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"; - -// Defaults -let packageManagerName = "pip"; -// Pass through user args as-is -const argv = process.argv.slice(2); +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; // Set eco system -// This can be used in other parts of the code to determine which eco system we are working with setEcoSystem(ECOSYSTEM_PY); -initializePackageManager(packageManagerName); -var exitCode = await main(argv); +// Set current invocation +setCurrentPipInvocation(PIP_INVOCATIONS.PIP); +initializePackageManager(PIP_PACKAGE_MANAGER); + +// Pass through only user-supplied pip args +var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index e24fda4..e388383 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -3,17 +3,17 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; -// Explicit pip3 entrypoint -const packageManagerName = "pip3"; - -// Copy argv as-is -const argv = process.argv.slice(2); - -// Set ecosystem to Python +// Set eco system setEcoSystem(ECOSYSTEM_PY); -initializePackageManager(packageManagerName); -var exitCode = await main(argv); +// Set current invocation +setCurrentPipInvocation(PIP_INVOCATIONS.PIP3); +// Create package manager +initializePackageManager(PIP_PACKAGE_MANAGER); + +// Pass through only user-supplied pip args +var exitCode = await main(process.argv.slice(2)); process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index c22c601..fba6b70 100644 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -1,22 +1,25 @@ #!/usr/bin/env node - import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; import { main } from "../src/main.js"; -const argv = process.argv.slice(2); +// Set eco system +setEcoSystem(ECOSYSTEM_PY); -const supportedArgs = ["pip", "pip3"]; -if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) { +// Strip '-m pip' or '-m pip3' from args if present +let argv = process.argv.slice(2); +if (argv[0] === '-m' && argv[1] === 'pip') { setEcoSystem(ECOSYSTEM_PY); - - initializePackageManager(argv[1]); - var exitCode = await main(argv.slice(2)); - process.exit(exitCode); + setCurrentPipInvocation(PIP_INVOCATIONS.PY_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); + argv = argv.slice(2); + var exitCode = await main(argv); + process.exit(exitCode); } else { - // Fallback: run the real python - const { spawn } = await import("child_process"); - spawn("python", argv, { stdio: "inherit" }); + // Forward to real python binary for non-pip flows + const { spawn } = await import('child_process'); + spawn('python', argv, { stdio: 'inherit' }); } diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index 48659e5..c74a3f3 100644 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -1,22 +1,25 @@ #!/usr/bin/env node - import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; import { main } from "../src/main.js"; -const argv = process.argv.slice(2); +// Set eco system +setEcoSystem(ECOSYSTEM_PY); -const supportedArgs = ["pip", "pip3"]; - -if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) { +// Strip nodejs and wrapper script from args +let argv = process.argv.slice(2); +if (argv[0] === '-m' && argv[1] === 'pip') { setEcoSystem(ECOSYSTEM_PY); - // python3 -m pip or python3 -m pip3: always use pip3 package manager - initializePackageManager("pip3"); - var exitCode = await main(argv.slice(2)); - process.exit(exitCode); + setCurrentPipInvocation(PIP_INVOCATIONS.PY3_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); + // Strip '-m pip' or '-m pip3' from args if present + argv = argv.slice(2); + var exitCode = await main(argv); + process.exit(exitCode); } else { - // Fallback: run the real python3 - const { spawn } = await import("child_process"); - spawn("python3", argv, { stdio: "inherit" }); + // Forward to real python3 binary for non-pip flows + const { spawn } = await import('child_process'); + spawn('python3', argv, { stdio: 'inherit' }); } diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 42cb93e..2db4167 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -52,8 +52,8 @@ export function initializePackageManager(packageManagerName) { state.packageManagerName = createBunPackageManager(); } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); - } else if (packageManagerName === "pip" || packageManagerName === "pip3") { - state.packageManagerName = createPipPackageManager(packageManagerName); + } else if (packageManagerName === "pip") { + state.packageManagerName = createPipPackageManager(); } 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 cb5484d..6415dcc 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -1,12 +1,19 @@ import { runPip } from "./runPipCommand.js"; - +import { getCurrentPipInvocation } from "./pipSettings.js"; /** - * @param {string} [command] * @returns {import("../currentPackageManager.js").PackageManager} */ -export function createPipPackageManager(command = "pip") { +export function createPipPackageManager() { return { - runCommand: /** @param {string[]} args */ (args) => runPip(command, args), + /** + * @param {string[]} args + */ + runCommand: (args) => { + const invocation = getCurrentPipInvocation(); + const fullArgs = [...invocation.args, ...args]; + console.debug('[safe-chain debug] runCommand:', invocation.command, fullArgs); + return runPip(invocation.command, fullArgs); + }, // For pip, rely solely on MITM proxy to detect/deny downloads from known registries. isSupportedCommand: () => false, getDependencyUpdatesForCommand: () => [], diff --git a/packages/safe-chain/src/packagemanager/pip/pipSettings.js b/packages/safe-chain/src/packagemanager/pip/pipSettings.js new file mode 100644 index 0000000..2dd7929 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -0,0 +1,31 @@ +// Constant for pip package manager name +export const PIP_PACKAGE_MANAGER = "pip"; + +// Enum of possible Python/pip invocations for Safe Chain interception +export const PIP_INVOCATIONS = { + PIP: { command: "pip", args: [] }, + PIP3: { command: "pip3", args: [] }, + PY_PIP: { command: "python", args: ["-m", "pip"] }, + PY3_PIP: { command: "python3", args: ["-m", "pip"] } +}; + +/** + * @type {{ command: string, args: string[] }} + */ +let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip + +/** + * @param {{ command: string, args: string[] }} invocation + */ +export function setCurrentPipInvocation(invocation) { + console.debug('[safe-chain debug] setCurrentPipInvocation:', invocation); + currentInvocation = invocation; +} + +/** + * @returns {{ command: string, args: string[] }} + */ +export function getCurrentPipInvocation() { + console.debug('[safe-chain debug] getCurrentPipInvocation:', currentInvocation); + return currentInvocation; +} diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 6fae388..e3252f9 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -26,10 +26,10 @@ export async function runPip(command, args) { }); return { status: result.status }; } catch (/** @type any */ error) { + ui.writeError("Error executing command:", error.message); 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/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index c5e272b..57027fc 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -142,10 +142,12 @@ function handleConnect(req, clientSocket, head) { isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg)); } + // Debug: log CONNECT request URL and MITM/tunnel decision + ui.writeVerbose(`[Safe-chain debug] CONNECT request: url=${url}, ecosystem=${ecosystem}, isKnownRegistry=${isKnownRegistry}`); + if (isKnownRegistry) { mitmConnect(req, clientSocket, isAllowedUrl); } else { - // For other hosts, just tunnel the request to the destination tcp socket ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index e590d19..6c9743e 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -53,6 +53,7 @@ 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/python${PYTHON_VERSION} /usr/local/bin/python && \ ln -sf /usr/bin/pip3 /usr/local/bin/pip3 # Copy and install Safe chain diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index bcca90a..fe013bb 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -35,6 +35,22 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { assert.ok(result.output.includes("hello"), `Output was: ${result.output}`); assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command"); }); + + it("does not intercept python3 test.py", async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); + const result = await shell.runCommand("python3 test.py"); + assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 script execution"); + }); + + it("does not intercept python test.py", async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); + const result = await shell.runCommand("python test.py"); + assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`); + assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python script execution"); + }); }); for (let shell of ["bash", "zsh"]) { @@ -89,27 +105,6 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); }); - it(`setup-ci routes python -m pip3 through safe-chain for ${shell}`, async () => { - const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); - await installationShell.runCommand( - "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" - ); - await installationShell.runCommand( - "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" - ); - - const projectShell = await container.openShell(shell); - const result = await projectShell.runCommand( - "python -m pip3 install --break-system-packages certifi" - ); - - assert.ok( - result.output.includes("no malware found."), - `Output did not contain scan message. Output was:\n${result.output}` - ); - }); - it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand("safe-chain setup-ci"); @@ -131,7 +126,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { ); }); - it(`setup-ci routes python3 -m pip3 through safe-chain for ${shell}`, async () => { + it(`setup-ci routes pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); await installationShell.runCommand("safe-chain setup-ci"); await installationShell.runCommand( @@ -143,7 +138,28 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { const projectShell = await container.openShell(shell); const result = await projectShell.runCommand( - "python3 -m pip3 install --break-system-packages certifi" + "pip install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not contain scan message. Output was:\n${result.output}` + ); + }); + + it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => { + const installationShell = await container.openShell(shell); + await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" + ); + await installationShell.runCommand( + "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc" + ); + + const projectShell = await container.openShell(shell); + const result = await projectShell.runCommand( + "pip3 install --break-system-packages certifi" ); assert.ok( diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 5d046a7..0cb6c2b 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -161,24 +161,6 @@ describe("E2E: pip coverage", () => { ); }); - 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 malware 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( @@ -197,24 +179,6 @@ describe("E2E: pip coverage", () => { ); }); - 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 malware 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(`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 From a6956db8dc8f9a6ea5237bfcba0ff1273f584366 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 10:27:49 -0800 Subject: [PATCH 167/345] Remove debug log --- packages/safe-chain/src/registryProxy/registryProxy.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 57027fc..3344e8f 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -142,9 +142,6 @@ function handleConnect(req, clientSocket, head) { isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg)); } - // Debug: log CONNECT request URL and MITM/tunnel decision - ui.writeVerbose(`[Safe-chain debug] CONNECT request: url=${url}, ecosystem=${ecosystem}, isKnownRegistry=${isKnownRegistry}`); - if (isKnownRegistry) { mitmConnect(req, clientSocket, isAllowedUrl); } else { From 9bd29056c699554c643b119f3ff453595b40d05e Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 11:02:03 -0800 Subject: [PATCH 168/345] Some cleanup --- packages/safe-chain/bin/aikido-python.js | 8 +-- packages/safe-chain/bin/aikido-python3.js | 8 +-- .../src/shell-integration/setup-ci.spec.js | 63 +------------------ test/e2e/Dockerfile | 1 + 4 files changed, 10 insertions(+), 70 deletions(-) diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index fba6b70..e3d9046 100644 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -8,12 +8,12 @@ import { main } from "../src/main.js"; // Set eco system setEcoSystem(ECOSYSTEM_PY); - -// Strip '-m pip' or '-m pip3' from args if present +// Strip nodejs and wrapper script from args let argv = process.argv.slice(2); -if (argv[0] === '-m' && argv[1] === 'pip') { +// If no args are passed, argv[0] and argv[1] are undefined, so this condition just evaluates to false and does not throw. +if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(PIP_INVOCATIONS.PY_PIP); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP : PIP_INVOCATIONS.PY_PIP); initializePackageManager(PIP_PACKAGE_MANAGER); argv = argv.slice(2); var exitCode = await main(argv); diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index c74a3f3..8e16d6c 100644 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -10,12 +10,12 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); -if (argv[0] === '-m' && argv[1] === 'pip') { +// If no args are passed, argv[0] and argv[1] are undefined, so this condition just evaluates to false and does not throw. +if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(PIP_INVOCATIONS.PY3_PIP); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP : PIP_INVOCATIONS.PY_PIP); initializePackageManager(PIP_PACKAGE_MANAGER); - // Strip '-m pip' or '-m pip3' from args if present - argv = argv.slice(2); + argv = argv.slice(2); var exitCode = await main(argv); process.exit(exitCode); } else { diff --git a/packages/safe-chain/src/shell-integration/setup-ci.spec.js b/packages/safe-chain/src/shell-integration/setup-ci.spec.js index 5471f36..92ef82e 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.spec.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.spec.js @@ -146,66 +146,5 @@ describe("Setup CI shell integration", () => { const unixNpmShim = path.join(mockShimsDir, "npm"); assert.ok(!fs.existsSync(unixNpmShim), "Unix npm shim should not exist on Windows"); }); - - it("should create python and python3 shims from unix-python wrapper template", async () => { - // Add unix-python wrapper template to mock templates - const unixPythonTemplatePath = path.join( - mockTemplateDir, - "path-wrappers", - "templates", - "unix-python-wrapper.template.sh" - ); - fs.writeFileSync( - unixPythonTemplatePath, - "#!/bin/bash\n# Python wrapper\nexec aikido-pip \"$@\"\n", - "utf-8" - ); - - await setupCi(); - - // Check if python shim was created - const pythonShimPath = path.join(mockShimsDir, "python"); - assert.ok(fs.existsSync(pythonShimPath), "python shim should exist"); - // Check if python3 shim was created - const python3ShimPath = path.join(mockShimsDir, "python3"); - assert.ok(fs.existsSync(python3ShimPath), "python3 shim should exist"); - // Check content of python shim - const pythonShimContent = fs.readFileSync(pythonShimPath, "utf-8"); - assert.ok(pythonShimContent.includes("Python wrapper"), "python shim should use unix-python wrapper template"); - // Check content of python3 shim - const python3ShimContent = fs.readFileSync(python3ShimPath, "utf-8"); - assert.ok(python3ShimContent.includes("Python wrapper"), "python3 shim should use unix-python wrapper template"); - }); - - it("should create python.cmd and python3.cmd shims from windows-python wrapper template on win32 platform", async () => { - mockPlatform = "win32"; - // Add windows-python wrapper template to mock templates - const windowsPythonTemplatePath = path.join( - mockTemplateDir, - "path-wrappers", - "templates", - "windows-python-wrapper.template.cmd" - ); - fs.writeFileSync( - windowsPythonTemplatePath, - "@echo off\nREM Python wrapper\n{{AIKIDO_COMMAND}} %*\n", - "utf-8" - ); - - await setupCi(); - - // Check if python.cmd shim was created - const pythonCmdShimPath = path.join(mockShimsDir, "python.cmd"); - assert.ok(fs.existsSync(pythonCmdShimPath), "python.cmd shim should exist"); - // Check if python3.cmd shim was created - const python3CmdShimPath = path.join(mockShimsDir, "python3.cmd"); - assert.ok(fs.existsSync(python3CmdShimPath), "python3.cmd shim should exist"); - // Check content of python.cmd shim - const pythonCmdShimContent = fs.readFileSync(pythonCmdShimPath, "utf-8"); - assert.ok(pythonCmdShimContent.includes("Python wrapper"), "python.cmd should use windows-python wrapper template"); - // Check content of python3.cmd shim - const python3CmdShimContent = fs.readFileSync(python3CmdShimPath, "utf-8"); - assert.ok(python3CmdShimContent.includes("Python wrapper"), "python3.cmd should use windows-python wrapper template"); - }); }); -}); \ No newline at end of file +}); diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 6c9743e..778924a 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -50,6 +50,7 @@ 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 && \ From 032fc3847f6a79a88798579542192ab517c3d571 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 11:09:28 -0800 Subject: [PATCH 169/345] Fix args --- packages/safe-chain/bin/aikido-python.js | 2 +- packages/safe-chain/bin/aikido-python3.js | 2 +- packages/safe-chain/src/packagemanager/pip/pipSettings.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index e3d9046..8d19a9f 100644 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -13,7 +13,7 @@ let argv = process.argv.slice(2); // If no args are passed, argv[0] and argv[1] are undefined, so this condition just evaluates to false and does not throw. if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP : PIP_INVOCATIONS.PY_PIP); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP); initializePackageManager(PIP_PACKAGE_MANAGER); argv = argv.slice(2); var exitCode = await main(argv); diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index 8e16d6c..be96d6f 100644 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -13,7 +13,7 @@ let argv = process.argv.slice(2); // If no args are passed, argv[0] and argv[1] are undefined, so this condition just evaluates to false and does not throw. if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP : PIP_INVOCATIONS.PY_PIP); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP); initializePackageManager(PIP_PACKAGE_MANAGER); argv = argv.slice(2); var exitCode = await main(argv); diff --git a/packages/safe-chain/src/packagemanager/pip/pipSettings.js b/packages/safe-chain/src/packagemanager/pip/pipSettings.js index 2dd7929..2b2f6ad 100644 --- a/packages/safe-chain/src/packagemanager/pip/pipSettings.js +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -6,7 +6,9 @@ export const PIP_INVOCATIONS = { PIP: { command: "pip", args: [] }, PIP3: { command: "pip3", args: [] }, PY_PIP: { command: "python", args: ["-m", "pip"] }, - PY3_PIP: { command: "python3", args: ["-m", "pip"] } + PY3_PIP: { command: "python3", args: ["-m", "pip"] }, + PY_PIP3: { command: "python", args: ["-m", "pip3"] }, + PY3_PIP3: { command: "python3", args: ["-m", "pip3"] } }; /** @@ -18,7 +20,6 @@ let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip * @param {{ command: string, args: string[] }} invocation */ export function setCurrentPipInvocation(invocation) { - console.debug('[safe-chain debug] setCurrentPipInvocation:', invocation); currentInvocation = invocation; } @@ -26,6 +27,5 @@ export function setCurrentPipInvocation(invocation) { * @returns {{ command: string, args: string[] }} */ export function getCurrentPipInvocation() { - console.debug('[safe-chain debug] getCurrentPipInvocation:', currentInvocation); return currentInvocation; } From dd2894faabfcd76968f869a83ec042cde6a010e2 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 11:30:13 -0800 Subject: [PATCH 170/345] Extend test --- test/e2e/pip.e2e.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 0cb6c2b..305e312 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -287,4 +287,12 @@ describe("E2E: pip coverage", () => { `Should not have SSL/certificate errors for tunneled hosts. Output was:\n${result.output}` ); }); + + it(`pip3 install requests with --safe-chain-logging=verbose`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "pip3 install --break-system-packages requests --safe-chain-logging=verbose" + ); + assert.ok(result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}`); + }); }); From e88aede939a170c60bdcfee856ad18a18182ed24 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 12:25:55 -0800 Subject: [PATCH 171/345] Remove some debug logging --- packages/safe-chain/bin/aikido-python.js | 0 packages/safe-chain/bin/aikido-python3.js | 0 .../safe-chain/src/packagemanager/pip/createPackageManager.js | 1 - .../path-wrappers/templates/unix-wrapper.template.sh | 2 -- .../src/shell-integration/startup-scripts/init-posix.sh | 1 - 5 files changed, 4 deletions(-) mode change 100644 => 100755 packages/safe-chain/bin/aikido-python.js mode change 100644 => 100755 packages/safe-chain/bin/aikido-python3.js diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js old mode 100644 new mode 100755 diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js old mode 100644 new mode 100755 diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 6415dcc..6ec5d1a 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -11,7 +11,6 @@ export function createPipPackageManager() { runCommand: (args) => { const invocation = getCurrentPipInvocation(); const fullArgs = [...invocation.args, ...args]; - console.debug('[safe-chain debug] runCommand:', invocation.command, fullArgs); return runPip(invocation.command, fullArgs); }, // For pip, rely solely on MITM proxy to detect/deny downloads from known registries. diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh index 7663006..e914e5b 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -7,8 +7,6 @@ remove_shim_from_path() { echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g" } -echo "[safe-chain debug] command -v {{AIKIDO_COMMAND}} (raw PATH): $(command -v {{AIKIDO_COMMAND}} 2>/dev/null || echo notfound)" >&2 -echo "[safe-chain debug] PATH (raw): $PATH" >&2 if command -v {{AIKIDO_COMMAND}} >/dev/null 2>&1; then # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@" 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 278b31a..d78b9a4 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 @@ -21,7 +21,6 @@ function wrapSafeChainCommand() { else # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" - command "$original_cmd" "$@" fi } From a293c76ed998b69195c3051a2768598d51079af8 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 12:53:24 -0800 Subject: [PATCH 172/345] Add better logging --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index e3252f9..058f38f 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -26,7 +26,8 @@ export async function runPip(command, args) { }); return { status: result.status }; } catch (/** @type any */ error) { - ui.writeError("Error executing command:", error.message); + ui.writeError(`Error executing command: ${error.message}`); + ui.writeError(`Is '${command}' installed and available on your system?`); if (error.status) { return { status: error.status }; } else { From 61a53b24fd92de1b6c3bbd8c603ba6d782e11693 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 13:24:00 -0800 Subject: [PATCH 173/345] Some cleanup --- packages/safe-chain/bin/aikido-pip.js | 1 - packages/safe-chain/src/packagemanager/pip/pipSettings.js | 1 - packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 4 ++-- packages/safe-chain/src/registryProxy/registryProxy.js | 1 + packages/safe-chain/src/shell-integration/setup-ci.js | 2 -- .../src/shell-integration/startup-scripts/init-posix.sh | 1 + test/e2e/Dockerfile | 1 - 7 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 59951ed..39184f0 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -1,6 +1,5 @@ #!/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"; diff --git a/packages/safe-chain/src/packagemanager/pip/pipSettings.js b/packages/safe-chain/src/packagemanager/pip/pipSettings.js index 2b2f6ad..5e47644 100644 --- a/packages/safe-chain/src/packagemanager/pip/pipSettings.js +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -1,4 +1,3 @@ -// Constant for pip package manager name export const PIP_PACKAGE_MANAGER = "pip"; // Enum of possible Python/pip invocations for Safe Chain interception diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 058f38f..793302d 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -26,11 +26,11 @@ export async function runPip(command, args) { }); return { status: result.status }; } catch (/** @type any */ error) { - ui.writeError(`Error executing command: ${error.message}`); - ui.writeError(`Is '${command}' installed and available on your system?`); if (error.status) { return { status: error.status }; } else { + ui.writeError(`Error executing command: ${error.message}`); + ui.writeError(`Is '${command}' installed and available on your system?`); return { status: 1 }; } } diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 3344e8f..c5e272b 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -145,6 +145,7 @@ function handleConnect(req, clientSocket, head) { if (isKnownRegistry) { mitmConnect(req, clientSocket, isAllowedUrl); } else { + // For other hosts, just tunnel the request to the destination tcp socket ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); tunnelRequest(req, clientSocket, head); } diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 926386d..8793832 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -71,8 +71,6 @@ function createUnixShims(shimsDir) { ); } - - /** * @param {string} shimsDir * 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 d78b9a4..278b31a 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 @@ -21,6 +21,7 @@ function wrapSafeChainCommand() { else # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" + command "$original_cmd" "$@" fi } diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 778924a..6c9743e 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -50,7 +50,6 @@ 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 && \ From 01cc0b06c02209f30f5d03cd222ac4c1eb84fa42 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 13:40:09 -0800 Subject: [PATCH 174/345] Reverse e2e test removals --- test/e2e/Dockerfile | 13 ++++++++++++- test/e2e/pip.e2e.spec.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 6c9743e..cf5f39b 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -54,7 +54,18 @@ RUN curl -fsSL https://bun.sh/install | bash 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/python${PYTHON_VERSION} /usr/local/bin/python && \ - ln -sf /usr/bin/pip3 /usr/local/bin/pip3 + ln -sf /usr/bin/pip3 /usr/local/bin/pip3 && \ + cat <<'EOF' > /usr/lib/python3/dist-packages/pip3.py +""" +Shim module so 'python[3] -m pip3 …' resolves to pip's CLI entry point. +""" +try: + import pip._internal + pip._internal.main() +except Exception as exc: + print("pip3 module shim failed:", exc) + raise +EOF # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 305e312..3d3b4dd 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -161,6 +161,24 @@ describe("E2E: pip coverage", () => { ); }); + 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 malware 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( @@ -179,6 +197,24 @@ describe("E2E: pip coverage", () => { ); }); + 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 malware 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(`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 From d3a4f81b3c84eba5271bac1686b3332e482e71f3 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 6 Nov 2025 13:44:34 -0800 Subject: [PATCH 175/345] More cleanup --- packages/safe-chain/bin/aikido-python.js | 9 ++++++--- packages/safe-chain/bin/aikido-python3.js | 7 +++++-- .../safe-chain/src/packagemanager/pip/pipSettings.js | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index 8d19a9f..1ef4e34 100755 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -10,13 +10,16 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); -// If no args are passed, argv[0] and argv[1] are undefined, so this condition just evaluates to false and does not throw. + if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { setEcoSystem(ECOSYSTEM_PY); setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP); initializePackageManager(PIP_PACKAGE_MANAGER); - argv = argv.slice(2); - var exitCode = await main(argv); + + // Strip off the '-m pip' or '-m pip3' from the args + argv = argv.slice(2); + + var exitCode = await main(argv); process.exit(exitCode); } else { // Forward to real python binary for non-pip flows diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index be96d6f..f53e5d2 100755 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -10,13 +10,16 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); -// If no args are passed, argv[0] and argv[1] are undefined, so this condition just evaluates to false and does not throw. + if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { setEcoSystem(ECOSYSTEM_PY); setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP); initializePackageManager(PIP_PACKAGE_MANAGER); + + // Strip off the '-m pip' or '-m pip3' from the args argv = argv.slice(2); - var exitCode = await main(argv); + + var exitCode = await main(argv); process.exit(exitCode); } else { // Forward to real python3 binary for non-pip flows diff --git a/packages/safe-chain/src/packagemanager/pip/pipSettings.js b/packages/safe-chain/src/packagemanager/pip/pipSettings.js index 5e47644..0316b77 100644 --- a/packages/safe-chain/src/packagemanager/pip/pipSettings.js +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -1,6 +1,6 @@ export const PIP_PACKAGE_MANAGER = "pip"; -// Enum of possible Python/pip invocations for Safe Chain interception +// All supported python/pip invocations for Safe Chain interception export const PIP_INVOCATIONS = { PIP: { command: "pip", args: [] }, PIP3: { command: "pip3", args: [] }, From f4694ba11954a7b7a257924c24a4d8a688fc6c04 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 7 Nov 2025 10:10:27 +0100 Subject: [PATCH 176/345] Move npm and pip mitm interception to separate files --- .../createInterceptorForEcoSystem.js | 25 +++ .../interceptors/npmInterceptor.js | 82 ++++++++++ .../npmInterceptor.spec.js} | 148 +++++++----------- .../pipInterceptor.js} | 98 ++++-------- .../interceptors/pipInterceptor.spec.js | 135 ++++++++++++++++ .../src/registryProxy/mitmRequestHandler.js | 2 +- .../src/registryProxy/registryProxy.js | 68 +------- .../safe-chain/src/scanning/audit/index.js | 16 ++ 8 files changed, 350 insertions(+), 224 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js create mode 100644 packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js rename packages/safe-chain/src/registryProxy/{parsePackageFromUrl.spec.js => interceptors/npmInterceptor.spec.js} (50%) rename packages/safe-chain/src/registryProxy/{parsePackageFromUrl.js => interceptors/pipInterceptor.js} (50%) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js b/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js new file mode 100644 index 0000000..c97d867 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js @@ -0,0 +1,25 @@ +import { + ECOSYSTEM_JS, + ECOSYSTEM_PY, + getEcoSystem, +} from "../../config/settings.js"; +import { npmInterceptorForUrl } from "./npmInterceptor.js"; +import { pipInterceptorForUrl } from "./pipInterceptor.js"; + +/** + * @param {string} url + * @returns {import("./interceptorBuilder.js").Interceptor | undefined} + */ +export function createInterceptorForUrl(url) { + const ecosystem = getEcoSystem(); + + if (ecosystem === ECOSYSTEM_JS) { + return npmInterceptorForUrl(url); + } + + if (ecosystem === ECOSYSTEM_PY) { + return pipInterceptorForUrl(url); + } + + return undefined; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js new file mode 100644 index 0000000..557e9cb --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -0,0 +1,82 @@ +import { isMalwarePackage } from "../../scanning/audit/index.js"; +import { createInterceptorBuilder } from "./interceptorBuilder.js"; + +const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; + +/** + * @param {string} url + * @returns {import("./interceptorBuilder.js").Interceptor | undefined} + */ +export function npmInterceptorForUrl(url) { + const registry = knownJsRegistries.find((reg) => url.includes(reg)); + + if (registry) { + return buildNpmInterceptor(registry); + } + + return undefined; +} + +/** + * @param {string} registry + * @returns {import("./interceptorBuilder.js").Interceptor | undefined} + */ +function buildNpmInterceptor(registry) { + const builder = createInterceptorBuilder(); + + builder.onRequest(async (req) => { + const { packageName, version } = parseNpmPackageUrl( + req.targetUrl, + registry + ); + if (await isMalwarePackage(packageName, version)) { + req.blockRequest(403, "Forbidden - blocked by safe-chain"); + } + }); + + return builder.build(); +} + +/** + * @param {string} url + * @param {string} registry + * @returns {{packageName: string | undefined, version: string | undefined}} + */ +export function parseNpmPackageUrl(url, registry) { + let packageName, version; + if (!registry || !url.endsWith(".tgz")) { + return { packageName, version }; + } + + const registryIndex = url.indexOf(registry); + const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash + + const separatorIndex = afterRegistry.indexOf("/-/"); + if (separatorIndex === -1) { + return { packageName, version }; + } + + packageName = afterRegistry.substring(0, separatorIndex); + const filename = afterRegistry.substring( + separatorIndex + 3, + afterRegistry.length - 4 + ); // Remove /-/ and .tgz + + // Extract version from filename + // For scoped packages like @babel/core, the filename is core-7.21.4.tgz + // For regular packages like lodash, the filename is lodash-4.17.21.tgz + if (packageName.startsWith("@")) { + const scopedPackageName = packageName.substring( + packageName.lastIndexOf("/") + 1 + ); + if (filename.startsWith(scopedPackageName + "-")) { + version = filename.substring(scopedPackageName.length + 1); + } + } else { + if (filename.startsWith(packageName + "-")) { + version = filename.substring(packageName.length + 1); + } + } + + return { packageName, version }; +} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js similarity index 50% rename from packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js rename to packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js index d052e9d..dd09527 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js @@ -1,14 +1,22 @@ -import { describe, it, beforeEach } from "node:test"; +import { describe, it, mock } 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); +describe("npmInterceptor", async () => { + let lastPackage; + let malwareResponse = false; + + mock.module("../../scanning/audit/index.js", { + namedExports: { + isMalwarePackage: async (packageName, version) => { + lastPackage = { packageName, version }; + return malwareResponse; + }, + }, }); - const testCases = [ + const { npmInterceptorForUrl } = await import("./npmInterceptor.js"); + + const parserCases = [ // Regular packages { url: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -83,11 +91,6 @@ describe("parsePackageFromUrl", () => { url: "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz", expected: { packageName: "@babel/core", version: "7.21.4" }, }, - // Invalid URLs should return undefined values - { - url: "https://example.com/package.tgz", - expected: { packageName: undefined, version: undefined }, - }, // URL to get package info, not tarball { url: "https://registry.npmjs.org/lodash", @@ -110,92 +113,51 @@ describe("parsePackageFromUrl", () => { }, ]; - testCases.forEach(({ url, expected }, index) => { - it(`should parse URL ${index + 1}: ${url}`, () => { - const result = parsePackageFromUrl(url); - assert.deepEqual(result, expected); + parserCases.forEach(({ url, expected }, index) => { + it(`should parse URL ${index + 1}: ${url}`, async () => { + const interceptor = npmInterceptorForUrl(url); + assert.ok( + interceptor, + "Interceptor should be created for known npm registry" + ); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, expected); }); }); -}); -describe("parsePackageFromUrl - pip URLs", () => { - beforeEach(() => { - setEcoSystem(ECOSYSTEM_PY); + it("should not create interceptor for unknown registry", () => { + const url = "https://example.com/some-package/-/some-package-1.0.0.tgz"; + + const interceptor = npmInterceptorForUrl(url); + + assert.equal( + interceptor, + undefined, + "Interceptor should be undefined for unknown registry" + ); }); - 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 }, - }, - ]; + it("should block malicious package", async () => { + const url = + "https://registry.npmjs.org/malicious-package/-/malicious-package-1.0.0.tgz"; + malwareResponse = true; - pipTestCases.forEach(({ url, expected }, index) => { - it(`should parse pip URL ${index + 1}: ${url}`, () => { - const result = parsePackageFromUrl(url); - assert.deepEqual(result, expected); - }); + const interceptor = npmInterceptorForUrl(url); + + const result = await interceptor.handleRequest(url); + + assert.ok(result.blockResponse, "Should contain a blockResponse"); + assert.equal( + result.blockResponse.statusCode, + 403, + "Block response should have status code 403" + ); + assert.equal( + result.blockResponse.message, + "Forbidden - blocked by safe-chain", + "Block response should have correct status message" + ); }); }); diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js similarity index 50% rename from packages/safe-chain/src/registryProxy/parsePackageFromUrl.js rename to packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 1fda121..90099b1 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -1,79 +1,45 @@ -import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { isMalwarePackage } from "../../scanning/audit/index.js"; +import { createInterceptorBuilder } from "./interceptorBuilder.js"; -export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"]; -export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"]; +const knownPipRegistries = [ + "files.pythonhosted.org", + "pypi.org", + "pypi.python.org", + "pythonhosted.org", +]; /** * @param {string} url - * @returns {{packageName: string | undefined, version: string | undefined}} + * @returns {import("./interceptorBuilder.js").Interceptor | undefined} */ -export function parsePackageFromUrl(url) { - const ecosystem = getEcoSystem(); - let registry; +export function pipInterceptorForUrl(url) { + const registry = knownPipRegistries.find((reg) => url.includes(reg)); - // 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); - } - } - } else if (ecosystem === ECOSYSTEM_PY) { - for (const knownRegistry of knownPipRegistries) { - if (url.includes(knownRegistry)) { - registry = knownRegistry; - return parsePipPackageFromUrl(url, registry); - } - } + if (registry) { + return buildPipInterceptor(registry); } - // If no known registry matched, return { packageName: undefined, version: undefined } - return { packageName: undefined, version: undefined }; + return undefined; } /** - * @param {string} url * @param {string} registry - * @returns {{packageName: string | undefined, version: string | undefined}} + * @returns {import("./interceptorBuilder.js").Interceptor | undefined} */ -function parseJsPackageFromUrl(url, registry) { - let packageName, version; - if (!registry || !url.endsWith(".tgz")) { - return { packageName, version }; - } +function buildPipInterceptor(registry) { + const builder = createInterceptorBuilder(); - const registryIndex = url.indexOf(registry); - const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash - - const separatorIndex = afterRegistry.indexOf("/-/"); - if (separatorIndex === -1) { - return { packageName, version }; - } - - packageName = afterRegistry.substring(0, separatorIndex); - const filename = afterRegistry.substring( - separatorIndex + 3, - afterRegistry.length - 4 - ); // Remove /-/ and .tgz - - // Extract version from filename - // For scoped packages like @babel/core, the filename is core-7.21.4.tgz - // For regular packages like lodash, the filename is lodash-4.17.21.tgz - if (packageName.startsWith("@")) { - const scopedPackageName = packageName.substring( - packageName.lastIndexOf("/") + 1 + builder.onRequest(async (req) => { + const { packageName, version } = parsePipPackageFromUrl( + req.targetUrl, + registry ); - if (filename.startsWith(scopedPackageName + "-")) { - version = filename.substring(scopedPackageName.length + 1); + if (await isMalwarePackage(packageName, version)) { + req.blockRequest(403, "Forbidden - blocked by safe-chain"); } - } else { - if (filename.startsWith(packageName + "-")) { - version = filename.substring(packageName.length + 1); - } - } + }); - return { packageName, version }; + return builder.build(); } /** @@ -82,11 +48,11 @@ function parseJsPackageFromUrl(url, registry) { * @returns {{packageName: string | undefined, version: string | undefined}} */ function parsePipPackageFromUrl(url, registry) { - let packageName, version + let packageName, version; // Basic validation if (!registry || typeof url !== "string") { - return { packageName, version}; + return { packageName, version }; } // Quick sanity check on the URL + parse @@ -94,13 +60,13 @@ function parsePipPackageFromUrl(url, registry) { try { urlObj = new URL(url); } catch { - return { packageName, version}; + return { packageName, version }; } // Get the last path segment (filename) and decode it (strip query & fragment automatically) const lastSegment = urlObj.pathname.split("/").filter(Boolean).pop(); - if (!lastSegment){ - return { packageName, version}; + if (!lastSegment) { + return { packageName, version }; } const filename = decodeURIComponent(lastSegment); @@ -114,8 +80,8 @@ function parsePipPackageFromUrl(url, registry) { 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 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 diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js new file mode 100644 index 0000000..8b60b9b --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js @@ -0,0 +1,135 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert"; + +describe("pipInterceptor", async () => { + let lastPackage; + let malwareResponse = false; + + mock.module("../../scanning/audit/index.js", { + namedExports: { + isMalwarePackage: async (packageName, version) => { + lastPackage = { packageName, version }; + return malwareResponse; + }, + }, + }); + + const { pipInterceptorForUrl } = await import("./pipInterceptor.js"); + + const parserCases = [ + // 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 }, + }, + ]; + + parserCases.forEach(({ url, expected }, index) => { + it(`should parse URL ${index + 1}: ${url}`, async () => { + const interceptor = pipInterceptorForUrl(url); + assert.ok( + interceptor, + "Interceptor should be created for known npm registry" + ); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, expected); + }); + }); + + it("should not create interceptor for unknown registry", () => { + const url = "https://example.com/packages/xx/yy/foobar-1.2.3.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + + assert.equal( + interceptor, + undefined, + "Interceptor should be undefined for unknown registry" + ); + }); + + it("should block malicious package", async () => { + const url = + "https://files.pythonhosted.org/packages/xx/yy/malicious_package-1.0.0.tar.gz"; + malwareResponse = true; + + const interceptor = pipInterceptorForUrl(url); + + const result = await interceptor.handleRequest(url); + + assert.ok(result.blockResponse, "Should contain a blockResponse"); + assert.equal( + result.blockResponse.statusCode, + 403, + "Block response should have status code 403" + ); + assert.equal( + result.blockResponse.message, + "Forbidden - blocked by safe-chain", + "Block response should have correct status message" + ); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 58f220c..c3ad934 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -69,7 +69,7 @@ function createHttpsServer(hostname, interceptor) { const targetUrl = `https://${hostname}${pathAndQuery}`; const interceptorResult = await interceptor.handleRequest(targetUrl); - const blockResponse = interceptorResult?.blockResponse; + const blockResponse = interceptorResult.blockResponse; if (blockResponse) { ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index d66f397..d41a8bb 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -3,20 +3,9 @@ import { tunnelRequest } from "./tunnelRequestHandler.js"; import { mitmConnect } from "./mitmRequestHandler.js"; 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"; -import { createInterceptorBuilder } from "./interceptors/interceptorBuilder.js"; +import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; const SERVER_STOP_TIMEOUT_MS = 1000; /** @@ -141,18 +130,10 @@ function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL - const ecosystem = getEcoSystem(); - const url = req.url || ""; + const interceptor = createInterceptorForUrl(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, createMitmInterceptor()); + if (interceptor) { + mitmConnect(req, clientSocket, interceptor); } else { // For other hosts, just tunnel the request to the destination tcp socket ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`); @@ -160,47 +141,6 @@ function handleConnect(req, clientSocket, head) { } } -/** - * - * @returns {import("./interceptors/interceptorBuilder.js").Interceptor} - */ -function createMitmInterceptor() { - const builder = createInterceptorBuilder(); - - builder.onRequest(async (req) => { - if (!(await isAllowedUrl(req.targetUrl))) { - req.blockRequest(403, "Forbidden - blocked by safe-chain"); - } - }); - - return builder.build(); -} - -/** - * @param {string} url - * @returns {Promise} - */ -async function isAllowedUrl(url) { - const { packageName, version } = parsePackageFromUrl(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) { - return true; - } - - const auditResult = await auditChanges([ - { name: packageName, version, type: "add" }, - ]); - - if (!auditResult.isAllowed) { - state.blockedRequests.push({ packageName, version, url }); - return false; - } - - return true; -} - function verifyNoMaliciousPackages() { if (state.blockedRequests.length === 0) { // No malicious packages were blocked, so nothing to block diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 803051a..09fcfd8 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -41,6 +41,22 @@ export function getAuditStats() { return auditStats; } +/** + * + * @param {string | undefined} name + * @param {string | undefined} version + * @returns {Promise} + */ +export async function isMalwarePackage(name, version) { + if (!name || !version) { + return false; + } + + const auditResult = await auditChanges([{ name, version, type: "add" }]); + + return !auditResult.isAllowed; +} + /** * @param {PackageChange[]} changes * From 1f570a9f393909d58bdd6577abd8ab9918561e39 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 7 Nov 2025 11:39:41 +0100 Subject: [PATCH 177/345] Keep track of amount of malware packages blocked --- .../interceptors/interceptorBuilder.js | 17 +++++++++- .../interceptors/npmInterceptor.js | 4 +-- .../interceptors/pipInterceptor.js | 2 +- .../interceptors/requestInterceptorBuilder.js | 34 ++++++++++++++++--- .../src/registryProxy/registryProxy.js | 16 +++++++++ 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index 574abb9..73bde02 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -8,8 +8,11 @@ * * @typedef {Object} Interceptor * @property {(targetUrl: string) => Promise} handleRequest + * @property {(event: string, listener: (...args: any[]) => void) => Interceptor} on + * @property {(event: string, ...args: any[]) => boolean} emit */ +import { EventEmitter } from "events"; import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js"; /** @@ -36,9 +39,14 @@ export function createInterceptorBuilder() { * @returns {Interceptor} */ function buildInterceptor(requestHandlers) { + const eventEmitter = new EventEmitter(); + return { async handleRequest(targetUrl) { - const reqInterceptorBuilder = createRequestInterceptorBuilder(targetUrl); + const reqInterceptorBuilder = createRequestInterceptorBuilder( + targetUrl, + eventEmitter + ); for (const handler of requestHandlers) { await handler(reqInterceptorBuilder); @@ -46,5 +54,12 @@ function buildInterceptor(requestHandlers) { return reqInterceptorBuilder.build(); }, + on(event, listener) { + eventEmitter.on(event, listener); + return this; + }, + emit(event, ...args) { + return eventEmitter.emit(event, ...args); + }, }; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js index 557e9cb..6e33dd0 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -30,7 +30,7 @@ function buildNpmInterceptor(registry) { registry ); if (await isMalwarePackage(packageName, version)) { - req.blockRequest(403, "Forbidden - blocked by safe-chain"); + req.blockMalware(packageName, version, req.targetUrl); } }); @@ -42,7 +42,7 @@ function buildNpmInterceptor(registry) { * @param {string} registry * @returns {{packageName: string | undefined, version: string | undefined}} */ -export function parseNpmPackageUrl(url, registry) { +function parseNpmPackageUrl(url, registry) { let packageName, version; if (!registry || !url.endsWith(".tgz")) { return { packageName, version }; diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 90099b1..7d793d3 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -35,7 +35,7 @@ function buildPipInterceptor(registry) { registry ); if (await isMalwarePackage(packageName, version)) { - req.blockRequest(403, "Forbidden - blocked by safe-chain"); + req.blockMalware(packageName, version, req.targetUrl); } }); diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js index e0d560a..a8b98c6 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -2,6 +2,7 @@ * @typedef {Object} RequestInterceptorBuilder * @property {string} targetUrl * @property {(statusCode: number, message: string) => void} blockRequest + * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware * @property {() => RequestInterceptor} build * * @typedef {Object} RequestInterceptor @@ -10,17 +11,42 @@ /** * @param {string} targetUrl + * @param {import('events').EventEmitter} eventEmitter * @returns {RequestInterceptorBuilder} */ -export function createRequestInterceptorBuilder(targetUrl) { +export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; + /** + * @param {number} statusCode + * @param {string} message + */ + function blockRequest(statusCode, message) { + blockResponse = { statusCode, message }; + } + + /** + * @param {string | undefined} packageName + * @param {string | undefined} version + * @param {string} url + */ + function blockMalware(packageName, version, url) { + blockRequest(403, "Forbidden - blocked by safe-chain"); + + // Emit the malwareBlocked event + eventEmitter.emit("malwareBlocked", { + packageName, + version, + url, + timestamp: Date.now(), + }); + } + return { targetUrl, - blockRequest(statusCode, message) { - blockResponse = { statusCode, message }; - }, + blockRequest, + blockMalware, build() { return { blockResponse, diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index d41a8bb..f366a93 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -6,6 +6,7 @@ import { getCaCertPath } from "./certUtils.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; +import { on } from "events"; const SERVER_STOP_TIMEOUT_MS = 1000; /** @@ -133,6 +134,11 @@ function handleConnect(req, clientSocket, head) { const interceptor = createInterceptorForUrl(req.url || ""); if (interceptor) { + // Subscribe to malware blocked events + interceptor.on("malwareBlocked", (event) => { + onMalwareBlocked(event.packageName, event.version, event.url); + }); + mitmConnect(req, clientSocket, interceptor); } else { // For other hosts, just tunnel the request to the destination tcp socket @@ -141,6 +147,16 @@ function handleConnect(req, clientSocket, head) { } } +/** + * + * @param {string} packageName + * @param {string} version + * @param {string} url + */ +function onMalwareBlocked(packageName, version, url) { + state.blockedRequests.push({ packageName, version, url }); +} + function verifyNoMaliciousPackages() { if (state.blockedRequests.length === 0) { // No malicious packages were blocked, so nothing to block From 76a1100b8cb11d1aeee434b7530c983764338786 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 7 Nov 2025 11:42:53 +0100 Subject: [PATCH 178/345] Fix linter issues --- packages/safe-chain/src/registryProxy/registryProxy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index f366a93..beaa1ef 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -6,7 +6,6 @@ import { getCaCertPath } from "./certUtils.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; -import { on } from "events"; const SERVER_STOP_TIMEOUT_MS = 1000; /** From 3bf7279195e9bbe68df415ef650c60c864226ca9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 7 Nov 2025 16:16:37 +0100 Subject: [PATCH 179/345] Implement modification of request headerrs --- .../interceptors/npmInterceptor.js | 10 ++++ .../interceptors/requestInterceptorBuilder.js | 49 ++++++++++++++++++- .../src/registryProxy/mitmRequestHandler.js | 41 ++++++++++------ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js index 6e33dd0..97ac15d 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -1,3 +1,4 @@ +import chalk from "chalk"; import { isMalwarePackage } from "../../scanning/audit/index.js"; import { createInterceptorBuilder } from "./interceptorBuilder.js"; @@ -32,6 +33,15 @@ function buildNpmInterceptor(registry) { if (await isMalwarePackage(packageName, version)) { req.blockMalware(packageName, version, req.targetUrl); } + + req.modifyRequestHeaders((headers) => { + if (headers["accept"]?.includes("application/vnd.npm.install-v1+json")) { + // The npm registry sometimes serves a more compact format that lacks + // the time metadata we need to filter out too new packages. + // Force the registry to return the full metadata by changing the Accept header. + headers["accept"] = "application/json"; + } + }); }); return builder.build(); diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js index a8b98c6..e492f57 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -3,10 +3,13 @@ * @property {string} targetUrl * @property {(statusCode: number, message: string) => void} blockRequest * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware + * @property {(modificationFunc: (headers: NodeJS.Dict) => void) => void} modifyRequestHeaders * @property {() => RequestInterceptor} build * * @typedef {Object} RequestInterceptor * @property {{statusCode: number, message: string} | undefined} blockResponse + * @property {(headers: NodeJS.Dict | undefined) => void} modifyRequestHeaders + * @property {() => boolean} modifiesResponse */ /** @@ -18,6 +21,15 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; + /** + * @type {{ + * requestHeaders: Array<(headers: NodeJS.Dict) => void> + * }} + */ + let modificationFuncs = { + requestHeaders: [], + }; + /** * @param {number} statusCode * @param {string} message @@ -47,10 +59,43 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { targetUrl, blockRequest, blockMalware, + modifyRequestHeaders(modificationFunc) { + modificationFuncs.requestHeaders.push(modificationFunc); + }, build() { - return { + return createRequestInterceptor( blockResponse, - }; + modificationFuncs.requestHeaders + ); }, }; } + +/** + * @param {{statusCode: number, message: string} | undefined} blockResponse + * @param {Array<(headers: NodeJS.Dict) => void>} requestHeadersModficationFuncs + * @returns {RequestInterceptor} + */ +function createRequestInterceptor( + blockResponse, + requestHeadersModficationFuncs +) { + /** + * @param {NodeJS.Dict | undefined} headers + */ + function modifyRequestHeaders(headers) { + if (!headers) { + return; + } + + for (const modificationFunc of requestHeadersModficationFuncs) { + modificationFunc(headers); + } + } + + function modifiesResponse() { + return false; + } + + return { blockResponse, modifyRequestHeaders, modifiesResponse }; +} diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index c3ad934..a76efb4 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -5,6 +5,7 @@ import { ui } from "../environment/userInteraction.js"; /** * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor + * @typedef {import("./interceptors/requestInterceptorBuilder.js").RequestInterceptor} RequestInterceptor */ /** @@ -68,18 +69,20 @@ function createHttpsServer(hostname, interceptor) { const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; - const interceptorResult = await interceptor.handleRequest(targetUrl); - const blockResponse = interceptorResult.blockResponse; + const requestInterceptor = await interceptor.handleRequest(targetUrl); - if (blockResponse) { + if (requestInterceptor.blockResponse) { ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); - res.writeHead(blockResponse.statusCode, blockResponse.message); - res.end(blockResponse.message); + res.writeHead( + requestInterceptor.blockResponse.statusCode, + requestInterceptor.blockResponse.message + ); + res.end(requestInterceptor.blockResponse.message); return; } // Collect request body - forwardRequest(req, hostname, res); + forwardRequest(req, hostname, res, requestInterceptor); } const server = https.createServer( @@ -109,9 +112,10 @@ function getRequestPathAndQuery(url) { * @param {import("http").IncomingMessage} req * @param {string} hostname * @param {import("http").ServerResponse} res + * @param {RequestInterceptor} requestInterceptor */ -function forwardRequest(req, hostname, res) { - const proxyReq = createProxyRequest(hostname, req, res); +function forwardRequest(req, hostname, res, requestInterceptor) { + const proxyReq = createProxyRequest(hostname, req, res, requestInterceptor); proxyReq.on("error", (err) => { ui.writeVerbose( @@ -142,10 +146,17 @@ function forwardRequest(req, hostname, res) { * @param {string} hostname * @param {import("http").IncomingMessage} req * @param {import("http").ServerResponse} res + * @param {RequestInterceptor} requestInterceptor * * @returns {import("http").ClientRequest} */ -function createProxyRequest(hostname, req, res) { +function createProxyRequest(hostname, req, res, requestInterceptor) { + const headers = { ...req.headers }; + if (headers.host) { + delete headers.host; + } + requestInterceptor.modifyRequestHeaders(headers); + /** @type {import("http").RequestOptions} */ const options = { hostname: hostname, @@ -155,10 +166,6 @@ function createProxyRequest(hostname, req, res) { headers: { ...req.headers }, }; - if (options.headers && "host" in options.headers) { - delete options.headers.host; - } - const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; if (httpsProxy) { options.agent = new HttpsProxyAgent(httpsProxy); @@ -183,7 +190,13 @@ function createProxyRequest(hostname, req, res) { } res.writeHead(proxyRes.statusCode, proxyRes.headers); - proxyRes.pipe(res); + + if (!requestInterceptor.modifiesResponse) { + // If the response is not being modified, we can + // just pipe without the need for + proxyRes.pipe(res); + } else { + } }); return proxyReq; From 9b102412af95b1f992b550524686d16eb2640941 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 10:37:39 -0800 Subject: [PATCH 180/345] Add extra ENV vars --- .../src/packagemanager/pip/runPipCommand.js | 31 +++++++++++++++++++ .../packagemanager/pip/runPipCommand.spec.js | 20 ++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 793302d..4da7ab4 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,6 +2,10 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { knownPipRegistries } from "../../registryProxy/parsePackageFromUrl.js"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; /** * @param {string} command @@ -20,6 +24,33 @@ export async function runPip(command, args) { env.REQUESTS_CA_BUNDLE = combinedCaPath; env.SSL_CERT_FILE = combinedCaPath; + // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, + // 1. Set additional env vars for pip + // 2. Create a pip config file that specifies the cert and trusted hosts + + env.PIP_CERT = combinedCaPath; + + // Create a temporary pip config file + const tmpDir = os.tmpdir(); + const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); + + // Trusted hosts: use knownPipRegistries from parsePackageFromUrl + const trustedHosts = Array.from(new Set(knownPipRegistries)); + + // Proxy settings + const httpProxy = env.HTTP_PROXY || ''; + const httpsProxy = env.HTTPS_PROXY || ''; + + // Build pip config INI + let pipConfig = '[global]\n'; + pipConfig += `cert = ${combinedCaPath}\n`; + if (httpProxy) pipConfig += `proxy = ${httpProxy}\n`; + if (httpsProxy) pipConfig += `proxy = ${httpsProxy}\n`; + if (trustedHosts.length) pipConfig += `trusted-host = ${trustedHosts.join(' ')}\n`; + + await fs.writeFile(pipConfigPath, pipConfig); + env.PIP_CONFIG_FILE = pipConfigPath; + 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 d7a0f93..9d330da 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -43,6 +43,23 @@ describe("runPipCommand environment variable handling", () => { mock.reset(); }); + it("should set PIP_CERT env var and create config file", async () => { + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + // Check PIP_CERT env var + assert.strictEqual( + capturedArgs.options.env.PIP_CERT, + "/tmp/test-combined-ca.pem", + "PIP_CERT should be set to combined bundle path" + ); + // Check PIP_CONFIG_FILE env var exists and is a non-empty string + const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; + assert.ok(configPath, "PIP_CONFIG_FILE should be set"); + assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string"); + assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path"); + }); + 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); @@ -60,9 +77,6 @@ describe("runPipCommand environment variable handling", () => { "/tmp/test-combined-ca.pem", "SSL_CERT_FILE should be set to combined bundle path" ); - - // Args should be unchanged (no arg injection) - assert.deepStrictEqual(capturedArgs.args, ["install", "requests"]); }); it("should set CA environment variables even for external/test PyPI mirror (covers non-CLI traffic)", async () => { From 6a94271a101b523ccc9fa585272a5c65910d85bb Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 14:28:31 -0800 Subject: [PATCH 181/345] Do not add list of trusted hosts, is security risk --- .../safe-chain/src/packagemanager/pip/runPipCommand.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 4da7ab4..30dade4 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -25,18 +25,13 @@ export async function runPip(command, args) { env.SSL_CERT_FILE = combinedCaPath; // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, - // 1. Set additional env vars for pip - // 2. Create a pip config file that specifies the cert and trusted hosts - + // We will set additional env vars for pip env.PIP_CERT = combinedCaPath; // Create a temporary pip config file const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); - // Trusted hosts: use knownPipRegistries from parsePackageFromUrl - const trustedHosts = Array.from(new Set(knownPipRegistries)); - // Proxy settings const httpProxy = env.HTTP_PROXY || ''; const httpsProxy = env.HTTPS_PROXY || ''; @@ -46,7 +41,6 @@ export async function runPip(command, args) { pipConfig += `cert = ${combinedCaPath}\n`; if (httpProxy) pipConfig += `proxy = ${httpProxy}\n`; if (httpsProxy) pipConfig += `proxy = ${httpsProxy}\n`; - if (trustedHosts.length) pipConfig += `trusted-host = ${trustedHosts.join(' ')}\n`; await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; From f9d241e4747558de313806a31e76a870338ea7a9 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 14:32:12 -0800 Subject: [PATCH 182/345] Fix unused import --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 30dade4..2e8bcbf 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,7 +2,6 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; -import { knownPipRegistries } from "../../registryProxy/parsePackageFromUrl.js"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; From 6bcd3d3b8f08dc051a3edb382e4aca8ed4d48bb3 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 15:22:06 -0800 Subject: [PATCH 183/345] Make sure we don't override any environments --- .../src/packagemanager/pip/runPipCommand.js | 42 ++++++++++++------- .../packagemanager/pip/runPipCommand.spec.js | 23 +++++++++- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 2e8bcbf..5024655 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -20,29 +20,39 @@ export async function runPip(command, args) { // 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; + if (!env.REQUESTS_CA_BUNDLE) { + env.REQUESTS_CA_BUNDLE = combinedCaPath; + } + + if (!env.SSL_CERT_FILE) { + env.SSL_CERT_FILE = combinedCaPath; + } + // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, // We will set additional env vars for pip - env.PIP_CERT = combinedCaPath; + if (!env.PIP_CERT) { + env.PIP_CERT = combinedCaPath; + } - // Create a temporary pip config file - const tmpDir = os.tmpdir(); - const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); + // Only create and set PIP_CONFIG_FILE if not already set + if (!env.PIP_CONFIG_FILE) { + const tmpDir = os.tmpdir(); + const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); - // Proxy settings - const httpProxy = env.HTTP_PROXY || ''; - const httpsProxy = env.HTTPS_PROXY || ''; + // Proxy settings + const httpProxy = env.HTTP_PROXY || ''; + const httpsProxy = env.HTTPS_PROXY || ''; - // Build pip config INI - let pipConfig = '[global]\n'; - pipConfig += `cert = ${combinedCaPath}\n`; - if (httpProxy) pipConfig += `proxy = ${httpProxy}\n`; - if (httpsProxy) pipConfig += `proxy = ${httpsProxy}\n`; + // Build pip config INI + let pipConfig = '[global]\n'; + pipConfig += `cert = ${combinedCaPath}\n`; + if (httpProxy) pipConfig += `proxy = ${httpProxy}\n`; + if (httpsProxy) pipConfig += `proxy = ${httpsProxy}\n`; - await fs.writeFile(pipConfigPath, pipConfig); - env.PIP_CONFIG_FILE = pipConfigPath; + await fs.writeFile(pipConfigPath, pipConfig); + env.PIP_CONFIG_FILE = pipConfigPath; + } const result = await safeSpawn(command, args, { 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 9d330da..7128bde 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -4,6 +4,7 @@ import assert from "node:assert"; describe("runPipCommand environment variable handling", () => { let runPip; let capturedArgs = null; + let customEnv = null; beforeEach(async () => { capturedArgs = null; @@ -18,11 +19,12 @@ describe("runPipCommand environment variable handling", () => { }, }); - // Mock proxy env merge + // Mock proxy env merge, allow custom env override mock.module("../../registryProxy/registryProxy.js", { namedExports: { mergeSafeChainProxyEnvironmentVariables: (env) => ({ ...env, + ...(customEnv || {}), HTTPS_PROXY: "http://localhost:8080", }), }, @@ -43,6 +45,25 @@ describe("runPipCommand environment variable handling", () => { mock.reset(); }); + it("should not overwrite existing env vars for certs and config", async () => { + // Set custom env vars before merge + customEnv = { + REQUESTS_CA_BUNDLE: "/custom/ca-bundle.pem", + SSL_CERT_FILE: "/custom/ssl-cert.pem", + PIP_CERT: "/custom/pip-cert.pem", + PIP_CONFIG_FILE: "/custom/pip.conf" + }; + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + // Should preserve custom env vars + assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, "/custom/ca-bundle.pem"); + assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, "/custom/ssl-cert.pem"); + assert.strictEqual(capturedArgs.options.env.PIP_CERT, "/custom/pip-cert.pem"); + assert.strictEqual(capturedArgs.options.env.PIP_CONFIG_FILE, "/custom/pip.conf"); + customEnv = null; + }); + it("should set PIP_CERT env var and create config file", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); From a3d57cbd240e9d6f785cdc42096a90c6709116bb Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 15:24:59 -0800 Subject: [PATCH 184/345] Cleanup --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 5024655..f2eccc5 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -24,18 +24,16 @@ export async function runPip(command, args) { if (!env.REQUESTS_CA_BUNDLE) { env.REQUESTS_CA_BUNDLE = combinedCaPath; } - if (!env.SSL_CERT_FILE) { env.SSL_CERT_FILE = combinedCaPath; } - + // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, // We will set additional env vars for pip + if (!env.PIP_CERT) { env.PIP_CERT = combinedCaPath; } - - // Only create and set PIP_CONFIG_FILE if not already set if (!env.PIP_CONFIG_FILE) { const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); From f2bf5869ba0072243fa35ea4d87d8520253a56ec Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 15:49:25 -0800 Subject: [PATCH 185/345] Fix linting issue --- .../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 7128bde..627d909 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -24,7 +24,7 @@ describe("runPipCommand environment variable handling", () => { namedExports: { mergeSafeChainProxyEnvironmentVariables: (env) => ({ ...env, - ...(customEnv || {}), + ...customEnv, HTTPS_PROXY: "http://localhost:8080", }), }, From 8bd2ace3db27e8010d74505bfc5989410b9eeb97 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 12 Nov 2025 13:39:17 +0100 Subject: [PATCH 186/345] Remove too new packages from npm response --- packages/safe-chain/src/config/settings.js | 13 +- .../createInterceptorForEcoSystem.js | 2 +- .../interceptors/npm/npmInterceptor.js | 231 +++++++++++++++++ .../npm/npmInterceptor.minPackageAge.spec.js | 237 ++++++++++++++++++ .../npmInterceptor.packageDownload.spec.js} | 2 +- .../interceptors/npmInterceptor.js | 92 ------- .../interceptors/requestInterceptorBuilder.js | 48 ++-- .../responseInterceptorBuilder.js | 43 ++++ .../src/registryProxy/mitmRequestHandler.js | 33 ++- 9 files changed, 582 insertions(+), 119 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js create mode 100644 packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js rename packages/safe-chain/src/registryProxy/interceptors/{npmInterceptor.spec.js => npm/npmInterceptor.packageDownload.spec.js} (99%) delete mode 100644 packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js create mode 100644 packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 46cc30c..5b27118 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -1,5 +1,9 @@ import * as cliArguments from "./cliArguments.js"; +export const LOGGING_SILENT = "silent"; +export const LOGGING_NORMAL = "normal"; +export const LOGGING_VERBOSE = "verbose"; + export function getLoggingLevel() { const level = cliArguments.getLoggingLevel(); @@ -14,9 +18,6 @@ export function getLoggingLevel() { return LOGGING_NORMAL; } -export const MALWARE_ACTION_BLOCK = "block"; -export const MALWARE_ACTION_PROMPT = "prompt"; - export const ECOSYSTEM_JS = "js"; export const ECOSYSTEM_PY = "py"; @@ -36,6 +37,6 @@ export function setEcoSystem(setting) { ecosystemSettings.ecoSystem = setting; } -export const LOGGING_SILENT = "silent"; -export const LOGGING_NORMAL = "normal"; -export const LOGGING_VERBOSE = "verbose"; +export function getMinimumPackageAgeHours() { + return 24 * 6; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js b/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js index c97d867..79b5200 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +++ b/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js @@ -3,7 +3,7 @@ import { ECOSYSTEM_PY, getEcoSystem, } from "../../config/settings.js"; -import { npmInterceptorForUrl } from "./npmInterceptor.js"; +import { npmInterceptorForUrl } from "./npm/npmInterceptor.js"; import { pipInterceptorForUrl } from "./pipInterceptor.js"; /** diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js new file mode 100644 index 0000000..e1fd16b --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js @@ -0,0 +1,231 @@ +import chalk from "chalk"; +import { getMinimumPackageAgeHours } from "../../../config/settings.js"; +import { isMalwarePackage } from "../../../scanning/audit/index.js"; +import { createInterceptorBuilder } from "../interceptorBuilder.js"; +import { ui } from "../../../environment/userInteraction.js"; +import { writeFileSync } from "node:fs"; + +const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; + +/** + * @param {string} url + * @returns {import("../interceptorBuilder.js").Interceptor | undefined} + */ +export function npmInterceptorForUrl(url) { + const registry = knownJsRegistries.find((reg) => url.includes(reg)); + + if (registry) { + return buildNpmInterceptor(registry); + } + + return undefined; +} + +/** + * @param {string} registry + * @returns {import("../interceptorBuilder.js").Interceptor | undefined} + */ +function buildNpmInterceptor(registry) { + const builder = createInterceptorBuilder(); + + builder.onRequest(async (req) => { + const { packageName, version } = parseNpmPackageUrl( + req.targetUrl, + registry + ); + if (await isMalwarePackage(packageName, version)) { + req.blockMalware(packageName, version, req.targetUrl); + } + + if (isPackageInfoUrl(req.targetUrl)) { + req.modifyRequestHeaders((headers) => { + if ( + headers["accept"]?.includes("application/vnd.npm.install-v1+json") + ) { + // The npm registry sometimes serves a more compact format that lacks + // the time metadata we need to filter out too new packages. + // Force the registry to return the full metadata by changing the Accept header. + headers["accept"] = "application/json"; + } + }); + + req.modifyResponse((res) => { + res.modifyBody(modifyNpmInfoRequestBody); + }); + } + }); + + return builder.build(); +} + +/** + * @param {string} url + * @param {string} registry + * @returns {{packageName: string | undefined, version: string | undefined}} + */ +function parseNpmPackageUrl(url, registry) { + let packageName, version; + if (!registry || !url.endsWith(".tgz")) { + return { packageName, version }; + } + + const registryIndex = url.indexOf(registry); + const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash + + const separatorIndex = afterRegistry.indexOf("/-/"); + if (separatorIndex === -1) { + return { packageName, version }; + } + + packageName = afterRegistry.substring(0, separatorIndex); + const filename = afterRegistry.substring( + separatorIndex + 3, + afterRegistry.length - 4 + ); // Remove /-/ and .tgz + + // Extract version from filename + // For scoped packages like @babel/core, the filename is core-7.21.4.tgz + // For regular packages like lodash, the filename is lodash-4.17.21.tgz + if (packageName.startsWith("@")) { + const scopedPackageName = packageName.substring( + packageName.lastIndexOf("/") + 1 + ); + if (filename.startsWith(scopedPackageName + "-")) { + version = filename.substring(scopedPackageName.length + 1); + } + } else { + if (filename.startsWith(packageName + "-")) { + version = filename.substring(packageName.length + 1); + } + } + + return { packageName, version }; +} + +/** + * @param {string} url + * @returns {boolean} + */ +function isPackageInfoUrl(url) { + // Remove query string and fragment to get the actual path + const urlWithoutParams = url.split("?")[0].split("#")[0]; + + // Tarball downloads end with .tgz + if (urlWithoutParams.endsWith(".tgz")) return false; + + // Special endpoints start with /-/ and should not be modified + // Examples: /-/npm/v1/security/advisories/bulk, /-/v1/search, /-/package/foo/access + if (urlWithoutParams.includes("/-/")) return false; + + // Everything else is package metadata that can be modified + return true; +} + +/** + * + * @param {Buffer} body + * @returns Buffer + */ +function modifyNpmInfoRequestBody(body) { + try { + const bodyContent = body.toString("utf8"); + const bodyJson = JSON.parse(bodyContent); + + if (!bodyJson.time || !bodyJson["dist-tags"] || !bodyJson.versions) { + // Just return the body if the + return body; + } + + const cutOff = new Date( + new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 + ).toISOString(); + + const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; + + const versions = Object.entries(bodyJson.time) + .map(([version, timestamp]) => ({ + version, + timestamp, + })) + .filter((x) => x.version != "created" && x.version != "modified"); + + for (const { version, timestamp } of versions) { + if (version === "created" || version === "modified") { + continue; + } + + if (timestamp > cutOff) { + deleteVersionFromJson(bodyJson, version); + continue; + } + } + + if (hasLatestTag && !bodyJson["dist-tags"]["latest"]) { + // The latest tag was removed because it contained a package younger than the treshold. + // A new latest tag needs to be calculated + bodyJson["dist-tags"]["latest"] = calculateLatestTag(bodyJson); + } + + return Buffer.from(JSON.stringify(bodyJson)); + } catch (err) { + // TODO: better error handling + return body; + } +} + +function deleteVersionFromJson(json, version) { + ui.writeVerbose( + `Safe-chain: Deleting ${version} from npm info request, it's newer than the minimumPackageAgeInHours` + ); + + delete json.time[version]; + delete json.versions[version]; + + for (const [tag, distVersion] of Object.entries(json["dist-tags"])) { + if (version == distVersion) { + delete json["dist-tags"][tag]; + } + } +} + +function calculateLatestTag(json) { + if (!json.time) { + return undefined; + } + + let latest, preview, latestDate, previewDate; + + for (const [version, timestamp] of Object.entries(json.time)) { + if (version == "created" || version == "modified") continue; + + if (version.includes("-")) { + // preview versions include "-" in the name + [preview, previewDate] = getLatest( + preview, + previewDate, + version, + timestamp + ); + } else { + [latest, latestDate] = getLatest(latest, latestDate, version, timestamp); + } + } + + if (latest) { + return latest; + } else { + return preview; + } + + function getLatest(currentLatest, currentLatestDate, version, timestamp) { + if (!currentLatest) { + return [version, timestamp]; + } + + if (timestamp > currentLatestDate) { + return [version, timestamp]; + } + + return [currentLatest, currentLatestDate]; + } +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js new file mode 100644 index 0000000..ea23f9e --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -0,0 +1,237 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert"; + +describe("npmInterceptor minimum package age", async () => { + let minimumPackageAgeSettings = 48; + + mock.module("../../../config/settings.js", { + namedExports: { + getMinimumPackageAgeHours: () => minimumPackageAgeSettings, + }, + }); + + mock.module("../../../scanning/audit/index.js", { + namedExports: { + isMalwarePackage: async () => { + return false; + }, + }, + }); + const { npmInterceptorForUrl } = await import("./npmInterceptor.js"); + + for (const packageInfoUrl of [ + // Basic package metadata + "https://registry.npmjs.org/lodash", + "https://registry.npmjs.org/express", + // Scoped packages + "https://registry.npmjs.org/@vercel/functions", + "https://registry.npmjs.org/@babel/core", + "https://registry.npmjs.org/@types/node", + // With query parameters + "https://registry.npmjs.org/lodash?write=true", + "https://registry.npmjs.org/@babel/core?param=value&other=test", + // With fragments + "https://registry.npmjs.org/lodash#readme", + "https://registry.npmjs.org/@babel/core#installation", + // Version-specific metadata + "https://registry.npmjs.org/lodash/4.17.21", + "https://registry.npmjs.org/lodash/latest", + "https://registry.npmjs.org/@babel/core/7.21.4", + // URL-encoded scoped packages + "https://registry.npmjs.org/@types%2Fnode", + "https://registry.npmjs.org/@babel%2Fcore", + // With trailing slashes + "https://registry.npmjs.org/lodash/", + "https://registry.npmjs.org/@babel/core/", + ]) { + it(`modifyResponse should be true for package info requests: ${packageInfoUrl}`, async () => { + const interceptor = npmInterceptorForUrl(packageInfoUrl); + const requestInterceptor = await interceptor.handleRequest( + packageInfoUrl + ); + + assert.equal(requestInterceptor.modifiesResponse(), true); + }); + } + + for (const packageUrl of [ + // Regular package tarballs + "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + // Scoped package tarballs + "https://registry.npmjs.org/@babel/core/-/core-8.0.0-alpha.1.tgz", + "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + // Tarballs with query parameters (integrity checks) + "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz?integrity=sha512-abc123", + "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz?integrity=sha512-def456&cache=false", + // Tarballs with fragments + "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#sha512-abc123", + "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz#hash", + // Prerelease versions + "https://registry.npmjs.org/react/-/react-18.3.0-canary-abc123.tgz", + "https://registry.npmjs.org/lodash/-/lodash-5.0.0-beta.1.tgz", + ]) { + it(`modifyResponse should be false for package downloads: ${packageUrl}`, async () => { + const interceptor = npmInterceptorForUrl(packageUrl); + const requestInterceptor = await interceptor.handleRequest(packageUrl); + + assert.equal(requestInterceptor.modifiesResponse(), false); + }); + } + + for (const specialEndpoint of [ + // Security advisory endpoints + "https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", + "https://registry.npmjs.org/-/npm/v1/security/audits", + "https://registry.npmjs.org/-/npm/v1/security/audits/quick", + // Search endpoints + "https://registry.npmjs.org/-/v1/search?text=lodash&size=20", + "https://registry.npmjs.org/-/v1/search?text=react&from=0", + // Package access/collaboration endpoints + "https://registry.npmjs.org/-/package/lodash/access", + "https://registry.npmjs.org/-/package/@babel/core/collaborators", + "https://registry.npmjs.org/-/package/lodash/dist-tags", + "https://registry.npmjs.org/-/package/@babel/core/dist-tags/latest", + // User/organization endpoints + "https://registry.npmjs.org/-/user/org.couchdb.user:username", + "https://registry.npmjs.org/-/org/myorg/package", + // Anonymous metrics + "https://registry.npmjs.org/-/npm/anon-metrics/v1/", + // Ping/health check + "https://registry.npmjs.org/-/ping", + ]) { + it(`modifyResponse should be false for special endpoints: ${specialEndpoint}`, async () => { + const interceptor = npmInterceptorForUrl(specialEndpoint); + const requestInterceptor = await interceptor.handleRequest( + specialEndpoint + ); + + assert.equal(requestInterceptor.modifiesResponse(), false); + }); + } + + it("Should remove packages older than the treshold", async () => { + minimumPackageAgeSettings = 5; + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "3.0.0", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + }, + time: { + created: getDate(-365 * 24), + ["1.0.0"]: getDate(-7), + // cutoff-date here + ["2.0.0"]: getDate(-4), + ["3.0.0"]: getDate(-3), + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + + assert.equal(Object.keys(modifiedJson.time).length, 2); + assert.equal(Object.keys(modifiedJson.versions).length, 1); + assert.ok(Object.keys(modifiedJson.time).includes("1.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(!Object.keys(modifiedJson.time).includes("2.0.0")); + assert.ok(!Object.keys(modifiedJson.versions).includes("2.0.0")); + assert.ok(!Object.keys(modifiedJson.time).includes("3.0.0")); + assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); + }); + + it("Should set the package to the new latest non-preview release", async () => { + minimumPackageAgeSettings = 5; + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "3.0.0", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + }, + time: { + created: getDate(-365 * 24), + ["1.0.0"]: getDate(-7), + ["0.0.1"]: getDate(-8), // package order: this package is older than 1.0.0, it should not be considered latest + ["2.0.0-alpha"]: getDate(-6), //package is a pre-release, it should not be latest + // cutoff-date here + ["2.0.0"]: getDate(-4), + ["3.0.0"]: getDate(-3), + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + + assert.equal(modifiedJson["dist-tags"]["latest"], "1.0.0"); + }); + + it("Should remove dist-tags if version was removed", async () => { + minimumPackageAgeSettings = 5; + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "3.0.0", + alpha: "2.0.0-alpha", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + }, + time: { + created: getDate(-365 * 24), + ["1.0.0"]: getDate(-7), + // cutoff-date here + ["2.0.0-alpha"]: getDate(-4), + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + console.log(modifiedJson); + + assert.equal(modifiedJson["dist-tags"]["alpha"], undefined); + }); + + /** + * @param {import("../interceptorBuilder.js").Interceptor} interceptor + * @param {string} body + * @returns {Promise} + */ + async function runModifyNpmInfoRequest(url, body) { + const interceptor = npmInterceptorForUrl(url); + const requestInterceptor = await interceptor.handleRequest(url); + const responseInterceptor = requestInterceptor.handleResponse(); + + const modifiedBuffer = responseInterceptor.modifyBody(Buffer.from(body)); + + return modifiedBuffer.toString("utf8"); + } + + function getDate(plusHours) { + const date = new Date(); + date.setHours(date.getHours() + plusHours); + + return date; + } +}); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js similarity index 99% rename from packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js rename to packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js index dd09527..a90432e 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.packageDownload.spec.js @@ -5,7 +5,7 @@ describe("npmInterceptor", async () => { let lastPackage; let malwareResponse = false; - mock.module("../../scanning/audit/index.js", { + mock.module("../../../scanning/audit/index.js", { namedExports: { isMalwarePackage: async (packageName, version) => { lastPackage = { packageName, version }; diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js deleted file mode 100644 index 97ac15d..0000000 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js +++ /dev/null @@ -1,92 +0,0 @@ -import chalk from "chalk"; -import { isMalwarePackage } from "../../scanning/audit/index.js"; -import { createInterceptorBuilder } from "./interceptorBuilder.js"; - -const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; - -/** - * @param {string} url - * @returns {import("./interceptorBuilder.js").Interceptor | undefined} - */ -export function npmInterceptorForUrl(url) { - const registry = knownJsRegistries.find((reg) => url.includes(reg)); - - if (registry) { - return buildNpmInterceptor(registry); - } - - return undefined; -} - -/** - * @param {string} registry - * @returns {import("./interceptorBuilder.js").Interceptor | undefined} - */ -function buildNpmInterceptor(registry) { - const builder = createInterceptorBuilder(); - - builder.onRequest(async (req) => { - const { packageName, version } = parseNpmPackageUrl( - req.targetUrl, - registry - ); - if (await isMalwarePackage(packageName, version)) { - req.blockMalware(packageName, version, req.targetUrl); - } - - req.modifyRequestHeaders((headers) => { - if (headers["accept"]?.includes("application/vnd.npm.install-v1+json")) { - // The npm registry sometimes serves a more compact format that lacks - // the time metadata we need to filter out too new packages. - // Force the registry to return the full metadata by changing the Accept header. - headers["accept"] = "application/json"; - } - }); - }); - - return builder.build(); -} - -/** - * @param {string} url - * @param {string} registry - * @returns {{packageName: string | undefined, version: string | undefined}} - */ -function parseNpmPackageUrl(url, registry) { - let packageName, version; - if (!registry || !url.endsWith(".tgz")) { - return { packageName, version }; - } - - const registryIndex = url.indexOf(registry); - const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash - - const separatorIndex = afterRegistry.indexOf("/-/"); - if (separatorIndex === -1) { - return { packageName, version }; - } - - packageName = afterRegistry.substring(0, separatorIndex); - const filename = afterRegistry.substring( - separatorIndex + 3, - afterRegistry.length - 4 - ); // Remove /-/ and .tgz - - // Extract version from filename - // For scoped packages like @babel/core, the filename is core-7.21.4.tgz - // For regular packages like lodash, the filename is lodash-4.17.21.tgz - if (packageName.startsWith("@")) { - const scopedPackageName = packageName.substring( - packageName.lastIndexOf("/") + 1 - ); - if (filename.startsWith(scopedPackageName + "-")) { - version = filename.substring(scopedPackageName.length + 1); - } - } else { - if (filename.startsWith(packageName + "-")) { - version = filename.substring(packageName.length + 1); - } - } - - return { packageName, version }; -} diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js index e492f57..2944968 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -4,14 +4,18 @@ * @property {(statusCode: number, message: string) => void} blockRequest * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware * @property {(modificationFunc: (headers: NodeJS.Dict) => void) => void} modifyRequestHeaders + * @property {(requestFunc: (responseInterceptorBuilder: import('./responseInterceptorBuilder.js').ResponseInterceptorBuilder) => void) => void} modifyResponse * @property {() => RequestInterceptor} build * * @typedef {Object} RequestInterceptor * @property {{statusCode: number, message: string} | undefined} blockResponse * @property {(headers: NodeJS.Dict | undefined) => void} modifyRequestHeaders + * @property {() => import("./responseInterceptorBuilder.js").ResponseInterceptor} handleResponse * @property {() => boolean} modifiesResponse */ +import { createResponseInterceptorBuilder } from "./responseInterceptorBuilder.js"; + /** * @param {string} targetUrl * @param {import('events').EventEmitter} eventEmitter @@ -20,15 +24,10 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; - - /** - * @type {{ - * requestHeaders: Array<(headers: NodeJS.Dict) => void> - * }} - */ - let modificationFuncs = { - requestHeaders: [], - }; + /** @type {Array<(headers: NodeJS.Dict) => void>} */ + let requestHeaderFuncs = []; + /** @type {Array<(requestFunc: import('./responseInterceptorBuilder.js').ResponseInterceptorBuilder) => void>} */ + let responseModifierFuncs = []; /** * @param {number} statusCode @@ -60,12 +59,16 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { blockRequest, blockMalware, modifyRequestHeaders(modificationFunc) { - modificationFuncs.requestHeaders.push(modificationFunc); + requestHeaderFuncs.push(modificationFunc); + }, + modifyResponse(modificationFunc) { + responseModifierFuncs.push(modificationFunc); }, build() { return createRequestInterceptor( blockResponse, - modificationFuncs.requestHeaders + requestHeaderFuncs, + responseModifierFuncs ); }, }; @@ -74,11 +77,13 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { /** * @param {{statusCode: number, message: string} | undefined} blockResponse * @param {Array<(headers: NodeJS.Dict) => void>} requestHeadersModficationFuncs + * @param {Array<(requestFunc: import('./responseInterceptorBuilder.js').ResponseInterceptorBuilder) => void>} responseModifierFuncs * @returns {RequestInterceptor} */ function createRequestInterceptor( blockResponse, - requestHeadersModficationFuncs + requestHeadersModficationFuncs, + responseModifierFuncs ) { /** * @param {NodeJS.Dict | undefined} headers @@ -94,8 +99,23 @@ function createRequestInterceptor( } function modifiesResponse() { - return false; + return responseModifierFuncs.length > 0; } - return { blockResponse, modifyRequestHeaders, modifiesResponse }; + function handleResponse() { + const responseInterceptorBuilder = createResponseInterceptorBuilder(); + + for (const func of responseModifierFuncs) { + func(responseInterceptorBuilder); + } + + return responseInterceptorBuilder.build(); + } + + return { + blockResponse, + modifyRequestHeaders, + modifiesResponse, + handleResponse, + }; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js new file mode 100644 index 0000000..86d79e5 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js @@ -0,0 +1,43 @@ +/** + * @typedef {Object} ResponseInterceptorBuilder + * @property {() => ResponseInterceptor} build + * @property {(modificationFunc: (body: Buffer) => Buffer) => void} modifyBody + * + * @typedef {Object} ResponseInterceptor + * @property {(buffer: Buffer) => Buffer} modifyBody + */ + +/** + * @returns {ResponseInterceptorBuilder} + */ +export function createResponseInterceptorBuilder() { + /** @type {Array<(body: Buffer) => Buffer>} */ + let modifyBodyFuncs = []; + + return { + modifyBody: (func) => modifyBodyFuncs.push(func), + build: () => createResponseInterceptor(modifyBodyFuncs), + }; +} + +/** + * @returns {ResponseInterceptor} + * @param {Array<(body: Buffer) => Buffer>} modifyBodyFuncs + */ +function createResponseInterceptor(modifyBodyFuncs) { + /** + * @param {Buffer} body + * @returns {Buffer} + */ + function modifyBody(body) { + let modifiedBody = body; + + for (var func of modifyBodyFuncs) { + modifiedBody = func(body); + } + + return modifiedBody; + } + + return { modifyBody }; +} diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index a76efb4..aa1391e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -2,6 +2,7 @@ import https from "https"; import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; +import { gunzipSync, gzipSync } from "zlib"; /** * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor @@ -188,14 +189,36 @@ function createProxyRequest(hostname, req, res, requestInterceptor) { res.end("Internal Server Error"); return; } - res.writeHead(proxyRes.statusCode, proxyRes.headers); - if (!requestInterceptor.modifiesResponse) { - // If the response is not being modified, we can - // just pipe without the need for - proxyRes.pipe(res); + if (requestInterceptor.modifiesResponse()) { + const responseInterceptor = requestInterceptor.handleResponse(); + + /** @type {Array} */ + let chunks = []; + + proxyRes.on("data", (chunk) => chunks.push(chunk)); + + proxyRes.on("end", () => { + /** @type {Buffer} */ + let buffer = Buffer.concat(chunks); + + if (proxyRes.headers["content-encoding"] === "gzip") { + buffer = gunzipSync(buffer); + } + + buffer = responseInterceptor.modifyBody(buffer); + + if (proxyRes.headers["content-encoding"] === "gzip") { + buffer = gzipSync(buffer); + } + + res.end(buffer); + }); } else { + // If the response is not being modified, we can + // just pipe without the need for buffering the output + proxyRes.pipe(res); } }); From 2cf23d5109e9cd1a0f0e1a4e0835b27cc7dc65f7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 12 Nov 2025 13:43:47 +0100 Subject: [PATCH 187/345] Don't expose blockRequest --- .../src/registryProxy/interceptors/requestInterceptorBuilder.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js index a8b98c6..ad1f145 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -1,7 +1,6 @@ /** * @typedef {Object} RequestInterceptorBuilder * @property {string} targetUrl - * @property {(statusCode: number, message: string) => void} blockRequest * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware * @property {() => RequestInterceptor} build * @@ -45,7 +44,6 @@ export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { return { targetUrl, - blockRequest, blockMalware, build() { return { From ad6d9bcdd5f3d2b599969c9c2dd261f84c9b3fbe Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 12 Nov 2025 14:03:33 +0100 Subject: [PATCH 188/345] Simplify interceptor code and rename variables for clarity. --- .../interceptors/interceptorBuilder.js | 87 +++++++++++++------ .../interceptors/npmInterceptor.js | 12 +-- .../interceptors/pipInterceptor.js | 12 +-- .../interceptors/requestInterceptorBuilder.js | 54 ------------ 4 files changed, 69 insertions(+), 96 deletions(-) delete mode 100644 packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index 73bde02..beed1f9 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -1,41 +1,32 @@ /** - * @typedef {import('./requestInterceptorBuilder.js').RequestInterceptorBuilder} RequestInterceptorBuilder - * @typedef {import('./requestInterceptorBuilder.js').RequestInterceptor} RequestInterceptor - * - * @typedef {Object} InterceptorBuilder - * @property {(requestFunc: (requestHandlerBuilder: RequestInterceptorBuilder) => Promise) => void} onRequest - * @property {() => Interceptor} build - * * @typedef {Object} Interceptor - * @property {(targetUrl: string) => Promise} handleRequest + * @property {(targetUrl: string) => Promise} handleRequest * @property {(event: string, listener: (...args: any[]) => void) => Interceptor} on * @property {(event: string, ...args: any[]) => boolean} emit + * + * + * @typedef {Object} RequestInterceptionContext + * @property {string} targetUrl + * @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware + * @property {() => RequestInterceptionHandler} build + * + * + * @typedef {Object} RequestInterceptionHandler + * @property {{statusCode: number, message: string} | undefined} blockResponse */ import { EventEmitter } from "events"; -import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js"; /** - * @returns {InterceptorBuilder} + * @param {(requestHandlerBuilder: RequestInterceptionContext) => Promise} requestInterceptionFunc + * @returns {Interceptor} */ -export function createInterceptorBuilder() { - /** - * @type {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise>} - */ - const requestHandlers = []; - - return { - onRequest(requestFunc) { - requestHandlers.push(requestFunc); - }, - build() { - return buildInterceptor(requestHandlers); - }, - }; +export function interceptRequests(requestInterceptionFunc) { + return buildInterceptor([requestInterceptionFunc]); } /** - * @param {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise>} requestHandlers + * @param {Array<(requestHandlerBuilder: RequestInterceptionContext) => Promise>} requestHandlers * @returns {Interceptor} */ function buildInterceptor(requestHandlers) { @@ -43,7 +34,7 @@ function buildInterceptor(requestHandlers) { return { async handleRequest(targetUrl) { - const reqInterceptorBuilder = createRequestInterceptorBuilder( + const reqInterceptorBuilder = createRequestContext( targetUrl, eventEmitter ); @@ -63,3 +54,47 @@ function buildInterceptor(requestHandlers) { }, }; } + +/** + * @param {string} targetUrl + * @param {import('events').EventEmitter} eventEmitter + * @returns {RequestInterceptionContext} + */ +function createRequestContext(targetUrl, eventEmitter) { + /** @type {{statusCode: number, message: string} | undefined} */ + let blockResponse = undefined; + + /** + * @param {number} statusCode + * @param {string} message + */ + function blockRequest(statusCode, message) { + blockResponse = { statusCode, message }; + } + + /** + * @param {string | undefined} packageName + * @param {string | undefined} version + */ + function blockMalware(packageName, version) { + blockRequest(403, "Forbidden - blocked by safe-chain"); + + // Emit the malwareBlocked event + eventEmitter.emit("malwareBlocked", { + packageName, + version, + targetUrl, + timestamp: Date.now(), + }); + } + + return { + targetUrl, + blockMalware, + build() { + return { + blockResponse, + }; + }, + }; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js index 6e33dd0..9a80890 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -1,5 +1,5 @@ import { isMalwarePackage } from "../../scanning/audit/index.js"; -import { createInterceptorBuilder } from "./interceptorBuilder.js"; +import { interceptRequests } from "./interceptorBuilder.js"; const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; @@ -22,19 +22,15 @@ export function npmInterceptorForUrl(url) { * @returns {import("./interceptorBuilder.js").Interceptor | undefined} */ function buildNpmInterceptor(registry) { - const builder = createInterceptorBuilder(); - - builder.onRequest(async (req) => { + return interceptRequests(async (reqContext) => { const { packageName, version } = parseNpmPackageUrl( - req.targetUrl, + reqContext.targetUrl, registry ); if (await isMalwarePackage(packageName, version)) { - req.blockMalware(packageName, version, req.targetUrl); + reqContext.blockMalware(packageName, version); } }); - - return builder.build(); } /** diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 7d793d3..212c830 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -1,5 +1,5 @@ import { isMalwarePackage } from "../../scanning/audit/index.js"; -import { createInterceptorBuilder } from "./interceptorBuilder.js"; +import { interceptRequests } from "./interceptorBuilder.js"; const knownPipRegistries = [ "files.pythonhosted.org", @@ -27,19 +27,15 @@ export function pipInterceptorForUrl(url) { * @returns {import("./interceptorBuilder.js").Interceptor | undefined} */ function buildPipInterceptor(registry) { - const builder = createInterceptorBuilder(); - - builder.onRequest(async (req) => { + return interceptRequests(async (reqContext) => { const { packageName, version } = parsePipPackageFromUrl( - req.targetUrl, + reqContext.targetUrl, registry ); if (await isMalwarePackage(packageName, version)) { - req.blockMalware(packageName, version, req.targetUrl); + reqContext.blockMalware(packageName, version); } }); - - return builder.build(); } /** diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js deleted file mode 100644 index ad1f145..0000000 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @typedef {Object} RequestInterceptorBuilder - * @property {string} targetUrl - * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware - * @property {() => RequestInterceptor} build - * - * @typedef {Object} RequestInterceptor - * @property {{statusCode: number, message: string} | undefined} blockResponse - */ - -/** - * @param {string} targetUrl - * @param {import('events').EventEmitter} eventEmitter - * @returns {RequestInterceptorBuilder} - */ -export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { - /** @type {{statusCode: number, message: string} | undefined} */ - let blockResponse = undefined; - - /** - * @param {number} statusCode - * @param {string} message - */ - function blockRequest(statusCode, message) { - blockResponse = { statusCode, message }; - } - - /** - * @param {string | undefined} packageName - * @param {string | undefined} version - * @param {string} url - */ - function blockMalware(packageName, version, url) { - blockRequest(403, "Forbidden - blocked by safe-chain"); - - // Emit the malwareBlocked event - eventEmitter.emit("malwareBlocked", { - packageName, - version, - url, - timestamp: Date.now(), - }); - } - - return { - targetUrl, - blockMalware, - build() { - return { - blockResponse, - }; - }, - }; -} From d8007f62362718a9c432bfbbc76b4eba435dfbdd Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 12 Nov 2025 14:07:35 +0100 Subject: [PATCH 189/345] Cleanup interceptorBuilder.js --- .../interceptors/interceptorBuilder.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index beed1f9..e6017d9 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -1,3 +1,5 @@ +import { EventEmitter } from "events"; + /** * @typedef {Object} Interceptor * @property {(targetUrl: string) => Promise} handleRequest @@ -15,8 +17,6 @@ * @property {{statusCode: number, message: string} | undefined} blockResponse */ -import { EventEmitter } from "events"; - /** * @param {(requestHandlerBuilder: RequestInterceptionContext) => Promise} requestInterceptionFunc * @returns {Interceptor} @@ -34,16 +34,13 @@ function buildInterceptor(requestHandlers) { return { async handleRequest(targetUrl) { - const reqInterceptorBuilder = createRequestContext( - targetUrl, - eventEmitter - ); + const requestContext = createRequestContext(targetUrl, eventEmitter); for (const handler of requestHandlers) { - await handler(reqInterceptorBuilder); + await handler(requestContext); } - return reqInterceptorBuilder.build(); + return requestContext.build(); }, on(event, listener) { eventEmitter.on(event, listener); From 27bf768cc6a80ea8925912c1153b0007f27a3835 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 12 Nov 2025 14:12:45 +0100 Subject: [PATCH 190/345] Remove blockResponse function entirely --- .../interceptors/interceptorBuilder.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index e6017d9..96c1e67 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -61,20 +61,15 @@ function createRequestContext(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; - /** - * @param {number} statusCode - * @param {string} message - */ - function blockRequest(statusCode, message) { - blockResponse = { statusCode, message }; - } - /** * @param {string | undefined} packageName * @param {string | undefined} version */ function blockMalware(packageName, version) { - blockRequest(403, "Forbidden - blocked by safe-chain"); + blockResponse = { + statusCode: 403, + message: "Forbidden - blocked by safe-chain", + }; // Emit the malwareBlocked event eventEmitter.emit("malwareBlocked", { From 988507f8e1ee058efd905926645df2d3cc961e3f Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 12 Nov 2025 16:15:32 +0100 Subject: [PATCH 191/345] Clarify support for ecosystems and pip status Updated README to clarify that Aikido Safe Chain currently supports only JavaScript ecosystems and marks pip and pip3 as beta. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index acea710..f169747 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, 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 **prevents developers from installing malware** on their workstations while developing 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. @@ -15,8 +15,8 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - ✅ **pnpx** - ✅ **bun** - ✅ **bunx** -- ✅ **pip** -- ✅ **pip3** +- ✅ **pip** (beta) +- ✅ **pip3** (beta) # Usage @@ -41,7 +41,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: npm install safe-chain-test ``` - For Python: + For Python (beta): ```shell pip3 install safe-chain-pi-test ``` From fdef9e0766aa603c09b5bfeee9acc49357b87fca Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 12 Nov 2025 13:11:02 -0800 Subject: [PATCH 192/345] Some tweaks --- .../safe-chain/src/packagemanager/pip/runPipCommand.js | 9 +++------ packages/safe-chain/src/registryProxy/certUtils.js | 4 ++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index f2eccc5..440c02b 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -30,7 +30,6 @@ export async function runPip(command, args) { // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, // We will set additional env vars for pip - if (!env.PIP_CERT) { env.PIP_CERT = combinedCaPath; } @@ -38,15 +37,13 @@ export async function runPip(command, args) { const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); - // Proxy settings - const httpProxy = env.HTTP_PROXY || ''; - const httpsProxy = env.HTTPS_PROXY || ''; + // Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY + const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; // Build pip config INI let pipConfig = '[global]\n'; pipConfig += `cert = ${combinedCaPath}\n`; - if (httpProxy) pipConfig += `proxy = ${httpProxy}\n`; - if (httpsProxy) pipConfig += `proxy = ${httpsProxy}\n`; + if (proxy) pipConfig += `proxy = ${proxy}\n`; await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index a2fb7bb..aa23d79 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -48,6 +48,10 @@ export function generateCertForHost(hostname) { digitalSignature: true, keyEncipherment: true, }, + { + name: "extKeyUsage", + serverAuth: true, + }, ]); cert.sign(ca.privateKey, forge.md.sha256.create()); From f215368c4a97531e2875044347174145c04fb84b Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 12 Nov 2025 13:30:22 -0800 Subject: [PATCH 193/345] Some small fixes --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 3 +++ packages/safe-chain/src/registryProxy/certUtils.js | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 440c02b..12a3748 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -24,6 +24,7 @@ export async function runPip(command, args) { if (!env.REQUESTS_CA_BUNDLE) { env.REQUESTS_CA_BUNDLE = combinedCaPath; } + if (!env.SSL_CERT_FILE) { env.SSL_CERT_FILE = combinedCaPath; } @@ -33,6 +34,8 @@ export async function runPip(command, args) { if (!env.PIP_CERT) { env.PIP_CERT = combinedCaPath; } + + // PIP_CONFIG file is created to ensure proxy and cert settings are applied even if env vars are ignored for certificates (e.g. Python 3.11 and up). if (!env.PIP_CONFIG_FILE) { const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index aa23d79..6b326c8 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -49,6 +49,12 @@ export function generateCertForHost(hostname) { keyEncipherment: true, }, { + /* + extKeyUsage serverAuth is required for TLS server authentication. + This is especially important for Python venv environments, which use their own + certificate validation logic and will reject certificates lacking the serverAuth EKU. + Adding serverAuth does not impact other usages + */ name: "extKeyUsage", serverAuth: true, }, From 285906ea9d610d9960928d1e41a10a109f531f27 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 12 Nov 2025 13:39:58 -0800 Subject: [PATCH 194/345] Update doc --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 12a3748..0f5e697 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -29,13 +29,12 @@ export async function runPip(command, args) { env.SSL_CERT_FILE = combinedCaPath; } - // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, - // We will set additional env vars for pip + // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via both INI and PIP_CERT) + // is necessary for pip to use custom certs for later versions. if (!env.PIP_CERT) { env.PIP_CERT = combinedCaPath; } - // PIP_CONFIG file is created to ensure proxy and cert settings are applied even if env vars are ignored for certificates (e.g. Python 3.11 and up). if (!env.PIP_CONFIG_FILE) { const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); From fbd11c6d443ea93ab90cd011a34f8078e3cffd31 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 12 Nov 2025 14:01:06 -0800 Subject: [PATCH 195/345] Update --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 0f5e697..f3e7aa9 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -24,13 +24,14 @@ export async function runPip(command, args) { if (!env.REQUESTS_CA_BUNDLE) { env.REQUESTS_CA_BUNDLE = combinedCaPath; } + + // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via both INI and PIP_CERT) + // Testing has shown that REQUESTS_CA_BUNDLE alone is not sufficient; PIP_CERT, SSL_CERT_FILE, or pip config cert is also needed in some cases. if (!env.SSL_CERT_FILE) { env.SSL_CERT_FILE = combinedCaPath; } - // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via both INI and PIP_CERT) - // is necessary for pip to use custom certs for later versions. if (!env.PIP_CERT) { env.PIP_CERT = combinedCaPath; } From 6ae93686b7a06a622f505021cd44b114cb58b522 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 13 Nov 2025 14:51:57 +0100 Subject: [PATCH 196/345] Finish npm info modification. --- packages/safe-chain/src/config/settings.js | 3 +- .../interceptors/interceptorBuilder.js | 54 ++++- .../interceptors/npm/modifyNpmInfo.js | 148 +++++++++++++ .../interceptors/npm/npmInterceptor.js | 197 +----------------- .../npm/npmInterceptor.minPackageAge.spec.js | 29 ++- .../interceptors/npm/parseNpmPackageUrl.js | 43 ++++ .../src/registryProxy/mitmRequestHandler.js | 19 +- 7 files changed, 281 insertions(+), 212 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js create mode 100644 packages/safe-chain/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 5b27118..cef66c3 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -37,6 +37,7 @@ export function setEcoSystem(setting) { ecosystemSettings.ecoSystem = setting; } +const defaultMinimumPackageAge = 24; export function getMinimumPackageAgeHours() { - return 24 * 6; + return defaultMinimumPackageAge; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index 96c1e67..c8ef3fc 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -10,11 +10,16 @@ import { EventEmitter } from "events"; * @typedef {Object} RequestInterceptionContext * @property {string} targetUrl * @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware + * @property {(modificationFunc: (headers: NodeJS.Dict) => void) => void} modifyRequestHeaders + * @property {(modificationFunc: (body: Buffer) => Buffer) => void} modifyBody * @property {() => RequestInterceptionHandler} build * * * @typedef {Object} RequestInterceptionHandler * @property {{statusCode: number, message: string} | undefined} blockResponse + * @property {(headers: NodeJS.Dict | undefined) => void} modifyRequestHeaders + * @property {() => boolean} modifiesResponse + * @property {(body: Buffer) => Buffer} modifyBody */ /** @@ -60,12 +65,16 @@ function buildInterceptor(requestHandlers) { function createRequestContext(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; + /** @type {Array<(headers: NodeJS.Dict) => void>} */ + let reqheaderModificationFuncs = []; + /** @type {Array<(body: Buffer) => Buffer>} */ + let modifyBodyFuncs = []; /** * @param {string | undefined} packageName * @param {string | undefined} version */ - function blockMalware(packageName, version) { + function blockMalwareSetup(packageName, version) { blockResponse = { statusCode: 403, message: "Forbidden - blocked by safe-chain", @@ -80,13 +89,44 @@ function createRequestContext(targetUrl, eventEmitter) { }); } + /** @returns {RequestInterceptionHandler} */ + function build() { + /** @param {NodeJS.Dict | undefined} headers */ + function modifyRequestHeaders(headers) { + if (!headers) return; + + for (const func of reqheaderModificationFuncs) { + func(headers); + } + } + + /** + * @param {Buffer} body + * @returns {Buffer} + */ + function modifyBody(body) { + let modifiedBody = body; + + for (var func of modifyBodyFuncs) { + modifiedBody = func(body); + } + + return modifiedBody; + } + + return { + blockResponse, + modifyRequestHeaders: modifyRequestHeaders, + modifiesResponse: () => modifyBodyFuncs.length > 0, + modifyBody, + }; + } + return { targetUrl, - blockMalware, - build() { - return { - blockResponse, - }; - }, + blockMalware: blockMalwareSetup, + modifyRequestHeaders: (func) => reqheaderModificationFuncs.push(func), + modifyBody: (func) => modifyBodyFuncs.push(func), + build, }; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js new file mode 100644 index 0000000..b69159a --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -0,0 +1,148 @@ +import { getMinimumPackageAgeHours } from "../../../config/settings.js"; +import { ui } from "../../../environment/userInteraction.js"; + +/** + * @param {NodeJS.Dict} headers + */ +export function modifyNpmInfoRequestHeaders(headers) { + if (headers["accept"]?.includes("application/vnd.npm.install-v1+json")) { + // The npm registry sometimes serves a more compact format that lacks + // the time metadata we need to filter out too new packages. + // Force the registry to return the full metadata by changing the Accept header. + headers["accept"] = "application/json"; + } +} + +/** + * @param {string} url + * @returns {boolean} + */ +export function isPackageInfoUrl(url) { + // Remove query string and fragment to get the actual path + const urlWithoutParams = url.split("?")[0].split("#")[0]; + + // Tarball downloads end with .tgz + if (urlWithoutParams.endsWith(".tgz")) return false; + + // Special endpoints start with /-/ and should not be modified + // Examples: /-/npm/v1/security/advisories/bulk, /-/v1/search, /-/package/foo/access + if (urlWithoutParams.includes("/-/")) return false; + + // Everything else is package metadata that can be modified + return true; +} +/** + * + * @param {Buffer} body + * @returns Buffer + */ +export function modifyNpmInfoResponse(body) { + try { + if (body.byteLength === 0) { + return body; + } + + const bodyContent = body.toString("utf8"); + const bodyJson = JSON.parse(bodyContent); + + if (!bodyJson.time || !bodyJson["dist-tags"] || !bodyJson.versions) { + // Just return the current body if the format is not + return body; + } + + const cutOff = new Date( + new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 + ).toISOString(); + + const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; + + const versions = Object.entries(bodyJson.time) + .map(([version, timestamp]) => ({ + version, + timestamp, + })) + .filter((x) => x.version !== "created" && x.version !== "modified"); + + for (const { version, timestamp } of versions) { + if (version === "created" || version === "modified") { + continue; + } + + if (timestamp > cutOff) { + deleteVersionFromJson(bodyJson, version); + continue; + } + } + + if (hasLatestTag && !bodyJson["dist-tags"]["latest"]) { + // The latest tag was removed because it contained a package younger than the treshold. + // A new latest tag needs to be calculated + bodyJson["dist-tags"]["latest"] = calculateLatestTag(bodyJson.time); + } + + return Buffer.from(JSON.stringify(bodyJson)); + } catch (err) { + ui.writeVerbose( + `Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}` + ); + return body; + } +} + +/** + * @param {any} json + * @param {string} version + */ +function deleteVersionFromJson(json, version) { + ui.writeVerbose( + `Safe-chain: ${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` + ); + + delete json.time[version]; + delete json.versions[version]; + + for (const [tag, distVersion] of Object.entries(json["dist-tags"])) { + if (version == distVersion) { + delete json["dist-tags"][tag]; + } + } +} + +/** + * @param {Record} tagList + * @returns {string | undefined} + */ +function calculateLatestTag(tagList) { + const entries = Object.entries(tagList).filter( + ([version, _]) => version !== "created" && version !== "modified" + ); + + const latestFullRelease = getMostRecentTag( + Object.fromEntries(entries.filter(([version, _]) => !version.includes("-"))) + ); + if (latestFullRelease) { + return latestFullRelease; + } + + const latestPrerelease = getMostRecentTag( + Object.fromEntries(entries.filter(([version, _]) => version.includes("-"))) + ); + return latestPrerelease; +} + +/** + * @param {Record} tagList + * @returns {string | undefined} + */ +function getMostRecentTag(tagList) { + let current, currentDate; + + for (const [version, timestamp] of Object.entries(tagList)) { + if (!currentDate || currentDate < timestamp) { + current = version; + currentDate = timestamp; + } + } + + return current; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js index 0514636..467e5f0 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js @@ -1,7 +1,11 @@ -import { getMinimumPackageAgeHours } from "../../../config/settings.js"; import { isMalwarePackage } from "../../../scanning/audit/index.js"; import { interceptRequests } from "../interceptorBuilder.js"; -import { ui } from "../../../environment/userInteraction.js"; +import { + isPackageInfoUrl, + modifyNpmInfoRequestHeaders, + modifyNpmInfoResponse, +} from "./modifyNpmInfo.js"; +import { parseNpmPackageUrl } from "./parseNpmPackageUrl.js"; const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; @@ -29,197 +33,14 @@ function buildNpmInterceptor(registry) { reqContext.targetUrl, registry ); + if (await isMalwarePackage(packageName, version)) { reqContext.blockMalware(packageName, version); } if (isPackageInfoUrl(reqContext.targetUrl)) { - reqContext.modifyRequestHeaders((headers) => { - if ( - headers["accept"]?.includes("application/vnd.npm.install-v1+json") - ) { - // The npm registry sometimes serves a more compact format that lacks - // the time metadata we need to filter out too new packages. - // Force the registry to return the full metadata by changing the Accept header. - headers["accept"] = "application/json"; - } - }); - - reqContext.modifyResponse((res) => { - res.modifyBody(modifyNpmInfoRequestBody); - }); + reqContext.modifyRequestHeaders(modifyNpmInfoRequestHeaders); + reqContext.modifyBody(modifyNpmInfoResponse); } }); } - -/** - * @param {string} url - * @param {string} registry - * @returns {{packageName: string | undefined, version: string | undefined}} - */ -function parseNpmPackageUrl(url, registry) { - let packageName, version; - if (!registry || !url.endsWith(".tgz")) { - return { packageName, version }; - } - - const registryIndex = url.indexOf(registry); - const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash - - const separatorIndex = afterRegistry.indexOf("/-/"); - if (separatorIndex === -1) { - return { packageName, version }; - } - - packageName = afterRegistry.substring(0, separatorIndex); - const filename = afterRegistry.substring( - separatorIndex + 3, - afterRegistry.length - 4 - ); // Remove /-/ and .tgz - - // Extract version from filename - // For scoped packages like @babel/core, the filename is core-7.21.4.tgz - // For regular packages like lodash, the filename is lodash-4.17.21.tgz - if (packageName.startsWith("@")) { - const scopedPackageName = packageName.substring( - packageName.lastIndexOf("/") + 1 - ); - if (filename.startsWith(scopedPackageName + "-")) { - version = filename.substring(scopedPackageName.length + 1); - } - } else { - if (filename.startsWith(packageName + "-")) { - version = filename.substring(packageName.length + 1); - } - } - - return { packageName, version }; -} - -/** - * @param {string} url - * @returns {boolean} - */ -function isPackageInfoUrl(url) { - // Remove query string and fragment to get the actual path - const urlWithoutParams = url.split("?")[0].split("#")[0]; - - // Tarball downloads end with .tgz - if (urlWithoutParams.endsWith(".tgz")) return false; - - // Special endpoints start with /-/ and should not be modified - // Examples: /-/npm/v1/security/advisories/bulk, /-/v1/search, /-/package/foo/access - if (urlWithoutParams.includes("/-/")) return false; - - // Everything else is package metadata that can be modified - return true; -} - -/** - * - * @param {Buffer} body - * @returns Buffer - */ -function modifyNpmInfoRequestBody(body) { - try { - const bodyContent = body.toString("utf8"); - const bodyJson = JSON.parse(bodyContent); - - if (!bodyJson.time || !bodyJson["dist-tags"] || !bodyJson.versions) { - // Just return the body if the - return body; - } - - const cutOff = new Date( - new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 - ).toISOString(); - - const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; - - const versions = Object.entries(bodyJson.time) - .map(([version, timestamp]) => ({ - version, - timestamp, - })) - .filter((x) => x.version != "created" && x.version != "modified"); - - for (const { version, timestamp } of versions) { - if (version === "created" || version === "modified") { - continue; - } - - if (timestamp > cutOff) { - deleteVersionFromJson(bodyJson, version); - continue; - } - } - - if (hasLatestTag && !bodyJson["dist-tags"]["latest"]) { - // The latest tag was removed because it contained a package younger than the treshold. - // A new latest tag needs to be calculated - bodyJson["dist-tags"]["latest"] = calculateLatestTag(bodyJson); - } - - return Buffer.from(JSON.stringify(bodyJson)); - } catch (err) { - // TODO: better error handling - return body; - } -} - -function deleteVersionFromJson(json, version) { - ui.writeVerbose( - `Safe-chain: Deleting ${version} from npm info request, it's newer than the minimumPackageAgeInHours` - ); - - delete json.time[version]; - delete json.versions[version]; - - for (const [tag, distVersion] of Object.entries(json["dist-tags"])) { - if (version == distVersion) { - delete json["dist-tags"][tag]; - } - } -} - -function calculateLatestTag(json) { - if (!json.time) { - return undefined; - } - - let latest, preview, latestDate, previewDate; - - for (const [version, timestamp] of Object.entries(json.time)) { - if (version == "created" || version == "modified") continue; - - if (version.includes("-")) { - // preview versions include "-" in the name - [preview, previewDate] = getLatest( - preview, - previewDate, - version, - timestamp - ); - } else { - [latest, latestDate] = getLatest(latest, latestDate, version, timestamp); - } - } - - if (latest) { - return latest; - } else { - return preview; - } - - function getLatest(currentLatest, currentLatestDate, version, timestamp) { - if (!currentLatest) { - return [version, timestamp]; - } - - if (timestamp > currentLatestDate) { - return [version, timestamp]; - } - - return [currentLatest, currentLatestDate]; - } -} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index ea23f9e..269a241 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -1,5 +1,6 @@ import { describe, it, mock } from "node:test"; import assert from "node:assert"; +import { buffer } from "node:stream/consumers"; describe("npmInterceptor minimum package age", async () => { let minimumPackageAgeSettings = 48; @@ -17,6 +18,19 @@ describe("npmInterceptor minimum package age", async () => { }, }, }); + mock.module("../../../environment/userInteraction.js", { + namedExports: { + ui: { + startProcess: () => {}, + writeError: () => {}, + writeInformation: () => {}, + writeWarning: () => {}, + writeVerbose: () => {}, + writeExitWithoutInstallingMaliciousPackages: () => {}, + emptyLine: () => {}, + }, + }, + }); const { npmInterceptorForUrl } = await import("./npmInterceptor.js"); for (const packageInfoUrl of [ @@ -128,6 +142,7 @@ describe("npmInterceptor minimum package age", async () => { }, time: { created: getDate(-365 * 24), + modified: getDate(-3), ["1.0.0"]: getDate(-7), // cutoff-date here ["2.0.0"]: getDate(-4), @@ -138,7 +153,7 @@ describe("npmInterceptor minimum package age", async () => { const modifiedJson = JSON.parse(modifiedBody); - assert.equal(Object.keys(modifiedJson.time).length, 2); + assert.equal(Object.keys(modifiedJson.time).length, 3); assert.equal(Object.keys(modifiedJson.versions).length, 1); assert.ok(Object.keys(modifiedJson.time).includes("1.0.0")); assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); @@ -166,6 +181,7 @@ describe("npmInterceptor minimum package age", async () => { }, time: { created: getDate(-365 * 24), + modified: getDate(-3), ["1.0.0"]: getDate(-7), ["0.0.1"]: getDate(-8), // package order: this package is older than 1.0.0, it should not be considered latest ["2.0.0-alpha"]: getDate(-6), //package is a pre-release, it should not be latest @@ -200,6 +216,7 @@ describe("npmInterceptor minimum package age", async () => { }, time: { created: getDate(-365 * 24), + modified: getDate(-4), ["1.0.0"]: getDate(-7), // cutoff-date here ["2.0.0-alpha"]: getDate(-4), @@ -220,12 +237,14 @@ describe("npmInterceptor minimum package age", async () => { */ async function runModifyNpmInfoRequest(url, body) { const interceptor = npmInterceptorForUrl(url); - const requestInterceptor = await interceptor.handleRequest(url); - const responseInterceptor = requestInterceptor.handleResponse(); + const requestHandler = await interceptor.handleRequest(url); - const modifiedBuffer = responseInterceptor.modifyBody(Buffer.from(body)); + if (requestHandler.modifiesResponse()) { + const modifiedBuffer = requestHandler.modifyBody(Buffer.from(body)); + return modifiedBuffer.toString("utf8"); + } - return modifiedBuffer.toString("utf8"); + return buffer; } function getDate(plusHours) { diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js b/packages/safe-chain/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js new file mode 100644 index 0000000..fa256d4 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js @@ -0,0 +1,43 @@ +/** + * @param {string} url + * @param {string} registry + * @returns {{packageName: string | undefined, version: string | undefined}} + */ +export function parseNpmPackageUrl(url, registry) { + let packageName, version; + if (!registry || !url.endsWith(".tgz")) { + return { packageName, version }; + } + + const registryIndex = url.indexOf(registry); + const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash + + const separatorIndex = afterRegistry.indexOf("/-/"); + if (separatorIndex === -1) { + return { packageName, version }; + } + + packageName = afterRegistry.substring(0, separatorIndex); + const filename = afterRegistry.substring( + separatorIndex + 3, + afterRegistry.length - 4 + ); // Remove /-/ and .tgz + + // Extract version from filename + // For scoped packages like @babel/core, the filename is core-7.21.4.tgz + // For regular packages like lodash, the filename is lodash-4.17.21.tgz + if (packageName.startsWith("@")) { + const scopedPackageName = packageName.substring( + packageName.lastIndexOf("/") + 1 + ); + if (filename.startsWith(scopedPackageName + "-")) { + version = filename.substring(scopedPackageName.length + 1); + } + } else { + if (filename.startsWith(packageName + "-")) { + version = filename.substring(packageName.length + 1); + } + } + + return { packageName, version }; +} diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index aa1391e..70e0a51 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -6,7 +6,6 @@ import { gunzipSync, gzipSync } from "zlib"; /** * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor - * @typedef {import("./interceptors/requestInterceptorBuilder.js").RequestInterceptor} RequestInterceptor */ /** @@ -113,10 +112,10 @@ function getRequestPathAndQuery(url) { * @param {import("http").IncomingMessage} req * @param {string} hostname * @param {import("http").ServerResponse} res - * @param {RequestInterceptor} requestInterceptor + * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler */ -function forwardRequest(req, hostname, res, requestInterceptor) { - const proxyReq = createProxyRequest(hostname, req, res, requestInterceptor); +function forwardRequest(req, hostname, res, requestHandler) { + const proxyReq = createProxyRequest(hostname, req, res, requestHandler); proxyReq.on("error", (err) => { ui.writeVerbose( @@ -147,16 +146,16 @@ function forwardRequest(req, hostname, res, requestInterceptor) { * @param {string} hostname * @param {import("http").IncomingMessage} req * @param {import("http").ServerResponse} res - * @param {RequestInterceptor} requestInterceptor + * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler * * @returns {import("http").ClientRequest} */ -function createProxyRequest(hostname, req, res, requestInterceptor) { +function createProxyRequest(hostname, req, res, requestHandler) { const headers = { ...req.headers }; if (headers.host) { delete headers.host; } - requestInterceptor.modifyRequestHeaders(headers); + requestHandler.modifyRequestHeaders(headers); /** @type {import("http").RequestOptions} */ const options = { @@ -191,9 +190,7 @@ function createProxyRequest(hostname, req, res, requestInterceptor) { } res.writeHead(proxyRes.statusCode, proxyRes.headers); - if (requestInterceptor.modifiesResponse()) { - const responseInterceptor = requestInterceptor.handleResponse(); - + if (requestHandler.modifiesResponse()) { /** @type {Array} */ let chunks = []; @@ -207,7 +204,7 @@ function createProxyRequest(hostname, req, res, requestInterceptor) { buffer = gunzipSync(buffer); } - buffer = responseInterceptor.modifyBody(buffer); + buffer = requestHandler.modifyBody(buffer); if (proxyRes.headers["content-encoding"] === "gzip") { buffer = gzipSync(buffer); From a9a4d7670505ffed672c9aa2d2b99dfb31fd4f1c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 13 Nov 2025 15:08:36 +0100 Subject: [PATCH 197/345] Fix type error in modifyNpmInfo.js --- .../src/registryProxy/interceptors/npm/modifyNpmInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index b69159a..d334c27 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -81,7 +81,7 @@ export function modifyNpmInfoResponse(body) { } return Buffer.from(JSON.stringify(bodyJson)); - } catch (err) { + } catch (/** @type {any} */ err) { ui.writeVerbose( `Safe-chain: Package metadata not in expected format - bypassing modification. Error: ${err.message}` ); From f64ee3bccf088d5988558b17012743c2d7f26d42 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 13 Nov 2025 15:14:44 +0100 Subject: [PATCH 198/345] Add skipMinimumPackageAge. --- packages/safe-chain/src/config/settings.js | 4 ++ .../interceptors/npm/npmInterceptor.js | 3 +- .../npm/npmInterceptor.minPackageAge.spec.js | 47 ++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index cef66c3..ce5af2e 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -41,3 +41,7 @@ const defaultMinimumPackageAge = 24; export function getMinimumPackageAgeHours() { return defaultMinimumPackageAge; } + +export function skipMinimumPackageAge() { + return false; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js index 467e5f0..eaf50db 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.js @@ -1,3 +1,4 @@ +import { skipMinimumPackageAge } from "../../../config/settings.js"; import { isMalwarePackage } from "../../../scanning/audit/index.js"; import { interceptRequests } from "../interceptorBuilder.js"; import { @@ -38,7 +39,7 @@ function buildNpmInterceptor(registry) { reqContext.blockMalware(packageName, version); } - if (isPackageInfoUrl(reqContext.targetUrl)) { + if (!skipMinimumPackageAge() && isPackageInfoUrl(reqContext.targetUrl)) { reqContext.modifyRequestHeaders(modifyNpmInfoRequestHeaders); reqContext.modifyBody(modifyNpmInfoResponse); } diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index 269a241..ab3802b 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -1,13 +1,14 @@ import { describe, it, mock } from "node:test"; import assert from "node:assert"; -import { buffer } from "node:stream/consumers"; describe("npmInterceptor minimum package age", async () => { let minimumPackageAgeSettings = 48; + let skipMinimumPackageAgeSetting = false; mock.module("../../../config/settings.js", { namedExports: { getMinimumPackageAgeHours: () => minimumPackageAgeSettings, + skipMinimumPackageAge: () => skipMinimumPackageAgeSetting, }, }); @@ -244,9 +245,51 @@ describe("npmInterceptor minimum package age", async () => { return modifiedBuffer.toString("utf8"); } - return buffer; + return body; } + it("Should not filter packages when skipMinimumPackageAge is enabled", async () => { + minimumPackageAgeSettings = 5; + skipMinimumPackageAgeSetting = true; + const packageUrl = "https://registry.npmjs.org/lodash"; + + const originalBody = JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "3.0.0", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + }, + time: { + created: getDate(-365 * 24), + modified: getDate(-3), + ["1.0.0"]: getDate(-7), + // cutoff-date here + ["2.0.0"]: getDate(-4), + ["3.0.0"]: getDate(-3), + }, + }); + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + originalBody + ); + + const modifiedJson = JSON.parse(modifiedBody); + + // All versions should remain unchanged + assert.equal(Object.keys(modifiedJson.versions).length, 3); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("3.0.0")); + + // Latest should remain unchanged + assert.equal(modifiedJson["dist-tags"]["latest"], "3.0.0"); + }); + function getDate(plusHours) { const date = new Date(); date.setHours(date.getHours() + plusHours); From 752504dcc8a94f5b593df7422ab11683772c5b7f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 13 Nov 2025 16:04:24 +0100 Subject: [PATCH 199/345] Add --safe-chain-skip-minimum-package-age cli flag --- .../safe-chain/src/config/cliArguments.js | 35 ++++++++++- .../src/config/cliArguments.spec.js | 62 ++++++++++++++++++- packages/safe-chain/src/config/settings.js | 9 ++- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 04645d8..794c97a 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,8 +1,9 @@ /** - * @type {{loggingLevel: string | undefined}} + * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined}} */ const state = { loggingLevel: undefined, + skipMinimumPackageAge: undefined, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; @@ -14,6 +15,7 @@ const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; export function initializeCliArguments(args) { // Reset state on each call state.loggingLevel = undefined; + state.skipMinimumPackageAge = undefined; const safeChainArgs = []; const remainingArgs = []; @@ -27,6 +29,7 @@ export function initializeCliArguments(args) { } setLoggingLevel(safeChainArgs); + setSkipMinimumPackageAge(safeChainArgs); return remainingArgs; } @@ -47,6 +50,20 @@ function getLastArgEqualsValue(args, prefix) { return undefined; } +/** + * @param {string[]} args + * @param {string} flagName + * @returns {boolean} + */ +function hasFlagArg(args, flagName) { + for (const arg of args) { + if (arg.toLowerCase() === flagName.toLowerCase()) { + return true; + } + } + return false; +} + /** * @param {string[]} args * @returns {void} @@ -64,3 +81,19 @@ function setLoggingLevel(args) { export function getLoggingLevel() { return state.loggingLevel; } + +/** + * @param {string[]} args + * @returns {void} + */ +function setSkipMinimumPackageAge(args) { + const flagName = SAFE_CHAIN_ARG_PREFIX + "skip-minimum-package-age"; + + if (hasFlagArg(args, flagName)) { + state.skipMinimumPackageAge = true; + } +} + +export function getSkipMinimumPackageAge() { + return state.skipMinimumPackageAge; +} diff --git a/packages/safe-chain/src/config/cliArguments.spec.js b/packages/safe-chain/src/config/cliArguments.spec.js index 415d34a..3c8b7da 100644 --- a/packages/safe-chain/src/config/cliArguments.spec.js +++ b/packages/safe-chain/src/config/cliArguments.spec.js @@ -1,6 +1,10 @@ import { describe, it } from "node:test"; import assert from "node:assert"; -import { initializeCliArguments, getLoggingLevel } from "./cliArguments.js"; +import { + initializeCliArguments, + getLoggingLevel, + getSkipMinimumPackageAge, +} from "./cliArguments.js"; describe("initializeCliArguments", () => { it("should return all args when no safe-chain args are present", () => { @@ -118,4 +122,60 @@ describe("initializeCliArguments", () => { assert.deepEqual(result, ["install"]); assert.strictEqual(getLoggingLevel(), "silent"); }); + + it("should not set skipMinimumPackageAge when flag is absent", () => { + const args = ["install", "express", "--save"]; + initializeCliArguments(args); + + assert.strictEqual(getSkipMinimumPackageAge(), undefined); + }); + + it("should set skipMinimumPackageAge to true when flag is present", () => { + const args = ["--safe-chain-skip-minimum-package-age", "install", "lodash"]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "lodash"]); + assert.strictEqual(getSkipMinimumPackageAge(), true); + }); + + it("should handle skip-minimum-package-age flag case-insensitively", () => { + const args = ["--SAFE-CHAIN-SKIP-MINIMUM-PACKAGE-AGE", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getSkipMinimumPackageAge(), true); + }); + + it("should filter out skip-minimum-package-age flag from returned args", () => { + const args = [ + "install", + "--safe-chain-skip-minimum-package-age", + "express", + "--save", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "express", "--save"]); + }); + + it("should handle skip-minimum-package-age with other safe-chain arguments", () => { + const args = [ + "--safe-chain-logging=verbose", + "--safe-chain-skip-minimum-package-age", + "install", + "lodash", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "lodash"]); + assert.strictEqual(getLoggingLevel(), "verbose"); + assert.strictEqual(getSkipMinimumPackageAge(), true); + }); + + it("should handle skip-minimum-package-age flag in different positions", () => { + const args = ["install", "lodash", "--safe-chain-skip-minimum-package-age"]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "lodash"]); + assert.strictEqual(getSkipMinimumPackageAge(), true); + }); }); diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ce5af2e..ce7f35c 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -42,6 +42,13 @@ export function getMinimumPackageAgeHours() { return defaultMinimumPackageAge; } +const defaultSkipMinimumPackageAge = false; export function skipMinimumPackageAge() { - return false; + const cliValue = cliArguments.getSkipMinimumPackageAge(); + + if (cliValue === true) { + return true; + } + + return defaultSkipMinimumPackageAge; } From dc6f37b3ecdb6999180bc7275649754956a38db4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 13 Nov 2025 16:27:42 +0100 Subject: [PATCH 200/345] Remove etag from response when modifying headers --- .../registryProxy/interceptors/interceptorBuilder.js | 11 ++++++----- .../registryProxy/interceptors/npm/modifyNpmInfo.js | 8 +++++++- .../src/registryProxy/mitmRequestHandler.js | 7 +++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index c8ef3fc..362f31a 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -11,7 +11,7 @@ import { EventEmitter } from "events"; * @property {string} targetUrl * @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware * @property {(modificationFunc: (headers: NodeJS.Dict) => void) => void} modifyRequestHeaders - * @property {(modificationFunc: (body: Buffer) => Buffer) => void} modifyBody + * @property {(modificationFunc: (body: Buffer, headers: NodeJS.Dict | undefined) => Buffer) => void} modifyBody * @property {() => RequestInterceptionHandler} build * * @@ -19,7 +19,7 @@ import { EventEmitter } from "events"; * @property {{statusCode: number, message: string} | undefined} blockResponse * @property {(headers: NodeJS.Dict | undefined) => void} modifyRequestHeaders * @property {() => boolean} modifiesResponse - * @property {(body: Buffer) => Buffer} modifyBody + * @property {(body: Buffer, headers: NodeJS.Dict | undefined) => Buffer} modifyBody */ /** @@ -67,7 +67,7 @@ function createRequestContext(targetUrl, eventEmitter) { let blockResponse = undefined; /** @type {Array<(headers: NodeJS.Dict) => void>} */ let reqheaderModificationFuncs = []; - /** @type {Array<(body: Buffer) => Buffer>} */ + /** @type {Array<(body: Buffer, headers: NodeJS.Dict | undefined) => Buffer>} */ let modifyBodyFuncs = []; /** @@ -102,13 +102,14 @@ function createRequestContext(targetUrl, eventEmitter) { /** * @param {Buffer} body + * @param {NodeJS.Dict | undefined} headers * @returns {Buffer} */ - function modifyBody(body) { + function modifyBody(body, headers) { let modifiedBody = body; for (var func of modifyBodyFuncs) { - modifiedBody = func(body); + modifiedBody = func(body, headers); } return modifiedBody; diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index d334c27..54269a8 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -34,9 +34,10 @@ export function isPackageInfoUrl(url) { /** * * @param {Buffer} body + * @param {NodeJS.Dict | undefined} headers * @returns Buffer */ -export function modifyNpmInfoResponse(body) { +export function modifyNpmInfoResponse(body, headers) { try { if (body.byteLength === 0) { return body; @@ -70,6 +71,11 @@ export function modifyNpmInfoResponse(body) { if (timestamp > cutOff) { deleteVersionFromJson(bodyJson, version); + if (headers) { + // When modifying the response, the etag no longer matches the content + // so the etag needs to be removed before sending the response. + delete headers["etag"]; + } continue; } } diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 70e0a51..edc114d 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -188,7 +188,8 @@ function createProxyRequest(hostname, req, res, requestHandler) { res.end("Internal Server Error"); return; } - res.writeHead(proxyRes.statusCode, proxyRes.headers); + + const { statusCode, headers } = proxyRes; if (requestHandler.modifiesResponse()) { /** @type {Array} */ @@ -204,17 +205,19 @@ function createProxyRequest(hostname, req, res, requestHandler) { buffer = gunzipSync(buffer); } - buffer = requestHandler.modifyBody(buffer); + buffer = requestHandler.modifyBody(buffer, headers); if (proxyRes.headers["content-encoding"] === "gzip") { buffer = gzipSync(buffer); } + res.writeHead(statusCode, headers); res.end(buffer); }); } else { // If the response is not being modified, we can // just pipe without the need for buffering the output + res.writeHead(statusCode, headers); proxyRes.pipe(res); } }); From 59fa76a42f1ea15076d121cb948314c17a638094 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 13 Nov 2025 17:10:22 +0100 Subject: [PATCH 201/345] Notify the user when we modified the package versions --- packages/safe-chain/src/main.js | 13 +++++++++++++ .../registryProxy/interceptors/npm/modifyNpmInfo.js | 11 +++++++++++ .../safe-chain/src/registryProxy/registryProxy.js | 2 ++ 3 files changed, 26 insertions(+) diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index ea4fe0e..c46fc61 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -72,6 +72,19 @@ export async function main(args) { ); } + if (proxy.hasSuppressedVersions()) { + ui.writeInformation( + `${chalk.yellow( + "ℹ" + )} Safe-chain: Some package versions were suppressed due to minimum age requirement.` + ); + ui.writeInformation( + ` To disable this check, use: ${chalk.cyan( + "--safe-chain-skip-minimum-package-age" + )}` + ); + } + // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code return packageManagerResult.status; diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 54269a8..2ad8a68 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -1,6 +1,8 @@ import { getMinimumPackageAgeHours } from "../../../config/settings.js"; import { ui } from "../../../environment/userInteraction.js"; +let hasSuppressedVersions = false; + /** * @param {NodeJS.Dict} headers */ @@ -100,6 +102,8 @@ export function modifyNpmInfoResponse(body, headers) { * @param {string} version */ function deleteVersionFromJson(json, version) { + hasSuppressedVersions = true; + ui.writeVerbose( `Safe-chain: ${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` ); @@ -152,3 +156,10 @@ function getMostRecentTag(tagList) { return current; } + +/** + * @returns {boolean} + */ +export function getHasSuppressedVersions() { + return hasSuppressedVersions; +} diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index beaa1ef..8169086 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -6,6 +6,7 @@ import { getCaCertPath } from "./certUtils.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; +import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js"; const SERVER_STOP_TIMEOUT_MS = 1000; /** @@ -23,6 +24,7 @@ export function createSafeChainProxy() { startServer: () => startServer(server), stopServer: () => stopServer(server), verifyNoMaliciousPackages, + hasSuppressedVersions: getHasSuppressedVersions, }; } From 61c9f1a1efe6a173f2872a5aa70770918082b572 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 11:14:45 -0800 Subject: [PATCH 202/345] Merge config file if it exists --- package-lock.json | 10 ++ packages/safe-chain-bun/package.json | 2 +- packages/safe-chain/package.json | 1 + .../src/packagemanager/pip/runPipCommand.js | 59 ++++++-- .../packagemanager/pip/runPipCommand.spec.js | 130 ++++++++++++++++++ test/e2e/DockerTestContainer.js | 2 +- 6 files changed, 193 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9c32df..068e544 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1090,6 +1090,15 @@ "node": ">=0.8.19" } }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -2083,6 +2092,7 @@ "certifi": "^14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", + "ini": "^6.0.0", "make-fetch-happen": "14.0.3", "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", diff --git a/packages/safe-chain-bun/package.json b/packages/safe-chain-bun/package.json index b5a9e3e..ca154b8 100644 --- a/packages/safe-chain-bun/package.json +++ b/packages/safe-chain-bun/package.json @@ -27,4 +27,4 @@ "peerDependencies": { "bun": ">=1.2.21" } -} \ No newline at end of file +} diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index f21a372..5f8da60 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -38,6 +38,7 @@ "certifi": "^14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", + "ini": "^6.0.0", "make-fetch-happen": "14.0.3", "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index f3e7aa9..96cdcf8 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -3,8 +3,10 @@ import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; import fs from "node:fs/promises"; +import fsSync from "node:fs"; import os from "node:os"; import path from "node:path"; +import ini from "ini"; /** * @param {string} command @@ -36,20 +38,59 @@ export async function runPip(command, args) { env.PIP_CERT = combinedCaPath; } - if (!env.PIP_CONFIG_FILE) { - const tmpDir = os.tmpdir(); - const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); + // Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY + const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; - // Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY - const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; + const tmpDir = os.tmpdir(); + const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); + + if (!env.PIP_CONFIG_FILE) { // Build pip config INI - let pipConfig = '[global]\n'; - pipConfig += `cert = ${combinedCaPath}\n`; - if (proxy) pipConfig += `proxy = ${proxy}\n`; - + /** @type {{ global: { cert: string, proxy?: string } }} */ + const configObj = { global: { cert: combinedCaPath } }; + if (proxy) { + configObj.global.proxy = proxy; + } + const pipConfig = ini.stringify(configObj); await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; + } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { + // Existing pip config file present and exists on disk. + // Lets merge in our cert and proxy settings if not already present + const userConfig = env.PIP_CONFIG_FILE; + + ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); + + // Read the existing config without modifying it + let content = await fs.readFile(userConfig, "utf-8"); + const parsed = ini.parse(content); + + // Ensure [global] section exists + parsed.global = parsed.global || {}; + + // Adding CERT and PROXY + // If either is already set, there's no neeed to throw an error; mitm might fail and throw later if the proxy config is invalid + + // Cert + if (typeof parsed.global.cert === "undefined") { + ui.writeVerbose("Safe-chain: Adding cert to existing PIP_CONFIG_FILE."); + parsed.global.cert = combinedCaPath; + } + + // Proxy + if (typeof parsed.global.proxy === "undefined") { + if (proxy) { + ui.writeVerbose("Safe-chain: Adding proxy to existing PIP_CONFIG_FILE."); + parsed.global.proxy = proxy; + } + } + + const updated = ini.stringify(parsed); + + // Save to a new temp file to avoid overwriting user's original config + await fs.writeFile(pipConfigPath, updated, "utf-8"); + env.PIP_CONFIG_FILE = pipConfigPath; } const result = await safeSpawn(command, args, { diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 627d909..b3d7b2e 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -1,5 +1,9 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import ini from "ini"; describe("runPipCommand environment variable handling", () => { let runPip; @@ -145,4 +149,130 @@ describe("runPipCommand environment variable handling", () => { "HTTPS_PROXY should be set by proxy merge" ); }); + + it("should create a new temp config when existing config exists (original file untouched)", async () => { + const tmpDir = os.tmpdir(); + const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const initial = "[global]\nindex-url = https://example.com/simple\n"; + await fs.writeFile(userCfgPath, initial, "utf-8"); + + customEnv = { PIP_CONFIG_FILE: userCfgPath }; + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; + assert.notStrictEqual(newCfgPath, userCfgPath, "should point to a new temp config file"); + + // Original file unchanged + const originalContent = await fs.readFile(userCfgPath, "utf-8"); + const originalParsed = ini.parse(originalContent); + assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); + + // New file has merged settings + const newContent = await fs.readFile(newCfgPath, "utf-8"); + const newParsed = ini.parse(newContent); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env"); + assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved"); + customEnv = null; + }); + + it("should create new config with proxy set from env (ini-validated)", async () => { + // No PIP_CONFIG_FILE in env => creation path + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + + const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; + const content = await fs.readFile(configPath, "utf-8"); + const parsed = ini.parse(content); + assert.ok(parsed.global, "[global] should exist after creation"); + assert.strictEqual( + parsed.global.proxy, + "http://localhost:8080", + "proxy should be set from merged env" + ); + assert.strictEqual( + parsed.global.cert, + "/tmp/test-combined-ca.pem", + "cert should be set during creation" + ); + }); + + it("should create new temp config adding cert but preserving existing proxy (original file unchanged)", async () => { + const tmpDir = os.tmpdir(); + const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const initial = "[global]\nproxy = http://original:9999\n"; + await fs.writeFile(userCfgPath, initial, "utf-8"); + + customEnv = { PIP_CONFIG_FILE: userCfgPath }; + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; + assert.notStrictEqual(newCfgPath, userCfgPath, "should use a new temp config file"); + + // Original file unchanged + const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); + assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); + assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains"); + + // New file merged: cert added (was missing), proxy preserved (was present) + const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new cert injected"); + assert.strictEqual(newParsed.global.proxy, "http://original:9999", "existing proxy should be preserved in new file"); + customEnv = null; + }); + + it("should create new temp config preserving existing cert and proxy while leaving original file unchanged", async () => { + const tmpDir = os.tmpdir(); + const cfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const initialIni = [ + "[global]", + "cert = /path/to/existing.pem", + "proxy = http://original:9999", + "" + ].join("\n"); + await fs.writeFile(cfgPath, initialIni, "utf-8"); + + customEnv = { PIP_CONFIG_FILE: cfgPath }; + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0, "execution should succeed"); + const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; + assert.notStrictEqual(newCfgPath, cfgPath, "should use a newly generated temp config file"); + + // Original file stays untouched + const originalContent = await fs.readFile(cfgPath, "utf-8"); + const originalParsed = ini.parse(originalContent); + assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved"); + assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved"); + + // New temp config preserves existing values (no override when already set) + const newContent = await fs.readFile(newCfgPath, "utf-8"); + const newParsed = ini.parse(newContent); + assert.strictEqual(newParsed.global.cert, "/path/to/existing.pem", "existing cert preserved in new temp config"); + assert.strictEqual(newParsed.global.proxy, "http://original:9999", "existing proxy preserved in new temp config"); + customEnv = null; + }); + + it("should create new temp config preserving existing cert and adding missing proxy", async () => { + const tmpDir = os.tmpdir(); + const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`); + const initial = "[global]\ncert = /path/to/existing.pem\n"; + await fs.writeFile(userCfgPath, initial, "utf-8"); + + customEnv = { PIP_CONFIG_FILE: userCfgPath }; + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE; + assert.notStrictEqual(newCfgPath, userCfgPath, "should produce a new temp config file"); + + // Original remains unchanged + const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8")); + assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged"); + assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing"); + + // New file preserves existing cert and adds proxy (since it was missing) + const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); + assert.strictEqual(newParsed.global.cert, "/path/to/existing.pem", "existing cert preserved (not overridden)"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy added from env"); + customEnv = null; + }); }); diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index ec1af3c..289b451 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -33,7 +33,7 @@ export class DockerTestContainer { ].join(" "); execSync( - `docker build -t ${imageName} -f ${dockerFile} ${contextPath} ${buildArgs}`, + `docker build --no-cache -t ${imageName} -f ${dockerFile} ${contextPath} ${buildArgs}`, { stdio: "ignore", } From a0e24b172275ef450e14dd1625977645ee070465 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 11:21:53 -0800 Subject: [PATCH 203/345] Update comments --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 96cdcf8..4198686 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -70,7 +70,9 @@ export async function runPip(command, args) { parsed.global = parsed.global || {}; // Adding CERT and PROXY - // If either is already set, there's no neeed to throw an error; mitm might fail and throw later if the proxy config is invalid + // If either is already set, there's no neeed to throw an error + // MITM might fail and throw later if the proxy config is invalid + // This ensures that no malware will be installed by safe-chain // Cert if (typeof parsed.global.cert === "undefined") { From 4ee18973dee7dc53ae8f764f3fc21056af002caa Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 12:48:04 -0800 Subject: [PATCH 204/345] Fix unit test --- .../safe-chain/src/packagemanager/pip/runPipCommand.spec.js | 3 +++ test/e2e/DockerTestContainer.js | 2 +- 2 files changed, 4 insertions(+), 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 b3d7b2e..ecd8e0f 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -29,7 +29,10 @@ describe("runPipCommand environment variable handling", () => { mergeSafeChainProxyEnvironmentVariables: (env) => ({ ...env, ...customEnv, + // Force deterministic proxy for tests regardless of ambient env + GLOBAL_AGENT_HTTP_PROXY: "http://localhost:8080", HTTPS_PROXY: "http://localhost:8080", + HTTP_PROXY: "", }), }, }); diff --git a/test/e2e/DockerTestContainer.js b/test/e2e/DockerTestContainer.js index 289b451..ec1af3c 100644 --- a/test/e2e/DockerTestContainer.js +++ b/test/e2e/DockerTestContainer.js @@ -33,7 +33,7 @@ export class DockerTestContainer { ].join(" "); execSync( - `docker build --no-cache -t ${imageName} -f ${dockerFile} ${contextPath} ${buildArgs}`, + `docker build -t ${imageName} -f ${dockerFile} ${contextPath} ${buildArgs}`, { stdio: "ignore", } From f4ff18304aa50a1c201c41fe76b387400388670d Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 13:20:11 -0800 Subject: [PATCH 205/345] Fix imports --- package-lock.json | 8 ++++++++ packages/safe-chain/package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index 068e544..94b6921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -411,6 +411,13 @@ "node": ">=14" } }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/make-fetch-happen": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.4.tgz", @@ -2114,6 +2121,7 @@ "safe-chain": "bin/safe-chain.js" }, "devDependencies": { + "@types/ini": "^4.1.1", "@types/make-fetch-happen": "^10.0.4", "@types/node": "^18.19.130", "@types/node-forge": "^1.3.14", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 5f8da60..6394bc6 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -46,6 +46,7 @@ "semver": "7.7.2" }, "devDependencies": { + "@types/ini": "^4.1.1", "@types/make-fetch-happen": "^10.0.4", "@types/node": "^18.19.130", "@types/npm-registry-fetch": "^8.0.9", From 474d91d29ae1bb87741875274edfaa63d3501066 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 13:32:49 -0800 Subject: [PATCH 206/345] Indentation --- packages/safe-chain/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 6394bc6..186d810 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -46,7 +46,7 @@ "semver": "7.7.2" }, "devDependencies": { - "@types/ini": "^4.1.1", + "@types/ini": "^4.1.1", "@types/make-fetch-happen": "^10.0.4", "@types/node": "^18.19.130", "@types/npm-registry-fetch": "^8.0.9", From 0b3cc1c17543ebb864df0f0d72a8751dfd4756ff Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 15:50:14 -0800 Subject: [PATCH 207/345] Some more cleanup --- .../src/packagemanager/pip/runPipCommand.js | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 4198686..9d68c81 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -23,20 +23,8 @@ export async function runPip(command, args) { // validates correctly under both MITM'd and tunneled HTTPS. const combinedCaPath = getCombinedCaBundlePath(); - if (!env.REQUESTS_CA_BUNDLE) { - env.REQUESTS_CA_BUNDLE = combinedCaPath; - } - - // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via both INI and PIP_CERT) - // Testing has shown that REQUESTS_CA_BUNDLE alone is not sufficient; PIP_CERT, SSL_CERT_FILE, or pip config cert is also needed in some cases. - - if (!env.SSL_CERT_FILE) { - env.SSL_CERT_FILE = combinedCaPath; - } - - if (!env.PIP_CERT) { - env.PIP_CERT = combinedCaPath; - } + // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via INI file) + // will tell pip to use the provided CA bundle for HTTPS verification. // Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; @@ -45,7 +33,6 @@ export async function runPip(command, args) { const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); if (!env.PIP_CONFIG_FILE) { - // Build pip config INI /** @type {{ global: { cert: string, proxy?: string } }} */ const configObj = { global: { cert: combinedCaPath } }; @@ -55,6 +42,7 @@ export async function runPip(command, args) { const pipConfig = ini.stringify(configObj); await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; + } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { // Existing pip config file present and exists on disk. // Lets merge in our cert and proxy settings if not already present @@ -72,18 +60,17 @@ export async function runPip(command, args) { // Adding CERT and PROXY // If either is already set, there's no neeed to throw an error // MITM might fail and throw later if the proxy config is invalid - // This ensures that no malware will be installed by safe-chain // Cert if (typeof parsed.global.cert === "undefined") { - ui.writeVerbose("Safe-chain: Adding cert to existing PIP_CONFIG_FILE."); + ui.writeVerbose("Safe-chain: Adding cert to temporary PIP_CONFIG_FILE."); parsed.global.cert = combinedCaPath; } // Proxy if (typeof parsed.global.proxy === "undefined") { if (proxy) { - ui.writeVerbose("Safe-chain: Adding proxy to existing PIP_CONFIG_FILE."); + ui.writeVerbose("Safe-chain: Adding proxy to temporary PIP_CONFIG_FILE."); parsed.global.proxy = proxy; } } @@ -93,6 +80,21 @@ export async function runPip(command, args) { // Save to a new temp file to avoid overwriting user's original config await fs.writeFile(pipConfigPath, updated, "utf-8"); env.PIP_CONFIG_FILE = pipConfigPath; + + } else { + // The user provided PIP_CONFIG_FILE does not exist on disk + // PIP will handle this as an error and inform the user + } + + // REQUESTS_CA_BUNDLE, SSL_CERT_FILE and PIP_CERT as extra safety nets. + if (!env.REQUESTS_CA_BUNDLE) { + env.REQUESTS_CA_BUNDLE = combinedCaPath; + } + if (!env.SSL_CERT_FILE) { + env.SSL_CERT_FILE = combinedCaPath; + } + if (!env.PIP_CERT) { + env.PIP_CERT = combinedCaPath; } const result = await safeSpawn(command, args, { From 7039961d4ccd50054c55876cd8f7812838e485db Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 13 Nov 2025 15:50:37 -0800 Subject: [PATCH 208/345] Bugfix --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 9d68c81..309ee47 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -23,7 +23,7 @@ export async function runPip(command, args) { // validates correctly under both MITM'd and tunneled HTTPS. const combinedCaPath = getCombinedCaBundlePath(); - // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via INI file) + // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file) // will tell pip to use the provided CA bundle for HTTPS verification. // Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY @@ -46,9 +46,8 @@ export async function runPip(command, args) { } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { // Existing pip config file present and exists on disk. // Lets merge in our cert and proxy settings if not already present - const userConfig = env.PIP_CONFIG_FILE; - ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); + const userConfig = env.PIP_CONFIG_FILE; // Read the existing config without modifying it let content = await fs.readFile(userConfig, "utf-8"); From 06b287d4d422d3f55ac35329e0bff3194a48badb Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 09:08:27 +0100 Subject: [PATCH 209/345] Use correct header collection for forwarding --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index edc114d..9afadaa 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -163,7 +163,7 @@ function createProxyRequest(hostname, req, res, requestHandler) { port: 443, path: req.url, method: req.method, - headers: { ...req.headers }, + headers: { ...headers }, }; const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; From 86fb69a9311f4126d7de36128e08f155f838de12 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 12 Nov 2025 16:15:32 +0100 Subject: [PATCH 210/345] Clarify support for ecosystems and pip status Updated README to clarify that Aikido Safe Chain currently supports only JavaScript ecosystems and marks pip and pip3 as beta. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index acea710..f169747 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, 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 **prevents developers from installing malware** on their workstations while developing 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. @@ -15,8 +15,8 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi - ✅ **pnpx** - ✅ **bun** - ✅ **bunx** -- ✅ **pip** -- ✅ **pip3** +- ✅ **pip** (beta) +- ✅ **pip3** (beta) # Usage @@ -41,7 +41,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: npm install safe-chain-test ``` - For Python: + For Python (beta): ```shell pip3 install safe-chain-pi-test ``` From 40523f29ddd91fffafd1f469081826ed5fe7ee0e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 09:30:55 +0100 Subject: [PATCH 211/345] Document minimum package age in README.md --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f169747..687bedf 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,19 @@ 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/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. + 4. **Verify the installation** by running one of the following commands: For JavaScript/Node.js: + ```shell npm install safe-chain-test ``` For Python (beta): + ```shell pip3 install safe-chain-pi-test ``` @@ -58,7 +62,17 @@ 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. +### Malware Blocking + +The Aikido Safe Chain runs 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. + +### Minimum package age (npm only) + +**⚠️ This feature only applies to npm-based package managers (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip.** + +For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag. + +### Shell Integration 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: From 290a6305268aae77b89372c4d51007cb93a7ac5b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 10:23:06 +0100 Subject: [PATCH 212/345] Better header check + remove last-modified header --- .../interceptors/npm/modifyNpmInfo.js | 31 ++++++++++++++-- .../npm/npmInterceptor.minPackageAge.spec.js | 36 ++++++++++--------- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 2ad8a68..acb7d07 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -7,7 +7,8 @@ let hasSuppressedVersions = false; * @param {NodeJS.Dict} headers */ export function modifyNpmInfoRequestHeaders(headers) { - if (headers["accept"]?.includes("application/vnd.npm.install-v1+json")) { + const accept = getHeaderValueAsString(headers, "accept"); + if (accept?.includes("application/vnd.npm.install-v1+json")) { // The npm registry sometimes serves a more compact format that lacks // the time metadata we need to filter out too new packages. // Force the registry to return the full metadata by changing the Accept header. @@ -41,6 +42,11 @@ export function isPackageInfoUrl(url) { */ export function modifyNpmInfoResponse(body, headers) { try { + const contentType = getHeaderValueAsString(headers, "content-type"); + if (!contentType?.toLowerCase().includes("application/json")) { + return body; + } + if (body.byteLength === 0) { return body; } @@ -74,9 +80,10 @@ export function modifyNpmInfoResponse(body, headers) { if (timestamp > cutOff) { deleteVersionFromJson(bodyJson, version); if (headers) { - // When modifying the response, the etag no longer matches the content - // so the etag needs to be removed before sending the response. + // When modifying the response, the etag and last-modified headers + // no longer match the content so they needs to be removed before sending the response. delete headers["etag"]; + delete headers["last-modified"]; } continue; } @@ -163,3 +170,21 @@ function getMostRecentTag(tagList) { export function getHasSuppressedVersions() { return hasSuppressedVersions; } + +/** + * @param {NodeJS.Dict | undefined} headers + * @param {string} headerName + */ +function getHeaderValueAsString(headers, headerName) { + if (!headers) { + return undefined; + } + + let header = headers[headerName]; + + if (Array.isArray(header)) { + return header.join(", "); + } + + return header; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index ab3802b..2ff5a52 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -231,23 +231,6 @@ describe("npmInterceptor minimum package age", async () => { assert.equal(modifiedJson["dist-tags"]["alpha"], undefined); }); - /** - * @param {import("../interceptorBuilder.js").Interceptor} interceptor - * @param {string} body - * @returns {Promise} - */ - async function runModifyNpmInfoRequest(url, body) { - const interceptor = npmInterceptorForUrl(url); - const requestHandler = await interceptor.handleRequest(url); - - if (requestHandler.modifiesResponse()) { - const modifiedBuffer = requestHandler.modifyBody(Buffer.from(body)); - return modifiedBuffer.toString("utf8"); - } - - return body; - } - it("Should not filter packages when skipMinimumPackageAge is enabled", async () => { minimumPackageAgeSettings = 5; skipMinimumPackageAgeSetting = true; @@ -296,4 +279,23 @@ describe("npmInterceptor minimum package age", async () => { return date; } + + /** + * @param {import("../interceptorBuilder.js").Interceptor} interceptor + * @param {string} body + * @returns {Promise} + */ + async function runModifyNpmInfoRequest(url, body) { + const interceptor = npmInterceptorForUrl(url); + const requestHandler = await interceptor.handleRequest(url); + + if (requestHandler.modifiesResponse()) { + const modifiedBuffer = requestHandler.modifyBody(Buffer.from(body), { + ["content-type"]: "application/json", + }); + return modifiedBuffer.toString("utf8"); + } + + return body; + } }); From 157725a25a5367257cc0cb716f4292283f104e28 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 10:29:09 +0100 Subject: [PATCH 213/345] Cleanup --- .../interceptors/npm/modifyNpmInfo.js | 8 ++-- .../responseInterceptorBuilder.js | 43 ------------------- 2 files changed, 5 insertions(+), 46 deletions(-) delete mode 100644 packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index acb7d07..ea30d31 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -1,7 +1,9 @@ import { getMinimumPackageAgeHours } from "../../../config/settings.js"; import { ui } from "../../../environment/userInteraction.js"; -let hasSuppressedVersions = false; +const state = { + hasSuppressedVersions: false, +}; /** * @param {NodeJS.Dict} headers @@ -109,7 +111,7 @@ export function modifyNpmInfoResponse(body, headers) { * @param {string} version */ function deleteVersionFromJson(json, version) { - hasSuppressedVersions = true; + state.hasSuppressedVersions = true; ui.writeVerbose( `Safe-chain: ${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).` @@ -168,7 +170,7 @@ function getMostRecentTag(tagList) { * @returns {boolean} */ export function getHasSuppressedVersions() { - return hasSuppressedVersions; + return state.hasSuppressedVersions; } /** diff --git a/packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js deleted file mode 100644 index 86d79e5..0000000 --- a/packages/safe-chain/src/registryProxy/interceptors/responseInterceptorBuilder.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @typedef {Object} ResponseInterceptorBuilder - * @property {() => ResponseInterceptor} build - * @property {(modificationFunc: (body: Buffer) => Buffer) => void} modifyBody - * - * @typedef {Object} ResponseInterceptor - * @property {(buffer: Buffer) => Buffer} modifyBody - */ - -/** - * @returns {ResponseInterceptorBuilder} - */ -export function createResponseInterceptorBuilder() { - /** @type {Array<(body: Buffer) => Buffer>} */ - let modifyBodyFuncs = []; - - return { - modifyBody: (func) => modifyBodyFuncs.push(func), - build: () => createResponseInterceptor(modifyBodyFuncs), - }; -} - -/** - * @returns {ResponseInterceptor} - * @param {Array<(body: Buffer) => Buffer>} modifyBodyFuncs - */ -function createResponseInterceptor(modifyBodyFuncs) { - /** - * @param {Buffer} body - * @returns {Buffer} - */ - function modifyBody(body) { - let modifiedBody = body; - - for (var func of modifyBodyFuncs) { - modifiedBody = func(body); - } - - return modifiedBody; - } - - return { modifyBody }; -} From 4b5bef8d6a949ac2a9e948537e4730c8465b8bb9 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 12 Nov 2025 16:15:32 +0100 Subject: [PATCH 214/345] Clarify support for ecosystems and pip status Updated README to clarify that Aikido Safe Chain currently supports only JavaScript ecosystems and marks pip and pip3 as beta. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 687bedf..44ac933 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ``` For Python (beta): - ```shell pip3 install safe-chain-pi-test ``` From ddf867bf535e7e397e787495406b05bd24d5e3d7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 10:41:53 +0100 Subject: [PATCH 215/345] Fix readme indentation --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 44ac933..718db41 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,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/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. 4. **Verify the installation** by running one of the following commands: From 59963a6f3481ea11036c196d3a473f033a0cb41d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 11:40:29 +0100 Subject: [PATCH 216/345] Make warning in readme less prominent --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 718db41..6c7ca9a 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,10 @@ The Aikido Safe Chain runs a lightweight proxy server that intercepts package do ### Minimum package age (npm only) -**⚠️ This feature only applies to npm-based package managers (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip.** - For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag. +⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip. + ### Shell Integration 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: From c6bcd6f646e331cdb7282f8b73dc4ebea3518c93 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 14:12:44 +0100 Subject: [PATCH 217/345] Add feature flag in setup for python support. --- packages/safe-chain/bin/safe-chain.js | 19 +++- .../safe-chain/src/config/cliArguments.js | 32 +++++- .../src/shell-integration/helpers.js | 24 ++-- .../src/shell-integration/setup-ci.js | 26 +++-- .../safe-chain/src/shell-integration/setup.js | 7 +- .../include-python/init-fish.fish | 88 ++++++++++++++ .../include-python/init-posix.sh | 80 +++++++++++++ .../include-python/init-pwsh.ps1 | 107 ++++++++++++++++++ .../startup-scripts/init-fish.fish | 18 --- .../startup-scripts/init-posix.sh | 18 --- .../startup-scripts/init-pwsh.ps1 | 19 ---- test/e2e/pip-ci.e2e.spec.js | 64 ++++++++--- test/e2e/pip.e2e.spec.js | 11 +- 13 files changed, 413 insertions(+), 100 deletions(-) create mode 100644 packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish create mode 100644 packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh create mode 100644 packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 30f4086..94e4e1f 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -6,6 +6,7 @@ import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; import { setupCi } from "../src/shell-integration/setup-ci.js"; +import { initializeCliArguments } from "../src/config/cliArguments.js"; if (process.argv.length < 3) { ui.writeError("No command provided. Please provide a command to execute."); @@ -14,6 +15,8 @@ if (process.argv.length < 3) { process.exit(1); } +initializeCliArguments(process.argv); + const command = process.argv[2]; if (command === "help" || command === "--help" || command === "-h") { @@ -56,6 +59,11 @@ function writeHelp() { "safe-chain setup" )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` ); + ui.writeInformation( + ` ${chalk.yellow( + "--include-python" + )}: Experimental: include Python package managers (pip, pip3) in the setup.` + ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown" @@ -67,9 +75,14 @@ function writeHelp() { )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` ); ui.writeInformation( - `- ${chalk.cyan( - "safe-chain --version" - )} (or ${chalk.cyan("-v")}): Display the current version of safe-chain.` + ` ${chalk.yellow( + "--include-python" + )}: Experimental: include Python package managers (pip, pip3) in the setup.` + ); + ui.writeInformation( + `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan( + "-v" + )}): Display the current version of safe-chain.` ); ui.emptyLine(); } diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 04645d8..ba16042 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,8 +1,9 @@ /** - * @type {{loggingLevel: string | undefined}} + * @type {{loggingLevel: string | undefined, includePython: boolean}} */ const state = { loggingLevel: undefined, + includePython: false, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; @@ -27,6 +28,7 @@ export function initializeCliArguments(args) { } setLoggingLevel(safeChainArgs); + setIncludePython(args); return remainingArgs; } @@ -64,3 +66,31 @@ function setLoggingLevel(args) { export function getLoggingLevel() { return state.loggingLevel; } + +/** + * @param {string[]} args + */ +function setIncludePython(args) { + // This flag doesn't have the --safe-chain- prefix because + // it is only used for the safe-chain command itself and + // not when wrapped around package manager commands. + state.includePython = hasFlagArg(args, "--include-python"); +} + +export function includePython() { + return state.includePython; +} + +/** + * @param {string[]} args + * @param {string} flagName + * @returns {boolean} + */ +function hasFlagArg(args, flagName) { + for (const arg of args) { + if (arg.toLowerCase() === flagName.toLowerCase()) { + return true; + } + } + return false; +} diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index c405c54..da8e98b 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -2,28 +2,30 @@ import { spawnSync } from "child_process"; import * as os from "os"; import fs from "fs"; import path from "path"; +import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; /** * @typedef {Object} AikidoTool * @property {string} tool * @property {string} aikidoCommand + * @property {string} ecoSystem */ /** * @type {AikidoTool[]} */ export const knownAikidoTools = [ - { tool: "npm", aikidoCommand: "aikido-npm" }, - { tool: "npx", aikidoCommand: "aikido-npx" }, - { tool: "yarn", aikidoCommand: "aikido-yarn" }, - { tool: "pnpm", aikidoCommand: "aikido-pnpm" }, - { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, - { tool: "bun", aikidoCommand: "aikido-bun" }, - { tool: "bunx", aikidoCommand: "aikido-bunx" }, - { tool: "pip", aikidoCommand: "aikido-pip" }, - { tool: "pip3", aikidoCommand: "aikido-pip3" }, - { tool: "python", aikidoCommand: "aikido-python" }, - { tool: "python3", aikidoCommand: "aikido-python3" }, + { tool: "npm", aikidoCommand: "aikido-npm", ecoSystem: ECOSYSTEM_JS }, + { tool: "npx", aikidoCommand: "aikido-npx", ecoSystem: ECOSYSTEM_JS }, + { tool: "yarn", aikidoCommand: "aikido-yarn", ecoSystem: ECOSYSTEM_JS }, + { tool: "pnpm", aikidoCommand: "aikido-pnpm", ecoSystem: ECOSYSTEM_JS }, + { tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS }, + { tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS }, + { tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS }, + { tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY }, + { tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY }, + { tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY }, + { tool: "python3", aikidoCommand: "aikido-python3", ecoSystem: ECOSYSTEM_PY }, // 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/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 8793832..f63ad32 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -1,10 +1,12 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; -import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +import { getPackageManagerList, knownAikidoTools } from "./helpers.js"; import fs from "fs"; import os from "os"; import path from "path"; import { fileURLToPath } from "url"; +import { includePython } from "../config/cliArguments.js"; +import { ECOSYSTEM_PY } from "../config/settings.js"; /** * Loops over the detected shells and calls the setup function for each. @@ -53,7 +55,7 @@ function createUnixShims(shimsDir) { // Create a shim for each tool let created = 0; - for (const toolInfo of knownAikidoTools) { + for (const toolInfo of getToolsToSetup()) { const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); @@ -66,9 +68,7 @@ function createUnixShims(shimsDir) { created++; } - ui.writeInformation( - `Created ${created} Unix shim(s) in ${shimsDir}` - ); + ui.writeInformation(`Created ${created} Unix shim(s) in ${shimsDir}`); } /** @@ -96,19 +96,17 @@ function createWindowsShims(shimsDir) { // Create a shim for each tool let created = 0; - for (const toolInfo of knownAikidoTools) { + for (const toolInfo of getToolsToSetup()) { const shimContent = template .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool) .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand); - const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`; + const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`; fs.writeFileSync(shimPath, shimContent, "utf-8"); created++; } - ui.writeInformation( - `Created ${created} Windows shim(s) in ${shimsDir}` - ); + ui.writeInformation(`Created ${created} Windows shim(s) in ${shimsDir}`); } /** @@ -145,3 +143,11 @@ function modifyPathForCi(shimsDir) { ui.writeInformation("##vso[task.prependpath]" + shimsDir); } } + +function getToolsToSetup() { + if (includePython()) { + return knownAikidoTools; + } else { + return knownAikidoTools.filter((tool) => tool.ecoSystem !== ECOSYSTEM_PY); + } +} diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index 7185c5a..e734858 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -6,6 +6,7 @@ import fs from "fs"; import os from "os"; import path from "path"; import { fileURLToPath } from "url"; +import { includePython } from "../config/cliArguments.js"; /** * Loops over the detected shells and calls the setup function for each. @@ -104,7 +105,11 @@ function copyStartupFiles() { // Use absolute path for source const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - const sourcePath = path.resolve(__dirname, "startup-scripts", file); + const sourcePath = path.resolve( + __dirname, + includePython() ? "startup-scripts/include-python" : "startup-scripts", + file + ); fs.copyFileSync(sourcePath, targetPath); } } diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish new file mode 100644 index 0000000..ebf89ff --- /dev/null +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish @@ -0,0 +1,88 @@ +function printSafeChainWarning + set original_cmd $argv[1] + + # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" + set_color -b yellow black + printf "Warning:" + set_color normal + printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd + + # Cyan text for the install command + printf "Install safe-chain by using " + set_color cyan + printf "npm install -g @aikidosec/safe-chain" + set_color normal + printf ".\n" +end + +function wrapSafeChainCommand + set original_cmd $argv[1] + set aikido_cmd $argv[2] + set cmd_args $argv[3..-1] + + if type -q $aikido_cmd + # If the aikido command is available, just run it with the provided arguments + $aikido_cmd $cmd_args + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning $original_cmd + command $original_cmd $cmd_args + end +end + +function npx + wrapSafeChainCommand "npx" "aikido-npx" $argv +end + +function yarn + wrapSafeChainCommand "yarn" "aikido-yarn" $argv +end + +function pnpm + wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv +end + +function pnpx + wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv +end + +function bun + wrapSafeChainCommand "bun" "aikido-bun" $argv +end + +function bunx + wrapSafeChainCommand "bunx" "aikido-bunx" $argv +end + +function npm + # If args is just -v or --version and nothing else, just run the `npm -v` command + # This is because nvm uses this to check the version of npm + set argc (count $argv) + if test $argc -eq 1 + switch $argv[1] + case "-v" "--version" + command npm $argv + return + end + end + + wrapSafeChainCommand "npm" "aikido-npm" $argv +end + +function pip + wrapSafeChainCommand "pip" "aikido-pip" $argv +end + +function pip3 + wrapSafeChainCommand "pip3" "aikido-pip3" $argv +end + +# `python -m pip`, `python -m pip3`. +function python + wrapSafeChainCommand "python" "aikido-python" $argv +end + +# `python3 -m pip`, `python3 -m pip3'. +function python3 + wrapSafeChainCommand "python3" "aikido-python3" $argv +end diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh new file mode 100644 index 0000000..278b31a --- /dev/null +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh @@ -0,0 +1,80 @@ + +function printSafeChainWarning() { + # \033[43;30m is used to set the background color to yellow and text color to black + # \033[0m is used to reset the text formatting + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" + # \033[36m is used to set the text color to cyan + printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" +} + +function wrapSafeChainCommand() { + local original_cmd="$1" + local aikido_cmd="$2" + + # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@ + # so that "$@" now contains only the arguments passed to the original command + shift 2 + + if command -v "$aikido_cmd" > /dev/null 2>&1; then + # If the aikido command is available, just run it with the provided arguments + "$aikido_cmd" "$@" + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning "$original_cmd" + + command "$original_cmd" "$@" + fi +} + +function npx() { + wrapSafeChainCommand "npx" "aikido-npx" "$@" +} + +function yarn() { + wrapSafeChainCommand "yarn" "aikido-yarn" "$@" +} + +function pnpm() { + wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@" +} + +function pnpx() { + wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" +} + +function bun() { + wrapSafeChainCommand "bun" "aikido-bun" "$@" +} + +function bunx() { + wrapSafeChainCommand "bunx" "aikido-bunx" "$@" +} + +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 + # This is because nvm uses this to check the version of npm + command npm "$@" + return + fi + + wrapSafeChainCommand "npm" "aikido-npm" "$@" +} + +function pip() { + wrapSafeChainCommand "pip" "aikido-pip" "$@" +} + +function pip3() { + wrapSafeChainCommand "pip3" "aikido-pip3" "$@" +} + +# `python -m pip`, `python -m pip3`. +function python() { + wrapSafeChainCommand "python" "aikido-python" "$@" +} + +# `python3 -m pip`, `python3 -m pip3'. +function python3() { + wrapSafeChainCommand "python3" "aikido-python3" "$@" +} diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 new file mode 100644 index 0000000..b692107 --- /dev/null +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -0,0 +1,107 @@ +function Write-SafeChainWarning { + param([string]$Command) + + # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" + Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline + Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." + + # Cyan text for the install command + Write-Host "Install safe-chain by using " -NoNewline + Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline + Write-Host "." +} + +function Test-CommandAvailable { + param([string]$Command) + + try { + Get-Command $Command -ErrorAction Stop | Out-Null + return $true + } + catch { + return $false + } +} + +function Invoke-RealCommand { + param( + [string]$Command, + [string[]]$Arguments + ) + + # Find the real executable to avoid calling our wrapped functions + $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 + if ($realCommand) { + & $realCommand.Source @Arguments + } +} + +function Invoke-WrappedCommand { + param( + [string]$OriginalCmd, + [string]$AikidoCmd, + [string[]]$Arguments + ) + + if (Test-CommandAvailable $AikidoCmd) { + & $AikidoCmd @Arguments + } + else { + Write-SafeChainWarning $OriginalCmd + Invoke-RealCommand $OriginalCmd $Arguments + } +} + +function npx { + Invoke-WrappedCommand "npx" "aikido-npx" $args +} + +function yarn { + Invoke-WrappedCommand "yarn" "aikido-yarn" $args +} + +function pnpm { + Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args +} + +function pnpx { + Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args +} + +function bun { + Invoke-WrappedCommand "bun" "aikido-bun" $args +} + +function bunx { + Invoke-WrappedCommand "bunx" "aikido-bunx" $args +} + +function npm { + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) { + Invoke-RealCommand "npm" $args + return + } + + Invoke-WrappedCommand "npm" "aikido-npm" $args +} + +function pip { + Invoke-WrappedCommand "pip" "aikido-pip" $args +} + +function pip3 { + Invoke-WrappedCommand "pip3" "aikido-pip3" $args +} + +# `python -m pip`, `python -m pip3`. +function python { + Invoke-WrappedCommand 'python' 'aikido-python' $args +} + +# `python3 -m pip`, `python3 -m pip3'. +function python3 { + Invoke-WrappedCommand 'python3' 'aikido-python3' $args +} + 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 ebf89ff..29d6bf3 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,21 +68,3 @@ function npm wrapSafeChainCommand "npm" "aikido-npm" $argv end - -function pip - wrapSafeChainCommand "pip" "aikido-pip" $argv -end - -function pip3 - wrapSafeChainCommand "pip3" "aikido-pip3" $argv -end - -# `python -m pip`, `python -m pip3`. -function python - wrapSafeChainCommand "python" "aikido-python" $argv -end - -# `python3 -m pip`, `python3 -m pip3'. -function python3 - wrapSafeChainCommand "python3" "aikido-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 278b31a..353c6c0 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 @@ -60,21 +60,3 @@ function npm() { wrapSafeChainCommand "npm" "aikido-npm" "$@" } - -function pip() { - wrapSafeChainCommand "pip" "aikido-pip" "$@" -} - -function pip3() { - wrapSafeChainCommand "pip3" "aikido-pip3" "$@" -} - -# `python -m pip`, `python -m pip3`. -function python() { - wrapSafeChainCommand "python" "aikido-python" "$@" -} - -# `python3 -m pip`, `python3 -m pip3'. -function python3() { - wrapSafeChainCommand "python3" "aikido-python3" "$@" -} 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 b692107..a449405 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,22 +86,3 @@ function npm { Invoke-WrappedCommand "npm" "aikido-npm" $args } - -function pip { - Invoke-WrappedCommand "pip" "aikido-pip" $args -} - -function pip3 { - Invoke-WrappedCommand "pip3" "aikido-pip3" $args -} - -# `python -m pip`, `python -m pip3`. -function python { - Invoke-WrappedCommand 'python' 'aikido-python' $args -} - -# `python3 -m pip`, `python3 -m pip3'. -function python3 { - Invoke-WrappedCommand 'python3' 'aikido-python3' $args -} - diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index fe013bb..63bfd90 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -25,31 +25,55 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it("does not intercept python3 --version", async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("python3 --version"); - assert.ok(result.output.match(/Python \d+\.\d+\.\d+/), `Output was: ${result.output}`); - assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 command"); + assert.ok( + result.output.match(/Python \d+\.\d+\.\d+/), + `Output was: ${result.output}` + ); + assert.ok( + !result.output.includes("Safe-chain"), + "Safe Chain should not intercept generic python3 command" + ); }); it("does not intercept python3 -c 'print(\"hello\")'", async () => { const shell = await container.openShell("zsh"); const result = await shell.runCommand("python3 -c 'print(\"hello\")'"); - assert.ok(result.output.includes("hello"), `Output was: ${result.output}`); - assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command"); + assert.ok( + result.output.includes("hello"), + `Output was: ${result.output}` + ); + assert.ok( + !result.output.includes("Safe-chain"), + "Safe Chain should not intercept generic python3 -c command" + ); }); it("does not intercept python3 test.py", async () => { const shell = await container.openShell("zsh"); await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); const result = await shell.runCommand("python3 test.py"); - assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`); - assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 script execution"); + assert.ok( + result.output.includes("Hello from test.py!"), + `Output was: ${result.output}` + ); + assert.ok( + !result.output.includes("Safe-chain"), + "Safe Chain should not intercept generic python3 script execution" + ); }); it("does not intercept python test.py", async () => { const shell = await container.openShell("zsh"); await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); const result = await shell.runCommand("python test.py"); - assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`); - assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python script execution"); + assert.ok( + result.output.includes("Hello from test.py!"), + `Output was: ${result.output}` + ); + assert.ok( + !result.output.includes("Safe-chain"), + "Safe Chain should not intercept generic python script execution" + ); }); }); @@ -57,7 +81,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => { // Setup safe-chain CI shims const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "safe-chain setup-ci --include-python" + ); // Add $HOME/.safe-chain/shims to PATH for subsequent shells await installationShell.runCommand( @@ -73,9 +99,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { "pip3 install --break-system-packages certifi" ); - const hasExpectedOutput = result.output.includes( - "no malware found." - ); + const hasExpectedOutput = result.output.includes("no malware found."); assert.ok( hasExpectedOutput, hasExpectedOutput @@ -86,7 +110,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes python -m pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "safe-chain setup-ci --include-python" + ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" ); @@ -107,7 +133,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "safe-chain setup-ci --include-python" + ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" ); @@ -128,7 +156,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes pip through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "safe-chain setup-ci --include-python" + ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" ); @@ -149,7 +179,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => { const installationShell = await container.openShell(shell); - await installationShell.runCommand("safe-chain setup-ci"); + await installationShell.runCommand( + "safe-chain setup-ci --include-python" + ); await installationShell.runCommand( "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" ); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 3d3b4dd..9a1adec 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -15,7 +15,7 @@ describe("E2E: pip coverage", () => { await container.start(); const installationShell = await container.openShell("zsh"); - await installationShell.runCommand("safe-chain setup"); + await installationShell.runCommand("safe-chain setup --include-python"); }); afterEach(async () => { @@ -96,7 +96,9 @@ 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 --break-system-packages requests"); + const result = await shell.runCommand( + "python3 -m pip install --break-system-packages requests" + ); assert.ok( result.output.includes("no malware found."), @@ -329,6 +331,9 @@ describe("E2E: pip coverage", () => { const result = await shell.runCommand( "pip3 install --break-system-packages requests --safe-chain-logging=verbose" ); - assert.ok(result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}`); + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); }); }); From 41998dff956f16b68457e391b1740af1d3301dd1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 14 Nov 2025 14:18:12 +0100 Subject: [PATCH 218/345] Describe safe-chain setup --include-python in documentation. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index f169747..5541150 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ```shell safe-chain setup ``` + + To enable Python (pip/pip3) support (beta), use the `--include-python` flag: + ```shell + safe-chain setup --include-python + ``` + 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 one of the following commands: @@ -120,6 +126,12 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after safe-chain setup-ci ``` +To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag: + +```shell +safe-chain setup-ci --include-python +``` + This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands. ## Supported Platforms From 87fcb7239a34a19d9fbd65b3f0bf75ec839cb729 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 17 Nov 2025 10:03:38 -0800 Subject: [PATCH 219/345] Adapt per review --- .../src/packagemanager/pip/runPipCommand.js | 46 +++++++++---------- .../packagemanager/pip/runPipCommand.spec.js | 24 +++++----- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 309ee47..f37e9b0 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -32,8 +32,7 @@ export async function runPip(command, args) { const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); - if (!env.PIP_CONFIG_FILE) { - // Build pip config INI + if (!env.PIP_CONFIG_FILE) { // Build pip config INI /** @type {{ global: { cert: string, proxy?: string } }} */ const configObj = { global: { cert: combinedCaPath } }; if (proxy) { @@ -43,9 +42,7 @@ export async function runPip(command, args) { await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; - } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { - // Existing pip config file present and exists on disk. - // Lets merge in our cert and proxy settings if not already present + } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { // Merge pip config INI ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); const userConfig = env.PIP_CONFIG_FILE; @@ -56,24 +53,20 @@ export async function runPip(command, args) { // Ensure [global] section exists parsed.global = parsed.global || {}; - // Adding CERT and PROXY - // If either is already set, there's no neeed to throw an error - // MITM might fail and throw later if the proxy config is invalid - // Cert - if (typeof parsed.global.cert === "undefined") { - ui.writeVerbose("Safe-chain: Adding cert to temporary PIP_CONFIG_FILE."); - parsed.global.cert = combinedCaPath; + if (typeof parsed.global.cert !== "undefined") { + ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config."); } + parsed.global.cert = combinedCaPath; // Proxy - if (typeof parsed.global.proxy === "undefined") { - if (proxy) { - ui.writeVerbose("Safe-chain: Adding proxy to temporary PIP_CONFIG_FILE."); - parsed.global.proxy = proxy; - } + if (typeof parsed.global.proxy !== "undefined") { + ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config."); } - + if (proxy) { + parsed.global.proxy = proxy; + } + const updated = ini.stringify(parsed); // Save to a new temp file to avoid overwriting user's original config @@ -86,15 +79,20 @@ export async function runPip(command, args) { } // REQUESTS_CA_BUNDLE, SSL_CERT_FILE and PIP_CERT as extra safety nets. - if (!env.REQUESTS_CA_BUNDLE) { - env.REQUESTS_CA_BUNDLE = combinedCaPath; + if (env.REQUESTS_CA_BUNDLE) { + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); } - if (!env.SSL_CERT_FILE) { - env.SSL_CERT_FILE = combinedCaPath; + env.REQUESTS_CA_BUNDLE = combinedCaPath; + + if (env.SSL_CERT_FILE) { + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); } - if (!env.PIP_CERT) { - env.PIP_CERT = combinedCaPath; + env.SSL_CERT_FILE = combinedCaPath; + + if (env.PIP_CERT) { + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); } + env.PIP_CERT = combinedCaPath; const result = await safeSpawn(command, args, { 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 ecd8e0f..f67077a 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -217,10 +217,10 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains"); - // New file merged: cert added (was missing), proxy preserved (was present) + // New file: cert and proxy always overwritten const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); - assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new cert injected"); - assert.strictEqual(newParsed.global.proxy, "http://original:9999", "existing proxy should be preserved in new file"); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); @@ -247,11 +247,11 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved"); assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved"); - // New temp config preserves existing values (no override when already set) - const newContent = await fs.readFile(newCfgPath, "utf-8"); - const newParsed = ini.parse(newContent); - assert.strictEqual(newParsed.global.cert, "/path/to/existing.pem", "existing cert preserved in new temp config"); - assert.strictEqual(newParsed.global.proxy, "http://original:9999", "existing proxy preserved in new temp config"); + // New temp config: cert and proxy always overwritten + const newContent = await fs.readFile(newCfgPath, "utf-8"); + const newParsed = ini.parse(newContent); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); @@ -272,10 +272,10 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged"); assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing"); - // New file preserves existing cert and adds proxy (since it was missing) - const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); - assert.strictEqual(newParsed.global.cert, "/path/to/existing.pem", "existing cert preserved (not overridden)"); - assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy added from env"); + // New file: cert and proxy always overwritten + const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); + assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); + assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); }); From 0e5b9b23f16a460631afe9fe27f9edb6bd56d9fd Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Mon, 17 Nov 2025 10:18:47 -0800 Subject: [PATCH 220/345] Fix tests --- .../packagemanager/pip/runPipCommand.spec.js | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index f67077a..4af0b40 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -52,25 +52,6 @@ describe("runPipCommand environment variable handling", () => { mock.reset(); }); - it("should not overwrite existing env vars for certs and config", async () => { - // Set custom env vars before merge - customEnv = { - REQUESTS_CA_BUNDLE: "/custom/ca-bundle.pem", - SSL_CERT_FILE: "/custom/ssl-cert.pem", - PIP_CERT: "/custom/pip-cert.pem", - PIP_CONFIG_FILE: "/custom/pip.conf" - }; - const res = await runPip("pip3", ["install", "requests"]); - assert.strictEqual(res.status, 0); - assert.ok(capturedArgs, "safeSpawn should have been called"); - // Should preserve custom env vars - assert.strictEqual(capturedArgs.options.env.REQUESTS_CA_BUNDLE, "/custom/ca-bundle.pem"); - assert.strictEqual(capturedArgs.options.env.SSL_CERT_FILE, "/custom/ssl-cert.pem"); - assert.strictEqual(capturedArgs.options.env.PIP_CERT, "/custom/pip-cert.pem"); - assert.strictEqual(capturedArgs.options.env.PIP_CONFIG_FILE, "/custom/pip.conf"); - customEnv = null; - }); - it("should set PIP_CERT env var and create config file", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); @@ -278,4 +259,35 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; }); + + it("should log warnings when cert and proxy are already set in user config file", async () => { + const tmpDir = os.tmpdir(); + const cfgPath = path.join(tmpDir, `safe-chain-test-pip-warn-${Date.now()}.ini`); + const initialIni = [ + "[global]", + "cert = /user/cert.pem", + "proxy = http://user-proxy:9999", + "" + ].join("\n"); + await fs.writeFile(cfgPath, initialIni, "utf-8"); + + process.env.PIP_CONFIG_FILE = cfgPath; + const mod = await import("./runPipCommand.js"); + // Capture stdout/stderr + let output = ""; + const originalWrite = process.stdout.write; + const originalError = process.stderr.write; + process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); }; + process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); }; + + await mod.runPip("pip3", ["install", "requests"]); + + process.stdout.write = originalWrite; + process.stderr.write = originalError; + + assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output"); + assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output"); + delete process.env.PIP_CONFIG_FILE; + customEnv = null; + }); }); From f030b16adf49620202141be8ac435ee1988a27c3 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Fri, 21 Nov 2025 13:33:33 +0100 Subject: [PATCH 221/345] rm obvious comments --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index f37e9b0..cb85c89 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -32,7 +32,7 @@ export async function runPip(command, args) { const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); - if (!env.PIP_CONFIG_FILE) { // Build pip config INI + if (!env.PIP_CONFIG_FILE) { /** @type {{ global: { cert: string, proxy?: string } }} */ const configObj = { global: { cert: combinedCaPath } }; if (proxy) { @@ -42,7 +42,7 @@ export async function runPip(command, args) { await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; - } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { // Merge pip config INI + } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); const userConfig = env.PIP_CONFIG_FILE; From 0a0ac85542ab471e6b6d75e279e46238f9bf734d Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 21 Nov 2025 09:41:07 -0800 Subject: [PATCH 222/345] Adapt per review --- .../src/packagemanager/pip/runPipCommand.js | 79 ++++++++++++++----- .../packagemanager/pip/runPipCommand.spec.js | 45 +++++++---- 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index cb85c89..8efa000 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -9,10 +9,46 @@ import path from "node:path"; import ini from "ini"; /** - * @param {string} command - * @param {string[]} args - * - * @returns {Promise<{status: number}>} + * Sets fallback CA bundle environment variables used by Python libraries. + * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python + * network libraries respect the combined CA bundle, even if they don't read pip's config. + * + * @param {NodeJS.ProcessEnv} env - Environment object to modify + * @param {string} combinedCaPath - Path to the combined CA bundle + */ +function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { + // REQUESTS_CA_BUNDLE: Used by the popular 'requests' library + if (env.REQUESTS_CA_BUNDLE) { + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); + } + env.REQUESTS_CA_BUNDLE = combinedCaPath; + + // SSL_CERT_FILE: Used by some Python SSL libraries and urllib + if (env.SSL_CERT_FILE) { + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); + } + env.SSL_CERT_FILE = combinedCaPath; + + // PIP_CERT: Pip's own environment variable for certificate verification + if (env.PIP_CERT) { + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); + } + env.PIP_CERT = combinedCaPath; +} + +/** + * Runs a pip command with safe-chain's certificate bundle and proxy configuration. + * + * Creates a temporary pip config file (cleaned up automatically after execution) to configure: + * - Certificate bundle for HTTPS verification + * - Proxy settings if available + * + * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges + * their settings with safe-chain's, leaving the original file unchanged. + * + * @param {string} command - The pip command to execute (e.g., 'pip3') + * @param {string[]} args - Command line arguments to pass to pip + * @returns {Promise<{status: number}>} Exit status of the pip command */ export async function runPip(command, args) { try { @@ -26,12 +62,15 @@ export async function runPip(command, args) { // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file) // will tell pip to use the provided CA bundle for HTTPS verification. - // Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY + // Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active), + // otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || ''; const tmpDir = os.tmpdir(); const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); + let cleanupConfigPath = null; // Track temp file for cleanup + // Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order if (!env.PIP_CONFIG_FILE) { /** @type {{ global: { cert: string, proxy?: string } }} */ const configObj = { global: { cert: combinedCaPath } }; @@ -41,6 +80,7 @@ export async function runPip(command, args) { const pipConfig = ini.stringify(configObj); await fs.writeFile(pipConfigPath, pipConfig); env.PIP_CONFIG_FILE = pipConfigPath; + cleanupConfigPath = pipConfigPath; } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) { ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings."); @@ -72,32 +112,31 @@ export async function runPip(command, args) { // Save to a new temp file to avoid overwriting user's original config await fs.writeFile(pipConfigPath, updated, "utf-8"); env.PIP_CONFIG_FILE = pipConfigPath; + cleanupConfigPath = pipConfigPath; } else { // The user provided PIP_CONFIG_FILE does not exist on disk // PIP will handle this as an error and inform the user } - // REQUESTS_CA_BUNDLE, SSL_CERT_FILE and PIP_CERT as extra safety nets. - if (env.REQUESTS_CA_BUNDLE) { - ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); - } - env.REQUESTS_CA_BUNDLE = combinedCaPath; - - if (env.SSL_CERT_FILE) { - ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); - } - env.SSL_CERT_FILE = combinedCaPath; - - if (env.PIP_CERT) { - ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); - } - env.PIP_CERT = combinedCaPath; + // Set fallback CA bundle environment variables for Python libraries that don't read pip config + setFallbackCaBundleEnvironmentVariables(env, combinedCaPath); const result = await safeSpawn(command, args, { stdio: "inherit", env, }); + + // Cleanup temporary config file if we created one + if (cleanupConfigPath) { + try { + await fs.unlink(cleanupConfigPath); + } catch (error) { + // Ignore cleanup errors - the file may have already been deleted or is inaccessible + // Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform + } + } + return { status: result.status }; } catch (/** @type any */ error) { if (error.status) { diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 4af0b40..5d88b0e 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -9,15 +9,25 @@ describe("runPipCommand environment variable handling", () => { let runPip; let capturedArgs = null; let customEnv = null; + let capturedConfigContent = null; // Capture config file content before cleanup beforeEach(async () => { capturedArgs = null; + capturedConfigContent = null; - // Mock safeSpawn to capture args + // Mock safeSpawn to capture args and config file content before cleanup mock.module("../../utils/safeSpawn.js", { namedExports: { safeSpawn: async (command, args, options) => { capturedArgs = { command, args, options }; + // Capture the config file content before the function cleans it up + if (options.env.PIP_CONFIG_FILE) { + try { + capturedConfigContent = await fs.readFile(options.env.PIP_CONFIG_FILE, "utf-8"); + } catch (e) { + // Ignore if file doesn't exist or can't be read + } + } return { status: 0 }; }, }, @@ -151,9 +161,9 @@ describe("runPipCommand environment variable handling", () => { const originalParsed = ini.parse(originalContent); assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); - // New file has merged settings - const newContent = await fs.readFile(newCfgPath, "utf-8"); - const newParsed = ini.parse(newContent); + // New file has merged settings (read from captured content before cleanup) + assert.ok(capturedConfigContent, "config content should have been captured"); + const newParsed = ini.parse(capturedConfigContent); assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert"); assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env"); assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved"); @@ -166,8 +176,8 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(res.status, 0); const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; - const content = await fs.readFile(configPath, "utf-8"); - const parsed = ini.parse(content); + assert.ok(capturedConfigContent, "config content should have been captured"); + const parsed = ini.parse(capturedConfigContent); assert.ok(parsed.global, "[global] should exist after creation"); assert.strictEqual( parsed.global.proxy, @@ -198,8 +208,9 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert"); assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains"); - // New file: cert and proxy always overwritten - const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); + // New file: cert and proxy always overwritten (read from captured content) + assert.ok(capturedConfigContent, "config content should have been captured"); + const newParsed = ini.parse(capturedConfigContent); assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; @@ -228,9 +239,9 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved"); assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved"); - // New temp config: cert and proxy always overwritten - const newContent = await fs.readFile(newCfgPath, "utf-8"); - const newParsed = ini.parse(newContent); + // New temp config: cert and proxy always overwritten (read from captured content) + assert.ok(capturedConfigContent, "config content should have been captured"); + const newParsed = ini.parse(capturedConfigContent); assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; @@ -253,8 +264,9 @@ describe("runPipCommand environment variable handling", () => { assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged"); assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing"); - // New file: cert and proxy always overwritten - const newParsed = ini.parse(await fs.readFile(newCfgPath, "utf-8")); + // New file: cert and proxy always overwritten (read from captured content) + assert.ok(capturedConfigContent, "config content should have been captured"); + const newParsed = ini.parse(capturedConfigContent); assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config"); assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config"); customEnv = null; @@ -271,8 +283,8 @@ describe("runPipCommand environment variable handling", () => { ].join("\n"); await fs.writeFile(cfgPath, initialIni, "utf-8"); - process.env.PIP_CONFIG_FILE = cfgPath; - const mod = await import("./runPipCommand.js"); + customEnv = { PIP_CONFIG_FILE: cfgPath }; + // Capture stdout/stderr let output = ""; const originalWrite = process.stdout.write; @@ -280,14 +292,13 @@ describe("runPipCommand environment variable handling", () => { process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); }; process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); }; - await mod.runPip("pip3", ["install", "requests"]); + await runPip("pip3", ["install", "requests"]); process.stdout.write = originalWrite; process.stderr.write = originalError; assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output"); assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output"); - delete process.env.PIP_CONFIG_FILE; customEnv = null; }); }); From ab1aa0dce9f0f72a94b2d472694290288d1513b7 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 21 Nov 2025 09:58:43 -0800 Subject: [PATCH 223/345] Little cleanup --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 8efa000..3e83346 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -39,9 +39,9 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { /** * Runs a pip command with safe-chain's certificate bundle and proxy configuration. * - * Creates a temporary pip config file (cleaned up automatically after execution) to configure: - * - Certificate bundle for HTTPS verification - * - Proxy settings if available + * Creates a temporary pip config file to configure: + * - Cert bundle for HTTPS verification + * - Proxy settings * * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges * their settings with safe-chain's, leaving the original file unchanged. From 72bf44cb6d5b1f92a025605d757a20deb77095a0 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Fri, 21 Nov 2025 10:31:57 -0800 Subject: [PATCH 224/345] Fix linting issue --- packages/safe-chain/src/packagemanager/pip/runPipCommand.js | 2 +- .../safe-chain/src/packagemanager/pip/runPipCommand.spec.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 3e83346..23485ff 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -131,7 +131,7 @@ export async function runPip(command, args) { if (cleanupConfigPath) { try { await fs.unlink(cleanupConfigPath); - } catch (error) { + } catch { // Ignore cleanup errors - the file may have already been deleted or is inaccessible // Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform } diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index 5d88b0e..d0df961 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -24,7 +24,7 @@ describe("runPipCommand environment variable handling", () => { if (options.env.PIP_CONFIG_FILE) { try { capturedConfigContent = await fs.readFile(options.env.PIP_CONFIG_FILE, "utf-8"); - } catch (e) { + } catch { // Ignore if file doesn't exist or can't be read } } @@ -175,7 +175,6 @@ describe("runPipCommand environment variable handling", () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); - const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; assert.ok(capturedConfigContent, "config content should have been captured"); const parsed = ini.parse(capturedConfigContent); assert.ok(parsed.global, "[global] should exist after creation"); From f7de81645c15293faf2651901f3d98b772b4558c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 14:17:47 +0100 Subject: [PATCH 225/345] Fix cliArgument.js merge issue --- packages/safe-chain/src/config/cliArguments.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 0b7876c..180c565 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -52,20 +52,6 @@ function getLastArgEqualsValue(args, prefix) { return undefined; } -/** - * @param {string[]} args - * @param {string} flagName - * @returns {boolean} - */ -function hasFlagArg(args, flagName) { - for (const arg of args) { - if (arg.toLowerCase() === flagName.toLowerCase()) { - return true; - } - } - return false; -} - /** * @param {string[]} args * @returns {void} From e02e36cfea206fbb60123b68c077848328a5d98c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 14:49:40 +0100 Subject: [PATCH 226/345] Apply suggestion from @bitterpanda63 Adds comment about "utf8" encoding of json response. Co-authored-by: bitterpanda --- .../src/registryProxy/interceptors/npm/modifyNpmInfo.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index ea30d31..9080d60 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -53,6 +53,7 @@ export function modifyNpmInfoResponse(body, headers) { return body; } + // utf-8 is default encoding for JSON, so we don't check if charset is defined in content-type header const bodyContent = body.toString("utf8"); const bodyJson = JSON.parse(bodyContent); From 78c8da6faef85902296a40db102cf786bd3b0d9b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 14:44:01 +0100 Subject: [PATCH 227/345] Restore old "how it works" text in Readme.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17657bb..17b4abc 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,19 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: npm install -g @aikidosec/safe-chain ``` 2. **Setup the shell integration** by running: + ```shell safe-chain setup ``` To enable Python (pip/pip3) support (beta), use the `--include-python` flag: + ```shell safe-chain setup --include-python ``` 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 one of the following commands: @@ -50,6 +53,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: ``` For Python (beta): + ```shell pip3 install safe-chain-pi-test ``` @@ -68,7 +72,7 @@ safe-chain --version ### Malware Blocking -The Aikido Safe Chain runs 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)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine. ### Minimum package age (npm only) From 9a1092199dd6fec3c4ffd3710994573c04573de9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:08:31 +0100 Subject: [PATCH 228/345] Move getHeaderValueAsString to separate utils file --- .../src/registryProxy/http-utils.js | 17 +++++++++++++++ .../interceptors/npm/modifyNpmInfo.js | 21 +++---------------- 2 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/http-utils.js diff --git a/packages/safe-chain/src/registryProxy/http-utils.js b/packages/safe-chain/src/registryProxy/http-utils.js new file mode 100644 index 0000000..e14a977 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/http-utils.js @@ -0,0 +1,17 @@ +/** + * @param {NodeJS.Dict | undefined} headers + * @param {string} headerName + */ +export function getHeaderValueAsString(headers, headerName) { + if (!headers) { + return undefined; + } + + let header = headers[headerName]; + + if (Array.isArray(header)) { + return header.join(", "); + } + + return header; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 9080d60..0e7e41d 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -1,5 +1,6 @@ import { getMinimumPackageAgeHours } from "../../../config/settings.js"; import { ui } from "../../../environment/userInteraction.js"; +import { getHeaderValueAsString } from "../../http-utils.js"; const state = { hasSuppressedVersions: false, @@ -80,6 +81,8 @@ export function modifyNpmInfoResponse(body, headers) { continue; } + // Timestamps are compared as strings. + // This can be done because they are formatted in ISO8601, which is sortable. if (timestamp > cutOff) { deleteVersionFromJson(bodyJson, version); if (headers) { @@ -173,21 +176,3 @@ function getMostRecentTag(tagList) { export function getHasSuppressedVersions() { return state.hasSuppressedVersions; } - -/** - * @param {NodeJS.Dict | undefined} headers - * @param {string} headerName - */ -function getHeaderValueAsString(headers, headerName) { - if (!headers) { - return undefined; - } - - let header = headers[headerName]; - - if (Array.isArray(header)) { - return header.join(", "); - } - - return header; -} From 5834229427d1173456432850375471e09b9c2cd8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:13:25 +0100 Subject: [PATCH 229/345] Add comment in interceptorBuilder.js to clarify which api is for setup, and which api is used by the proxy. --- .../src/registryProxy/interceptors/interceptorBuilder.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index 362f31a..003aae7 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -115,6 +115,7 @@ function createRequestContext(targetUrl, eventEmitter) { return modifiedBody; } + // These functions are invoked in the proxy, allowing to apply the configured modifications return { blockResponse, modifyRequestHeaders: modifyRequestHeaders, @@ -123,6 +124,7 @@ function createRequestContext(targetUrl, eventEmitter) { }; } + // These functions are used to setup the modifications return { targetUrl, blockMalware: blockMalwareSetup, From 44ee58aa9be4797a76cb6a4b8edd1c3b06d24b43 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:22:41 +0100 Subject: [PATCH 230/345] Let modifyNpmInfoRequestHeaders return the header collection as well. --- .../interceptors/interceptorBuilder.js | 21 ++++++++++++------- .../interceptors/npm/modifyNpmInfo.js | 2 ++ .../src/registryProxy/mitmRequestHandler.js | 5 +++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index 003aae7..e25e641 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -10,14 +10,14 @@ import { EventEmitter } from "events"; * @typedef {Object} RequestInterceptionContext * @property {string} targetUrl * @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware - * @property {(modificationFunc: (headers: NodeJS.Dict) => void) => void} modifyRequestHeaders + * @property {(modificationFunc: (headers: NodeJS.Dict) => NodeJS.Dict) => void} modifyRequestHeaders * @property {(modificationFunc: (body: Buffer, headers: NodeJS.Dict | undefined) => Buffer) => void} modifyBody * @property {() => RequestInterceptionHandler} build * * * @typedef {Object} RequestInterceptionHandler * @property {{statusCode: number, message: string} | undefined} blockResponse - * @property {(headers: NodeJS.Dict | undefined) => void} modifyRequestHeaders + * @property {(headers: NodeJS.Dict | undefined) => NodeJS.Dict | undefined} modifyRequestHeaders * @property {() => boolean} modifiesResponse * @property {(body: Buffer, headers: NodeJS.Dict | undefined) => Buffer} modifyBody */ @@ -65,7 +65,7 @@ function buildInterceptor(requestHandlers) { function createRequestContext(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; - /** @type {Array<(headers: NodeJS.Dict) => void>} */ + /** @type {Array<(headers: NodeJS.Dict) => NodeJS.Dict>} */ let reqheaderModificationFuncs = []; /** @type {Array<(body: Buffer, headers: NodeJS.Dict | undefined) => Buffer>} */ let modifyBodyFuncs = []; @@ -91,13 +91,18 @@ function createRequestContext(targetUrl, eventEmitter) { /** @returns {RequestInterceptionHandler} */ function build() { - /** @param {NodeJS.Dict | undefined} headers */ + /** + * @param {NodeJS.Dict | undefined} headers + * @returns {NodeJS.Dict | undefined} + */ function modifyRequestHeaders(headers) { - if (!headers) return; - - for (const func of reqheaderModificationFuncs) { - func(headers); + if (headers) { + for (const func of reqheaderModificationFuncs) { + func(headers); + } } + + return headers; } /** diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 0e7e41d..47c63d0 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -8,6 +8,7 @@ const state = { /** * @param {NodeJS.Dict} headers + * @returns {NodeJS.Dict} */ export function modifyNpmInfoRequestHeaders(headers) { const accept = getHeaderValueAsString(headers, "accept"); @@ -17,6 +18,7 @@ export function modifyNpmInfoRequestHeaders(headers) { // Force the registry to return the full metadata by changing the Accept header. headers["accept"] = "application/json"; } + return headers; } /** diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 9afadaa..9845cd2 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -151,11 +151,12 @@ function forwardRequest(req, hostname, res, requestHandler) { * @returns {import("http").ClientRequest} */ function createProxyRequest(hostname, req, res, requestHandler) { - const headers = { ...req.headers }; + /** @type {NodeJS.Dict | undefined} */ + let headers = { ...req.headers }; if (headers.host) { delete headers.host; } - requestHandler.modifyRequestHeaders(headers); + headers = requestHandler.modifyRequestHeaders(headers); /** @type {import("http").RequestOptions} */ const options = { From faae0488c8c966c07be8d93381668f0b26e618d8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:23:55 +0100 Subject: [PATCH 231/345] Undo small refactor --- .../safe-chain/src/registryProxy/mitmRequestHandler.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 9845cd2..c4e7bb7 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -70,14 +70,12 @@ function createHttpsServer(hostname, interceptor) { const targetUrl = `https://${hostname}${pathAndQuery}`; const requestInterceptor = await interceptor.handleRequest(targetUrl); + const blockResponse = requestInterceptor.blockResponse; - if (requestInterceptor.blockResponse) { + if (blockResponse) { ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); - res.writeHead( - requestInterceptor.blockResponse.statusCode, - requestInterceptor.blockResponse.message - ); - res.end(requestInterceptor.blockResponse.message); + res.writeHead(blockResponse.statusCode, blockResponse.message); + res.end(blockResponse.message); return; } From 0a8dacda24216640e7318830bd8bd58a8d564c71 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:25:56 +0100 Subject: [PATCH 232/345] Add small comment on why we're removing the host header before forwarding. --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index c4e7bb7..bfc6c3e 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -151,6 +151,8 @@ function forwardRequest(req, hostname, res, requestHandler) { function createProxyRequest(hostname, req, res, requestHandler) { /** @type {NodeJS.Dict | undefined} */ let headers = { ...req.headers }; + // Remove the host header from the incoming request before forwarding. + // Node's http module sets the correct host header for the target hostname automatically. if (headers.host) { delete headers.host; } From ea751791437f64c8e6eacdb2cd07d5f4202d9329 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:31:26 +0100 Subject: [PATCH 233/345] Update readme to reflect our support for node 16+ and delete broken screenshot. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 17b4abc..c2ac0ad 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ The Aikido Safe Chain **prevents developers from installing malware** on their w 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) - -Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers: +Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers: - ✅ **npm** - ✅ **npx** From 900bf8e6ea80b776d22c8deb7c30cc2c4923dcba Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 15:52:17 +0100 Subject: [PATCH 234/345] Parse npm registry's timestamps. --- .../registryProxy/interceptors/npm/modifyNpmInfo.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 47c63d0..ddcafea 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -67,7 +67,7 @@ export function modifyNpmInfoResponse(body, headers) { const cutOff = new Date( new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000 - ).toISOString(); + ); const hasLatestTag = !!bodyJson["dist-tags"]["latest"]; @@ -79,13 +79,8 @@ export function modifyNpmInfoResponse(body, headers) { .filter((x) => x.version !== "created" && x.version !== "modified"); for (const { version, timestamp } of versions) { - if (version === "created" || version === "modified") { - continue; - } - - // Timestamps are compared as strings. - // This can be done because they are formatted in ISO8601, which is sortable. - if (timestamp > cutOff) { + const timestampValue = new Date(timestamp); + if (timestampValue > cutOff) { deleteVersionFromJson(bodyJson, version); if (headers) { // When modifying the response, the etag and last-modified headers @@ -93,7 +88,6 @@ export function modifyNpmInfoResponse(body, headers) { delete headers["etag"]; delete headers["last-modified"]; } - continue; } } From 5629b640cc4ede9912c3f07e23555f20bf076eca Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 18:16:09 +0100 Subject: [PATCH 235/345] Prevent package manager from caching modified response --- .../src/registryProxy/interceptors/npm/modifyNpmInfo.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index ddcafea..5cc0f33 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -87,6 +87,8 @@ export function modifyNpmInfoResponse(body, headers) { // no longer match the content so they needs to be removed before sending the response. delete headers["etag"]; delete headers["last-modified"]; + // Todo: add comment + delete headers["cache-control"]; } } } From c695d0cb5d3f75a801d1867f0582b4ca1990ad3f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 24 Nov 2025 18:29:35 +0100 Subject: [PATCH 236/345] Add explaining comment --- .../src/registryProxy/interceptors/npm/modifyNpmInfo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js index 5cc0f33..2ee4eb8 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/modifyNpmInfo.js @@ -87,7 +87,8 @@ export function modifyNpmInfoResponse(body, headers) { // no longer match the content so they needs to be removed before sending the response. delete headers["etag"]; delete headers["last-modified"]; - // Todo: add comment + // Removing the cache-control header will prevent the package manager from caching + // the modified response. delete headers["cache-control"]; } } From e976c28b8ae99fab365a149c9fc6d99938aa042d Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 24 Nov 2025 18:45:14 +0100 Subject: [PATCH 237/345] Publish using OIDC --- .github/workflows/build-and-release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 987db03..2c1a423 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -5,6 +5,10 @@ on: tags: - "*" +permissions: + id-token: write + contents: read + jobs: build: runs-on: ubuntu-latest @@ -50,6 +54,4 @@ jobs: - name: Publish to npm run: | echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" - npm publish --workspace=packages/safe-chain --access public - env: - NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + npm publish --workspace=packages/safe-chain --access public --provenance From eac173dfa34da21762e36e4d79ee09db0dd6cc61 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 25 Nov 2025 12:31:50 +0100 Subject: [PATCH 238/345] Update intro in README.md --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c2ac0ad..d4faa16 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ # Aikido Safe Chain -The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing 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. +- ✅ **Block malware on developer laptops and CI/CD** +- ✅ **Supports npm and PyPI** more package managers coming +- ✅ **Blocks packages newer than 24 hours** without breaking your build +- ✅ **Tokenless, free, no build data shared** Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers: -- ✅ **npm** -- ✅ **npx** -- ✅ **yarn** -- ✅ **pnpm** -- ✅ **pnpx** -- ✅ **bun** -- ✅ **bunx** -- ✅ **pip** (beta) -- ✅ **pip3** (beta) +- 📦 **npm** +- 📦 **npx** +- 📦 **yarn** +- 📦 **pnpm** +- 📦 **pnpx** +- 📦 **bun** +- 📦 **bunx** +- 📦 **pip** (beta) +- 📦 **pip3** (beta) # Usage From c8df7566b52dc6a35abbbaad7693177a855dfab7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 25 Nov 2025 14:22:31 +0100 Subject: [PATCH 239/345] Remove ora dependency --- package-lock.json | 226 +----------------- packages/safe-chain/package.json | 7 +- .../src/environment/userInteraction.js | 57 ----- packages/safe-chain/src/scanning/index.js | 41 +--- .../src/scanning/index.scanCommand.spec.js | 112 --------- 5 files changed, 17 insertions(+), 426 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94b6921..73ed31e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -659,33 +659,6 @@ "node": ">=18" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -909,18 +882,6 @@ "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", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "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", @@ -1128,30 +1089,6 @@ "node": ">=8" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1188,34 +1125,6 @@ ], "license": "MIT" }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1274,18 +1183,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -1515,79 +1412,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "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.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": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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", @@ -1687,37 +1511,6 @@ "node": ">=10" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "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", @@ -1835,18 +1628,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2096,14 +1877,13 @@ "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { - "certifi": "^14.5.15", + "certifi": "14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "ini": "^6.0.0", "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" }, "bin": { @@ -2113,10 +1893,10 @@ "aikido-npx": "bin/aikido-npx.js", "aikido-pip": "bin/aikido-pip.js", "aikido-pip3": "bin/aikido-pip3.js", - "aikido-python": "bin/aikido-python.js", - "aikido-python3": "bin/aikido-python3.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", + "aikido-python": "bin/aikido-python.js", + "aikido-python3": "bin/aikido-python3.js", "aikido-yarn": "bin/aikido-yarn.js", "safe-chain": "bin/safe-chain.js" }, diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 186d810..8bdad1f 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -35,23 +35,22 @@ "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": { - "certifi": "^14.5.15", + "certifi": "14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", - "ini": "^6.0.0", + "ini": "6.0.0", "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" }, "devDependencies": { "@types/ini": "^4.1.1", "@types/make-fetch-happen": "^10.0.4", "@types/node": "^18.19.130", + "@types/node-forge": "^1.3.14", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", - "@types/node-forge": "^1.3.14", "typescript": "^5.9.3" }, "main": "src/main.js", diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 3222874..9115b58 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -1,6 +1,5 @@ // oxlint-disable no-console import chalk from "chalk"; -import ora from "ora"; import { isCi } from "./environment.js"; import { getLoggingLevel, @@ -98,61 +97,6 @@ function writeOrBuffer(messageFunction) { } } -/** - * @typedef {Object} Spinner - * @property {(message: string) => void} succeed - * @property {(message: string) => void} fail - * @property {() => void} stop - * @property {(message: string) => void} setText - */ - -/** - * @param {string} message - * - * @returns {Spinner} - */ -function startProcess(message) { - if (isSilentMode()) { - return { - succeed: () => {}, - fail: () => {}, - stop: () => {}, - setText: () => {}, - }; - } - - if (isCi()) { - return { - succeed: (message) => { - writeInformation(message); - }, - fail: (message) => { - writeError(message); - }, - stop: () => {}, - setText: (message) => { - writeInformation(message); - }, - }; - } else { - const spinner = ora(message).start(); - return { - succeed: (message) => { - spinner.succeed(message); - }, - fail: (message) => { - spinner.fail(message); - }, - stop: () => { - spinner.stop(); - }, - setText: (message) => { - spinner.text = message; - }, - }; - } -} - function startBufferingLogs() { state.bufferOutput = true; state.bufferedMessages = []; @@ -173,7 +117,6 @@ export const ui = { writeError, writeExitWithoutInstallingMaliciousPackages, emptyLine, - startProcess, startBufferingLogs, writeBufferedLogsAndStopBuffering, }; diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 44ff57c..abfc420 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -29,36 +29,19 @@ export async function scanCommand(args) { } let timedOut = false; - - const spinner = ui.startProcess( - "Safe-chain: Scanning for malicious packages..." - ); /** @type {import("./audit/index.js").AuditResult | undefined} */ let audit; await Promise.race([ (async () => { - try { - const packageManager = getPackageManager(); - const changes = await packageManager.getDependencyUpdatesForCommand( - args - ); + const packageManager = getPackageManager(); + const changes = await packageManager.getDependencyUpdatesForCommand(args); - if (timedOut) { - return; - } - - if (changes.length > 0) { - spinner.setText( - `Safe-chain: Scanning ${changes.length} package(s)...` - ); - } - - audit = await auditChanges(changes); - } catch (/** @type any */ error) { - spinner.fail(`Safe-chain: Error while scanning.`); - throw error; + if (timedOut) { + return; } + + audit = await auditChanges(changes); })(), setTimeout(getScanTimeout()).then(() => { timedOut = true; @@ -66,15 +49,13 @@ export async function scanCommand(args) { ]); if (timedOut) { - spinner.fail("Safe-chain: Timeout exceeded while scanning."); throw new Error("Timeout exceeded while scanning npm install command."); } if (!audit || audit.isAllowed) { - spinner.stop(); return 0; } else { - printMaliciousChanges(audit.disallowedChanges, spinner); + printMaliciousChanges(audit.disallowedChanges); onMalwareFound(); return 1; } @@ -82,12 +63,12 @@ export async function scanCommand(args) { /** * @param {import("./audit/index.js").PackageChange[]} changes - * @param spinner {import("../environment/userInteraction.js").Spinner} - * * @return {void} */ -function printMaliciousChanges(changes, spinner) { - spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:")); +function printMaliciousChanges(changes) { + ui.writeInformation( + chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:") + ); for (const change of changes) { ui.writeInformation(` - ${change.name}@${change.version}`); diff --git a/packages/safe-chain/src/scanning/index.scanCommand.spec.js b/packages/safe-chain/src/scanning/index.scanCommand.spec.js index c47555f..944cf11 100644 --- a/packages/safe-chain/src/scanning/index.scanCommand.spec.js +++ b/packages/safe-chain/src/scanning/index.scanCommand.spec.js @@ -5,12 +5,6 @@ import { setTimeout } from "node:timers/promises"; describe("scanCommand", async () => { const getScanTimeoutMock = mock.fn(() => 1000); const mockGetDependencyUpdatesForCommand = mock.fn(); - const mockStartProcess = mock.fn(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => {}, - stop: () => {}, - })); // import { getPackageManager } from "../packagemanager/currentPackageManager.js"; mock.module("../packagemanager/currentPackageManager.js", { @@ -36,7 +30,6 @@ describe("scanCommand", async () => { mock.module("../environment/userInteraction.js", { namedExports: { ui: { - startProcess: mockStartProcess, writeError: () => {}, writeInformation: () => {}, writeWarning: () => {}, @@ -75,51 +68,20 @@ describe("scanCommand", async () => { const { scanCommand } = await import("./index.js"); it("should succeed when there are no changes", async () => { - let progressWasStopped = false; - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => {}, - stop: () => { - progressWasStopped = true; - }, - })); mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => []); await scanCommand(["install", "lodash"]); - - assert.equal(progressWasStopped, true); }); it("should succeed when changes are not malicious", async () => { - let progressWasStopped = false; - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => {}, - stop: () => { - progressWasStopped = true; - }, - })); mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "lodash", version: "4.17.21" }, ]); await scanCommand(["install", "lodash"]); - - assert.equal(progressWasStopped, true); }); it("should throw an error when timing out", async () => { - let failureMessageWasSet = false; - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => { - failureMessageWasSet = true; - }, - stop: () => {}, - })); getScanTimeoutMock.mock.mockImplementationOnce(() => 100); mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => { await setTimeout(150); @@ -127,83 +89,9 @@ describe("scanCommand", async () => { }); await assert.rejects(scanCommand(["install", "lodash"])); - - assert.equal(failureMessageWasSet, true); }); it("should fail and return 1 malicious changes are detected", async () => { - let failureMessageWasSet = false; - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => { - failureMessageWasSet = true; - }, - stop: () => {}, - })); - mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ - { name: "malicious", version: "1.0.0" }, - ]); - - const result = await scanCommand(["install", "malicious"]); - - assert.equal(failureMessageWasSet, true); - assert.equal(result, 1); - }); - - it("should not report a timeout when the user takes a long time to respond (it should not affect the timeout)", async () => { - let failureMessages = []; - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: (message) => { - failureMessages.push(message); - }, - stop: () => {}, - })); - getScanTimeoutMock.mock.mockImplementationOnce(() => 100); - mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => { - return [{ name: "malicious", version: "4.17.21" }]; - }); - - await scanCommand(["install", "malicious"]); - - assert.equal(failureMessages.length, 1); - const failureMessage = failureMessages[0]; - assert.equal(failureMessage.toLowerCase().includes("timeout"), false); - assert.equal(failureMessage.toLowerCase().includes("malicious"), true); - }); - - it("should exit immediately when malicious changes are detected in block mode", async () => { - let failureMessageWasSet = false; - - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => { - failureMessageWasSet = true; - }, - stop: () => {}, - })); - - mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ - { name: "malicious", version: "1.0.0" }, - ]); - - const result = await scanCommand(["install", "malicious"]); - - assert.equal(failureMessageWasSet, true); - assert.equal(result, 1); - }); - - it("should exit immediately when malicious changes are detected in block mode without prompting", async () => { - mockStartProcess.mock.mockImplementationOnce(() => ({ - setText: () => {}, - succeed: () => {}, - fail: () => {}, - stop: () => {}, - })); - mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [ { name: "malicious", version: "1.0.0" }, ]); From 77e9d3d843bab2f75e5da697e495e47d39badd1f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 25 Nov 2025 14:56:12 +0100 Subject: [PATCH 240/345] Fix e2e tests --- test/e2e/setup-ci.e2e.spec.js | 4 +--- test/e2e/setup.teardown.e2e.spec.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/e2e/setup-ci.e2e.spec.js b/test/e2e/setup-ci.e2e.spec.js index 9356f88..f22f884 100644 --- a/test/e2e/setup-ci.e2e.spec.js +++ b/test/e2e/setup-ci.e2e.spec.js @@ -41,9 +41,7 @@ describe("E2E: safe-chain setup-ci command", () => { const projectShell = await container.openShell(shell); const result = await projectShell.runCommand("npm i axios"); - const hasExpectedOutput = result.output.includes( - "Scanning for malicious packages..." - ); + const hasExpectedOutput = result.output.includes("Safe-chain: Scanned"); assert.ok( hasExpectedOutput, hasExpectedOutput diff --git a/test/e2e/setup.teardown.e2e.spec.js b/test/e2e/setup.teardown.e2e.spec.js index c4a0c49..b5c58bb 100644 --- a/test/e2e/setup.teardown.e2e.spec.js +++ b/test/e2e/setup.teardown.e2e.spec.js @@ -31,9 +31,7 @@ describe("E2E: safe-chain setup command", () => { await projectShell.runCommand("cd /testapp"); const result = await projectShell.runCommand("npm i axios"); - const hasExpectedOutput = result.output.includes( - "Scanning for malicious packages..." - ); + const hasExpectedOutput = result.output.includes("Safe-chain: Scanned"); assert.ok( hasExpectedOutput, hasExpectedOutput From 156522912e7b6e22342d5eb366db6026f0e216b4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 25 Nov 2025 15:10:42 +0100 Subject: [PATCH 241/345] Remove the safe-chain-bun package --- package-lock.json | 206 +--------------------- package.json | 2 +- packages/safe-chain-bun/package.json | 30 ---- packages/safe-chain-bun/src/index.js | 38 ---- packages/safe-chain-bun/src/index.spec.js | 140 --------------- 5 files changed, 2 insertions(+), 414 deletions(-) delete mode 100644 packages/safe-chain-bun/package.json delete mode 100644 packages/safe-chain-bun/src/index.js delete mode 100644 packages/safe-chain-bun/src/index.spec.js diff --git a/package-lock.json b/package-lock.json index 73ed31e..60bd1bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,6 @@ "resolved": "packages/safe-chain", "link": true }, - "node_modules/@aikidosec/safe-chain-bun": { - "resolved": "packages/safe-chain-bun", - "link": true - }, "node_modules/@aikidosec/safe-chain-e2e-tests": { "resolved": "test/e2e", "link": true @@ -143,160 +139,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@oven/bun-darwin-aarch64": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@oven/bun-darwin-x64": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@oven/bun-darwin-x64-baseline": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@oven/bun-linux-aarch64": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oven/bun-linux-aarch64-musl": { - "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": [ - "aarch64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oven/bun-linux-x64": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oven/bun-linux-x64-baseline": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oven/bun-linux-x64-musl": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oven/bun-linux-x64-musl-baseline": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oven/bun-windows-x64": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@oven/bun-windows-x64-baseline": { - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, "node_modules/@oxlint/darwin-arm64": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.22.0.tgz", @@ -559,41 +401,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/bun": { - "version": "1.2.21", - "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", - "integrity": "sha512-y0lJ02dS90U3PJm+7KAKY8Se95AQvP5Xm77LouUwrpNOHpv59kBG4SK1+9iE1cAhpUaFipq+0EJ56S6MmE3row==", - "cpu": [ - "arm64", - "x64", - "aarch64" - ], - "hasInstallScript": true, - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "peer": true, - "bin": { - "bun": "bin/bun.exe", - "bunx": "bin/bunx.exe" - }, - "optionalDependencies": { - "@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": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -1880,7 +1687,7 @@ "certifi": "14.5.15", "chalk": "5.4.1", "https-proxy-agent": "7.0.6", - "ini": "^6.0.0", + "ini": "6.0.0", "make-fetch-happen": "14.0.3", "node-forge": "1.3.1", "npm-registry-fetch": "18.0.2", @@ -1910,17 +1717,6 @@ "typescript": "^5.9.3" } }, - "packages/safe-chain-bun": { - "name": "@aikidosec/safe-chain-bun", - "version": "1.0.0", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@aikidosec/safe-chain": "file:../safe-chain" - }, - "peerDependencies": { - "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", diff --git a/package.json b/package.json index 6a5dec3..aa40862 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test/e2e" ], "scripts": { - "test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun", + "test": "npm run test --workspace=packages/safe-chain", "test:e2e": "npm run test --workspace=test/e2e", "lint": "npm run lint --workspace=packages/safe-chain", "typecheck": "npm run typecheck --workspace=packages/safe-chain" diff --git a/packages/safe-chain-bun/package.json b/packages/safe-chain-bun/package.json deleted file mode 100644 index ca154b8..0000000 --- a/packages/safe-chain-bun/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@aikidosec/safe-chain-bun", - "version": "1.0.0", - "type": "module", - "main": "src/index.js", - "scripts": { - "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'" - }, - "exports": { - ".": { - "bun": "./src/index.js", - "default": "./src/index.js" - } - }, - "keywords": ["bun", "security", "scanner", "malware", "aikido"], - "author": "Aikido Security", - "license": "AGPL-3.0-or-later", - "description": "Aikido Security Scanner for Bun package manager - detects malware and security threats during package installation", - "repository": { - "type": "git", - "url": "git+https://github.com/AikidoSec/safe-chain.git", - "directory": "packages/safe-chain-bun" - }, - "dependencies": { - "@aikidosec/safe-chain": "file:../safe-chain" - }, - "peerDependencies": { - "bun": ">=1.2.21" - } -} diff --git a/packages/safe-chain-bun/src/index.js b/packages/safe-chain-bun/src/index.js deleted file mode 100644 index 6e933c3..0000000 --- a/packages/safe-chain-bun/src/index.js +++ /dev/null @@ -1,38 +0,0 @@ -// oxlint-disable no-console -import { auditChanges } from "@aikidosec/safe-chain/scanning"; - -// Bun Security Scanner for Safe-Chain -// This is the entry point for Bun's native security scanner integration - -export const scanner = { - version: "1", // Our scanner is using version 1 of the bun security scanner API. - - async scan({ packages }) { - const advisories = []; - - try { - const changes = packages.map((pkg) => ({ - name: pkg.name, - version: pkg.version, - type: "add", - })); - - const audit = await auditChanges(changes); - - if (!audit.isAllowed) { - for (const change of audit.disallowedChanges) { - advisories.push({ - level: "fatal", // Fatal will block the installation process, this is what we want for packages that contain malware. - package: change.name, - url: null, - description: `Package ${change.name}@${change.version} contains known security threats (${change.reason}). Installation blocked by Safe-Chain.`, - }); - } - } - } catch (/** @type any */ error) { - console.warn(`Safe-Chain security scan failed: ${error.message}`); - } - - return advisories; - }, -}; diff --git a/packages/safe-chain-bun/src/index.spec.js b/packages/safe-chain-bun/src/index.spec.js deleted file mode 100644 index 3293b56..0000000 --- a/packages/safe-chain-bun/src/index.spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it, mock } from "node:test"; - -describe("Bun Scanner", async () => { - const mockAuditChanges = mock.fn(); - - // Mock the scanning module - mock.module("@aikidosec/safe-chain/scanning", { - namedExports: { - auditChanges: mockAuditChanges, - }, - }); - - const { scanner } = await import("./index.js"); - - it("should export scanner object with version", () => { - assert.strictEqual(scanner.version, "1"); - assert.strictEqual(typeof scanner.scan, "function"); - }); - - it("should return empty advisories for clean packages", async () => { - mockAuditChanges.mock.mockImplementation(() => ({ - allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }], - disallowedChanges: [], - isAllowed: true, - })); - - const packages = [{ name: "express", version: "4.18.2" }]; - const result = await scanner.scan({ packages }); - - assert.deepEqual(result, []); - assert.strictEqual(mockAuditChanges.mock.callCount(), 1); - assert.deepEqual(mockAuditChanges.mock.calls[0].arguments[0], [ - { name: "express", version: "4.18.2", type: "add" }, - ]); - }); - - it("should return fatal advisory for malware packages", async () => { - mockAuditChanges.mock.mockImplementation(() => ({ - allowedChanges: [], - disallowedChanges: [ - { - name: "malicious-pkg", - version: "1.0.0", - type: "add", - reason: "MALWARE", - }, - ], - isAllowed: false, - })); - - const packages = [{ name: "malicious-pkg", version: "1.0.0" }]; - const result = await scanner.scan({ packages }); - - assert.strictEqual(result.length, 1); - assert.deepEqual(result[0], { - level: "fatal", - package: "malicious-pkg", - url: null, - description: "Package malicious-pkg@1.0.0 contains known security threats (MALWARE). Installation blocked by Safe-Chain.", - }); - }); - - it("should handle multiple packages with mixed results", async () => { - mockAuditChanges.mock.mockImplementation(() => ({ - allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }], - disallowedChanges: [ - { - name: "malicious-pkg", - version: "1.0.0", - type: "add", - reason: "MALWARE", - }, - { - name: "another-bad-pkg", - version: "2.1.0", - type: "add", - reason: "MALWARE", - }, - ], - isAllowed: false, - })); - - const packages = [ - { name: "express", version: "4.18.2" }, - { name: "malicious-pkg", version: "1.0.0" }, - { name: "another-bad-pkg", version: "2.1.0" }, - ]; - const result = await scanner.scan({ packages }); - - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0].package, "malicious-pkg"); - assert.strictEqual(result[0].level, "fatal"); - assert.strictEqual(result[1].package, "another-bad-pkg"); - assert.strictEqual(result[1].level, "fatal"); - }); - - it("should handle empty package list", async () => { - mockAuditChanges.mock.mockImplementation(() => ({ - allowedChanges: [], - disallowedChanges: [], - isAllowed: true, - })); - - const result = await scanner.scan({ packages: [] }); - - assert.deepEqual(result, []); - assert.deepEqual( - mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1] - .arguments[0], - [] - ); - }); - - it("should convert Bun package format to safe-chain format correctly", async () => { - mockAuditChanges.mock.mockImplementation(() => ({ - allowedChanges: [], - disallowedChanges: [], - isAllowed: true, - })); - - const bunPackages = [ - { name: "lodash", version: "4.17.21" }, - { name: "@types/node", version: "20.0.0" }, - ]; - - await scanner.scan({ packages: bunPackages }); - - const expectedChanges = [ - { name: "lodash", version: "4.17.21", type: "add" }, - { name: "@types/node", version: "20.0.0", type: "add" }, - ]; - - assert.deepEqual( - mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1] - .arguments[0], - expectedChanges - ); - }); -}); From cab3a0aba32036b90027e1ead65f6b77a4e6d1b2 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 25 Nov 2025 14:10:20 -0800 Subject: [PATCH 242/345] Add uv (Astral Python package manager) support - Add uv package manager implementation following pip pattern - Configure MITM proxy with CA bundle for PyPI packages - Add shell integration (bash/zsh/fish/PowerShell) - Conditional on --include-python flag - Add 33 comprehensive E2E tests covering: - uv pip install/sync/compile commands - uv add for project dependencies - uv tool install for global tools - uv run --with for ephemeral dependencies - uv sync for project syncing - Malware blocking verification for all methods - Update documentation and package.json - Install uv in Docker test environment --- README.md | 15 +- packages/safe-chain/bin/aikido-uv.js | 15 + packages/safe-chain/package.json | 3 +- .../packagemanager/currentPackageManager.js | 7 +- .../uv/createUvPackageManager.js | 19 + .../uv/createUvPackageManager.spec.js | 30 + .../src/packagemanager/uv/runUvCommand.js | 76 +++ .../src/packagemanager/uv/uvSettings.js | 5 + .../src/shell-integration/helpers.js | 1 + .../include-python/init-fish.fish | 4 + .../include-python/init-posix.sh | 4 + .../include-python/init-pwsh.ps1 | 4 + test/e2e/Dockerfile | 4 + test/e2e/uv.e2e.spec.js | 561 ++++++++++++++++++ 14 files changed, 739 insertions(+), 9 deletions(-) create mode 100755 packages/safe-chain/bin/aikido-uv.js create mode 100644 packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js create mode 100644 packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js create mode 100644 packages/safe-chain/src/packagemanager/uv/runUvCommand.js create mode 100644 packages/safe-chain/src/packagemanager/uv/uvSettings.js create mode 100644 test/e2e/uv.e2e.spec.js diff --git a/README.md b/README.md index c2ac0ad..7969fba 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 while developing 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 Javascript and Python ecosystems (through npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, 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/), [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. +The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, pip/pip3 or uv from downloading or running the malware. Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers: @@ -15,6 +15,7 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi - ✅ **bunx** - ✅ **pip** (beta) - ✅ **pip3** (beta) +- ✅ **uv** (beta) # Usage @@ -32,7 +33,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: safe-chain setup ``` - To enable Python (pip/pip3) support (beta), use the `--include-python` flag: + To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag: ```shell safe-chain setup --include-python @@ -58,7 +59,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 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. +When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `uv`, `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: @@ -70,17 +71,17 @@ safe-chain --version ### Malware Blocking -The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, 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, uv, `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. ### Minimum package age (npm only) For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag. -⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip. +⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3). ### Shell Integration -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: +The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (uv, pip). It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support: - ✅ **Bash** - ✅ **Zsh** diff --git a/packages/safe-chain/bin/aikido-uv.js b/packages/safe-chain/bin/aikido-uv.js new file mode 100755 index 0000000..b8cf210 --- /dev/null +++ b/packages/safe-chain/bin/aikido-uv.js @@ -0,0 +1,15 @@ +#!/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"; +import { UV_PACKAGE_MANAGER } from "../src/packagemanager/uv/uvSettings.js"; + +// Set eco system +setEcoSystem(ECOSYSTEM_PY); + +initializePackageManager(UV_PACKAGE_MANAGER); + +// Pass through only user-supplied uv args +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 8bdad1f..15279d6 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -15,6 +15,7 @@ "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-bun": "bin/aikido-bun.js", "aikido-bunx": "bin/aikido-bunx.js", + "aikido-uv": "bin/aikido-uv.js", "aikido-pip": "bin/aikido-pip.js", "aikido-pip3": "bin/aikido-pip3.js", "aikido-python": "bin/aikido-python.js", @@ -33,7 +34,7 @@ "keywords": [], "author": "Aikido Security", "license": "AGPL-3.0-or-later", - "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), 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.", + "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.", "dependencies": { "certifi": "14.5.15", "chalk": "5.4.1", diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2db4167..f18105f 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -10,6 +10,9 @@ import { } from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; import { createPipPackageManager } from "./pip/createPackageManager.js"; +import { createUvPackageManager } from "./uv/createUvPackageManager.js"; +import { PIP_PACKAGE_MANAGER } from "./pip/pipSettings.js"; +import { UV_PACKAGE_MANAGER } from "./uv/uvSettings.js"; /** * @type {{packageManagerName: PackageManager | null}} @@ -52,8 +55,10 @@ export function initializePackageManager(packageManagerName) { state.packageManagerName = createBunPackageManager(); } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); - } else if (packageManagerName === "pip") { + } else if (packageManagerName === PIP_PACKAGE_MANAGER) { state.packageManagerName = createPipPackageManager(); + } else if (packageManagerName === UV_PACKAGE_MANAGER) { + state.packageManagerName = createUvPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js new file mode 100644 index 0000000..ecd3367 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js @@ -0,0 +1,19 @@ +import { runUv } from "./runUvCommand.js"; + +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ +export function createUvPackageManager() { + return { + /** + * @param {string[]} args + */ + runCommand: (args) => { + // uv is always invoked as 'uv' - no invocation variations like pip + return runUv("uv", args); + }, + // For uv, rely solely on MITM proxy to detect/deny downloads from PyPI. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} diff --git a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js new file mode 100644 index 0000000..ba79722 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js @@ -0,0 +1,30 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { createUvPackageManager } from "./createUvPackageManager.js"; + +test("createUvPackageManager", async (t) => { + await t.test("should create package manager with required interface", () => { + const pm = createUvPackageManager(); + + 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 use proxy-only approach (MITM)", () => { + const pm = createUvPackageManager(); + + // uv uses proxy-only approach, so it doesn't scan args + assert.strictEqual(pm.isSupportedCommand(["pip", "install", "requests"]), false); + assert.strictEqual(pm.isSupportedCommand(["add", "requests"]), false); + assert.strictEqual(pm.isSupportedCommand([]), false); + }); + + await t.test("should return empty dependency updates", () => { + const pm = createUvPackageManager(); + + const result = pm.getDependencyUpdatesForCommand(["pip", "install", "requests"]); + assert.deepStrictEqual(result, []); + }); +}); diff --git a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js new file mode 100644 index 0000000..85302eb --- /dev/null +++ b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js @@ -0,0 +1,76 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; + +/** + * Sets CA bundle environment variables used by Python libraries and uv. + * These are applied to ensure all Python network libraries respect the combined CA bundle. + * + * @param {NodeJS.ProcessEnv} env - Environment object to modify + * @param {string} combinedCaPath - Path to the combined CA bundle + */ +function setUvCaBundleEnvironmentVariables(env, combinedCaPath) { + // UV_NATIVE_TLS: Use system-provided TLS certificates (default is true) + // But we also need to provide our CA bundle for MITM'd connections + + // SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients + if (env.SSL_CERT_FILE) { + ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); + } + env.SSL_CERT_FILE = combinedCaPath; + + // REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally) + if (env.REQUESTS_CA_BUNDLE) { + ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten."); + } + env.REQUESTS_CA_BUNDLE = combinedCaPath; + + // PIP_CERT: Some underlying pip operations may respect this + if (env.PIP_CERT) { + ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten."); + } + env.PIP_CERT = combinedCaPath; +} + +/** + * Runs a uv command with safe-chain's certificate bundle and proxy configuration. + * + * uv respects standard environment variables for proxy and TLS configuration: + * - HTTP_PROXY / HTTPS_PROXY: Proxy settings + * - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification + * + * @param {string} command - The uv command to execute (typically 'uv') + * @param {string[]} args - Command line arguments to pass to uv + * @returns {Promise<{status: number}>} Exit status of the uv command + */ +export async function runUv(command, args) { + try { + const env = mergeSafeChainProxyEnvironmentVariables(process.env); + + // Provide uv with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) + // so that network requests validate correctly under both MITM'd and tunneled HTTPS. + const combinedCaPath = getCombinedCaBundlePath(); + + // Set CA bundle environment variables for uv and underlying Python libraries + setUvCaBundleEnvironmentVariables(env, combinedCaPath); + + // Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration + // These are already set by mergeSafeChainProxyEnvironmentVariables + + const result = await safeSpawn(command, args, { + stdio: "inherit", + env, + }); + + return { status: result.status }; + } catch (/** @type any */ error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError(`Error executing command: ${error.message}`); + ui.writeError(`Is '${command}' installed and available on your system?`); + return { status: 1 }; + } + } +} diff --git a/packages/safe-chain/src/packagemanager/uv/uvSettings.js b/packages/safe-chain/src/packagemanager/uv/uvSettings.js new file mode 100644 index 0000000..6f68ea7 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/uv/uvSettings.js @@ -0,0 +1,5 @@ +export const UV_PACKAGE_MANAGER = "uv"; + +// Unlike pip, uv only has one invocation method: the 'uv' command. +// There is no 'uv3' or 'python -m uv' pattern, so we don't need +// invocation tracking like pip does. diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index da8e98b..7f45669 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -22,6 +22,7 @@ export const knownAikidoTools = [ { tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS }, { tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS }, { tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS }, + { tool: "uv", aikidoCommand: "aikido-uv", ecoSystem: ECOSYSTEM_PY }, { tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY }, { tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY }, { tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY }, diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish index ebf89ff..235ecb8 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish @@ -77,6 +77,10 @@ function pip3 wrapSafeChainCommand "pip3" "aikido-pip3" $argv end +function uv + wrapSafeChainCommand "uv" "aikido-uv" $argv +end + # `python -m pip`, `python -m pip3`. function python wrapSafeChainCommand "python" "aikido-python" $argv diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh index 278b31a..9f51010 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh @@ -69,6 +69,10 @@ function pip3() { wrapSafeChainCommand "pip3" "aikido-pip3" "$@" } +function uv() { + wrapSafeChainCommand "uv" "aikido-uv" "$@" +} + # `python -m pip`, `python -m pip3`. function python() { wrapSafeChainCommand "python" "aikido-python" "$@" diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 index b692107..e2ea1c9 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -95,6 +95,10 @@ function pip3 { Invoke-WrappedCommand "pip3" "aikido-pip3" $args } +function uv { + Invoke-WrappedCommand "uv" "aikido-uv" $args +} + # `python -m pip`, `python -m pip3`. function python { Invoke-WrappedCommand 'python' 'aikido-python' $args diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index cf5f39b..52ff3dc 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -67,6 +67,10 @@ except Exception as exc: raise EOF +# Install uv (Astral's fast Python package manager) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + echo 'source $HOME/.local/bin/env' >> ~/.bashrc + # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ RUN npm install -g /pkgs/*.tgz diff --git a/test/e2e/uv.e2e.spec.js b/test/e2e/uv.e2e.spec.js new file mode 100644 index 0000000..eae7c12 --- /dev/null +++ b/test/e2e/uv.e2e.spec.js @@ -0,0 +1,561 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: uv coverage", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup --include-python"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`successfully installs known safe packages with uv pip install`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages requests" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with specific version`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages requests==2.32.3" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with version specifiers (>=)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + 'uv pip install --system --break-system-packages "Jinja2>=3.1"' + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with extras such as requests[socks]`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + 'uv pip install --system --break-system-packages "requests[socks]==2.32.3"' + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install multiple packages`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages requests certifi urllib3" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install from requirements file`, async () => { + const shell = await container.openShell("zsh"); + + // Create a requirements.txt file + await shell.runCommand("echo 'requests==2.32.3' > requirements.txt"); + await shell.runCommand("echo 'certifi>=2024.0.0' >> requirements.txt"); + + const result = await shell.runCommand( + "uv pip install --system --break-system-packages -r requirements.txt" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip sync with requirements file`, async () => { + const shell = await container.openShell("zsh"); + + // Create a requirements.txt file + await shell.runCommand("echo 'requests==2.32.3' > requirements-sync.txt"); + + const result = await shell.runCommand( + "uv pip sync --system --break-system-packages requirements-sync.txt" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks installation of malicious Python packages via uv`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand( + "uv pip install --system --break-system-packages safe-chain-pi-test" + ); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("safe_chain_pi_test@0.0.1"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + const listResult = await shell.runCommand("uv pip list --system"); + assert.ok( + !listResult.output.includes("safe-chain-pi-test"), + `Malicious package was installed despite safe-chain protection. Output of 'uv pip list' was:\n${listResult.output}` + ); + }); + + it(`uv pip install from GitHub URL using the CA bundle`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages git+https://github.com/psf/requests.git@v2.32.3" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + // Verify installation succeeded (would fail if certificate validation via env CA bundle broke) + assert.ok( + result.output.includes("Installed") || + result.output.includes("installed"), + `Installation from GitHub failed - CA bundle may not be working. Output was:\n${result.output}` + ); + }); + + it(`uv pip successfully validates certificates for HTTPS downloads`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand( + "uv pip install --system --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + // Verify successful installation (would fail with SSL/certificate errors if the env CA bundle wasn't working) + assert.ok( + result.output.includes("Installed") || + result.output.includes("installed"), + `Installation should succeed with proper certificate validation. Output was:\n${result.output}` + ); + + // Should NOT contain SSL or certificate errors + assert.ok( + !result.output.match( + /SSL|certificate verify failed|CERTIFICATE_VERIFY_FAILED/i + ), + `Should not have SSL/certificate errors. Output was:\n${result.output}` + ); + }); + + it(`uv pip install from direct HTTPS wheel URL`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + assert.ok( + result.output.includes("Installed") || + result.output.includes("installed"), + `Installation from direct HTTPS URL failed. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with --upgrade flag`, async () => { + const shell = await container.openShell("zsh"); + + // First install a package + await shell.runCommand("uv pip install --system --break-system-packages requests==2.31.0"); + + // Then upgrade it + const result = await shell.runCommand( + "uv pip install --system --break-system-packages --upgrade requests" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with --no-deps flag`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages --no-deps requests" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with --editable flag from local directory`, async () => { + const shell = await container.openShell("zsh"); + + // Create a simple package structure + await shell.runCommand("mkdir -p /tmp/test-pkg"); + await shell.runCommand("echo 'from setuptools import setup' > /tmp/test-pkg/setup.py"); + await shell.runCommand("echo \"setup(name='test-pkg', version='0.1.0')\" >> /tmp/test-pkg/setup.py"); + + const result = await shell.runCommand( + "uv pip install --system --break-system-packages -e /tmp/test-pkg" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip compile creates locked requirements`, async () => { + const shell = await container.openShell("zsh"); + + // Create an input requirements file + await shell.runCommand("echo 'requests' > requirements.in"); + + const result = await shell.runCommand( + "uv pip compile requirements.in" + ); + + // uv pip compile doesn't install packages, just resolves dependencies + // It should complete successfully and output resolved requirements + assert.ok( + result.output.includes("requests==") || result.output.includes("# via"), + `Output did not include compiled requirements. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with --index-url for alternate registry`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages --index-url https://test.pypi.org/simple certifi" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + + // Should succeed if CA bundle properly handles tunneled hosts + assert.ok( + result.output.includes("Installed") || + result.output.includes("installed"), + `Installation from Test PyPI failed. This may indicate the CA bundle lacks public roots. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with --safe-chain-logging=verbose`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv pip install --system --break-system-packages requests --safe-chain-logging=verbose" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip install with version range constraint`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + 'uv pip install --system --break-system-packages "requests>=2.31.0,<2.33.0"' + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv pip list shows installed packages`, async () => { + const shell = await container.openShell("zsh"); + + // Install a package first + await shell.runCommand("uv pip install --system --break-system-packages requests"); + + // Then list packages - this shouldn't trigger safe-chain scanning + const result = await shell.runCommand("uv pip list --system"); + + // List command should work without malware scanning + assert.ok( + result.output.includes("requests") || result.output.length > 0, + `Output did not show package list. Output was:\n${result.output}` + ); + }); + + it(`uv add installs package and updates project`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project and add package in same command + const result = await shell.runCommand( + "uv init test-project && cd test-project && uv add requests" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv add with specific version`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project + await shell.runCommand("uv init test-project-version"); + + const result = await shell.runCommand( + "cd test-project-version && uv add requests==2.32.3" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv add --dev for development dependencies`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project + await shell.runCommand("uv init test-project-dev"); + + const result = await shell.runCommand( + "cd test-project-dev && uv add --dev pytest" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv add multiple packages at once`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project + await shell.runCommand("uv init test-project-multi"); + + const result = await shell.runCommand( + "cd test-project-multi && uv add requests certifi urllib3" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks malicious packages via uv add`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project + await shell.runCommand("uv init test-project-malware"); + + const result = await shell.runCommand( + "cd test-project-malware && uv add safe-chain-pi-test" + ); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("safe_chain_pi_test@0.0.1"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv tool install installs a global tool`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv tool install ruff" + ); + + assert.ok( + result.output.includes("no malware found.") || result.output.includes("Installed"), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks malicious packages via uv tool install`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "uv tool install safe-chain-pi-test" + ); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("safe_chain_pi_test@0.0.1"), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv run --with installs ephemeral dependency`, async () => { + const shell = await container.openShell("zsh"); + + // Create a simple Python script + await shell.runCommand("echo 'import requests; print(requests.__version__)' > test_script.py"); + + const result = await shell.runCommand( + "uv run --with requests test_script.py" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks malicious packages via uv run --with`, async () => { + const shell = await container.openShell("zsh"); + + // Create a simple Python script + await shell.runCommand("echo 'print(\"test\")' > test_script2.py"); + + const result = await shell.runCommand( + "uv run --with safe-chain-pi-test test_script2.py" + ); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads:"), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv sync syncs project dependencies`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project, add a dependency, remove venv, and sync in one command chain + const result = await shell.runCommand( + "uv init test-sync-project && cd test-sync-project && uv add requests && rm -rf .venv && uv sync" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv add from git URL`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project + await shell.runCommand("uv init test-git-add"); + + const result = await shell.runCommand( + "cd test-git-add && uv add git+https://github.com/psf/requests.git@v2.32.3" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv add with --optional group`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize a new uv project + await shell.runCommand("uv init test-optional"); + + const result = await shell.runCommand( + "cd test-optional && uv add --optional dev pytest" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv run --with-requirements installs from requirements file`, async () => { + const shell = await container.openShell("zsh"); + + // Create requirements file and script + await shell.runCommand("echo 'requests' > run_requirements.txt"); + await shell.runCommand("echo 'import requests; print(requests.__version__)' > run_script.py"); + + const result = await shell.runCommand( + "uv run --with-requirements run_requirements.txt run_script.py" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`uv sync --all-extras syncs all optional dependencies`, async () => { + const shell = await container.openShell("zsh"); + + // Initialize project with optional dependency and sync in one command chain + const result = await shell.runCommand( + "uv init test-extras && cd test-extras && uv add --optional dev pytest && uv sync --all-extras" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); +}); + From e03bceba88cc3332906bc61b455fd2ad620d5dbd Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 25 Nov 2025 14:37:31 -0800 Subject: [PATCH 243/345] Some cleanup --- README.md | 2 +- packages/safe-chain/bin/aikido-uv.js | 3 +-- .../src/packagemanager/currentPackageManager.js | 6 ++---- .../uv/createUvPackageManager.spec.js | 16 ---------------- .../src/packagemanager/uv/runUvCommand.js | 13 ++++--------- .../src/packagemanager/uv/uvSettings.js | 5 ----- 6 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 packages/safe-chain/src/packagemanager/uv/uvSettings.js diff --git a/README.md b/README.md index 7969fba..437b76f 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after safe-chain setup-ci ``` -To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag: +To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag: ```shell safe-chain setup-ci --include-python diff --git a/packages/safe-chain/bin/aikido-uv.js b/packages/safe-chain/bin/aikido-uv.js index b8cf210..14180f2 100755 --- a/packages/safe-chain/bin/aikido-uv.js +++ b/packages/safe-chain/bin/aikido-uv.js @@ -3,12 +3,11 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; -import { UV_PACKAGE_MANAGER } from "../src/packagemanager/uv/uvSettings.js"; // Set eco system setEcoSystem(ECOSYSTEM_PY); -initializePackageManager(UV_PACKAGE_MANAGER); +initializePackageManager("uv"); // Pass through only user-supplied uv args var exitCode = await main(process.argv.slice(2)); diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index f18105f..c6f4484 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -11,8 +11,6 @@ import { import { createYarnPackageManager } from "./yarn/createPackageManager.js"; import { createPipPackageManager } from "./pip/createPackageManager.js"; import { createUvPackageManager } from "./uv/createUvPackageManager.js"; -import { PIP_PACKAGE_MANAGER } from "./pip/pipSettings.js"; -import { UV_PACKAGE_MANAGER } from "./uv/uvSettings.js"; /** * @type {{packageManagerName: PackageManager | null}} @@ -55,9 +53,9 @@ export function initializePackageManager(packageManagerName) { state.packageManagerName = createBunPackageManager(); } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); - } else if (packageManagerName === PIP_PACKAGE_MANAGER) { + } else if (packageManagerName === "pip") { state.packageManagerName = createPipPackageManager(); - } else if (packageManagerName === UV_PACKAGE_MANAGER) { + } else if (packageManagerName === "uv") { state.packageManagerName = createUvPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); diff --git a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js index ba79722..eb42924 100644 --- a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js +++ b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.spec.js @@ -11,20 +11,4 @@ test("createUvPackageManager", async (t) => { assert.strictEqual(typeof pm.isSupportedCommand, "function"); assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function"); }); - - await t.test("should use proxy-only approach (MITM)", () => { - const pm = createUvPackageManager(); - - // uv uses proxy-only approach, so it doesn't scan args - assert.strictEqual(pm.isSupportedCommand(["pip", "install", "requests"]), false); - assert.strictEqual(pm.isSupportedCommand(["add", "requests"]), false); - assert.strictEqual(pm.isSupportedCommand([]), false); - }); - - await t.test("should return empty dependency updates", () => { - const pm = createUvPackageManager(); - - const result = pm.getDependencyUpdatesForCommand(["pip", "install", "requests"]); - assert.deepStrictEqual(result, []); - }); }); diff --git a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js index 85302eb..ed02fe3 100644 --- a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js +++ b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js @@ -5,15 +5,11 @@ import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; /** * Sets CA bundle environment variables used by Python libraries and uv. - * These are applied to ensure all Python network libraries respect the combined CA bundle. * - * @param {NodeJS.ProcessEnv} env - Environment object to modify + * @param {NodeJS.ProcessEnv} env - Env object * @param {string} combinedCaPath - Path to the combined CA bundle */ function setUvCaBundleEnvironmentVariables(env, combinedCaPath) { - // UV_NATIVE_TLS: Use system-provided TLS certificates (default is true) - // But we also need to provide our CA bundle for MITM'd connections - // SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients if (env.SSL_CERT_FILE) { ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten."); @@ -40,6 +36,9 @@ function setUvCaBundleEnvironmentVariables(env, combinedCaPath) { * - HTTP_PROXY / HTTPS_PROXY: Proxy settings * - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification * + * Unlike pip (which requires a temporary config file for cert configuration), uv directly + * honors environment variables, so no config/ini file is needed. + * * @param {string} command - The uv command to execute (typically 'uv') * @param {string[]} args - Command line arguments to pass to uv * @returns {Promise<{status: number}>} Exit status of the uv command @@ -48,11 +47,7 @@ export async function runUv(command, args) { try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); - // Provide uv with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots) - // so that network requests validate correctly under both MITM'd and tunneled HTTPS. const combinedCaPath = getCombinedCaBundlePath(); - - // Set CA bundle environment variables for uv and underlying Python libraries setUvCaBundleEnvironmentVariables(env, combinedCaPath); // Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration diff --git a/packages/safe-chain/src/packagemanager/uv/uvSettings.js b/packages/safe-chain/src/packagemanager/uv/uvSettings.js deleted file mode 100644 index 6f68ea7..0000000 --- a/packages/safe-chain/src/packagemanager/uv/uvSettings.js +++ /dev/null @@ -1,5 +0,0 @@ -export const UV_PACKAGE_MANAGER = "uv"; - -// Unlike pip, uv only has one invocation method: the 'uv' command. -// There is no 'uv3' or 'python -m uv' pattern, so we don't need -// invocation tracking like pip does. From 5cb1bb935bd60410e8c935b36a18d19e0763e26c Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 25 Nov 2025 15:03:33 -0800 Subject: [PATCH 244/345] More cleanup' --- .../safe-chain/src/packagemanager/uv/createUvPackageManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js index ecd3367..1b1913c 100644 --- a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js +++ b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js @@ -12,7 +12,7 @@ export function createUvPackageManager() { // uv is always invoked as 'uv' - no invocation variations like pip return runUv("uv", args); }, - // For uv, rely solely on MITM proxy to detect/deny downloads from PyPI. + // For uv, rely solely on MITM isSupportedCommand: () => false, getDependencyUpdatesForCommand: () => [], }; From 023bccec11c681b1613046e351be45c36a32c0c7 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 25 Nov 2025 19:55:36 -0800 Subject: [PATCH 245/345] Some more cleanup --- .../safe-chain/src/packagemanager/uv/createUvPackageManager.js | 1 - test/e2e/Dockerfile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js index 1b1913c..76f642b 100644 --- a/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js +++ b/packages/safe-chain/src/packagemanager/uv/createUvPackageManager.js @@ -9,7 +9,6 @@ export function createUvPackageManager() { * @param {string[]} args */ runCommand: (args) => { - // uv is always invoked as 'uv' - no invocation variations like pip return runUv("uv", args); }, // For uv, rely solely on MITM diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 52ff3dc..8c3b0a5 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -67,7 +67,7 @@ except Exception as exc: raise EOF -# Install uv (Astral's fast Python package manager) +# Install uv RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ echo 'source $HOME/.local/bin/env' >> ~/.bashrc From 13892efa700547682b17597ec407cb70c96cff63 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 26 Nov 2025 16:42:51 +0100 Subject: [PATCH 246/345] Allow to configure the minimum package age --- README.md | 51 +++++++- .../safe-chain/src/config/cliArguments.js | 25 +++- .../src/config/cliArguments.spec.js | 93 ++++++++++++++ packages/safe-chain/src/config/configFile.js | 34 +++++- .../safe-chain/src/config/configFile.spec.js | 113 ++++++++++++++++++ .../src/config/environmentVariables.js | 7 ++ packages/safe-chain/src/config/settings.js | 46 +++++++ .../npm/npmInterceptor.minPackageAge.spec.js | 83 +++++++++++++ 8 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 packages/safe-chain/src/config/environmentVariables.js diff --git a/README.md b/README.md index 437b76f..b9d0357 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The Aikido Safe Chain works by running a lightweight proxy server that intercept ### Minimum package age (npm only) -For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can bypass this protection for specific installs using the `--safe-chain-skip-minimum-package-age` flag. +For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours (by default) until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can configure this threshold or bypass this protection entirely - see the [Minimum Package Age Configuration](#minimum-package-age) section below. ⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3). @@ -127,6 +127,55 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin npm install express --safe-chain-logging=verbose ``` +## Minimum Package Age + +You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 24 hours old before they can be installed through npm-based package managers. + +### Configuration Options + +You can set the minimum package age through multiple sources (in order of priority): + +1. **CLI Argument** (highest priority): + + ```shell + npm install express --safe-chain-minimum-package-age-hours=48 + ``` + +2. **Environment Variable**: + + ```shell + export AIKIDO_MINIMUM_PACKAGE_AGE_HOURS=48 + npm install express + ``` + +3. **Config File** (`~/.aikido/config.json`): + + ```json + { + "minimumPackageAgeHours": 48 + } + ``` + +### Examples + +- **Set to 48 hours for extra caution:** + + ```shell + npm install express --safe-chain-minimum-package-age-hours=48 + ``` + +- **Set to 1 hour for faster access to new packages:** + + ```shell + npm install express --safe-chain-minimum-package-age-hours=1 + ``` + +- **Completely bypass the age check for a specific install:** + + ```shell + npm install express --safe-chain-skip-minimum-package-age + ``` + # Usage in CI/CD You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 180c565..ddcd8b9 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,9 +1,10 @@ /** - * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, includePython: boolean}} + * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, includePython: boolean}} */ const state = { loggingLevel: undefined, skipMinimumPackageAge: undefined, + minimumPackageAgeHours: undefined, includePython: false, }; @@ -17,6 +18,7 @@ export function initializeCliArguments(args) { // Reset state on each call state.loggingLevel = undefined; state.skipMinimumPackageAge = undefined; + state.minimumPackageAgeHours = undefined; const safeChainArgs = []; const remainingArgs = []; @@ -31,6 +33,7 @@ export function initializeCliArguments(args) { setLoggingLevel(safeChainArgs); setSkipMinimumPackageAge(safeChainArgs); + setMinimumPackageAgeHours(safeChainArgs); setIncludePython(args); return remainingArgs; @@ -86,6 +89,26 @@ export function getSkipMinimumPackageAge() { return state.skipMinimumPackageAge; } +/** + * @param {string[]} args + * @returns {void} + */ +function setMinimumPackageAgeHours(args) { + const argName = SAFE_CHAIN_ARG_PREFIX + "minimum-package-age-hours="; + + const value = getLastArgEqualsValue(args, argName); + if (value) { + state.minimumPackageAgeHours = value; + } +} + +/** + * @returns {string | undefined} + */ +export function getMinimumPackageAgeHours() { + return state.minimumPackageAgeHours; +} + /** * @param {string[]} args */ diff --git a/packages/safe-chain/src/config/cliArguments.spec.js b/packages/safe-chain/src/config/cliArguments.spec.js index 3c8b7da..bbd5121 100644 --- a/packages/safe-chain/src/config/cliArguments.spec.js +++ b/packages/safe-chain/src/config/cliArguments.spec.js @@ -4,6 +4,7 @@ import { initializeCliArguments, getLoggingLevel, getSkipMinimumPackageAge, + getMinimumPackageAgeHours, } from "./cliArguments.js"; describe("initializeCliArguments", () => { @@ -178,4 +179,96 @@ describe("initializeCliArguments", () => { assert.deepEqual(result, ["install", "lodash"]); assert.strictEqual(getSkipMinimumPackageAge(), true); }); + + it("should return undefined when no minimum-package-age-hours argument is passed", () => { + const args = ["install", "express", "--save"]; + initializeCliArguments(args); + + assert.strictEqual(getMinimumPackageAgeHours(), undefined); + }); + + it("should parse minimum-package-age-hours value and set state", () => { + const args = [ + "--safe-chain-minimum-package-age-hours=48", + "install", + "lodash", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "lodash"]); + assert.strictEqual(getMinimumPackageAgeHours(), "48"); + }); + + it("should handle minimum-package-age-hours with zero value", () => { + const args = ["--safe-chain-minimum-package-age-hours=0", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getMinimumPackageAgeHours(), "0"); + }); + + it("should handle minimum-package-age-hours with decimal values", () => { + const args = ["--safe-chain-minimum-package-age-hours=1.5", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getMinimumPackageAgeHours(), "1.5"); + }); + + it("should handle minimum-package-age-hours case-insensitively", () => { + const args = ["--SAFE-CHAIN-MINIMUM-PACKAGE-AGE-HOURS=72", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getMinimumPackageAgeHours(), "72"); + }); + + it("should use the last minimum-package-age-hours argument when multiple are provided", () => { + const args = [ + "--safe-chain-minimum-package-age-hours=12", + "--safe-chain-minimum-package-age-hours=36", + "install", + ]; + initializeCliArguments(args); + + assert.strictEqual(getMinimumPackageAgeHours(), "36"); + }); + + it("should filter out minimum-package-age-hours argument from returned args", () => { + const args = [ + "install", + "--safe-chain-minimum-package-age-hours=48", + "express", + "--save", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "express", "--save"]); + }); + + it("should handle minimum-package-age-hours with other safe-chain arguments", () => { + const args = [ + "--safe-chain-logging=verbose", + "--safe-chain-minimum-package-age-hours=96", + "install", + "lodash", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "lodash"]); + assert.strictEqual(getLoggingLevel(), "verbose"); + assert.strictEqual(getMinimumPackageAgeHours(), "96"); + }); + + it("should handle non-numeric values without validation (validation in settings.js)", () => { + const args = ["--safe-chain-minimum-package-age-hours=invalid", "install"]; + initializeCliArguments(args); + + // cliArguments.js just captures the value; validation is in settings.js + assert.strictEqual(getMinimumPackageAgeHours(), "invalid"); + }); + + it("should handle negative values as strings (validation in settings.js)", () => { + const args = ["--safe-chain-minimum-package-age-hours=-24", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getMinimumPackageAgeHours(), "-24"); + }); }); diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index cb56705..ae25a1d 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -9,7 +9,8 @@ import { getEcoSystem } from "./settings.js"; * * This should be a number, but can be anything because it is user-input. * We cannot trust the input and should add the necessary validations. - * @property {any} scanTimeout + * @property {unknown} scanTimeout + * @property {unknown} minimumPackageAgeHours */ /** @@ -48,6 +49,35 @@ function validateTimeout(value) { return null; } +/** + * @param {any} value + * @returns {number | undefined} + */ +function validateMinimumPackageAgeHours(value) { + const hours = Number(value); + if (!Number.isNaN(hours)) { + return hours; + } + return undefined; +} + +/** + * Gets the minimum package age in hours from config file only + * @returns {number | undefined} + */ +export function getMinimumPackageAgeHours() { + const config = readConfigFile(); + if (config.minimumPackageAgeHours) { + const validated = validateMinimumPackageAgeHours( + config.minimumPackageAgeHours + ); + if (validated !== undefined) { + return validated; + } + } + return undefined; +} + /** * @param {import("../api/aikido.js").MalwarePackage[]} data * @param {string | number} version @@ -111,6 +141,7 @@ function readConfigFile() { if (!fs.existsSync(configFilePath)) { return { scanTimeout: undefined, + minimumPackageAgeHours: undefined, }; } @@ -120,6 +151,7 @@ function readConfigFile() { } catch { return { scanTimeout: undefined, + minimumPackageAgeHours: undefined, }; } } diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index 8ec980c..18415bc 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -170,3 +170,116 @@ describe("getScanTimeout", () => { assert.strictEqual(timeout, 10000); }); }); + +describe("getMinimumPackageAgeHours", () => { + let fsMock; + let getMinimumPackageAgeHours; + + beforeEach(async () => { + // Mock fs module + fsMock = { + existsSync: mock.fn(() => false), + readFileSync: mock.fn(() => "{}"), + writeFileSync: mock.fn(), + mkdirSync: mock.fn(), + }; + + mock.module("fs", { + namedExports: fsMock, + }); + + // Re-import the module to get the mocked version + const configFileModule = await import( + `./configFile.js?update=${Date.now()}` + ); + getMinimumPackageAgeHours = configFileModule.getMinimumPackageAgeHours; + }); + + afterEach(() => { + // Reset all mocks + mock.restoreAll(); + }); + + it("should return null when config file doesn't exist", () => { + fsMock.existsSync.mock.mockImplementation(() => false); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, undefined); + }); + + it("should return null when config file exists but minimumPackageAgeHours is not set", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 5000 }) + ); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, undefined); + }); + + it("should return value from config file when set to valid number", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ minimumPackageAgeHours: 48 }) + ); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, 48); + }); + + it("should handle string numbers in config file", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ minimumPackageAgeHours: "72" }) + ); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, 72); + }); + + it("should handle decimal values", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ minimumPackageAgeHours: 1.5 }) + ); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, 1.5); + }); + + it("should return null for non-numeric strings", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ minimumPackageAgeHours: "invalid" }) + ); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, undefined); + }); + + it("should return null for values with units suffix", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ minimumPackageAgeHours: "48h" }) + ); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, undefined); + }); + + it("should handle malformed JSON and return null", () => { + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => "{ invalid json"); + + const hours = getMinimumPackageAgeHours(); + + assert.strictEqual(hours, undefined); + }); +}); diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js new file mode 100644 index 0000000..95616c5 --- /dev/null +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -0,0 +1,7 @@ +/** + * Gets the minimum package age in hours from environment variable + * @returns {string | undefined} + */ +export function getMinimumPackageAgeHours() { + return process.env.AIKIDO_MINIMUM_PACKAGE_AGE_HOURS; +} diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ce7f35c..7c20358 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -1,4 +1,6 @@ import * as cliArguments from "./cliArguments.js"; +import * as configFile from "./configFile.js"; +import * as environmentVariables from "./environmentVariables.js"; export const LOGGING_SILENT = "silent"; export const LOGGING_NORMAL = "normal"; @@ -38,10 +40,54 @@ export function setEcoSystem(setting) { } const defaultMinimumPackageAge = 24; +/** @returns {number} */ export function getMinimumPackageAgeHours() { + // Priority 1: CLI argument + const cliValue = validateMinimumPackageAgeHours( + cliArguments.getMinimumPackageAgeHours() + ); + if (cliValue !== undefined) { + return cliValue; + } + + // Priority 2: Environment variable + const envValue = validateMinimumPackageAgeHours( + environmentVariables.getMinimumPackageAgeHours() + ); + if (envValue !== undefined) { + return envValue; + } + + // Priority 3: Config file + const configValue = configFile.getMinimumPackageAgeHours(); + if (configValue !== undefined) { + return configValue; + } + return defaultMinimumPackageAge; } +/** + * @param {string | undefined} value + * @returns {number | undefined} + */ +function validateMinimumPackageAgeHours(value) { + if (!value) { + return undefined; + } + + const numericValue = Number(value); + if (Number.isNaN(numericValue)) { + return undefined; + } + + if (numericValue > 0) { + return numericValue; + } + + return undefined; +} + const defaultSkipMinimumPackageAge = false; export function skipMinimumPackageAge() { const cliValue = cliArguments.getSkipMinimumPackageAge(); diff --git a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js index 2ff5a52..999e64a 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npm/npmInterceptor.minPackageAge.spec.js @@ -273,6 +273,89 @@ describe("npmInterceptor minimum package age", async () => { assert.equal(modifiedJson["dist-tags"]["latest"], "3.0.0"); }); + it("Should use custom minimum package age of 48 hours", async () => { + minimumPackageAgeSettings = 48; + skipMinimumPackageAgeSetting = false; + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "4.0.0", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + ["4.0.0"]: {}, + }, + time: { + created: getDate(-365 * 24), + modified: getDate(-24), + ["1.0.0"]: getDate(-72), // 3 days old - should remain + ["2.0.0"]: getDate(-50), // ~2 days old - should remain + // 48-hour cutoff here + ["3.0.0"]: getDate(-40), // ~1.7 days old - should be removed + ["4.0.0"]: getDate(-24), // 1 day old - should be removed + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + + // Versions older than 48 hours should remain + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); + + // Versions newer than 48 hours should be removed + assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); + assert.ok(!Object.keys(modifiedJson.versions).includes("4.0.0")); + + // Latest should be recalculated to 2.0.0 + assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0"); + + assert.equal(Object.keys(modifiedJson.versions).length, 2); + }); + + it("Should use very small minimum package age of 1 hour", async () => { + minimumPackageAgeSettings = 1; + skipMinimumPackageAgeSetting = false; + const packageUrl = "https://registry.npmjs.org/lodash"; + + const modifiedBody = await runModifyNpmInfoRequest( + packageUrl, + JSON.stringify({ + name: "lodash", + ["dist-tags"]: { + latest: "3.0.0", + }, + versions: { + ["1.0.0"]: {}, + ["2.0.0"]: {}, + ["3.0.0"]: {}, + }, + time: { + created: getDate(-48), + modified: getDate(0), + ["1.0.0"]: getDate(-3), // 3 hours old - should remain + ["2.0.0"]: getDate(-2), // 2 hours old - should remain + // 1-hour cutoff here + ["3.0.0"]: getDate(0), // just published - should be removed + }, + }) + ); + + const modifiedJson = JSON.parse(modifiedBody); + + assert.equal(Object.keys(modifiedJson.versions).length, 2); + assert.ok(Object.keys(modifiedJson.versions).includes("1.0.0")); + assert.ok(Object.keys(modifiedJson.versions).includes("2.0.0")); + assert.ok(!Object.keys(modifiedJson.versions).includes("3.0.0")); + assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0"); + }); + function getDate(plusHours) { const date = new Date(); date.setHours(date.getHours() + plusHours); From 3e6ff1ab5621bcbe9d731306ac392e80139b7aff Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 26 Nov 2025 16:46:01 +0100 Subject: [PATCH 247/345] Update readme file --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index b9d0357..7076ca2 100644 --- a/README.md +++ b/README.md @@ -156,26 +156,6 @@ You can set the minimum package age through multiple sources (in order of priori } ``` -### Examples - -- **Set to 48 hours for extra caution:** - - ```shell - npm install express --safe-chain-minimum-package-age-hours=48 - ``` - -- **Set to 1 hour for faster access to new packages:** - - ```shell - npm install express --safe-chain-minimum-package-age-hours=1 - ``` - -- **Completely bypass the age check for a specific install:** - - ```shell - npm install express --safe-chain-skip-minimum-package-age - ``` - # Usage in CI/CD You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. From 9b5b3cad22bee7534aedc3be40c7e962135ab5a4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 26 Nov 2025 16:47:46 +0100 Subject: [PATCH 248/345] Rename the environment variable --- README.md | 2 +- packages/safe-chain/src/config/environmentVariables.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7076ca2..bda2f9a 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ You can set the minimum package age through multiple sources (in order of priori 2. **Environment Variable**: ```shell - export AIKIDO_MINIMUM_PACKAGE_AGE_HOURS=48 + export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=48 npm install express ``` diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index 95616c5..5c6056a 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -3,5 +3,5 @@ * @returns {string | undefined} */ export function getMinimumPackageAgeHours() { - return process.env.AIKIDO_MINIMUM_PACKAGE_AGE_HOURS; + return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS; } From 3140dcc071149e2f9fc7be0204a852f2026ef2e2 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 26 Nov 2025 17:40:18 +0100 Subject: [PATCH 249/345] Add banner for safe-chain --- README.md | 2 + docs/banner.svg | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 docs/banner.svg diff --git a/README.md b/README.md index 8e3ec5f..47f0894 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Aikido Safe Chain](./docs/banner.svg) + # Aikido Safe Chain - ✅ **Block malware on developer laptops and CI/CD** diff --git a/docs/banner.svg b/docs/banner.svg new file mode 100644 index 0000000..5996435 --- /dev/null +++ b/docs/banner.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From da1d76e43f42eb014d6e162abdf6c78332422547 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 26 Nov 2025 18:23:53 +0100 Subject: [PATCH 250/345] Update banner with new tag line --- docs/banner.svg | 61 ++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/docs/banner.svg b/docs/banner.svg index 5996435..ce9a00c 100644 --- a/docs/banner.svg +++ b/docs/banner.svg @@ -1,8 +1,8 @@ - + - + @@ -24,11 +24,11 @@ - - + + - + @@ -86,11 +86,12 @@ - + - + + @@ -110,42 +111,40 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - + - + - - - - - + - + - + From c5b4fbf2388dfeeffa29cb6a93028b5d67f98908 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 10:34:11 +0100 Subject: [PATCH 251/345] Update node-forge, npm-registry-fetch and make-fetch-happen --- package-lock.json | 816 +++++++------------------------ packages/safe-chain/package.json | 6 +- 2 files changed, 167 insertions(+), 655 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60bd1bf..caf51f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,120 +23,62 @@ "resolved": "test/e2e", "link": true }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "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": { + "node_modules/@isaacs/balanced-match": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", "dependencies": { - "minipass": "^7.0.4" + "@isaacs/balanced-match": "^4.0.1" }, "engines": { - "node": ">=18.0.0" + "node": "20 || >=22" } }, "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", + "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/redact": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", - "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@oxlint/darwin-arm64": { @@ -243,16 +185,6 @@ "win32" ] }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@types/ini": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", @@ -357,71 +289,32 @@ "node": ">= 14" } }, - "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/ansi-styles": { - "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": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", "license": "ISC", "dependencies": { - "@npmcli/fs": "^4.0.0", + "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/call-bind-apply-helpers": { @@ -457,33 +350,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -496,20 +362,6 @@ "node": ">= 0.8" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -550,18 +402,6 @@ "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/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/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -572,19 +412,6 @@ "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", @@ -636,22 +463,6 @@ "node": ">= 0.4" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -727,35 +538,17 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "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" + "path-scurry": "^2.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -813,15 +606,15 @@ } }, "node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "license": "ISC", "dependencies": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/http-cache-semantics": { @@ -856,6 +649,19 @@ "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", @@ -875,54 +681,14 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } }, - "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", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@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", @@ -933,31 +699,34 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", "license": "ISC", "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "ssri": "^13.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/math-intrinsics": { @@ -990,6 +759,21 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "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", @@ -1012,9 +796,9 @@ } }, "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.0.tgz", + "integrity": "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==", "license": "MIT", "dependencies": { "minipass": "^7.0.3", @@ -1022,7 +806,7 @@ "minizlib": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -1052,12 +836,6 @@ "node": ">=8" } }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -1082,12 +860,6 @@ "node": ">=8" } }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", @@ -1112,16 +884,10 @@ "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -1130,21 +896,6 @@ "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", @@ -1167,9 +918,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -1186,37 +937,37 @@ } }, "node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "license": "ISC", "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^7.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-registry-fetch": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", - "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", "license": "ISC", "dependencies": { - "@npmcli/redact": "^3.0.0", + "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/oxlint": { @@ -1254,9 +1005,9 @@ } }, "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "license": "MIT", "engines": { "node": ">=18" @@ -1265,44 +1016,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/promise-retry": { @@ -1346,39 +1082,6 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -1390,12 +1093,12 @@ } }, "node_modules/socks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", - "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -1417,93 +1120,16 @@ "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", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "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==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "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.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/typescript": { @@ -1526,158 +1152,43 @@ "dev": true }, "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", "license": "ISC", "dependencies": { - "unique-slug": "^5.0.0" + "unique-slug": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.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", - "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.0.tgz", + "integrity": "sha512-bwVk/OK+Qu108aJcMAEiU4yavHUI7aN20TgZNBj9MR2iU1zPUl1Z1Otr7771ExfYTPTvfN8ZJ1pbr5Iklgt4xg==", + "license": "ISC", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "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": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "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", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "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/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/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": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", @@ -1688,9 +1199,9 @@ "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "ini": "6.0.0", - "make-fetch-happen": "14.0.3", - "node-forge": "1.3.1", - "npm-registry-fetch": "18.0.2", + "make-fetch-happen": "15.0.3", + "node-forge": "1.3.2", + "npm-registry-fetch": "19.1.1", "semver": "7.7.2" }, "bin": { @@ -1704,6 +1215,7 @@ "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-python": "bin/aikido-python.js", "aikido-python3": "bin/aikido-python3.js", + "aikido-uv": "bin/aikido-uv.js", "aikido-yarn": "bin/aikido-yarn.js", "safe-chain": "bin/safe-chain.js" }, diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 15279d6..9f90b42 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -40,9 +40,9 @@ "chalk": "5.4.1", "https-proxy-agent": "7.0.6", "ini": "6.0.0", - "make-fetch-happen": "14.0.3", - "node-forge": "1.3.1", - "npm-registry-fetch": "18.0.2", + "make-fetch-happen": "15.0.3", + "node-forge": "1.3.2", + "npm-registry-fetch": "19.1.1", "semver": "7.7.2" }, "devDependencies": { From b14ff4cb3375cc258db5e0e9c92a863d17b41b37 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 15:01:57 +0100 Subject: [PATCH 252/345] First time build of the safe-chain binaries --- .github/workflows/test-on-pr.yml | 12 + package-lock.json | 486 ++++++++++++++++++++++ packages/safe-chain/bin/aikido-bun.js | 6 +- packages/safe-chain/bin/aikido-bunx.js | 6 +- packages/safe-chain/bin/aikido-npm.js | 6 +- packages/safe-chain/bin/aikido-npx.js | 6 +- packages/safe-chain/bin/aikido-pip.js | 8 +- packages/safe-chain/bin/aikido-pip3.js | 8 +- packages/safe-chain/bin/aikido-pnpm.js | 6 +- packages/safe-chain/bin/aikido-pnpx.js | 6 +- packages/safe-chain/bin/aikido-python.js | 28 +- packages/safe-chain/bin/aikido-python3.js | 28 +- packages/safe-chain/bin/aikido-uv.js | 8 +- packages/safe-chain/bin/aikido-yarn.js | 6 +- packages/safe-chain/bin/safe-chain.js | 18 +- packages/safe-chain/package.json | 23 + 16 files changed, 608 insertions(+), 53 deletions(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index f8087ef..81a3e45 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -43,6 +43,18 @@ jobs: name: safe-chain-package path: aikidosec-safe-chain-*.tgz + - name: Create binaries + run: | + npm i -g esbuild@0.27.0 @yao-pkg/pkg@6.10.1 + mkdir "dist" + esbuild "./packages/safe-chain/bin/safe-chain.js" --bundle --platform=node --target=node22 > "./dist/safe-chain.cjs" + pkg "./dist/safe-chain.cjs" --targets node22-macos-x64 --output "./dist/macos-x64/safe-chain" + pkg "./dist/safe-chain.cjs" --targets node22-macos-arm64 --output "./dist/macos-arm64/safe-chain" + pkg "./dist/safe-chain.cjs" --targets node22-linux-x64 --output "./dist/linux-x64/safe-chain" + pkg "./dist/safe-chain.cjs" --targets node22-linux-arm64 --output "./dist/linux-arm64/safe-chain" + pkg "./dist/safe-chain.cjs" --targets node22-win-x64 --output "./dist/win-x64/safe-chain.exe" + pkg "./dist/safe-chain.cjs" --targets node22-win-arm64 --output "./dist/win-arm64/safe-chain.exe" + e2e-tests: name: Run E2E tests diff --git a/package-lock.json b/package-lock.json index 60bd1bf..e25b316 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,448 @@ "resolved": "test/e2e", "link": true }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -636,6 +1078,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1704,6 +2188,7 @@ "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-python": "bin/aikido-python.js", "aikido-python3": "bin/aikido-python3.js", + "aikido-uv": "bin/aikido-uv.js", "aikido-yarn": "bin/aikido-yarn.js", "safe-chain": "bin/safe-chain.js" }, @@ -1714,6 +2199,7 @@ "@types/node-forge": "^1.3.14", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", + "esbuild": "^0.27.0", "typescript": "^5.9.3" } }, diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index c128445..9d11784 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index 2e83793..bcc93a6 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index a50d9b5..7916f7e 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index e1687d3..58f3491 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 39184f0..006e661 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -13,6 +13,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP); initializePackageManager(PIP_PACKAGE_MANAGER); -// Pass through only user-supplied pip args -var exitCode = await main(process.argv.slice(2)); -process.exit(exitCode); +(async () => { + // Pass through only user-supplied pip args + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index e388383..e831afe 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -14,6 +14,8 @@ setCurrentPipInvocation(PIP_INVOCATIONS.PIP3); // Create package manager initializePackageManager(PIP_PACKAGE_MANAGER); -// Pass through only user-supplied pip args -var exitCode = await main(process.argv.slice(2)); -process.exit(exitCode); +(async () => { + // Pass through only user-supplied pip args + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index cf5125e..64bc755 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index 6182810..11ee45c 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index 1ef4e34..29c38e6 100755 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -11,18 +11,20 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); -if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { - setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP); - initializePackageManager(PIP_PACKAGE_MANAGER); +(async () => { + if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { + setEcoSystem(ECOSYSTEM_PY); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); - // Strip off the '-m pip' or '-m pip3' from the args - argv = argv.slice(2); + // Strip off the '-m pip' or '-m pip3' from the args + argv = argv.slice(2); - var exitCode = await main(argv); - process.exit(exitCode); -} else { - // Forward to real python binary for non-pip flows - const { spawn } = await import('child_process'); - spawn('python', argv, { stdio: 'inherit' }); -} + var exitCode = await main(argv); + process.exit(exitCode); + } else { + // Forward to real python binary for non-pip flows + const { spawn } = await import('child_process'); + spawn('python', argv, { stdio: 'inherit' }); + } +})(); diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index f53e5d2..997a88d 100755 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -11,18 +11,20 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); -if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { - setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP); - initializePackageManager(PIP_PACKAGE_MANAGER); +(async () => { + if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { + setEcoSystem(ECOSYSTEM_PY); + setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP); + initializePackageManager(PIP_PACKAGE_MANAGER); - // Strip off the '-m pip' or '-m pip3' from the args - argv = argv.slice(2); + // Strip off the '-m pip' or '-m pip3' from the args + argv = argv.slice(2); - var exitCode = await main(argv); - process.exit(exitCode); -} else { - // Forward to real python3 binary for non-pip flows - const { spawn } = await import('child_process'); - spawn('python3', argv, { stdio: 'inherit' }); -} + var exitCode = await main(argv); + process.exit(exitCode); + } else { + // Forward to real python3 binary for non-pip flows + const { spawn } = await import('child_process'); + spawn('python3', argv, { stdio: 'inherit' }); + } +})(); diff --git a/packages/safe-chain/bin/aikido-uv.js b/packages/safe-chain/bin/aikido-uv.js index 14180f2..4e635de 100755 --- a/packages/safe-chain/bin/aikido-uv.js +++ b/packages/safe-chain/bin/aikido-uv.js @@ -9,6 +9,8 @@ setEcoSystem(ECOSYSTEM_PY); initializePackageManager("uv"); -// Pass through only user-supplied uv args -var exitCode = await main(process.argv.slice(2)); -process.exit(exitCode); +(async () => { + // Pass through only user-supplied uv args + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index eee14e8..6c428db 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -7,6 +7,8 @@ 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)); -process.exit(exitCode); +(async () => { + var exitCode = await main(process.argv.slice(2)); + process.exit(exitCode); +})(); diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 94e4e1f..738c42b 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -7,6 +7,9 @@ import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; import { setupCi } from "../src/shell-integration/setup-ci.js"; import { initializeCliArguments } from "../src/config/cliArguments.js"; +import { ECOSYSTEM_JS, setEcoSystem } from "../src/config/settings.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; +import { main } from "../src/main.js"; if (process.argv.length < 3) { ui.writeError("No command provided. Please provide a command to execute."); @@ -19,12 +22,19 @@ initializeCliArguments(process.argv); const command = process.argv[2]; -if (command === "help" || command === "--help" || command === "-h") { +const pkgManagerCommands = ["npm", "npx", "yarn"]; + +if (pkgManagerCommands.includes(command)) { + setEcoSystem(ECOSYSTEM_JS); + initializePackageManager(command); + (async () => { + var exitCode = await main(process.argv.slice(3)); + process.exit(exitCode); + })(); +} else if (command === "help" || command === "--help" || command === "-h") { writeHelp(); process.exit(0); -} - -if (command === "setup") { +}else if (command === "setup") { setup(); } else if (command === "teardown") { teardown(); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 15279d6..abf2d69 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -52,6 +52,7 @@ "@types/node-forge": "^1.3.14", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", + "esbuild": "^0.27.0", "typescript": "^5.9.3" }, "main": "src/main.js", @@ -63,5 +64,27 @@ "type": "git", "url": "git+https://github.com/AikidoSec/safe-chain.git", "directory": "packages/safe-chain" + }, + "pkg": { + "targets": [ + "node22-linux-x64", + "node22-linux-arm64", + "node22-macos-x64", + "node22-macos-arm64", + "node22-win-x64", + "node22-win-arm64" + ], + "outputPath": "dist", + "assets": [ + "node_modules/certifi/**/*" + ], + "scripts": [ + "src/**/*.js", + "bin/**/*.js" + ], + "ignore": [ + "**/*.spec.js", + "test/**" + ] } } From 430792626b042a6cf4fe7699d546ad12d13a2322 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 15:07:54 +0100 Subject: [PATCH 253/345] Publish the created binaries --- .github/workflows/test-on-pr.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 81a3e45..e281d2c 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -54,6 +54,13 @@ jobs: pkg "./dist/safe-chain.cjs" --targets node22-linux-arm64 --output "./dist/linux-arm64/safe-chain" pkg "./dist/safe-chain.cjs" --targets node22-win-x64 --output "./dist/win-x64/safe-chain.exe" pkg "./dist/safe-chain.cjs" --targets node22-win-arm64 --output "./dist/win-arm64/safe-chain.exe" + ls -la ./dist + + - name: Upload safe-chain-binaries + uses: actions/upload-artifact@v4 + with: + name: safe-chain-package + path: aikidosec-safe-chain-*.tgz e2e-tests: name: Run E2E tests From a632ef9bddf07fb601b65dfc845968ced669fad6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 15:11:11 +0100 Subject: [PATCH 254/345] Add the correct binaries --- .github/workflows/test-on-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index e281d2c..644a928 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -59,8 +59,8 @@ jobs: - name: Upload safe-chain-binaries uses: actions/upload-artifact@v4 with: - name: safe-chain-package - path: aikidosec-safe-chain-*.tgz + name: safe-chain-binaries + path: dist/* e2e-tests: name: Run E2E tests From 543f10657cd3f8d4e005ba612a66629fad037ddc Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 15:21:36 +0100 Subject: [PATCH 255/345] Separate pipeline for binary creation --- .github/workflows/create-artifact.yml | 86 +++++++++++++++++++++++++++ .github/workflows/test-on-pr.yml | 19 ------ 2 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/create-artifact.yml diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml new file mode 100644 index 0000000..f7984f1 --- /dev/null +++ b/.github/workflows/create-artifact.yml @@ -0,0 +1,86 @@ +name: Create binaries + +on: pull_request + +jobs: + + create-binaries: + + name: Create binary for ${{ matrix.os }}-${{ matrix.arch }} + + runs-on: ${{ matrix.runner }} + + strategy: + matrix: + include: + - os: macos + arch: x64 + runner: macos-15-intel + target: node22-macos-x64 + extension: '' + - os: macos + arch: arm64 + runner: macos-latest + target: node22-macos-arm64 + extension: '' + - os: linux + arch: x64 + runner: ubuntu-latest + target: node22-linux-x64 + extension: '' + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + target: node22-linux-arm64 + extension: '' + - os: win + arch: x64 + runner: windows-latest + target: node22-win-x64 + extension: '.exe' + - os: win + arch: arm64 + runner: windows-11-arm + target: node22-win-arm64 + extension: '.exe' + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "lts/*" + + - name: Setup safe-chain + run: | + npm i -g @aikidosec/safe-chain + safe-chain setup-ci + + - name: Install dependencies + run: npm ci + + - name: Create binary (Unix) + if: matrix.os != 'win' + run: | + npm i -g esbuild@0.27.0 @yao-pkg/pkg@6.10.1 + mkdir -p "dist/${{ matrix.os }}-${{ matrix.arch }}" + esbuild "./packages/safe-chain/bin/safe-chain.js" --bundle --platform=node --target=node22 > "./dist/safe-chain.cjs" + pkg "./dist/safe-chain.cjs" --targets ${{ matrix.target }} --output "./dist/${{ matrix.os }}-${{ matrix.arch }}/safe-chain${{ matrix.extension }}" + shell: bash + + - name: Create binary (Windows) + if: matrix.os == 'win' + run: | + npm i -g esbuild@0.27.0 @yao-pkg/pkg@6.10.1 + New-Item -ItemType Directory -Force -Path "dist/${{ matrix.os }}-${{ matrix.arch }}" + esbuild "./packages/safe-chain/bin/safe-chain.js" --bundle --platform=node --target=node22 | Out-File -FilePath "./dist/safe-chain.cjs" -Encoding utf8 + pkg "./dist/safe-chain.cjs" --targets ${{ matrix.target }} --output "./dist/${{ matrix.os }}-${{ matrix.arch }}/safe-chain${{ matrix.extension }}" + shell: powershell + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: safe-chain-${{ matrix.os }}-${{ matrix.arch }} + path: dist/${{ matrix.os }}-${{ matrix.arch }}/* \ No newline at end of file diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 644a928..f8087ef 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -43,25 +43,6 @@ jobs: name: safe-chain-package path: aikidosec-safe-chain-*.tgz - - name: Create binaries - run: | - npm i -g esbuild@0.27.0 @yao-pkg/pkg@6.10.1 - mkdir "dist" - esbuild "./packages/safe-chain/bin/safe-chain.js" --bundle --platform=node --target=node22 > "./dist/safe-chain.cjs" - pkg "./dist/safe-chain.cjs" --targets node22-macos-x64 --output "./dist/macos-x64/safe-chain" - pkg "./dist/safe-chain.cjs" --targets node22-macos-arm64 --output "./dist/macos-arm64/safe-chain" - pkg "./dist/safe-chain.cjs" --targets node22-linux-x64 --output "./dist/linux-x64/safe-chain" - pkg "./dist/safe-chain.cjs" --targets node22-linux-arm64 --output "./dist/linux-arm64/safe-chain" - pkg "./dist/safe-chain.cjs" --targets node22-win-x64 --output "./dist/win-x64/safe-chain.exe" - pkg "./dist/safe-chain.cjs" --targets node22-win-arm64 --output "./dist/win-arm64/safe-chain.exe" - ls -la ./dist - - - name: Upload safe-chain-binaries - uses: actions/upload-artifact@v4 - with: - name: safe-chain-binaries - path: dist/* - e2e-tests: name: Run E2E tests From dbbe0f27bf02d01392ffebfcda28e8af1511f180 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 15:26:40 +0100 Subject: [PATCH 256/345] Speed up Windows --- .github/workflows/create-artifact.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index f7984f1..6be270e 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -54,6 +54,7 @@ jobs: node-version: "lts/*" - name: Setup safe-chain + if: matrix.os != 'win' run: | npm i -g @aikidosec/safe-chain safe-chain setup-ci From 98231b8d25d16ca447089a53d1a1c790f1df70e4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 27 Nov 2025 15:30:39 +0100 Subject: [PATCH 257/345] Ignore scripts on install for binaries --- .github/workflows/create-artifact.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 6be270e..44d2e3d 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -60,7 +60,7 @@ jobs: safe-chain setup-ci - name: Install dependencies - run: npm ci + run: npm ci --ignore-scripts - name: Create binary (Unix) if: matrix.os != 'win' From 9c149f3bb311da4609c3b18a4c8bb098039d6fcb Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 10:51:43 +0100 Subject: [PATCH 258/345] Create and run build.js --- .github/workflows/create-artifact.yml | 34 ++----- .gitignore | 6 +- build.js | 90 +++++++++++++++++++ package-lock.json | 1 + package.json | 3 +- packages/safe-chain/bin/safe-chain.js | 25 ++++-- .../safe-chain/src/shell-integration/setup.js | 4 +- 7 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 build.js diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 44d2e3d..7716312 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -3,9 +3,7 @@ name: Create binaries on: pull_request jobs: - create-binaries: - name: Create binary for ${{ matrix.os }}-${{ matrix.arch }} runs-on: ${{ matrix.runner }} @@ -17,32 +15,32 @@ jobs: arch: x64 runner: macos-15-intel target: node22-macos-x64 - extension: '' + extension: "" - os: macos arch: arm64 runner: macos-latest target: node22-macos-arm64 - extension: '' + extension: "" - os: linux arch: x64 runner: ubuntu-latest target: node22-linux-x64 - extension: '' + extension: "" - os: linux arch: arm64 runner: ubuntu-24.04-arm target: node22-linux-arm64 - extension: '' + extension: "" - os: win arch: x64 runner: windows-latest target: node22-win-x64 - extension: '.exe' + extension: ".exe" - os: win arch: arm64 runner: windows-11-arm target: node22-win-arm64 - extension: '.exe' + extension: ".exe" steps: - name: Checkout code @@ -51,10 +49,9 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: "lts/*" + node-version: "22.x" - name: Setup safe-chain - if: matrix.os != 'win' run: | npm i -g @aikidosec/safe-chain safe-chain setup-ci @@ -65,23 +62,10 @@ jobs: - name: Create binary (Unix) if: matrix.os != 'win' run: | - npm i -g esbuild@0.27.0 @yao-pkg/pkg@6.10.1 - mkdir -p "dist/${{ matrix.os }}-${{ matrix.arch }}" - esbuild "./packages/safe-chain/bin/safe-chain.js" --bundle --platform=node --target=node22 > "./dist/safe-chain.cjs" - pkg "./dist/safe-chain.cjs" --targets ${{ matrix.target }} --output "./dist/${{ matrix.os }}-${{ matrix.arch }}/safe-chain${{ matrix.extension }}" - shell: bash - - - name: Create binary (Windows) - if: matrix.os == 'win' - run: | - npm i -g esbuild@0.27.0 @yao-pkg/pkg@6.10.1 - New-Item -ItemType Directory -Force -Path "dist/${{ matrix.os }}-${{ matrix.arch }}" - esbuild "./packages/safe-chain/bin/safe-chain.js" --bundle --platform=node --target=node22 | Out-File -FilePath "./dist/safe-chain.cjs" -Encoding utf8 - pkg "./dist/safe-chain.cjs" --targets ${{ matrix.target }} --output "./dist/${{ matrix.os }}-${{ matrix.arch }}/safe-chain${{ matrix.extension }}" - shell: powershell + node build.js ${{ matrix.target }} - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: safe-chain-${{ matrix.os }}-${{ matrix.arch }} - path: dist/${{ matrix.os }}-${{ matrix.arch }}/* \ No newline at end of file + path: dist/* diff --git a/.gitignore b/.gitignore index acae695..7c44b34 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,8 @@ vite.config.ts.timestamp-* # AI Claude.md .claude -.reference \ No newline at end of file +.reference + +# Build files +build/ +dist/ diff --git a/build.js b/build.js new file mode 100644 index 0000000..55ad3ec --- /dev/null +++ b/build.js @@ -0,0 +1,90 @@ +import { build } from "esbuild"; +import { mkdir, cp, rm, readFile, writeFile } from "node:fs/promises"; +import { spawn } from "node:child_process"; + +const target = process.argv[2]; +if (!target) { + // eslint-disable-next-line no-console + console.error("Usage: node build.js "); + // eslint-disable-next-line no-console + console.error("Example: node build.js node22-macos-arm64"); + process.exit(1); +} + +(async function () { + await clearOutputFolder(); + await bundleSafeChain(); + await copyShellScripts(); + await copyAndModifyPackageJson(); + await buildSafeChainBinary(target); +})(); + +async function clearOutputFolder() { + await rm("./build", { recursive: true, force: true }); + await mkdir("./build"); +} + +async function bundleSafeChain() { + await build({ + entryPoints: ["./packages/safe-chain/bin/safe-chain.js"], + bundle: true, + platform: "node", + target: "node22", + outfile: "./build/bin/safe-chain.cjs", + }); +} + +async function copyShellScripts() { + await mkdir("./build/bin/startup-scripts", { recursive: true }); + await cp( + "./packages/safe-chain/src/shell-integration/startup-scripts/", + "./build/bin/startup-scripts", + { recursive: true } + ); +} +async function copyAndModifyPackageJson() { + const packageJsonContent = await readFile( + "./packages/safe-chain/package.json", + "utf-8" + ); + const packageJson = JSON.parse(packageJsonContent); + + delete packageJson.main; + delete packageJson.scripts; + delete packageJson.exports; + delete packageJson.dependencies; + delete packageJson.devDependencies; + + packageJson.bin = { + "safe-chain": "bin/safe-chain.cjs", + }; + packageJson.type = "commonjs"; + packageJson.pkg = { + outputPath: "dist", + assets: ["node_modules/certifi/**/*", "bin/startup-scripts/**/*"], + }; + + await writeFile("./build/package.json", JSON.stringify(packageJson, null, 2)); + + return packageJson; +} + +function buildSafeChainBinary(target) { + return new Promise((resolve, reject) => { + const pkg = spawn( + "npx", + ["pkg", "./build/package.json", `--target=${target}`], + { + stdio: "inherit", + } + ); + + pkg.on("close", (code) => { + if (code !== 0) { + reject(new Error(`pkg process exited with code ${code}`)); + } else { + resolve(); + } + }); + }); +} diff --git a/package-lock.json b/package-lock.json index a720a7f..16f42c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "test/e2e" ], "devDependencies": { + "esbuild": "^0.27.0", "oxlint": "^1.22.0" } }, diff --git a/package.json b/package.json index aa40862..8428fe4 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "author": "Aikido Security", "license": "AGPL-3.0-or-later", "devDependencies": { - "oxlint": "^1.22.0" + "oxlint": "^1.22.0", + "esbuild": "^0.27.0" } } diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 738c42b..667a880 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import chalk from "chalk"; -import { createRequire } from "module"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; @@ -10,6 +9,8 @@ import { initializeCliArguments } from "../src/config/cliArguments.js"; import { ECOSYSTEM_JS, setEcoSystem } from "../src/config/settings.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { main } from "../src/main.js"; +import path from "path"; +import fs from "fs"; if (process.argv.length < 3) { ui.writeError("No command provided. Please provide a command to execute."); @@ -25,6 +26,7 @@ const command = process.argv[2]; const pkgManagerCommands = ["npm", "npx", "yarn"]; if (pkgManagerCommands.includes(command)) { + ui.writeInformation(process.argv.join(", ")); setEcoSystem(ECOSYSTEM_JS); initializePackageManager(command); (async () => { @@ -34,14 +36,16 @@ if (pkgManagerCommands.includes(command)) { } else if (command === "help" || command === "--help" || command === "-h") { writeHelp(); process.exit(0); -}else if (command === "setup") { +} else if (command === "setup") { setup(); } else if (command === "teardown") { teardown(); } else if (command === "setup-ci") { setupCi(); } else if (command === "--version" || command === "-v" || command === "-v") { - ui.writeInformation(`Current safe-chain version: ${getVersion()}`); + (async () => { + ui.writeInformation(`Current safe-chain version: ${await getVersion()}`); + })(); } else { ui.writeError(`Unknown command: ${command}.`); ui.emptyLine(); @@ -97,8 +101,15 @@ function writeHelp() { ui.emptyLine(); } -function getVersion() { - const require = createRequire(import.meta.url); - const packageJson = require("../package.json"); - return packageJson.version; +async function getVersion() { + const packageJsonPath = path.join(__dirname, "..", "package.json"); + + const data = await fs.promises.readFile(packageJsonPath); + const json = JSON.parse(data.toString("utf8")); + + if (json && json.version) { + return json.version; + } + + return "1.0.0"; } diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index e734858..cd54bb8 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -103,9 +103,7 @@ function copyStartupFiles() { } // Use absolute path for source - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const sourcePath = path.resolve( + const sourcePath = path.join( __dirname, includePython() ? "startup-scripts/include-python" : "startup-scripts", file From a01314111861e5f32dddff094ddbfc8e0137a3a1 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 10:55:29 +0100 Subject: [PATCH 259/345] Try to fix the build --- .github/workflows/create-artifact.yml | 2 +- build.js | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 7716312..8a26cb1 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -53,7 +53,7 @@ jobs: - name: Setup safe-chain run: | - npm i -g @aikidosec/safe-chain + npm i -g @aikidosec/safe-chain pkg safe-chain setup-ci - name: Install dependencies diff --git a/build.js b/build.js index 55ad3ec..4ef0481 100644 --- a/build.js +++ b/build.js @@ -71,13 +71,9 @@ async function copyAndModifyPackageJson() { function buildSafeChainBinary(target) { return new Promise((resolve, reject) => { - const pkg = spawn( - "npx", - ["pkg", "./build/package.json", `--target=${target}`], - { - stdio: "inherit", - } - ); + const pkg = spawn("pkg", ["./build/package.json", `--target=${target}`], { + stdio: "inherit", + }); pkg.on("close", (code) => { if (code !== 0) { From ae514d60d8b7bbf907d22baa85084eb095825f68 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 10:56:34 +0100 Subject: [PATCH 260/345] Use shell for pkg --- build.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build.js b/build.js index 4ef0481..e5e9f7d 100644 --- a/build.js +++ b/build.js @@ -73,6 +73,7 @@ function buildSafeChainBinary(target) { return new Promise((resolve, reject) => { const pkg = spawn("pkg", ["./build/package.json", `--target=${target}`], { stdio: "inherit", + shell: true, }); pkg.on("close", (code) => { From 8733f53b6b112e91ecae05c40f7c7e177e30e49e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 10:58:10 +0100 Subject: [PATCH 261/345] Debug build --- build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.js b/build.js index e5e9f7d..3e813ef 100644 --- a/build.js +++ b/build.js @@ -70,6 +70,8 @@ async function copyAndModifyPackageJson() { } function buildSafeChainBinary(target) { + // eslint-disable-next-line no-console + console.error("Target: " + target); return new Promise((resolve, reject) => { const pkg = spawn("pkg", ["./build/package.json", `--target=${target}`], { stdio: "inherit", From ccc8d685b2063d17317989f2f701c9556b8c70b5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 10:59:52 +0100 Subject: [PATCH 262/345] Don't fail-fast in the pipeline matrix --- .github/workflows/create-artifact.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 8a26cb1..125e89b 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -9,6 +9,7 @@ jobs: runs-on: ${{ matrix.runner }} strategy: + fail-fast: false matrix: include: - os: macos From bc51c839d0f7c54f81d116b02fbaf61886deef21 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:02:48 +0100 Subject: [PATCH 263/345] Try fix build again --- .github/workflows/create-artifact.yml | 5 ++--- packages/safe-chain/src/shell-integration/setup.js | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 125e89b..dd279ae 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -50,7 +50,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: "22.x" + node-version: "lts/*" - name: Setup safe-chain run: | @@ -60,8 +60,7 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts - - name: Create binary (Unix) - if: matrix.os != 'win' + - name: Create binary run: | node build.js ${{ matrix.target }} diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index cd54bb8..f8072db 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -5,7 +5,6 @@ import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; import fs from "fs"; import os from "os"; import path from "path"; -import { fileURLToPath } from "url"; import { includePython } from "../config/cliArguments.js"; /** From c70659b7a1bfa580f1851cc8f1f99264d705c55b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:06:43 +0100 Subject: [PATCH 264/345] Use correct pkg arg --- .github/workflows/create-artifact.yml | 2 +- build.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index dd279ae..938994a 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -50,7 +50,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: "lts/*" + node-version: "22.x" - name: Setup safe-chain run: | diff --git a/build.js b/build.js index 3e813ef..3d3a9e6 100644 --- a/build.js +++ b/build.js @@ -73,7 +73,7 @@ function buildSafeChainBinary(target) { // eslint-disable-next-line no-console console.error("Target: " + target); return new Promise((resolve, reject) => { - const pkg = spawn("pkg", ["./build/package.json", `--target=${target}`], { + const pkg = spawn("pkg", ["./build/package.json", "--targets", target], { stdio: "inherit", shell: true, }); From 05f1289268b86869ea5c8d7e3746f22122c005e2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:14:14 +0100 Subject: [PATCH 265/345] Run pkg from ci step --- .github/workflows/create-artifact.yml | 1 + build.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 938994a..1ce0a53 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -63,6 +63,7 @@ jobs: - name: Create binary run: | node build.js ${{ matrix.target }} + pkg ./build/package.json -t ${{ matrix.target }} - name: Upload binary artifact uses: actions/upload-artifact@v4 diff --git a/build.js b/build.js index 3d3a9e6..7868390 100644 --- a/build.js +++ b/build.js @@ -16,7 +16,7 @@ if (!target) { await bundleSafeChain(); await copyShellScripts(); await copyAndModifyPackageJson(); - await buildSafeChainBinary(target); + // await buildSafeChainBinary(target); })(); async function clearOutputFolder() { @@ -73,7 +73,7 @@ function buildSafeChainBinary(target) { // eslint-disable-next-line no-console console.error("Target: " + target); return new Promise((resolve, reject) => { - const pkg = spawn("pkg", ["./build/package.json", "--targets", target], { + const pkg = spawn("pkg", ["./build/package.json", "-t", target], { stdio: "inherit", shell: true, }); From 7f1710fb7302efe6a0f0dd6976ce43fe2dd0d9e7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:17:28 +0100 Subject: [PATCH 266/345] Move target to package.json --- .github/workflows/create-artifact.yml | 2 +- build.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 1ce0a53..8684ac7 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -63,7 +63,7 @@ jobs: - name: Create binary run: | node build.js ${{ matrix.target }} - pkg ./build/package.json -t ${{ matrix.target }} + pkg ./build/package.json - name: Upload binary artifact uses: actions/upload-artifact@v4 diff --git a/build.js b/build.js index 7868390..b15b936 100644 --- a/build.js +++ b/build.js @@ -15,7 +15,7 @@ if (!target) { await clearOutputFolder(); await bundleSafeChain(); await copyShellScripts(); - await copyAndModifyPackageJson(); + await copyAndModifyPackageJson(target); // await buildSafeChainBinary(target); })(); @@ -42,7 +42,7 @@ async function copyShellScripts() { { recursive: true } ); } -async function copyAndModifyPackageJson() { +async function copyAndModifyPackageJson(target) { const packageJsonContent = await readFile( "./packages/safe-chain/package.json", "utf-8" @@ -62,6 +62,7 @@ async function copyAndModifyPackageJson() { packageJson.pkg = { outputPath: "dist", assets: ["node_modules/certifi/**/*", "bin/startup-scripts/**/*"], + targets: [target], }; await writeFile("./build/package.json", JSON.stringify(packageJson, null, 2)); From 97883a42c292e5fa24dc138acc6e5ff195d31acb Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:24:08 +0100 Subject: [PATCH 267/345] Use node 24 --- .github/workflows/create-artifact.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 8684ac7..8e718f9 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -15,32 +15,32 @@ jobs: - os: macos arch: x64 runner: macos-15-intel - target: node22-macos-x64 + target: node24-macos-x64 extension: "" - os: macos arch: arm64 runner: macos-latest - target: node22-macos-arm64 + target: node24-macos-arm64 extension: "" - os: linux arch: x64 runner: ubuntu-latest - target: node22-linux-x64 + target: node24-linux-x64 extension: "" - os: linux arch: arm64 runner: ubuntu-24.04-arm - target: node22-linux-arm64 + target: node24-linux-arm64 extension: "" - os: win arch: x64 runner: windows-latest - target: node22-win-x64 + target: node24-win-x64 extension: ".exe" - os: win arch: arm64 runner: windows-11-arm - target: node22-win-arm64 + target: node24-win-arm64 extension: ".exe" steps: @@ -50,7 +50,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: "22.x" + node-version: "24.x" - name: Setup safe-chain run: | @@ -63,7 +63,7 @@ jobs: - name: Create binary run: | node build.js ${{ matrix.target }} - pkg ./build/package.json + pkg ./build/package.json --output "./dist/safe-chain${{ matrix.extension }}" - name: Upload binary artifact uses: actions/upload-artifact@v4 From 832708299fc0492b4de468973aa087804d7fbfe0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:29:40 +0100 Subject: [PATCH 268/345] Use @yao-pkg/pkg --- .github/workflows/create-artifact.yml | 2 +- build.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 8e718f9..63badb5 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -54,7 +54,7 @@ jobs: - name: Setup safe-chain run: | - npm i -g @aikidosec/safe-chain pkg + npm i -g @aikidosec/safe-chain @yao-pkg/pkg safe-chain setup-ci - name: Install dependencies diff --git a/build.js b/build.js index b15b936..7c5ac27 100644 --- a/build.js +++ b/build.js @@ -29,7 +29,7 @@ async function bundleSafeChain() { entryPoints: ["./packages/safe-chain/bin/safe-chain.js"], bundle: true, platform: "node", - target: "node22", + target: "node24", outfile: "./build/bin/safe-chain.cjs", }); } From 8c2e8c959760a391a52557bf8487aa16bc11538f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 11:31:47 +0100 Subject: [PATCH 269/345] Build safe-chain binaries in build.js --- .github/workflows/create-artifact.yml | 3 +-- build.js | 14 +++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 63badb5..6d479c7 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -54,7 +54,7 @@ jobs: - name: Setup safe-chain run: | - npm i -g @aikidosec/safe-chain @yao-pkg/pkg + npm i -g @aikidosec/safe-chain safe-chain setup-ci - name: Install dependencies @@ -63,7 +63,6 @@ jobs: - name: Create binary run: | node build.js ${{ matrix.target }} - pkg ./build/package.json --output "./dist/safe-chain${{ matrix.extension }}" - name: Upload binary artifact uses: actions/upload-artifact@v4 diff --git a/build.js b/build.js index 7c5ac27..5dc7254 100644 --- a/build.js +++ b/build.js @@ -16,7 +16,7 @@ if (!target) { await bundleSafeChain(); await copyShellScripts(); await copyAndModifyPackageJson(target); - // await buildSafeChainBinary(target); + await buildSafeChainBinary(target); })(); async function clearOutputFolder() { @@ -74,10 +74,14 @@ function buildSafeChainBinary(target) { // eslint-disable-next-line no-console console.error("Target: " + target); return new Promise((resolve, reject) => { - const pkg = spawn("pkg", ["./build/package.json", "-t", target], { - stdio: "inherit", - shell: true, - }); + const pkg = spawn( + "npx", + ["@yao-pkg/pkg", "./build/package.json", "-t", target], + { + stdio: "inherit", + shell: true, + } + ); pkg.on("close", (code) => { if (code !== 0) { From f1ee6567df05a96715f94543be11a751d1802270 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 12:57:48 +0100 Subject: [PATCH 270/345] Fix __dirname for esm / fix e2e tests. --- build.js | 19 ++++++++++----- packages/safe-chain/bin/safe-chain.js | 23 +++++++++++++++++-- .../src/shell-integration/setup-ci.js | 18 ++++++++++----- .../safe-chain/src/shell-integration/setup.js | 13 ++++++++++- .../startup-scripts/init-posix.sh | 23 ++++++++----------- 5 files changed, 67 insertions(+), 29 deletions(-) diff --git a/build.js b/build.js index 5dc7254..3860646 100644 --- a/build.js +++ b/build.js @@ -15,7 +15,7 @@ if (!target) { await clearOutputFolder(); await bundleSafeChain(); await copyShellScripts(); - await copyAndModifyPackageJson(target); + await copyAndModifyPackageJson(); await buildSafeChainBinary(target); })(); @@ -41,8 +41,14 @@ async function copyShellScripts() { "./build/bin/startup-scripts", { recursive: true } ); + await mkdir("./build/bin/path-wrappers", { recursive: true }); + await cp( + "./packages/safe-chain/src/shell-integration/path-wrappers/", + "./build/bin/path-wrappers", + { recursive: true } + ); } -async function copyAndModifyPackageJson(target) { +async function copyAndModifyPackageJson() { const packageJsonContent = await readFile( "./packages/safe-chain/package.json", "utf-8" @@ -61,8 +67,11 @@ async function copyAndModifyPackageJson(target) { packageJson.type = "commonjs"; packageJson.pkg = { outputPath: "dist", - assets: ["node_modules/certifi/**/*", "bin/startup-scripts/**/*"], - targets: [target], + assets: [ + "node_modules/certifi/**/*", + "bin/startup-scripts/**/*", + "bin/path-wrappers/**/*", + ], }; await writeFile("./build/package.json", JSON.stringify(packageJson, null, 2)); @@ -71,8 +80,6 @@ async function copyAndModifyPackageJson(target) { } function buildSafeChainBinary(target) { - // eslint-disable-next-line no-console - console.error("Target: " + target); return new Promise((resolve, reject) => { const pkg = spawn( "npx", diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 667a880..48f38c5 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -10,8 +10,19 @@ import { ECOSYSTEM_JS, setEcoSystem } from "../src/config/settings.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { main } from "../src/main.js"; import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; +/** @type {string} */ +let dirname; + +if (import.meta.url) { + const filename = fileURLToPath(import.meta.url); + dirname = path.dirname(filename); +} else { + dirname = __dirname; +} + if (process.argv.length < 3) { ui.writeError("No command provided. Please provide a command to execute."); ui.emptyLine(); @@ -23,7 +34,15 @@ initializeCliArguments(process.argv); const command = process.argv[2]; -const pkgManagerCommands = ["npm", "npx", "yarn"]; +const pkgManagerCommands = [ + "npm", + "npx", + "yarn", + "bun", + "bunx", + "pnpm", + "pnpx", +]; if (pkgManagerCommands.includes(command)) { ui.writeInformation(process.argv.join(", ")); @@ -102,7 +121,7 @@ function writeHelp() { } async function getVersion() { - const packageJsonPath = path.join(__dirname, "..", "package.json"); + const packageJsonPath = path.join(dirname, "..", "package.json"); const data = await fs.promises.readFile(packageJsonPath); const json = JSON.parse(data.toString("utf8")); diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index f63ad32..9e0342d 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -8,6 +8,16 @@ import { fileURLToPath } from "url"; import { includePython } from "../config/cliArguments.js"; import { ECOSYSTEM_PY } from "../config/settings.js"; +/** @type {string} */ +let dirname; + +if (import.meta.url) { + const filename = fileURLToPath(import.meta.url); + dirname = path.dirname(filename); +} else { + dirname = __dirname; +} + /** * Loops over the detected shells and calls the setup function for each. */ @@ -37,10 +47,8 @@ export async function setupCi() { */ function createUnixShims(shimsDir) { // Read the template file - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); const templatePath = path.resolve( - __dirname, + dirname, "path-wrappers", "templates", "unix-wrapper.template.sh" @@ -78,10 +86,8 @@ function createUnixShims(shimsDir) { */ function createWindowsShims(shimsDir) { // Read the template file - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); const templatePath = path.resolve( - __dirname, + dirname, "path-wrappers", "templates", "windows-wrapper.template.cmd" diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index f8072db..45a1fb8 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -6,6 +6,17 @@ import fs from "fs"; import os from "os"; import path from "path"; import { includePython } from "../config/cliArguments.js"; +import { fileURLToPath } from "url"; + +/** @type {string} */ +let dirname; + +if (import.meta.url) { + const filename = fileURLToPath(import.meta.url); + dirname = path.dirname(filename); +} else { + dirname = __dirname; +} /** * Loops over the detected shells and calls the setup function for each. @@ -103,7 +114,7 @@ function copyStartupFiles() { // Use absolute path for source const sourcePath = path.join( - __dirname, + dirname, includePython() ? "startup-scripts/include-python" : "startup-scripts", file ); 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..a83e749 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 @@ -9,15 +9,10 @@ function printSafeChainWarning() { function wrapSafeChainCommand() { local original_cmd="$1" - local aikido_cmd="$2" - # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@ - # so that "$@" now contains only the arguments passed to the original command - shift 2 - - if command -v "$aikido_cmd" > /dev/null 2>&1; then + if command -v safe-chain > /dev/null 2>&1; then # If the aikido command is available, just run it with the provided arguments - "$aikido_cmd" "$@" + safe-chain "$@" else # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" @@ -27,27 +22,27 @@ function wrapSafeChainCommand() { } function npx() { - wrapSafeChainCommand "npx" "aikido-npx" "$@" + wrapSafeChainCommand "npx" "$@" } function yarn() { - wrapSafeChainCommand "yarn" "aikido-yarn" "$@" + wrapSafeChainCommand "yarn" "$@" } function pnpm() { - wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@" + wrapSafeChainCommand "pnpm" "$@" } function pnpx() { - wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" + wrapSafeChainCommand "pnpx" "$@" } function bun() { - wrapSafeChainCommand "bun" "aikido-bun" "$@" + wrapSafeChainCommand "bun" "$@" } function bunx() { - wrapSafeChainCommand "bunx" "aikido-bunx" "$@" + wrapSafeChainCommand "bunx" "$@" } function npm() { @@ -58,5 +53,5 @@ function npm() { return fi - wrapSafeChainCommand "npm" "aikido-npm" "$@" + wrapSafeChainCommand "npm" "$@" } From a3bff105ccaf9c1e759c9006e0616f93482e8b59 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 14:01:11 +0100 Subject: [PATCH 271/345] Update startup scripts to use safe-chain instead of aikido-* --- packages/safe-chain/bin/safe-chain.js | 1 - .../include-python/init-pwsh.ps1 | 31 +++++++++---------- .../startup-scripts/init-fish.fish | 27 ++++++++-------- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 48f38c5..3f32bff 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -45,7 +45,6 @@ const pkgManagerCommands = [ ]; if (pkgManagerCommands.includes(command)) { - ui.writeInformation(process.argv.join(", ")); setEcoSystem(ECOSYSTEM_JS); initializePackageManager(command); (async () => { diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 index e2ea1c9..deb127d 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -39,12 +39,11 @@ function Invoke-RealCommand { function Invoke-WrappedCommand { param( [string]$OriginalCmd, - [string]$AikidoCmd, [string[]]$Arguments ) - if (Test-CommandAvailable $AikidoCmd) { - & $AikidoCmd @Arguments + if (Test-CommandAvailable "safe-chain") { + & safe-chain $OriginalCmd @Arguments } else { Write-SafeChainWarning $OriginalCmd @@ -53,27 +52,27 @@ function Invoke-WrappedCommand { } function npx { - Invoke-WrappedCommand "npx" "aikido-npx" $args + Invoke-WrappedCommand "npx" $args } function yarn { - Invoke-WrappedCommand "yarn" "aikido-yarn" $args + Invoke-WrappedCommand "yarn" $args } function pnpm { - Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args + Invoke-WrappedCommand "pnpm" $args } function pnpx { - Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args + Invoke-WrappedCommand "pnpx" $args } function bun { - Invoke-WrappedCommand "bun" "aikido-bun" $args + Invoke-WrappedCommand "bun" $args } function bunx { - Invoke-WrappedCommand "bunx" "aikido-bunx" $args + Invoke-WrappedCommand "bunx" $args } function npm { @@ -83,29 +82,29 @@ function npm { Invoke-RealCommand "npm" $args return } - - Invoke-WrappedCommand "npm" "aikido-npm" $args + + Invoke-WrappedCommand "npm" $args } function pip { - Invoke-WrappedCommand "pip" "aikido-pip" $args + Invoke-WrappedCommand "pip" $args } function pip3 { - Invoke-WrappedCommand "pip3" "aikido-pip3" $args + Invoke-WrappedCommand "pip3" $args } function uv { - Invoke-WrappedCommand "uv" "aikido-uv" $args + Invoke-WrappedCommand "uv" $args } # `python -m pip`, `python -m pip3`. function python { - Invoke-WrappedCommand 'python' 'aikido-python' $args + Invoke-WrappedCommand 'python' $args } # `python3 -m pip`, `python3 -m pip3'. function python3 { - Invoke-WrappedCommand 'python3' 'aikido-python3' $args + Invoke-WrappedCommand 'python3' $args } 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..a72380f 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 @@ -17,41 +17,40 @@ end function wrapSafeChainCommand set original_cmd $argv[1] - set aikido_cmd $argv[2] - set cmd_args $argv[3..-1] - - if type -q $aikido_cmd - # If the aikido command is available, just run it with the provided arguments - $aikido_cmd $cmd_args + set cmd_args $argv[2..-1] + + if type -q safe-chain + # If the safe-chain command is available, just run it with the provided arguments + safe-chain $original_cmd $cmd_args else - # If the aikido command is not available, print a warning and run the original command + # If the safe-chain command is not available, print a warning and run the original command printSafeChainWarning $original_cmd command $original_cmd $cmd_args end end function npx - wrapSafeChainCommand "npx" "aikido-npx" $argv + wrapSafeChainCommand "npx" $argv end function yarn - wrapSafeChainCommand "yarn" "aikido-yarn" $argv + wrapSafeChainCommand "yarn" $argv end function pnpm - wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv + wrapSafeChainCommand "pnpm" $argv end function pnpx - wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv + wrapSafeChainCommand "pnpx" $argv end function bun - wrapSafeChainCommand "bun" "aikido-bun" $argv + wrapSafeChainCommand "bun" $argv end function bunx - wrapSafeChainCommand "bunx" "aikido-bunx" $argv + wrapSafeChainCommand "bunx" $argv end function npm @@ -66,5 +65,5 @@ function npm end end - wrapSafeChainCommand "npm" "aikido-npm" $argv + wrapSafeChainCommand "npm" $argv end From 161f256066c0b4bb86f156d677957600c91986e6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 14:10:01 +0100 Subject: [PATCH 272/345] Change pwsh startup script --- .../startup-scripts/init-pwsh.ps1 | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 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 a449405..2106cac 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 @@ -39,12 +39,11 @@ function Invoke-RealCommand { function Invoke-WrappedCommand { param( [string]$OriginalCmd, - [string]$AikidoCmd, [string[]]$Arguments ) - if (Test-CommandAvailable $AikidoCmd) { - & $AikidoCmd @Arguments + if (Test-CommandAvailable "safe-chain") { + & safe-chain $OriginalCmd @Arguments } else { Write-SafeChainWarning $OriginalCmd @@ -53,27 +52,27 @@ function Invoke-WrappedCommand { } function npx { - Invoke-WrappedCommand "npx" "aikido-npx" $args + Invoke-WrappedCommand "npx" $args } function yarn { - Invoke-WrappedCommand "yarn" "aikido-yarn" $args + Invoke-WrappedCommand "yarn" $args } function pnpm { - Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args + Invoke-WrappedCommand "pnpm" $args } function pnpx { - Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args + Invoke-WrappedCommand "pnpx" $args } function bun { - Invoke-WrappedCommand "bun" "aikido-bun" $args + Invoke-WrappedCommand "bun" $args } function bunx { - Invoke-WrappedCommand "bunx" "aikido-bunx" $args + Invoke-WrappedCommand "bunx" $args } function npm { @@ -83,6 +82,6 @@ function npm { Invoke-RealCommand "npm" $args return } - - Invoke-WrappedCommand "npm" "aikido-npm" $args -} + + Invoke-WrappedCommand "npm" $args +} \ No newline at end of file From c59b8263ca23418cb10d31d99da811ddb55bc707 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 14:44:47 +0100 Subject: [PATCH 273/345] Add certify --- build.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/build.js b/build.js index 3860646..00f7464 100644 --- a/build.js +++ b/build.js @@ -15,6 +15,7 @@ if (!target) { await clearOutputFolder(); await bundleSafeChain(); await copyShellScripts(); + await copyCertifi(); await copyAndModifyPackageJson(); await buildSafeChainBinary(target); })(); @@ -31,6 +32,7 @@ async function bundleSafeChain() { platform: "node", target: "node24", outfile: "./build/bin/safe-chain.cjs", + external: ["certifi"], }); } @@ -48,6 +50,15 @@ async function copyShellScripts() { { recursive: true } ); } + +async function copyCertifi() { + await mkdir("./build/node_modules/certifi", { recursive: true }); + await cp( + "./node_modules/certifi/", + "./build/node_modules/certifi", + { recursive: true } + ); +} async function copyAndModifyPackageJson() { const packageJsonContent = await readFile( "./packages/safe-chain/package.json", From 0fffcf2cc14c7e644b3fd29eeca11d592f375326 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 14:51:54 +0100 Subject: [PATCH 274/345] Add certificate command --- packages/safe-chain/bin/safe-chain.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 3f32bff..90d95cf 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -12,6 +12,7 @@ import { main } from "../src/main.js"; import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; +import { getCombinedCaBundlePath } from "../src/registryProxy/certBundle.js"; /** @type {string} */ let dirname; @@ -56,6 +57,12 @@ if (pkgManagerCommands.includes(command)) { process.exit(0); } else if (command === "setup") { setup(); +} else if (command === "certificate") { + (async function () { + const path = getCombinedCaBundlePath(); + const data = await fs.promises.readFile(path); + ui.writeInformation(data.toString("utf8")); + })(); } else if (command === "teardown") { teardown(); } else if (command === "setup-ci") { From bb3e50008a9ff95a7db0238ae5e156d3dda55702 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 14:59:28 +0100 Subject: [PATCH 275/345] Forge: usePureJavaScript --- packages/safe-chain/src/registryProxy/certUtils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 6b326c8..7a5a5c7 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -3,6 +3,11 @@ import path from "path"; import fs from "fs"; import os from "os"; +// Force node-forge to use pure JavaScript instead of native crypto +// This prevents segmentation faults in pkg binaries on Linux +// @ts-ignore - options exists but isn't in the type definitions +forge.options.usePureJavaScript = true; + const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); const ca = loadCa(); From 1d00084202e2b2c86b3c11042510e35f8babab7a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:03:36 +0100 Subject: [PATCH 276/345] Externalize node-forge --- build.js | 15 +++++++++------ .../safe-chain/src/registryProxy/certUtils.js | 5 ----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/build.js b/build.js index 00f7464..eefb2d3 100644 --- a/build.js +++ b/build.js @@ -32,7 +32,7 @@ async function bundleSafeChain() { platform: "node", target: "node24", outfile: "./build/bin/safe-chain.cjs", - external: ["certifi"], + external: ["certifi", "node-forge"], }); } @@ -53,11 +53,13 @@ async function copyShellScripts() { async function copyCertifi() { await mkdir("./build/node_modules/certifi", { recursive: true }); - await cp( - "./node_modules/certifi/", - "./build/node_modules/certifi", - { recursive: true } - ); + await mkdir("./build/node_modules/certifi", { recursive: true }); + await cp("./node_modules/certifi/", "./build/node_modules/certifi", { + recursive: true, + }); + await cp("./node_modules/node-forge/", "./build/node_modules/node-forge", { + recursive: true, + }); } async function copyAndModifyPackageJson() { const packageJsonContent = await readFile( @@ -80,6 +82,7 @@ async function copyAndModifyPackageJson() { outputPath: "dist", assets: [ "node_modules/certifi/**/*", + "node_modules/node-forge/**/*", "bin/startup-scripts/**/*", "bin/path-wrappers/**/*", ], diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 7a5a5c7..6b326c8 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -3,11 +3,6 @@ import path from "path"; import fs from "fs"; import os from "os"; -// Force node-forge to use pure JavaScript instead of native crypto -// This prevents segmentation faults in pkg binaries on Linux -// @ts-ignore - options exists but isn't in the type definitions -forge.options.usePureJavaScript = true; - const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); const ca = loadCa(); From ec9a266164be80afb90dcceb449aeb47897f581e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:17:29 +0100 Subject: [PATCH 277/345] Include node-forge in binary again --- build.js | 7 +------ packages/safe-chain/src/registryProxy/certUtils.js | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/build.js b/build.js index eefb2d3..e576c5a 100644 --- a/build.js +++ b/build.js @@ -32,7 +32,7 @@ async function bundleSafeChain() { platform: "node", target: "node24", outfile: "./build/bin/safe-chain.cjs", - external: ["certifi", "node-forge"], + external: ["certifi"], }); } @@ -52,14 +52,10 @@ async function copyShellScripts() { } async function copyCertifi() { - await mkdir("./build/node_modules/certifi", { recursive: true }); await mkdir("./build/node_modules/certifi", { recursive: true }); await cp("./node_modules/certifi/", "./build/node_modules/certifi", { recursive: true, }); - await cp("./node_modules/node-forge/", "./build/node_modules/node-forge", { - recursive: true, - }); } async function copyAndModifyPackageJson() { const packageJsonContent = await readFile( @@ -82,7 +78,6 @@ async function copyAndModifyPackageJson() { outputPath: "dist", assets: [ "node_modules/certifi/**/*", - "node_modules/node-forge/**/*", "bin/startup-scripts/**/*", "bin/path-wrappers/**/*", ], diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 6b326c8..178f764 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -3,6 +3,9 @@ import path from "path"; import fs from "fs"; import os from "os"; +// @ts-ignore +forge.options.usePureJavaScript = true; + const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); const ca = loadCa(); From 51616dda777c3a3acfdeb6a1ac608e1668039b42 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:23:52 +0100 Subject: [PATCH 278/345] Comment out cert generation --- .../src/registryProxy/certBundle.js | 8 +- .../safe-chain/src/registryProxy/certUtils.js | 106 +++++++++--------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js index 956279d..9b36c80 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -5,7 +5,7 @@ import path from "node:path"; import certifi from "certifi"; import tls from "node:tls"; import { X509Certificate } from "node:crypto"; -import { getCaCertPath } from "./certUtils.js"; +// import { getCaCertPath } from "./certUtils.js"; /** * Check if a PEM string contains only parsable cert blocks. @@ -58,10 +58,10 @@ export function getCombinedCaBundlePath() { const parts = []; // 1) Safe Chain CA (for MITM'd registries) - const safeChainPath = getCaCertPath(); + // const safeChainPath = getCaCertPath(); try { - const safeChainPem = fs.readFileSync(safeChainPath, "utf8"); - if (isParsable(safeChainPem)) parts.push(safeChainPem.trim()); + // const safeChainPem = fs.readFileSync(safeChainPath, "utf8"); + // if (isParsable(safeChainPem)) parts.push(safeChainPem.trim()); } catch { // Ignore if Safe Chain CA is not available } diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 178f764..c8f46d6 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -1,13 +1,13 @@ import forge from "node-forge"; import path from "path"; -import fs from "fs"; +// import fs from "fs"; import os from "os"; // @ts-ignore forge.options.usePureJavaScript = true; const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); -const ca = loadCa(); +// const ca = loadCa(); const certCache = new Map(); @@ -35,7 +35,7 @@ export function generateCertForHost(hostname) { const attrs = [{ name: "commonName", value: hostname }]; cert.setSubject(attrs); - cert.setIssuer(ca.certificate.subject.attributes); + // cert.setIssuer(ca.certificate.subject.attributes); cert.setExtensions([ { name: "subjectAltName", @@ -62,7 +62,7 @@ export function generateCertForHost(hostname) { serverAuth: true, }, ]); - cert.sign(ca.privateKey, forge.md.sha256.create()); + // cert.sign(ca.privateKey, forge.md.sha256.create()); const result = { privateKey: forge.pki.privateKeyToPem(keys.privateKey), @@ -74,58 +74,58 @@ export function generateCertForHost(hostname) { return result; } -function loadCa() { - const keyPath = path.join(certFolder, "ca-key.pem"); - const certPath = path.join(certFolder, "ca-cert.pem"); +// function loadCa() { +// const keyPath = path.join(certFolder, "ca-key.pem"); +// const certPath = path.join(certFolder, "ca-cert.pem"); - if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { - const privateKeyPem = fs.readFileSync(keyPath, "utf8"); - const certPem = fs.readFileSync(certPath, "utf8"); - const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); - const certificate = forge.pki.certificateFromPem(certPem); +// if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { +// const privateKeyPem = fs.readFileSync(keyPath, "utf8"); +// const certPem = fs.readFileSync(certPath, "utf8"); +// const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); +// const certificate = forge.pki.certificateFromPem(certPem); - // Don't return a cert that is valid for less than 1 hour - const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); - if (certificate.validity.notAfter > oneHourFromNow) { - return { privateKey, certificate }; - } - } +// // Don't return a cert that is valid for less than 1 hour +// const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); +// if (certificate.validity.notAfter > oneHourFromNow) { +// return { privateKey, certificate }; +// } +// } - const { privateKey, certificate } = generateCa(); - fs.mkdirSync(certFolder, { recursive: true }); - fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey)); - fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate)); - return { privateKey, certificate }; -} +// const { privateKey, certificate } = generateCa(); +// fs.mkdirSync(certFolder, { recursive: true }); +// fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey)); +// fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate)); +// return { privateKey, certificate }; +// } -function generateCa() { - const keys = forge.pki.rsa.generateKeyPair(2048); - const cert = forge.pki.createCertificate(); - cert.publicKey = keys.publicKey; - cert.serialNumber = "01"; - cert.validity.notBefore = new Date(); - cert.validity.notAfter = new Date(); - cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); +// function generateCa() { +// const keys = forge.pki.rsa.generateKeyPair(2048); +// const cert = forge.pki.createCertificate(); +// cert.publicKey = keys.publicKey; +// cert.serialNumber = "01"; +// cert.validity.notBefore = new Date(); +// cert.validity.notAfter = new Date(); +// cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); - const attrs = [{ name: "commonName", value: "safe-chain proxy" }]; - cert.setSubject(attrs); - cert.setIssuer(attrs); - cert.setExtensions([ - { - name: "basicConstraints", - cA: true, - }, - { - name: "keyUsage", - keyCertSign: true, - digitalSignature: true, - keyEncipherment: true, - }, - ]); - cert.sign(keys.privateKey, forge.md.sha256.create()); +// const attrs = [{ name: "commonName", value: "safe-chain proxy" }]; +// cert.setSubject(attrs); +// cert.setIssuer(attrs); +// cert.setExtensions([ +// { +// name: "basicConstraints", +// cA: true, +// }, +// { +// name: "keyUsage", +// keyCertSign: true, +// digitalSignature: true, +// keyEncipherment: true, +// }, +// ]); +// cert.sign(keys.privateKey, forge.md.sha256.create()); - return { - privateKey: keys.privateKey, - certificate: cert, - }; -} +// return { +// privateKey: keys.privateKey, +// certificate: cert, +// }; +// } From 8ab4d2955a28a322bc95cc24410aa9ca686badb5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:27:40 +0100 Subject: [PATCH 279/345] Add debug logs --- .../safe-chain/src/registryProxy/certUtils.js | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index c8f46d6..cb69473 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -1,13 +1,14 @@ import forge from "node-forge"; import path from "path"; -// import fs from "fs"; +import fs from "fs"; import os from "os"; +import { ui } from "../environment/userInteraction.js"; // @ts-ignore forge.options.usePureJavaScript = true; const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); -// const ca = loadCa(); +const ca = loadCa(); const certCache = new Map(); @@ -35,7 +36,7 @@ export function generateCertForHost(hostname) { const attrs = [{ name: "commonName", value: hostname }]; cert.setSubject(attrs); - // cert.setIssuer(ca.certificate.subject.attributes); + cert.setIssuer(ca.certificate.subject.attributes); cert.setExtensions([ { name: "subjectAltName", @@ -62,7 +63,7 @@ export function generateCertForHost(hostname) { serverAuth: true, }, ]); - // cert.sign(ca.privateKey, forge.md.sha256.create()); + cert.sign(ca.privateKey, forge.md.sha256.create()); const result = { privateKey: forge.pki.privateKeyToPem(keys.privateKey), @@ -74,58 +75,62 @@ export function generateCertForHost(hostname) { return result; } -// function loadCa() { -// const keyPath = path.join(certFolder, "ca-key.pem"); -// const certPath = path.join(certFolder, "ca-cert.pem"); +function loadCa() { + const keyPath = path.join(certFolder, "ca-key.pem"); + const certPath = path.join(certFolder, "ca-cert.pem"); -// if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { -// const privateKeyPem = fs.readFileSync(keyPath, "utf8"); -// const certPem = fs.readFileSync(certPath, "utf8"); -// const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); -// const certificate = forge.pki.certificateFromPem(certPem); + if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { + const privateKeyPem = fs.readFileSync(keyPath, "utf8"); + const certPem = fs.readFileSync(certPath, "utf8"); + const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); + const certificate = forge.pki.certificateFromPem(certPem); -// // Don't return a cert that is valid for less than 1 hour -// const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); -// if (certificate.validity.notAfter > oneHourFromNow) { -// return { privateKey, certificate }; -// } -// } + // Don't return a cert that is valid for less than 1 hour + const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); + if (certificate.validity.notAfter > oneHourFromNow) { + return { privateKey, certificate }; + } + } -// const { privateKey, certificate } = generateCa(); -// fs.mkdirSync(certFolder, { recursive: true }); -// fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey)); -// fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate)); -// return { privateKey, certificate }; -// } + const { privateKey, certificate } = generateCa(); + fs.mkdirSync(certFolder, { recursive: true }); + fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey)); + fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate)); + return { privateKey, certificate }; +} -// function generateCa() { -// const keys = forge.pki.rsa.generateKeyPair(2048); -// const cert = forge.pki.createCertificate(); -// cert.publicKey = keys.publicKey; -// cert.serialNumber = "01"; -// cert.validity.notBefore = new Date(); -// cert.validity.notAfter = new Date(); -// cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); +function generateCa() { + ui.writeInformation("1"); + const keys = forge.pki.rsa.generateKeyPair(2048); + ui.writeInformation("2"); + const cert = forge.pki.createCertificate(); + ui.writeInformation("3"); + cert.publicKey = keys.publicKey; + cert.serialNumber = "01"; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1); -// const attrs = [{ name: "commonName", value: "safe-chain proxy" }]; -// cert.setSubject(attrs); -// cert.setIssuer(attrs); -// cert.setExtensions([ -// { -// name: "basicConstraints", -// cA: true, -// }, -// { -// name: "keyUsage", -// keyCertSign: true, -// digitalSignature: true, -// keyEncipherment: true, -// }, -// ]); -// cert.sign(keys.privateKey, forge.md.sha256.create()); + const attrs = [{ name: "commonName", value: "safe-chain proxy" }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.setExtensions([ + { + name: "basicConstraints", + cA: true, + }, + { + name: "keyUsage", + keyCertSign: true, + digitalSignature: true, + keyEncipherment: true, + }, + ]); + cert.sign(keys.privateKey, forge.md.sha256.create()); + ui.writeInformation("4"); -// return { -// privateKey: keys.privateKey, -// certificate: cert, -// }; -// } + return { + privateKey: keys.privateKey, + certificate: cert, + }; +} From 20420f865e6d20b956f103d437f8626aae85f6ed Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:32:52 +0100 Subject: [PATCH 280/345] Try set pure javascript for node-forge --- build.js | 12 ++++++++++++ packages/safe-chain/src/registryProxy/certBundle.js | 8 ++++---- packages/safe-chain/src/registryProxy/certUtils.js | 3 --- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/build.js b/build.js index e576c5a..667e2ed 100644 --- a/build.js +++ b/build.js @@ -26,6 +26,15 @@ async function clearOutputFolder() { } async function bundleSafeChain() { + // Read the forge.js file and modify it to use pure JavaScript + const forgeContent = await readFile("./node_modules/node-forge/lib/forge.js", "utf-8"); + const modifiedForge = forgeContent.replace( + "usePureJavaScript: false", + "usePureJavaScript: true" + ); + await mkdir("./build/temp", { recursive: true }); + await writeFile("./build/temp/forge.js", modifiedForge); + await build({ entryPoints: ["./packages/safe-chain/bin/safe-chain.js"], bundle: true, @@ -33,6 +42,9 @@ async function bundleSafeChain() { target: "node24", outfile: "./build/bin/safe-chain.cjs", external: ["certifi"], + alias: { + "node-forge/lib/forge": "./build/temp/forge.js", + }, }); } diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js index 9b36c80..956279d 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -5,7 +5,7 @@ import path from "node:path"; import certifi from "certifi"; import tls from "node:tls"; import { X509Certificate } from "node:crypto"; -// import { getCaCertPath } from "./certUtils.js"; +import { getCaCertPath } from "./certUtils.js"; /** * Check if a PEM string contains only parsable cert blocks. @@ -58,10 +58,10 @@ export function getCombinedCaBundlePath() { const parts = []; // 1) Safe Chain CA (for MITM'd registries) - // const safeChainPath = getCaCertPath(); + const safeChainPath = getCaCertPath(); try { - // const safeChainPem = fs.readFileSync(safeChainPath, "utf8"); - // if (isParsable(safeChainPem)) parts.push(safeChainPem.trim()); + const safeChainPem = fs.readFileSync(safeChainPath, "utf8"); + if (isParsable(safeChainPem)) parts.push(safeChainPem.trim()); } catch { // Ignore if Safe Chain CA is not available } diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index cb69473..02a0f53 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -4,9 +4,6 @@ import fs from "fs"; import os from "os"; import { ui } from "../environment/userInteraction.js"; -// @ts-ignore -forge.options.usePureJavaScript = true; - const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); const ca = loadCa(); From 95d436100db9f7911f05b51c87ff47b0cce986ae Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:40:50 +0100 Subject: [PATCH 281/345] Again, try pure javascript --- build.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/build.js b/build.js index 667e2ed..ecc49e7 100644 --- a/build.js +++ b/build.js @@ -26,15 +26,6 @@ async function clearOutputFolder() { } async function bundleSafeChain() { - // Read the forge.js file and modify it to use pure JavaScript - const forgeContent = await readFile("./node_modules/node-forge/lib/forge.js", "utf-8"); - const modifiedForge = forgeContent.replace( - "usePureJavaScript: false", - "usePureJavaScript: true" - ); - await mkdir("./build/temp", { recursive: true }); - await writeFile("./build/temp/forge.js", modifiedForge); - await build({ entryPoints: ["./packages/safe-chain/bin/safe-chain.js"], bundle: true, @@ -42,10 +33,17 @@ async function bundleSafeChain() { target: "node24", outfile: "./build/bin/safe-chain.cjs", external: ["certifi"], - alias: { - "node-forge/lib/forge": "./build/temp/forge.js", - }, }); + + // Post-process: Replace all usePureJavaScript: false with true + // This ensures node-forge uses pure JavaScript crypto instead of native bindings + // which prevents segmentation faults in pkg binaries on Linux + let bundledContent = await readFile("./build/bin/safe-chain.cjs", "utf-8"); + bundledContent = bundledContent.replace( + /usePureJavaScript:\s*false/g, + "usePureJavaScript: true" + ); + await writeFile("./build/bin/safe-chain.cjs", bundledContent); } async function copyShellScripts() { From ae9bc8a75dab012f18be9cb4b607221b20e89f7d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:44:45 +0100 Subject: [PATCH 282/345] Try to get it to work :/ --- build.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/build.js b/build.js index ecc49e7..9cb8ca5 100644 --- a/build.js +++ b/build.js @@ -35,14 +35,25 @@ async function bundleSafeChain() { external: ["certifi"], }); - // Post-process: Replace all usePureJavaScript: false with true - // This ensures node-forge uses pure JavaScript crypto instead of native bindings - // which prevents segmentation faults in pkg binaries on Linux + // Post-process: Force node-forge to use pure JavaScript + // This prevents segmentation faults in pkg binaries on Linux let bundledContent = await readFile("./build/bin/safe-chain.cjs", "utf-8"); + + // 1. Set the option to true bundledContent = bundledContent.replace( /usePureJavaScript:\s*false/g, "usePureJavaScript: true" ); + + // 2. Replace all checks that would enable native crypto + // Change: if (!forge2.options.usePureJavaScript && ...) + // To: if (false && ...) + // This makes the native crypto branches unreachable + bundledContent = bundledContent.replace( + /!forge2\.options\.usePureJavaScript/g, + "false" + ); + await writeFile("./build/bin/safe-chain.cjs", bundledContent); } From 35ab58c440125182bcb6fd46e1ba854047840914 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 15:53:38 +0100 Subject: [PATCH 283/345] Try package downgrade --- build.js | 17 ----------------- package-lock.json | 20 ++++++++++---------- packages/safe-chain/package.json | 2 +- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/build.js b/build.js index 9cb8ca5..24230cd 100644 --- a/build.js +++ b/build.js @@ -35,25 +35,8 @@ async function bundleSafeChain() { external: ["certifi"], }); - // Post-process: Force node-forge to use pure JavaScript - // This prevents segmentation faults in pkg binaries on Linux let bundledContent = await readFile("./build/bin/safe-chain.cjs", "utf-8"); - // 1. Set the option to true - bundledContent = bundledContent.replace( - /usePureJavaScript:\s*false/g, - "usePureJavaScript: true" - ); - - // 2. Replace all checks that would enable native crypto - // Change: if (!forge2.options.usePureJavaScript && ...) - // To: if (false && ...) - // This makes the native crypto branches unreachable - bundledContent = bundledContent.replace( - /!forge2\.options\.usePureJavaScript/g, - "false" - ); - await writeFile("./build/bin/safe-chain.cjs", bundledContent); } diff --git a/package-lock.json b/package-lock.json index 16f42c7..b13d6d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1402,15 +1402,6 @@ "node": ">= 0.6" } }, - "node_modules/node-forge": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", - "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-pty": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", @@ -1685,7 +1676,7 @@ "https-proxy-agent": "7.0.6", "ini": "6.0.0", "make-fetch-happen": "15.0.3", - "node-forge": "1.3.2", + "node-forge": "1.3.1", "npm-registry-fetch": "19.1.1", "semver": "7.7.2" }, @@ -1724,6 +1715,15 @@ "undici-types": "~5.26.4" } }, + "packages/safe-chain/node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "packages/safe-chain/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index d35f4fc..b37c8fa 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -41,7 +41,7 @@ "https-proxy-agent": "7.0.6", "ini": "6.0.0", "make-fetch-happen": "15.0.3", - "node-forge": "1.3.2", + "node-forge": "1.3.1", "npm-registry-fetch": "19.1.1", "semver": "7.7.2" }, From 3add7aa25e7fe931b8e26e1cc3c30b45d0a69486 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 16:26:31 +0100 Subject: [PATCH 284/345] Remove debugging from certUtils.js --- packages/safe-chain/src/registryProxy/certUtils.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 02a0f53..e148711 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -97,11 +97,8 @@ function loadCa() { } function generateCa() { - ui.writeInformation("1"); const keys = forge.pki.rsa.generateKeyPair(2048); - ui.writeInformation("2"); const cert = forge.pki.createCertificate(); - ui.writeInformation("3"); cert.publicKey = keys.publicKey; cert.serialNumber = "01"; cert.validity.notBefore = new Date(); @@ -124,7 +121,6 @@ function generateCa() { }, ]); cert.sign(keys.privateKey, forge.md.sha256.create()); - ui.writeInformation("4"); return { privateKey: keys.privateKey, From 8d82d4d56fdef6f8720702d6e984274630d7026e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 16:28:45 +0100 Subject: [PATCH 285/345] Clean up the PR --- packages/safe-chain/package.json | 22 ------------------- .../safe-chain/src/registryProxy/certUtils.js | 1 - 2 files changed, 23 deletions(-) diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index b37c8fa..516077d 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -64,27 +64,5 @@ "type": "git", "url": "git+https://github.com/AikidoSec/safe-chain.git", "directory": "packages/safe-chain" - }, - "pkg": { - "targets": [ - "node22-linux-x64", - "node22-linux-arm64", - "node22-macos-x64", - "node22-macos-arm64", - "node22-win-x64", - "node22-win-arm64" - ], - "outputPath": "dist", - "assets": [ - "node_modules/certifi/**/*" - ], - "scripts": [ - "src/**/*.js", - "bin/**/*.js" - ], - "ignore": [ - "**/*.spec.js", - "test/**" - ] } } diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index e148711..6b326c8 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -2,7 +2,6 @@ import forge from "node-forge"; import path from "path"; import fs from "fs"; import os from "os"; -import { ui } from "../environment/userInteraction.js"; const certFolder = path.join(os.homedir(), ".safe-chain", "certs"); const ca = loadCa(); From 552fd37294a3fcad8fae4d9c580cd78761a59f32 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 16:30:18 +0100 Subject: [PATCH 286/345] Remove certificate command --- packages/safe-chain/bin/safe-chain.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 90d95cf..3f32bff 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -12,7 +12,6 @@ import { main } from "../src/main.js"; import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; -import { getCombinedCaBundlePath } from "../src/registryProxy/certBundle.js"; /** @type {string} */ let dirname; @@ -57,12 +56,6 @@ if (pkgManagerCommands.includes(command)) { process.exit(0); } else if (command === "setup") { setup(); -} else if (command === "certificate") { - (async function () { - const path = getCombinedCaBundlePath(); - const data = await fs.promises.readFile(path); - ui.writeInformation(data.toString("utf8")); - })(); } else if (command === "teardown") { teardown(); } else if (command === "setup-ci") { From ab446e081d31fc159580ea71d6f4d214f018007c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 16:33:09 +0100 Subject: [PATCH 287/345] Restore fork --- package-lock.json | 20 ++++++++++---------- packages/safe-chain/package.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index b13d6d3..16f42c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1402,6 +1402,15 @@ "node": ">= 0.6" } }, + "node_modules/node-forge": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-pty": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", @@ -1676,7 +1685,7 @@ "https-proxy-agent": "7.0.6", "ini": "6.0.0", "make-fetch-happen": "15.0.3", - "node-forge": "1.3.1", + "node-forge": "1.3.2", "npm-registry-fetch": "19.1.1", "semver": "7.7.2" }, @@ -1715,15 +1724,6 @@ "undici-types": "~5.26.4" } }, - "packages/safe-chain/node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, "packages/safe-chain/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 516077d..5353635 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -41,7 +41,7 @@ "https-proxy-agent": "7.0.6", "ini": "6.0.0", "make-fetch-happen": "15.0.3", - "node-forge": "1.3.1", + "node-forge": "1.3.2", "npm-registry-fetch": "19.1.1", "semver": "7.7.2" }, From 3af8b694fe248a73f909c18d9c4cadfa232e8077 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 16:36:33 +0100 Subject: [PATCH 288/345] Linux arm64: use node 20 --- .github/workflows/create-artifact.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 6d479c7..bda119c 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -30,7 +30,7 @@ jobs: - os: linux arch: arm64 runner: ubuntu-24.04-arm - target: node24-linux-arm64 + target: node20-linux-arm64 extension: "" - os: win arch: x64 From edec6ec57c81d77f0a630730b831c6c5738e4b93 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 16:51:39 +0100 Subject: [PATCH 289/345] Update shell scripts --- .github/workflows/create-artifact.yml | 12 +++--- .../include-python/init-fish.fish | 40 ++++++++++--------- .../include-python/init-posix.sh | 35 ++++++++-------- .../include-python/init-pwsh.ps1 | 2 + .../startup-scripts/init-fish.fish | 2 + .../startup-scripts/init-posix.sh | 1 + .../startup-scripts/init-pwsh.ps1 | 2 + 7 files changed, 50 insertions(+), 44 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index bda119c..e5be214 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -15,17 +15,17 @@ jobs: - os: macos arch: x64 runner: macos-15-intel - target: node24-macos-x64 + target: node20-macos-x64 extension: "" - os: macos arch: arm64 runner: macos-latest - target: node24-macos-arm64 + target: node20-macos-arm64 extension: "" - os: linux arch: x64 runner: ubuntu-latest - target: node24-linux-x64 + target: node20-linux-x64 extension: "" - os: linux arch: arm64 @@ -35,12 +35,12 @@ jobs: - os: win arch: x64 runner: windows-latest - target: node24-win-x64 + target: node20-win-x64 extension: ".exe" - os: win arch: arm64 runner: windows-11-arm - target: node24-win-arm64 + target: node20-win-arm64 extension: ".exe" steps: @@ -50,7 +50,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: "24.x" + node-version: "20.x" - name: Setup safe-chain run: | diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish index 235ecb8..81e28ef 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish @@ -1,3 +1,5 @@ +set -gx PATH $PATH $HOME/.safe-chain/bin + function printSafeChainWarning set original_cmd $argv[1] @@ -17,41 +19,40 @@ end function wrapSafeChainCommand set original_cmd $argv[1] - set aikido_cmd $argv[2] - set cmd_args $argv[3..-1] - - if type -q $aikido_cmd - # If the aikido command is available, just run it with the provided arguments - $aikido_cmd $cmd_args + set cmd_args $argv[2..-1] + + if type -q safe-chain + # If the safe-chain command is available, just run it with the provided arguments + safe-chain $original_cmd $cmd_args else - # If the aikido command is not available, print a warning and run the original command + # If the safe-chain command is not available, print a warning and run the original command printSafeChainWarning $original_cmd command $original_cmd $cmd_args end end function npx - wrapSafeChainCommand "npx" "aikido-npx" $argv + wrapSafeChainCommand "npx" $argv end function yarn - wrapSafeChainCommand "yarn" "aikido-yarn" $argv + wrapSafeChainCommand "yarn" $argv end function pnpm - wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv + wrapSafeChainCommand "pnpm" $argv end function pnpx - wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv + wrapSafeChainCommand "pnpx" $argv end function bun - wrapSafeChainCommand "bun" "aikido-bun" $argv + wrapSafeChainCommand "bun" $argv end function bunx - wrapSafeChainCommand "bunx" "aikido-bunx" $argv + wrapSafeChainCommand "bunx" $argv end function npm @@ -66,27 +67,28 @@ function npm end end - wrapSafeChainCommand "npm" "aikido-npm" $argv + wrapSafeChainCommand "npm" $argv end + function pip - wrapSafeChainCommand "pip" "aikido-pip" $argv + wrapSafeChainCommand "pip" $argv end function pip3 - wrapSafeChainCommand "pip3" "aikido-pip3" $argv + wrapSafeChainCommand "pip3" $argv end function uv - wrapSafeChainCommand "uv" "aikido-uv" $argv + wrapSafeChainCommand "uv" $argv end # `python -m pip`, `python -m pip3`. function python - wrapSafeChainCommand "python" "aikido-python" $argv + wrapSafeChainCommand "python" $argv end # `python3 -m pip`, `python3 -m pip3'. function python3 - wrapSafeChainCommand "python3" "aikido-python3" $argv + wrapSafeChainCommand "python3" $argv end diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh index 9f51010..fd844fc 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh @@ -1,3 +1,4 @@ +export PATH="$PATH:$HOME/.safe-chain/bin" function printSafeChainWarning() { # \033[43;30m is used to set the background color to yellow and text color to black @@ -9,15 +10,10 @@ function printSafeChainWarning() { function wrapSafeChainCommand() { local original_cmd="$1" - local aikido_cmd="$2" - # Remove the first 2 arguments (original_cmd and aikido_cmd) from $@ - # so that "$@" now contains only the arguments passed to the original command - shift 2 - - if command -v "$aikido_cmd" > /dev/null 2>&1; then + if command -v safe-chain > /dev/null 2>&1; then # If the aikido command is available, just run it with the provided arguments - "$aikido_cmd" "$@" + safe-chain "$@" else # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" @@ -27,27 +23,27 @@ function wrapSafeChainCommand() { } function npx() { - wrapSafeChainCommand "npx" "aikido-npx" "$@" + wrapSafeChainCommand "npx" "$@" } function yarn() { - wrapSafeChainCommand "yarn" "aikido-yarn" "$@" + wrapSafeChainCommand "yarn" "$@" } function pnpm() { - wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@" + wrapSafeChainCommand "pnpm" "$@" } function pnpx() { - wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" + wrapSafeChainCommand "pnpx" "$@" } function bun() { - wrapSafeChainCommand "bun" "aikido-bun" "$@" + wrapSafeChainCommand "bun" "$@" } function bunx() { - wrapSafeChainCommand "bunx" "aikido-bunx" "$@" + wrapSafeChainCommand "bunx" "$@" } function npm() { @@ -58,27 +54,28 @@ function npm() { return fi - wrapSafeChainCommand "npm" "aikido-npm" "$@" + wrapSafeChainCommand "npm" "$@" } + function pip() { - wrapSafeChainCommand "pip" "aikido-pip" "$@" + wrapSafeChainCommand "pip" "$@" } function pip3() { - wrapSafeChainCommand "pip3" "aikido-pip3" "$@" + wrapSafeChainCommand "pip3" "$@" } function uv() { - wrapSafeChainCommand "uv" "aikido-uv" "$@" + wrapSafeChainCommand "uv" "$@" } # `python -m pip`, `python -m pip3`. function python() { - wrapSafeChainCommand "python" "aikido-python" "$@" + wrapSafeChainCommand "python" "$@" } # `python3 -m pip`, `python3 -m pip3'. function python3() { - wrapSafeChainCommand "python3" "aikido-python3" "$@" + wrapSafeChainCommand "python3" "$@" } diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 index deb127d..27a5065 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -1,3 +1,5 @@ +$env:PATH = "$env:PATH;$HOME/.safe-chain/bin" + function Write-SafeChainWarning { param([string]$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 a72380f..f697da2 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 @@ -1,3 +1,5 @@ +set -gx PATH $PATH $HOME/.safe-chain/bin + function printSafeChainWarning set original_cmd $argv[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 a83e749..6d426c5 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 @@ -1,3 +1,4 @@ +export PATH="$PATH:$HOME/.safe-chain/bin" function printSafeChainWarning() { # \033[43;30m is used to set the background color to yellow and text color to black 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 2106cac..17ddeb2 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 @@ -1,3 +1,5 @@ +$env:PATH = "$env:PATH;$HOME/.safe-chain/bin" + function Write-SafeChainWarning { param([string]$Command) From 8852afb5faf4665d08a227d625190db0c0a8bcb4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 18:05:09 +0100 Subject: [PATCH 290/345] Fix e2e tests --- packages/safe-chain/bin/safe-chain.js | 73 +++++++++++++--- .../src/shell-integration/helpers.js | 85 ++++++++++++++++--- 2 files changed, 132 insertions(+), 26 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 3f32bff..a9a9ed3 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -6,12 +6,18 @@ import { setup } from "../src/shell-integration/setup.js"; import { teardown } from "../src/shell-integration/teardown.js"; import { setupCi } from "../src/shell-integration/setup-ci.js"; import { initializeCliArguments } from "../src/config/cliArguments.js"; -import { ECOSYSTEM_JS, setEcoSystem } from "../src/config/settings.js"; +import { setEcoSystem } from "../src/config/settings.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { main } from "../src/main.js"; import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; +import { knownAikidoTools } from "../src/shell-integration/helpers.js"; +import { + PIP_INVOCATIONS, + PIP_PACKAGE_MANAGER, + setCurrentPipInvocation, +} from "../src/packagemanager/pip/pipSettings.js"; /** @type {string} */ let dirname; @@ -34,21 +40,18 @@ initializeCliArguments(process.argv); const command = process.argv[2]; -const pkgManagerCommands = [ - "npm", - "npx", - "yarn", - "bun", - "bunx", - "pnpm", - "pnpx", -]; +const tool = knownAikidoTools.find((tool) => tool.tool === command); + +if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) { + await executePip(tool); +} else if (tool) { + const args = process.argv.slice(3); + + setEcoSystem(tool.ecoSystem); + initializePackageManager(tool.internalPackageManagerName); -if (pkgManagerCommands.includes(command)) { - setEcoSystem(ECOSYSTEM_JS); - initializePackageManager(command); (async () => { - var exitCode = await main(process.argv.slice(3)); + var exitCode = await main(args); process.exit(exitCode); })(); } else if (command === "help" || command === "--help" || command === "-h") { @@ -131,3 +134,45 @@ async function getVersion() { return "1.0.0"; } + +/** + * @param {import("../src/shell-integration/helpers.js").AikidoTool} tool + */ +async function executePip(tool) { + let args = process.argv.slice(3); + setEcoSystem(tool.ecoSystem); + initializePackageManager(PIP_PACKAGE_MANAGER); + + let shouldSkip = false; + if (tool.tool === "pip") { + setCurrentPipInvocation(PIP_INVOCATIONS.PIP); + } else if (tool.tool === "pip3") { + setCurrentPipInvocation(PIP_INVOCATIONS.PIP3); + } else if (tool.tool === "python") { + if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) { + setCurrentPipInvocation( + args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP + ); + args = args.slice(2); + } else { + shouldSkip = true; + } + } else if (tool.tool === "python3") { + if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) { + setCurrentPipInvocation( + args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP + ); + args = args.slice(2); + } else { + shouldSkip = true; + } + } + + if (shouldSkip) { + const { spawn } = await import("child_process"); + spawn(tool.tool, args, { stdio: "inherit" }); + } else { + var exitCode = await main(args); + process.exit(exitCode); + } +} diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 7f45669..1d32e82 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -9,24 +9,85 @@ import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; * @property {string} tool * @property {string} aikidoCommand * @property {string} ecoSystem + * @property {string} internalPackageManagerName */ /** * @type {AikidoTool[]} */ export const knownAikidoTools = [ - { tool: "npm", aikidoCommand: "aikido-npm", ecoSystem: ECOSYSTEM_JS }, - { tool: "npx", aikidoCommand: "aikido-npx", ecoSystem: ECOSYSTEM_JS }, - { tool: "yarn", aikidoCommand: "aikido-yarn", ecoSystem: ECOSYSTEM_JS }, - { tool: "pnpm", aikidoCommand: "aikido-pnpm", ecoSystem: ECOSYSTEM_JS }, - { tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS }, - { tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS }, - { tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS }, - { tool: "uv", aikidoCommand: "aikido-uv", ecoSystem: ECOSYSTEM_PY }, - { tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY }, - { tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY }, - { tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY }, - { tool: "python3", aikidoCommand: "aikido-python3", ecoSystem: ECOSYSTEM_PY }, + { + tool: "npm", + aikidoCommand: "aikido-npm", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "npm", + }, + { + tool: "npx", + aikidoCommand: "aikido-npx", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "npx", + }, + { + tool: "yarn", + aikidoCommand: "aikido-yarn", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "yarn", + }, + { + tool: "pnpm", + aikidoCommand: "aikido-pnpm", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "pnpm", + }, + { + tool: "pnpx", + aikidoCommand: "aikido-pnpx", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "pnpx", + }, + { + tool: "bun", + aikidoCommand: "aikido-bun", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "bun", + }, + { + tool: "bunx", + aikidoCommand: "aikido-bunx", + ecoSystem: ECOSYSTEM_JS, + internalPackageManagerName: "bunx", + }, + { + tool: "uv", + aikidoCommand: "aikido-uv", + ecoSystem: ECOSYSTEM_PY, + internalPackageManagerName: "uv", + }, + { + tool: "pip", + aikidoCommand: "aikido-pip", + ecoSystem: ECOSYSTEM_PY, + internalPackageManagerName: "pip", + }, + { + tool: "pip3", + aikidoCommand: "aikido-pip3", + ecoSystem: ECOSYSTEM_PY, + internalPackageManagerName: "pip", + }, + { + tool: "python", + aikidoCommand: "aikido-python", + ecoSystem: ECOSYSTEM_PY, + internalPackageManagerName: "pip", + }, + { + tool: "python3", + aikidoCommand: "aikido-python3", + ecoSystem: ECOSYSTEM_PY, + internalPackageManagerName: "pip", + }, // When adding a new tool here, also update the documentation for the new tool in the README.md ]; From 1361abc4e86a522e7a1a1e38e320e93016cf4ac2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 28 Nov 2025 18:06:31 +0100 Subject: [PATCH 291/345] Fix top-level await --- packages/safe-chain/bin/safe-chain.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index a9a9ed3..0a73f0e 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -43,7 +43,9 @@ const command = process.argv[2]; const tool = knownAikidoTools.find((tool) => tool.tool === command); if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) { - await executePip(tool); + (async function () { + await executePip(tool); + })(); } else if (tool) { const args = process.argv.slice(3); From 2e57057baaf73384c57c0e05809e6c9b84bf0aac Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 09:57:28 +0100 Subject: [PATCH 292/345] Update path wrappers --- .../path-wrappers/templates/unix-wrapper.template.sh | 4 ++-- .../path-wrappers/templates/windows-wrapper.template.cmd | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh index e914e5b..d6c9efd 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh @@ -7,9 +7,9 @@ remove_shim_from_path() { echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g" } -if command -v {{AIKIDO_COMMAND}} >/dev/null 2>&1; then +if command -v safe-chain >/dev/null 2>&1; then # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops - PATH=$(remove_shim_from_path) exec {{AIKIDO_COMMAND}} "$@" + PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@" else # Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory) original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}}) diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd index b7a65fa..d941a56 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd @@ -10,7 +10,7 @@ REM Check if aikido command is available with clean PATH set "PATH=%CLEAN_PATH%" & where {{AIKIDO_COMMAND}} >nul 2>&1 if %errorlevel%==0 ( REM Call aikido command with clean PATH - set "PATH=%CLEAN_PATH%" & {{AIKIDO_COMMAND}} %* + set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %* ) else ( REM Find the original command with clean PATH for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do ( From 20e9826ef0259e6491c5c6b3134d4aca72201253 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 12:31:55 +0100 Subject: [PATCH 293/345] Modify release pipeline to attach the binaries. --- .github/workflows/build-and-release.yml | 25 ++++++++++++++++++++++++- .github/workflows/create-artifact.yml | 4 +++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 2c1a423..e116281 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -7,10 +7,14 @@ on: permissions: id-token: write - contents: read + contents: write jobs: + create-binaries: + uses: ./.github/workflows/create-artifact.yml + build: + needs: create-binaries runs-on: ubuntu-latest steps: @@ -55,3 +59,22 @@ jobs: run: | echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" npm publish --workspace=packages/safe-chain --access public --provenance + + - name: Download all binary artifacts + uses: actions/download-artifact@v4 + with: + path: binaries/ + pattern: safe-chain-* + merge-multiple: false + + - name: Upload binaries to existing GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload ${{ steps.get_version.outputs.tag }} \ + binaries/safe-chain-macos-x64/* \ + binaries/safe-chain-macos-arm64/* \ + binaries/safe-chain-linux-x64/* \ + binaries/safe-chain-linux-arm64/* \ + binaries/safe-chain-win-x64/* \ + binaries/safe-chain-win-arm64/* diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index e5be214..7d573e5 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -1,6 +1,8 @@ name: Create binaries -on: pull_request +on: + pull_request: + workflow_call: jobs: create-binaries: From 3f60ea15f76d0d791465c31dd498219da83ebbe6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 13:28:11 +0100 Subject: [PATCH 294/345] Set release version on PR build --- .github/workflows/build-and-release.yml | 34 +++++++++++++++---------- .github/workflows/create-artifact.yml | 9 +++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index e116281..e310bc5 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -10,11 +10,25 @@ permissions: contents: write jobs: + set-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get_version.outputs.tag }} + steps: + - name: Set version number + id: get_version + run: | + version="${{ github.ref_name }}" + echo "tag=$version" >> $GITHUB_OUTPUT + create-binaries: + needs: set-version uses: ./.github/workflows/create-artifact.yml + with: + version: ${{ needs.set-version.outputs.version }} build: - needs: create-binaries + needs: [set-version, create-binaries] runs-on: ubuntu-latest steps: @@ -34,14 +48,8 @@ jobs: npm i -g @aikidosec/safe-chain safe-chain setup-ci - - name: Set version number - id: get_version - run: | - version="${{ github.ref_name }}" - echo "tag=$version" >> $GITHUB_OUTPUT - - name: Set the version in safe-chain package - run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }} --workspace=packages/safe-chain + run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain - name: Install dependencies run: npm ci @@ -55,10 +63,10 @@ jobs: cp LICENSE packages/safe-chain/ cp -r docs packages/safe-chain/ - - name: Publish to npm - run: | - echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance + # - name: Publish to npm + # run: | + # echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" + # npm publish --workspace=packages/safe-chain --access public --provenance - name: Download all binary artifacts uses: actions/download-artifact@v4 @@ -71,7 +79,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release upload ${{ steps.get_version.outputs.tag }} \ + gh release upload ${{ needs.set-version.outputs.version }} \ binaries/safe-chain-macos-x64/* \ binaries/safe-chain-macos-arm64/* \ binaries/safe-chain-linux-x64/* \ diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 7d573e5..ad43a9d 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -3,6 +3,11 @@ name: Create binaries on: pull_request: workflow_call: + inputs: + version: + description: 'Version to set in package.json' + required: false + type: string jobs: create-binaries: @@ -59,6 +64,10 @@ jobs: npm i -g @aikidosec/safe-chain safe-chain setup-ci + - name: Set the version in safe-chain package + if: inputs.version != '' + run: npm --no-git-tag-version version ${{ inputs.version }} --workspace=packages/safe-chain + - name: Install dependencies run: npm ci --ignore-scripts From 6f583ce396bd6956a14502256e0c3dd94e6fdd5d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 14:09:05 +0100 Subject: [PATCH 295/345] Rename build artifacts --- .github/workflows/build-and-release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index e310bc5..4464c02 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -75,6 +75,15 @@ jobs: pattern: safe-chain-* merge-multiple: false + - name: Rename binaries to include platform and architecture + run: | + mv binaries/safe-chain-macos-x64/safe-chain binaries/safe-chain-macos-x64/safe-chain-macos-x64 + mv binaries/safe-chain-macos-arm64/safe-chain binaries/safe-chain-macos-arm64/safe-chain-macos-arm64 + mv binaries/safe-chain-linux-x64/safe-chain binaries/safe-chain-linux-x64/safe-chain-linux-x64 + mv binaries/safe-chain-linux-arm64/safe-chain binaries/safe-chain-linux-arm64/safe-chain-linux-arm64 + mv binaries/safe-chain-win-x64/safe-chain.exe binaries/safe-chain-win-x64/safe-chain-win-x64.exe + mv binaries/safe-chain-win-arm64/safe-chain.exe binaries/safe-chain-win-arm64/safe-chain-win-arm64.exe + - name: Upload binaries to existing GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e58e77bc6323b29b525e3d8601cc13adc42eba06 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 14:39:41 +0100 Subject: [PATCH 296/345] Install scripts --- install-scripts/install-safe-chain.ps1 | 154 +++++++++++++++++++++++++ install-scripts/install-safe-chain.sh | 126 ++++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 install-scripts/install-safe-chain.ps1 create mode 100755 install-scripts/install-safe-chain.sh diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 new file mode 100644 index 0000000..ac85891 --- /dev/null +++ b/install-scripts/install-safe-chain.ps1 @@ -0,0 +1,154 @@ +# Downloads and installs safe-chain for Windows +# Usage: iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) + +param( + [string]$Version +) + +# Configuration +if (-not $Version) { + $Version = if ($env:SAFE_CHAIN_VERSION) { $env:SAFE_CHAIN_VERSION } else { "v0.0.2-binaries-beta" } +} + +$InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" +$RepoUrl = "https://github.com/AikidoSec/safe-chain" + +# Ensure TLS 1.2 is enabled for downloads +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Helper functions +function Write-Info { + param([string]$Message) + Write-Host "[INFO] $Message" -ForegroundColor Green +} + +function Write-Warn { + param([string]$Message) + Write-Host "[WARN] $Message" -ForegroundColor Yellow +} + +function Write-Error-Custom { + param([string]$Message) + Write-Host "[ERROR] $Message" -ForegroundColor Red + exit 1 +} + +# Detect architecture +function Get-Architecture { + $arch = $env:PROCESSOR_ARCHITECTURE + switch ($arch) { + "AMD64" { return "x64" } + "ARM64" { return "arm64" } + default { Write-Error-Custom "Unsupported architecture: $arch" } + } +} + +# Main installation +function Install-SafeChain { + Write-Info "Installing safe-chain $Version..." + + # Detect platform + $arch = Get-Architecture + $binaryName = "safe-chain-win-$arch.exe" + + Write-Info "Detected architecture: $arch" + + # Create installation directory + if (-not (Test-Path $InstallDir)) { + Write-Info "Creating installation directory: $InstallDir" + try { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + } + catch { + Write-Error-Custom "Failed to create directory $InstallDir : $_" + } + } + + # Download binary + $downloadUrl = "$RepoUrl/releases/download/$Version/$binaryName" + $tempFile = Join-Path $InstallDir $binaryName + $finalFile = Join-Path $InstallDir "safe-chain.exe" + + Write-Info "Downloading from: $downloadUrl" + + try { + # Download with progress suppressed for cleaner output + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing + $ProgressPreference = 'Continue' + } + catch { + Write-Error-Custom "Failed to download from $downloadUrl : $_" + } + + # Rename to final location + try { + Move-Item -Path $tempFile -Destination $finalFile -Force + } + catch { + Write-Error-Custom "Failed to move binary to $finalFile : $_" + } + + Write-Info "Binary installed to: $finalFile" + + # Check if directory is in PATH + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($userPath -like "*$InstallDir*") { + Write-Info "Installation directory is already in PATH" + } + else { + Write-Warn "Installation directory is not in PATH" + Write-Host "" + Write-Warn "Would you like to add it to your PATH now? (Y/N)" + $response = Read-Host + + if ($response -eq "Y" -or $response -eq "y") { + try { + $newPath = "$userPath;$InstallDir" + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") + Write-Info "Added to PATH. Please restart your terminal for changes to take effect." + } + catch { + Write-Warn "Failed to add to PATH automatically: $_" + Write-Warn "Please add the following directory to your PATH manually:" + Write-Host " $InstallDir" + } + } + else { + Write-Warn "Skipping PATH setup. Add the following directory to your PATH manually:" + Write-Host "" + Write-Host " $InstallDir" + Write-Host "" + } + } + + # Execute safe-chain setup + Write-Info "Running safe-chain setup..." + + try { + $env:Path = "$env:Path;$InstallDir" + & $finalFile setup + + if ($LASTEXITCODE -eq 0) { + Write-Info "✓ safe-chain installed and configured successfully!" + } + else { + Write-Warn "safe-chain was installed but setup encountered issues." + Write-Warn "You can run 'safe-chain setup' manually later." + } + } + catch { + Write-Warn "safe-chain was installed but setup encountered issues: $_" + Write-Warn "You can run 'safe-chain setup' manually later." + } + + Write-Info "Installation complete!" +} + +# Run installation +try { + Install-SafeChain +} +catch { + Write-Error-Custom "Installation failed: $_" +} diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh new file mode 100755 index 0000000..37f3136 --- /dev/null +++ b/install-scripts/install-safe-chain.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +# Downloads and installs safe-chain, depending on the operating system and architecture +# Usage: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh +# or: wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh + +set -e # Exit on error + +# Configuration +VERSION="${SAFE_CHAIN_VERSION:-v0.0.2-binaries-beta}" +INSTALL_DIR="${HOME}/.safe-chain/bin" +REPO_URL="https://github.com/AikidoSec/safe-chain" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Helper functions +info() { + printf "${GREEN}[INFO]${NC} %s\n" "$1" +} + +warn() { + printf "${YELLOW}[WARN]${NC} %s\n" "$1" +} + +error() { + printf "${RED}[ERROR]${NC} %s\n" "$1" >&2 + exit 1 +} + +# Detect OS +detect_os() { + case "$(uname -s)" in + Linux*) echo "linux" ;; + Darwin*) echo "macos" ;; + *) error "Unsupported operating system: $(uname -s)" ;; + esac +} + +# Detect architecture +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) echo "x64" ;; + aarch64|arm64) echo "arm64" ;; + *) error "Unsupported architecture: $(uname -m)" ;; + esac +} + +# Check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Download file +download() { + url="$1" + dest="$2" + + if command_exists curl; then + curl -fsSL "$url" -o "$dest" || error "Failed to download from $url" + elif command_exists wget; then + wget -q "$url" -O "$dest" || error "Failed to download from $url" + else + error "Neither curl nor wget found. Please install one of them." + fi +} + +# Main installation +main() { + info "Installing safe-chain ${VERSION}..." + + # Detect platform + OS=$(detect_os) + ARCH=$(detect_arch) + BINARY_NAME="safe-chain-${OS}-${ARCH}" + + info "Detected platform: ${OS}-${ARCH}" + + # Create installation directory + if [ ! -d "$INSTALL_DIR" ]; then + info "Creating installation directory: $INSTALL_DIR" + mkdir -p "$INSTALL_DIR" || error "Failed to create directory $INSTALL_DIR" + fi + + # Download binary + DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}/${BINARY_NAME}" + TEMP_FILE="${INSTALL_DIR}/${BINARY_NAME}" + FINAL_FILE="${INSTALL_DIR}/safe-chain" + + info "Downloading from: $DOWNLOAD_URL" + download "$DOWNLOAD_URL" "$TEMP_FILE" + + # Rename and make executable + mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE" + chmod +x "$FINAL_FILE" || error "Failed to make binary executable" + + info "Binary installed to: $FINAL_FILE" + + # Check if directory is in PATH + case ":$PATH:" in + *":$INSTALL_DIR:"*) + info "Installation directory is already in PATH" + ;; + *) + warn "Installation directory is not in PATH" + warn "Add the following line to your shell profile (~/.bashrc, ~/.zshrc, etc.):" + printf "\n export PATH=\"\$PATH:${INSTALL_DIR}\"\n\n" + ;; + esac + + # Execute safe-chain setup + info "Running safe-chain setup..." + if "$FINAL_FILE" setup; then + info "✓ safe-chain installed and configured successfully!" + else + warn "safe-chain was installed but setup encountered issues." + warn "You can run 'safe-chain setup' manually later." + fi + + info "Installation complete!" +} + +main From 8f80266ad3e8ed9d8b7c93b2e6d8e838fd12d946 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 14:52:15 +0100 Subject: [PATCH 297/345] Update powershell scripts and installation scripts --- install-scripts/install-safe-chain.ps1 | 40 +------------------ install-scripts/install-safe-chain.sh | 14 +------ .../include-python/init-pwsh.ps1 | 5 ++- .../startup-scripts/init-pwsh.ps1 | 5 ++- 4 files changed, 10 insertions(+), 54 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index ac85891..88c109a 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -1,15 +1,8 @@ # Downloads and installs safe-chain for Windows # Usage: iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) -param( - [string]$Version -) - -# Configuration -if (-not $Version) { - $Version = if ($env:SAFE_CHAIN_VERSION) { $env:SAFE_CHAIN_VERSION } else { "v0.0.2-binaries-beta" } -} +$Version = "v0.0.3-binaries-beta" $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" @@ -91,37 +84,6 @@ function Install-SafeChain { Write-Info "Binary installed to: $finalFile" - # Check if directory is in PATH - $userPath = [Environment]::GetEnvironmentVariable("Path", "User") - if ($userPath -like "*$InstallDir*") { - Write-Info "Installation directory is already in PATH" - } - else { - Write-Warn "Installation directory is not in PATH" - Write-Host "" - Write-Warn "Would you like to add it to your PATH now? (Y/N)" - $response = Read-Host - - if ($response -eq "Y" -or $response -eq "y") { - try { - $newPath = "$userPath;$InstallDir" - [Environment]::SetEnvironmentVariable("Path", $newPath, "User") - Write-Info "Added to PATH. Please restart your terminal for changes to take effect." - } - catch { - Write-Warn "Failed to add to PATH automatically: $_" - Write-Warn "Please add the following directory to your PATH manually:" - Write-Host " $InstallDir" - } - } - else { - Write-Warn "Skipping PATH setup. Add the following directory to your PATH manually:" - Write-Host "" - Write-Host " $InstallDir" - Write-Host "" - } - } - # Execute safe-chain setup Write-Info "Running safe-chain setup..." diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 37f3136..32d086b 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -7,7 +7,7 @@ set -e # Exit on error # Configuration -VERSION="${SAFE_CHAIN_VERSION:-v0.0.2-binaries-beta}" +VERSION="${SAFE_CHAIN_VERSION:-v0.0.3-binaries-beta}" INSTALL_DIR="${HOME}/.safe-chain/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" @@ -99,18 +99,6 @@ main() { info "Binary installed to: $FINAL_FILE" - # Check if directory is in PATH - case ":$PATH:" in - *":$INSTALL_DIR:"*) - info "Installation directory is already in PATH" - ;; - *) - warn "Installation directory is not in PATH" - warn "Add the following line to your shell profile (~/.bashrc, ~/.zshrc, etc.):" - printf "\n export PATH=\"\$PATH:${INSTALL_DIR}\"\n\n" - ;; - esac - # Execute safe-chain setup info "Running safe-chain setup..." if "$FINAL_FILE" setup; then diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 index 27a5065..50a6d0b 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -1,4 +1,7 @@ -$env:PATH = "$env:PATH;$HOME/.safe-chain/bin" +# Use cross-platform path separator (: on Unix, ; on Windows) +$pathSeparator = if ($IsWindows) { ';' } else { ':' } +$safeChainBin = Join-Path $HOME '.safe-chain' 'bin' +$env:PATH = "$env:PATH$pathSeparator$safeChainBin" function Write-SafeChainWarning { param([string]$Command) 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 17ddeb2..ddf5aee 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 @@ -1,4 +1,7 @@ -$env:PATH = "$env:PATH;$HOME/.safe-chain/bin" +# Use cross-platform path separator (: on Unix, ; on Windows) +$pathSeparator = if ($IsWindows) { ';' } else { ':' } +$safeChainBin = Join-Path $HOME '.safe-chain' 'bin' +$env:PATH = "$env:PATH$pathSeparator$safeChainBin" function Write-SafeChainWarning { param([string]$Command) From b6ea61170fcb056968eafe5d52fa799797082b12 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 14:59:20 +0100 Subject: [PATCH 298/345] Update release version --- install-scripts/install-safe-chain.ps1 | 2 +- install-scripts/install-safe-chain.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 88c109a..86c63db 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -2,7 +2,7 @@ # Usage: iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) -$Version = "v0.0.3-binaries-beta" +$Version = "v0.0.4-binaries-beta" $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 32d086b..dcc7a3c 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -7,7 +7,7 @@ set -e # Exit on error # Configuration -VERSION="${SAFE_CHAIN_VERSION:-v0.0.3-binaries-beta}" +VERSION="${SAFE_CHAIN_VERSION:-v0.0.4-binaries-beta}" INSTALL_DIR="${HOME}/.safe-chain/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" From 34c62c5268f0d947046f693d0f1c66e6c7ffbeb6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 15:28:10 +0100 Subject: [PATCH 299/345] Improve install script output --- install-scripts/install-safe-chain.ps1 | 9 +-------- install-scripts/install-safe-chain.sh | 7 +------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 86c63db..13d2f1a 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -85,16 +85,11 @@ function Install-SafeChain { Write-Info "Binary installed to: $finalFile" # Execute safe-chain setup - Write-Info "Running safe-chain setup..." - try { $env:Path = "$env:Path;$InstallDir" & $finalFile setup - if ($LASTEXITCODE -eq 0) { - Write-Info "✓ safe-chain installed and configured successfully!" - } - else { + if ($LASTEXITCODE -ne 0) { Write-Warn "safe-chain was installed but setup encountered issues." Write-Warn "You can run 'safe-chain setup' manually later." } @@ -103,8 +98,6 @@ function Install-SafeChain { Write-Warn "safe-chain was installed but setup encountered issues: $_" Write-Warn "You can run 'safe-chain setup' manually later." } - - Write-Info "Installation complete!" } # Run installation diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index dcc7a3c..cb899b6 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -100,15 +100,10 @@ main() { info "Binary installed to: $FINAL_FILE" # Execute safe-chain setup - info "Running safe-chain setup..." - if "$FINAL_FILE" setup; then - info "✓ safe-chain installed and configured successfully!" - else + if ! "$FINAL_FILE" setup; then warn "safe-chain was installed but setup encountered issues." warn "You can run 'safe-chain setup' manually later." fi - - info "Installation complete!" } main From 22b780ddcd0f8aee08fe926bae04ee1e88b3308b Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 1 Dec 2025 16:04:47 +0100 Subject: [PATCH 300/345] Remove npm-installed safe-chain --- install-scripts/install-safe-chain.ps1 | 27 ++++++++++++++++++++++++++ install-scripts/install-safe-chain.sh | 23 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 13d2f1a..e185d2a 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -36,10 +36,37 @@ function Get-Architecture { } } +# Check and uninstall npm global package if present +function Remove-NpmInstallation { + # Check if npm is available + if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + return + } + + # Check if safe-chain is installed as an npm global package + npm list -g @aikidosec/safe-chain 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Info "Detected npm global installation of @aikidosec/safe-chain" + Write-Info "Uninstalling npm version before installing binary version..." + + npm uninstall -g @aikidosec/safe-chain 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Info "Successfully uninstalled npm version" + } + else { + Write-Warn "Failed to uninstall npm version automatically" + Write-Warn "Please run: npm uninstall -g @aikidosec/safe-chain" + } + } +} + # Main installation function Install-SafeChain { Write-Info "Installing safe-chain $Version..." + # Check for existing npm installation + Remove-NpmInstallation + # Detect platform $arch = Get-Architecture $binaryName = "safe-chain-win-$arch.exe" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index cb899b6..6f8480e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -68,10 +68,33 @@ download() { fi } +# Check and uninstall npm global package if present +check_npm_installation() { + if ! command_exists npm; then + return + fi + + # Check if safe-chain is installed as an npm global package + if npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then + info "Detected npm global installation of @aikidosec/safe-chain" + info "Uninstalling npm version before installing binary version..." + + if npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then + info "Successfully uninstalled npm version" + else + warn "Failed to uninstall npm version automatically" + warn "Please run: npm uninstall -g @aikidosec/safe-chain" + fi + fi +} + # Main installation main() { info "Installing safe-chain ${VERSION}..." + # Check for existing npm installation + check_npm_installation + # Detect platform OS=$(detect_os) ARCH=$(detect_arch) From eddb4f3f75f611175f5b17ae0da8f5758b3c55e0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 08:43:38 +0100 Subject: [PATCH 301/345] Check for volta installation --- install-scripts/install-safe-chain.ps1 | 28 ++++++++++++++++++++++++++ install-scripts/install-safe-chain.sh | 24 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index e185d2a..9283170 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -60,6 +60,31 @@ function Remove-NpmInstallation { } } +# Check and uninstall Volta-managed package if present +function Remove-VoltaInstallation { + # Check if Volta is available + if (-not (Get-Command volta -ErrorAction SilentlyContinue)) { + return + } + + # Volta manages global packages in its own directory + # Check if safe-chain is installed via Volta + volta list @aikidosec/safe-chain 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Info "Detected Volta installation of @aikidosec/safe-chain" + Write-Info "Uninstalling Volta version before installing binary version..." + + volta uninstall @aikidosec/safe-chain 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Info "Successfully uninstalled Volta version" + } + else { + Write-Warn "Failed to uninstall Volta version automatically" + Write-Warn "Please run: volta uninstall @aikidosec/safe-chain" + } + } +} + # Main installation function Install-SafeChain { Write-Info "Installing safe-chain $Version..." @@ -67,6 +92,9 @@ function Install-SafeChain { # Check for existing npm installation Remove-NpmInstallation + # Check for existing Volta installation + Remove-VoltaInstallation + # Detect platform $arch = Get-Architecture $binaryName = "safe-chain-win-$arch.exe" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 6f8480e..671e52e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -88,6 +88,27 @@ check_npm_installation() { fi } +# Check and uninstall Volta-managed package if present +check_volta_installation() { + if ! command_exists volta; then + return + fi + + # Volta manages global packages in its own directory + # Check if safe-chain is installed via Volta + if volta list @aikidosec/safe-chain >/dev/null 2>&1; then + info "Detected Volta installation of @aikidosec/safe-chain" + info "Uninstalling Volta version before installing binary version..." + + if volta uninstall @aikidosec/safe-chain >/dev/null 2>&1; then + info "Successfully uninstalled Volta version" + else + warn "Failed to uninstall Volta version automatically" + warn "Please run: volta uninstall @aikidosec/safe-chain" + fi + fi +} + # Main installation main() { info "Installing safe-chain ${VERSION}..." @@ -95,6 +116,9 @@ main() { # Check for existing npm installation check_npm_installation + # Check for existing Volta installation + check_volta_installation + # Detect platform OS=$(detect_os) ARCH=$(detect_arch) From b60cb63fdb45f9fda526f862a045f1b6d482ee53 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 09:26:16 +0100 Subject: [PATCH 302/345] Add --include-python and --ci args --- install-scripts/install-safe-chain.ps1 | 38 ++++++++++++++-- install-scripts/install-safe-chain.sh | 63 ++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 9283170..b9a8cd8 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -1,6 +1,23 @@ # Downloads and installs safe-chain for Windows -# Usage: iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) +# +# Usage examples: +# +# Default (JavaScript packages only): +# iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) +# +# CI setup (JavaScript packages only): +# iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci" +# +# Include Python packages: +# iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -includepython" +# +# CI setup with Python packages: +# iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython" +param( + [switch]$ci, + [switch]$includepython +) $Version = "v0.0.4-binaries-beta" $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" @@ -139,19 +156,32 @@ function Install-SafeChain { Write-Info "Binary installed to: $finalFile" + # Build setup command based on parameters + $setupCmd = if ($ci) { "setup-ci" } else { "setup" } + $setupArgs = @() + if ($includepython) { + $setupArgs += "--include-python" + } + # Execute safe-chain setup + Write-Info "Running safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })..." try { $env:Path = "$env:Path;$InstallDir" - & $finalFile setup + + if ($setupArgs) { + & $finalFile $setupCmd $setupArgs + } else { + & $finalFile $setupCmd + } if ($LASTEXITCODE -ne 0) { Write-Warn "safe-chain was installed but setup encountered issues." - Write-Warn "You can run 'safe-chain setup' manually later." + Write-Warn "You can run 'safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })' manually later." } } catch { Write-Warn "safe-chain was installed but setup encountered issues: $_" - Write-Warn "You can run 'safe-chain setup' manually later." + Write-Warn "You can run 'safe-chain $setupCmd $(if ($setupArgs) { $setupArgs -join ' ' })' manually later." } } diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 671e52e..f51cf1f 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -1,8 +1,24 @@ #!/bin/sh # Downloads and installs safe-chain, depending on the operating system and architecture -# Usage: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -# or: wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh +# +# Usage examples: +# +# Default (JavaScript packages only): +# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh +# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh +# +# CI setup (JavaScript packages only): +# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci +# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci +# +# Include Python packages: +# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python +# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python +# +# CI setup with Python packages: +# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python +# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python set -e # Exit on error @@ -109,8 +125,32 @@ check_volta_installation() { fi } +# Parse command-line arguments +parse_arguments() { + for arg in "$@"; do + case "$arg" in + --ci) + USE_CI_SETUP=true + ;; + --include-python) + INCLUDE_PYTHON=true + ;; + *) + error "Unknown argument: $arg" + ;; + esac + done +} + # Main installation main() { + # Initialize argument flags + USE_CI_SETUP=false + INCLUDE_PYTHON=false + + # Parse command-line arguments + parse_arguments "$@" + info "Installing safe-chain ${VERSION}..." # Check for existing npm installation @@ -146,11 +186,24 @@ main() { info "Binary installed to: $FINAL_FILE" + # Build setup command based on arguments + SETUP_CMD="setup" + SETUP_ARGS="" + + if [ "$USE_CI_SETUP" = "true" ]; then + SETUP_CMD="setup-ci" + fi + + if [ "$INCLUDE_PYTHON" = "true" ]; then + SETUP_ARGS="--include-python" + fi + # Execute safe-chain setup - if ! "$FINAL_FILE" setup; then + info "Running safe-chain $SETUP_CMD $SETUP_ARGS..." + if ! "$FINAL_FILE" $SETUP_CMD $SETUP_ARGS; then warn "safe-chain was installed but setup encountered issues." - warn "You can run 'safe-chain setup' manually later." + warn "You can run 'safe-chain $SETUP_CMD $SETUP_ARGS' manually later." fi } -main +main "$@" From 2d87e1b8178ef12498659dabdc1f0de6f7ae9754 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 09:49:14 +0100 Subject: [PATCH 303/345] Improve volta installation check --- install-scripts/install-safe-chain.ps1 | 5 +++-- install-scripts/install-safe-chain.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index b9a8cd8..1d2dab2 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -86,7 +86,7 @@ function Remove-VoltaInstallation { # Volta manages global packages in its own directory # Check if safe-chain is installed via Volta - volta list @aikidosec/safe-chain 2>&1 | Out-Null + volta list safe-chain 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { Write-Info "Detected Volta installation of @aikidosec/safe-chain" Write-Info "Uninstalling Volta version before installing binary version..." @@ -170,7 +170,8 @@ function Install-SafeChain { if ($setupArgs) { & $finalFile $setupCmd $setupArgs - } else { + } + else { & $finalFile $setupCmd } diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index f51cf1f..a65aee9 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -112,7 +112,7 @@ check_volta_installation() { # Volta manages global packages in its own directory # Check if safe-chain is installed via Volta - if volta list @aikidosec/safe-chain >/dev/null 2>&1; then + if volta list safe-chain >/dev/null 2>&1; then info "Detected Volta installation of @aikidosec/safe-chain" info "Uninstalling Volta version before installing binary version..." From c4a33ca1512bc8b68e34e51458656754bdd4271d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 10:30:59 +0100 Subject: [PATCH 304/345] Update readme.md --- README.md | 96 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 47f0894..de5be05 100644 --- a/README.md +++ b/README.md @@ -24,29 +24,43 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi ## Installation -Installing the Aikido Safe Chain is easy. You just need 3 simple steps: +Installing the Aikido Safe Chain is easy with our one-line installer: -1. **Install the Aikido Safe Chain package globally** using npm: - ```shell - npm install -g @aikidosec/safe-chain - ``` -2. **Setup the shell integration** by running: +### Unix/Linux/macOS - ```shell - safe-chain setup - ``` +**Default installation (JavaScript packages only):** - To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag: +```shell +curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh +``` - ```shell - safe-chain setup --include-python - ``` +**Include Python support (pip/pip3/uv):** -3. **❗Restart your terminal** to start using the Aikido Safe Chain. +```shell +curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python +``` + +### Windows (PowerShell) + +**Default installation (JavaScript packages only):** + +```powershell +iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) +``` + +**Include Python support (pip/pip3/uv):** + +```powershell +iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -includepython" +``` + +### Verify the installation + +1. **❗Restart your terminal** to start using the Aikido Safe Chain. - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, 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 one of the following commands: +2. **Verify the installation** by running one of the following commands: For JavaScript/Node.js: @@ -54,7 +68,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps: npm install safe-chain-test ``` - For Python (beta): + For Python (if you enabled Python support): ```shell pip3 install safe-chain-pi-test @@ -165,21 +179,33 @@ You can protect your CI/CD pipelines from malicious packages by integrating Aiki For optimal protection in CI/CD environments, we recommend using **npm >= 10.4.0** as it provides full dependency tree scanning. Other package managers currently offer limited scanning of install command arguments only. -## Setup +## Installation for CI/CD -To use Aikido Safe Chain in CI/CD environments, run the following command after installing the package: +Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases. +### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.) + +**JavaScript only:** ```shell -safe-chain setup-ci +curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci ``` -To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag: - +**With Python support:** ```shell -safe-chain setup-ci --include-python +curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python ``` -This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands. +### Windows (Azure Pipelines, etc.) + +**JavaScript only:** +```powershell +iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci" +``` + +**With Python support:** +```powershell +iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython" +``` ## Supported Platforms @@ -195,16 +221,15 @@ This automatically configures your CI environment to use Aikido Safe Chain for a node-version: "22" cache: "npm" -- name: Setup safe-chain - run: | - npm i -g @aikidosec/safe-chain - safe-chain setup-ci +- name: Install safe-chain + run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python - name: Install dependencies - run: | - npm ci + run: npm ci ``` +> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support. + ## Azure DevOps Example ```yaml @@ -213,14 +238,13 @@ This automatically configures your CI environment to use Aikido Safe Chain for a versionSpec: "22.x" displayName: "Install Node.js" -- script: | - npm i -g @aikidosec/safe-chain - safe-chain setup-ci - displayName: "Install safe chain" +- script: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python + displayName: "Install safe-chain" -- script: | - npm ci - displayName: "npm install and build" +- script: npm ci + displayName: "Install dependencies" ``` +> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support. + After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. From 9e1bdd4a31dc8b758d3ccc9b500276b93e0ea6b0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 11:57:23 +0100 Subject: [PATCH 305/345] Update docs: migration guide --- README.md | 8 ++- docs/npm-to-binary-migration.md | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 docs/npm-to-binary-migration.md diff --git a/README.md b/README.md index de5be05..6e5470c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi ## Installation -Installing the Aikido Safe Chain is easy with our one-line installer: +Installing the Aikido Safe Chain is easy with our one-line installer. + +> ⚠️ **Already installed via npm?** See the [migration guide](docs/npm-to-binary-migration.md) to switch to the binary version. ### Unix/Linux/macOS @@ -186,11 +188,13 @@ Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD envir ### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.) **JavaScript only:** + ```shell curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci ``` **With Python support:** + ```shell curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python ``` @@ -198,11 +202,13 @@ curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-s ### Windows (Azure Pipelines, etc.) **JavaScript only:** + ```powershell iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci" ``` **With Python support:** + ```powershell iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython" ``` diff --git a/docs/npm-to-binary-migration.md b/docs/npm-to-binary-migration.md new file mode 100644 index 0000000..c0b8f9a --- /dev/null +++ b/docs/npm-to-binary-migration.md @@ -0,0 +1,89 @@ +# Migrating from npm global tool to binary installation + +If you previously installed safe-chain as an npm global package, you need to migrate to the binary installation. + +Depending on the version manager you're using, the uninstall process differs: + +### Standard npm (no version manager) + +1. **Clean up shell aliases:** + + ```bash + safe-chain teardown + ``` + +2. **Restart your terminal** + +3. **Uninstall the npm package:** + + ```bash + npm uninstall -g @aikidosec/safe-chain + ``` + +4. **Install the binary version** (see [Installation](../README.md#installation)) + +### nvm (Node Version Manager) + +**Important:** nvm installs global packages separately for each Node version, so safe-chain must be uninstalled from each version where it was installed. + +1. **Clean up shell aliases:** + + ```bash + safe-chain teardown + ``` + +2. **Restart your terminal** + +3. **Uninstall from all Node versions:** + + **Option A** - Automated script (recommended): + + ```bash + for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do nvm use $version && npm uninstall -g @aikidosec/safe-chain; done + ``` + + **Option B** - Manual per version: + + ```bash + nvm use + npm uninstall -g @aikidosec/safe-chain + ``` + + Repeat for each Node version where safe-chain was installed. + +4. **Install the binary version** (see [Installation](../README.md#installation)) + +### Volta + +1. **Clean up shell aliases:** + + ```bash + safe-chain teardown + ``` + +2. **Restart your terminal** + +3. **Uninstall the Volta package:** + + ```bash + volta uninstall @aikidosec/safe-chain + ``` + +4. **Install the binary version** (see [Installation](../README.md#installation)) + +## Troubleshooting + +### Shell aliases still present after migration + +1. Run `safe-chain teardown` (if the binary is installed) +2. Manually remove any safe-chain entries from your shell config files: + - Bash: `~/.bashrc` + - Zsh: `~/.zshrc` + - Fish: `~/.config/fish/config.fish` + - PowerShell: `$PROFILE` +3. Restart your terminal +4. Re-run the install script + +### "command not found: safe-chain" after migration + +The binary installation directory (`~/.safe-chain/bin`) may not be in your PATH. Restart your terminal. If the problem persists: re-run the installation of safe-chain. From 3002d272736dacac7705fe03480192a2067219a0 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 13:58:27 +0100 Subject: [PATCH 306/345] Fix safe-chain in CI --- install-scripts/install-safe-chain.ps1 | 2 +- install-scripts/install-safe-chain.sh | 2 +- .../safe-chain/src/shell-integration/setup-ci.js | 13 ++++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 1d2dab2..dcffe8a 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -19,7 +19,7 @@ param( [switch]$includepython ) -$Version = "v0.0.4-binaries-beta" +$Version = "v0.0.5-binaries-beta" $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index a65aee9..fb7328d 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -23,7 +23,7 @@ set -e # Exit on error # Configuration -VERSION="${SAFE_CHAIN_VERSION:-v0.0.4-binaries-beta}" +VERSION="${SAFE_CHAIN_VERSION:-v0.0.5-binaries-beta}" INSTALL_DIR="${HOME}/.safe-chain/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 9e0342d..28109fb 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -29,6 +29,7 @@ export async function setupCi() { ui.emptyLine(); const shimsDir = path.join(os.homedir(), ".safe-chain", "shims"); + const binDir = path.join(os.homedir(), ".safe-chain", "shims"); // Create the shims directory if it doesn't exist if (!fs.existsSync(shimsDir)) { fs.mkdirSync(shimsDir, { recursive: true }); @@ -36,7 +37,7 @@ export async function setupCi() { createShims(shimsDir); ui.writeInformation(`Created shims in ${shimsDir}`); - modifyPathForCi(shimsDir); + modifyPathForCi(shimsDir, binDir); ui.writeInformation(`Added shims directory to PATH for CI environments.`); } @@ -130,13 +131,18 @@ function createShims(shimsDir) { /** * @param {string} shimsDir + * @param {string} binDir * * @returns {void} */ -function modifyPathForCi(shimsDir) { +function modifyPathForCi(shimsDir, binDir) { if (process.env.GITHUB_PATH) { // In GitHub Actions, append the shims directory to GITHUB_PATH - fs.appendFileSync(process.env.GITHUB_PATH, shimsDir + os.EOL, "utf-8"); + fs.appendFileSync( + process.env.GITHUB_PATH, + shimsDir + os.EOL + binDir + os.EOL, + "utf-8" + ); ui.writeInformation( `Added shims directory to GITHUB_PATH for GitHub Actions.` ); @@ -147,6 +153,7 @@ function modifyPathForCi(shimsDir) { // ##vso[task.prependpath]/path/to/add // Logging this to stdout will cause the Azure Pipelines agent to pick it up ui.writeInformation("##vso[task.prependpath]" + shimsDir); + ui.writeInformation("##vso[task.prependpath]" + binDir); } } From f9b16cf03cef0927b43bb257e6066c78eccf410e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 14:19:53 +0100 Subject: [PATCH 307/345] Fix bins path for CI --- PR-TODOS.md | 9 +++++++++ packages/safe-chain/src/shell-integration/setup-ci.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 PR-TODOS.md diff --git a/PR-TODOS.md b/PR-TODOS.md new file mode 100644 index 0000000..dd23eef --- /dev/null +++ b/PR-TODOS.md @@ -0,0 +1,9 @@ +- [x] Update shims +- [x] Release pipeline +- [x] Install script +- [ ] Check if we can improve on python runner +- [x] Documentation +- [ ] Version in install scripts (to latest) +- [ ] Uncomment release pipeline (to latest) + +- [ ] Test with PATH shims (osx / windows) diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 28109fb..9228673 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -29,7 +29,7 @@ export async function setupCi() { ui.emptyLine(); const shimsDir = path.join(os.homedir(), ".safe-chain", "shims"); - const binDir = path.join(os.homedir(), ".safe-chain", "shims"); + const binDir = path.join(os.homedir(), ".safe-chain", "bin"); // Create the shims directory if it doesn't exist if (!fs.existsSync(shimsDir)) { fs.mkdirSync(shimsDir, { recursive: true }); From 998d0c4faf5b06e9bbace361d2378dd4827fb7a8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 14:21:50 +0100 Subject: [PATCH 308/345] Update scripts --- install-scripts/install-safe-chain.ps1 | 2 +- install-scripts/install-safe-chain.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index dcffe8a..84b8e23 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -19,7 +19,7 @@ param( [switch]$includepython ) -$Version = "v0.0.5-binaries-beta" +$Version = "v0.0.6-binaries-beta" $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index fb7328d..1f85e1e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -23,7 +23,7 @@ set -e # Exit on error # Configuration -VERSION="${SAFE_CHAIN_VERSION:-v0.0.5-binaries-beta}" +VERSION="${SAFE_CHAIN_VERSION:-v0.0.6-binaries-beta}" INSTALL_DIR="${HOME}/.safe-chain/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" From b632e0acdaa67389d0ca8ed45df001dcb3a4b465 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 15:00:51 +0100 Subject: [PATCH 309/345] Fix windows shim --- install-scripts/install-safe-chain.ps1 | 2 +- install-scripts/install-safe-chain.sh | 2 +- .../path-wrappers/templates/windows-wrapper.template.cmd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 84b8e23..1bd3f0a 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -19,7 +19,7 @@ param( [switch]$includepython ) -$Version = "v0.0.6-binaries-beta" +$Version = "v0.0.7-binaries-beta" $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 1f85e1e..867f5b7 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -23,7 +23,7 @@ set -e # Exit on error # Configuration -VERSION="${SAFE_CHAIN_VERSION:-v0.0.6-binaries-beta}" +VERSION="${SAFE_CHAIN_VERSION:-v0.0.7-binaries-beta}" INSTALL_DIR="${HOME}/.safe-chain/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" diff --git a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd index d941a56..082d553 100644 --- a/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +++ b/packages/safe-chain/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd @@ -7,7 +7,7 @@ set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims" call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%" REM Check if aikido command is available with clean PATH -set "PATH=%CLEAN_PATH%" & where {{AIKIDO_COMMAND}} >nul 2>&1 +set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1 if %errorlevel%==0 ( REM Call aikido command with clean PATH set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %* From ce1a2a6ca693a22a7d937b00c1a2976fcd5e9458 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 15:17:58 +0100 Subject: [PATCH 310/345] Remove todo list --- PR-TODOS.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 PR-TODOS.md diff --git a/PR-TODOS.md b/PR-TODOS.md deleted file mode 100644 index dd23eef..0000000 --- a/PR-TODOS.md +++ /dev/null @@ -1,9 +0,0 @@ -- [x] Update shims -- [x] Release pipeline -- [x] Install script -- [ ] Check if we can improve on python runner -- [x] Documentation -- [ ] Version in install scripts (to latest) -- [ ] Uncomment release pipeline (to latest) - -- [ ] Test with PATH shims (osx / windows) From dc6f16a03455d21993de413426f3ff9af21e453d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 2 Dec 2025 15:28:59 +0100 Subject: [PATCH 311/345] PR comments --- install-scripts/install-safe-chain.ps1 | 14 +------------- install-scripts/install-safe-chain.sh | 18 +----------------- packages/safe-chain/bin/safe-chain.js | 6 ++++++ 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 1bd3f0a..bc3aa23 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -1,18 +1,6 @@ # Downloads and installs safe-chain for Windows # -# Usage examples: -# -# Default (JavaScript packages only): -# iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1" -UseBasicParsing) -# -# CI setup (JavaScript packages only): -# iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci" -# -# Include Python packages: -# iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -includepython" -# -# CI setup with Python packages: -# iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.ps1' -UseBasicParsing) } -ci -includepython" +# Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md param( [switch]$ci, diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 867f5b7..3fc5043 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -2,23 +2,7 @@ # Downloads and installs safe-chain, depending on the operating system and architecture # -# Usage examples: -# -# Default (JavaScript packages only): -# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -# -# CI setup (JavaScript packages only): -# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci -# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci -# -# Include Python packages: -# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python -# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --include-python -# -# CI setup with Python packages: -# curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python -# wget -qO- https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci --include-python +# Usage with "curl -fsSL {url} | sh" --> See README.md set -e # Exit on error diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 0a73f0e..f3b790b 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -141,6 +141,12 @@ async function getVersion() { * @param {import("../src/shell-integration/helpers.js").AikidoTool} tool */ async function executePip(tool) { + // Scanners for pip / pip3 / python / python3 use a slightly different approach: + // - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so + // - It needs to set which tool to run (pip / pip3 / python / python3) + // - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager + // - Python / python3 skips safe-chain if not being run with -m pip or -m pip3 + let args = process.argv.slice(3); setEcoSystem(tool.ecoSystem); initializePackageManager(PIP_PACKAGE_MANAGER); From a4f9f590a49bcccf3cff10271f68bb24fcc857c0 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 2 Dec 2025 08:31:47 -0800 Subject: [PATCH 312/345] Don't modify config for config related commands --- .../src/packagemanager/pip/runPipCommand.js | 23 +++ .../packagemanager/pip/runPipCommand.spec.js | 97 +++++++++++ test/e2e/pip.e2e.spec.js | 153 ++++++++++++++++++ 3 files changed, 273 insertions(+) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 23485ff..37cfe76 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -46,6 +46,9 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges * their settings with safe-chain's, leaving the original file unchanged. * + * Special handling for 'pip config' commands: PIP_CONFIG_FILE is NOT overridden to allow + * users to read/write persistent config. Only CA environment variables are set for these commands. + * * @param {string} command - The pip command to execute (e.g., 'pip3') * @param {string[]} args - Command line arguments to pass to pip * @returns {Promise<{status: number}>} Exit status of the pip command @@ -59,6 +62,12 @@ export async function runPip(command, args) { // validates correctly under both MITM'd and tunneled HTTPS. const combinedCaPath = getCombinedCaBundlePath(); + // Commands that need access to persistent config/cache/state files + // These should not have PIP_CONFIG_FILE overridden as it would prevent them from + // reading/writing to the user's actual pip configuration and cache directories + const configRelatedCommands = ['config', 'cache', 'debug', 'completion']; + const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]); + // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file) // will tell pip to use the provided CA bundle for HTTPS verification. @@ -70,6 +79,20 @@ export async function runPip(command, args) { const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); let cleanupConfigPath = null; // Track temp file for cleanup + // For config-related commands, skip PIP_CONFIG_FILE override to allow persistent config/cache access + // Only set fallback CA environment variables which don't interfere with config operations + if (isConfigRelatedCommand) { + ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`); + setFallbackCaBundleEnvironmentVariables(env, combinedCaPath); + + const result = await safeSpawn(command, args, { + stdio: "inherit", + env, + }); + + return { status: result.status }; + } + // Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order if (!env.PIP_CONFIG_FILE) { /** @type {{ global: { cert: string, proxy?: string } }} */ diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index d0df961..cf121f6 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -62,6 +62,103 @@ describe("runPipCommand environment variable handling", () => { mock.reset(); }); + it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => { + const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + + // PIP_CONFIG_FILE should NOT be set for config commands + assert.strictEqual( + capturedArgs.options.env.PIP_CONFIG_FILE, + undefined, + "PIP_CONFIG_FILE should NOT be set for pip config commands" + ); + + // But CA environment variables should still be set + assert.strictEqual( + capturedArgs.options.env.REQUESTS_CA_BUNDLE, + "/tmp/test-combined-ca.pem", + "REQUESTS_CA_BUNDLE should still be set" + ); + assert.strictEqual( + capturedArgs.options.env.SSL_CERT_FILE, + "/tmp/test-combined-ca.pem", + "SSL_CERT_FILE should still be set" + ); + assert.strictEqual( + capturedArgs.options.env.PIP_CERT, + "/tmp/test-combined-ca.pem", + "PIP_CERT should still be set" + ); + }); + + it("should NOT set PIP_CONFIG_FILE for 'pip config get' commands", async () => { + const res = await runPip("pip3", ["config", "get", "global.index-url"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + + assert.strictEqual( + capturedArgs.options.env.PIP_CONFIG_FILE, + undefined, + "PIP_CONFIG_FILE should NOT be set for pip config get" + ); + }); + + it("should NOT set PIP_CONFIG_FILE for 'pip config list' commands", async () => { + const res = await runPip("pip3", ["config", "list"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + + assert.strictEqual( + capturedArgs.options.env.PIP_CONFIG_FILE, + undefined, + "PIP_CONFIG_FILE should NOT be set for pip config list" + ); + }); + + it("should NOT set PIP_CONFIG_FILE for 'pip cache' commands", async () => { + const res = await runPip("pip3", ["cache", "dir"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + + assert.strictEqual( + capturedArgs.options.env.PIP_CONFIG_FILE, + undefined, + "PIP_CONFIG_FILE should NOT be set for pip cache commands" + ); + + // CA env vars should still be set + assert.strictEqual( + capturedArgs.options.env.SSL_CERT_FILE, + "/tmp/test-combined-ca.pem", + "SSL_CERT_FILE should still be set" + ); + }); + + it("should NOT set PIP_CONFIG_FILE for 'pip debug' commands", async () => { + const res = await runPip("pip3", ["debug"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + + assert.strictEqual( + capturedArgs.options.env.PIP_CONFIG_FILE, + undefined, + "PIP_CONFIG_FILE should NOT be set for pip debug" + ); + }); + + it("should NOT set PIP_CONFIG_FILE for 'pip completion' commands", async () => { + const res = await runPip("pip3", ["completion", "--bash"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + + assert.strictEqual( + capturedArgs.options.env.PIP_CONFIG_FILE, + undefined, + "PIP_CONFIG_FILE should NOT be set for pip completion" + ); + }); + it("should set PIP_CERT env var and create config file", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 9a1adec..fa5260c 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -336,4 +336,157 @@ describe("E2E: pip coverage", () => { `Output did not include expected text. Output was:\n${result.output}` ); }); + + it(`pip3 config set should work and persist configuration`, async () => { + const shell = await container.openShell("zsh"); + + // Set a config value + const setResult = await shell.runCommand( + "pip3 config set global.timeout 60" + ); + + assert.ok( + setResult.output.includes("Writing to"), + `pip3 config set should write config. Output was:\n${setResult.output}` + ); + + // Verify it was persisted by reading it back + const getResult = await shell.runCommand( + "pip3 config get global.timeout" + ); + + assert.ok( + getResult.output.includes("60"), + `Config value should be 60. Output was:\n${getResult.output}` + ); + }); + + it(`pip3 config list should show user configuration`, async () => { + const shell = await container.openShell("zsh"); + + // Set a value first + await shell.runCommand("pip3 config set global.timeout 90"); + + // List config + const listResult = await shell.runCommand("pip3 config list"); + + assert.ok( + listResult.output.includes("timeout") && listResult.output.includes("90"), + `Config list should show timeout=90. Output was:\n${listResult.output}` + ); + }); + + it(`pip3 config unset should remove configuration`, async () => { + const shell = await container.openShell("zsh"); + + // Set a value + await shell.runCommand("pip3 config set global.timeout 120"); + + // Verify it exists + const getResult = await shell.runCommand("pip3 config get global.timeout"); + assert.ok(getResult.output.includes("120")); + + // Unset it + const unsetResult = await shell.runCommand("pip3 config unset global.timeout"); + assert.ok( + unsetResult.output.includes("Writing to"), + `pip3 config unset should write config. Output was:\n${unsetResult.output}` + ); + }); + + it(`pip3 cache dir should return cache directory path`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand("pip3 cache dir"); + + // Should output a directory path + assert.ok( + result.output.includes("/") && result.output.includes("cache"), + `Should output a cache directory path. Output was:\n${result.output}` + ); + }); + + it(`pip3 cache info should show cache information`, async () => { + const shell = await container.openShell("zsh"); + + // Install something first to populate cache + await shell.runCommand("pip3 install --break-system-packages certifi"); + + const result = await shell.runCommand("pip3 cache info"); + + // Output should contain cache-related information + assert.ok( + result.output.match(/cache|wheel|http/i), + `Should output cache information. Output was:\n${result.output}` + ); + }); + + it(`pip3 cache list should list cached packages`, async () => { + const shell = await container.openShell("zsh"); + + // Download a package to ensure something is in cache + await shell.runCommand("pip3 download certifi"); + + const result = await shell.runCommand("pip3 cache list certifi"); + + // Should show either cached wheels or "No locally built wheels" + assert.ok( + result.output.includes("certifi") || result.output.includes("No locally built"), + `Should output cache list information. Output was:\n${result.output}` + ); + }); + + it(`pip3 debug should output debug information`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand("pip3 debug"); + + // Should contain debug information about pip environment + assert.ok( + result.output.match(/pip version|sys\.version|sys\.executable/i), + `Should output debug information. Output was:\n${result.output}` + ); + + // Should NOT show safe-chain's temporary config file in the debug output + assert.ok( + !result.output.includes("safe-chain-pip-"), + `Debug output should not reference safe-chain temp config. Output was:\n${result.output}` + ); + }); + + it(`pip3 completion should generate shell completion script`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand("pip3 completion --zsh"); + + // Should output shell completion code + assert.ok( + result.output.includes("compdef") || result.output.includes("_pip") || result.output.includes("pip completion"), + `Should output completion code. Output was:\n${result.output}` + ); + }); + + it(`pip3 install still works after config operations`, async () => { + const shell = await container.openShell("zsh"); + + // Perform config operations + await shell.runCommand("pip3 config set global.timeout 60"); + await shell.runCommand("pip3 cache dir"); + + // Now install should still work with malware protection + const result = await shell.runCommand( + "pip3 install --break-system-packages certifi" + ); + + assert.ok( + result.output.includes("Successfully installed") || + result.output.includes("Requirement already satisfied"), + `Install should succeed after config operations. Output was:\n${result.output}` + ); + + assert.ok( + result.output.includes("no malware found."), + `Should still scan for malware. Output was:\n${result.output}` + ); + }); }); From 795e7af23e1daffea58860c5470cc86d711c342f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 2 Dec 2025 08:44:43 -0800 Subject: [PATCH 313/345] Clean up comments --- .../safe-chain/src/packagemanager/pip/runPipCommand.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 37cfe76..f7050a5 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -46,7 +46,7 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges * their settings with safe-chain's, leaving the original file unchanged. * - * Special handling for 'pip config' commands: PIP_CONFIG_FILE is NOT overridden to allow + * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow * users to read/write persistent config. Only CA environment variables are set for these commands. * * @param {string} command - The pip command to execute (e.g., 'pip3') @@ -79,10 +79,12 @@ export async function runPip(command, args) { const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); let cleanupConfigPath = null; // Track temp file for cleanup - // For config-related commands, skip PIP_CONFIG_FILE override to allow persistent config/cache access - // Only set fallback CA environment variables which don't interfere with config operations if (isConfigRelatedCommand) { ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`); + + // Still set the fallback CA bundle environment variables to avoid edge cases where a + // plugin or extension triggers a network call during config introspection + // This can do no harm setFallbackCaBundleEnvironmentVariables(env, combinedCaPath); const result = await safeSpawn(command, args, { From 20e63a58be225379844495d64191c796cc592e7e Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 2 Dec 2025 09:45:04 -0800 Subject: [PATCH 314/345] Add a better e2e test to cover the issue --- test/e2e/pip.e2e.spec.js | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index fa5260c..26ebb78 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -489,4 +489,54 @@ describe("E2E: pip coverage", () => { `Should still scan for malware. Output was:\n${result.output}` ); }); + + it(`pip3 download works after configuring pip settings`, async () => { + const shell = await container.openShell("zsh"); + + // Configure pip with timeout and extra index URL + const configTimeout = await shell.runCommand("pip3 config set global.timeout 60"); + assert.ok( + configTimeout.output.includes("Writing to"), + `Config set should succeed. Output was:\n${configTimeout.output}` + ); + + const configIndex = await shell.runCommand( + "pip3 config set global.extra-index-url https://pypi.org/simple" + ); + assert.ok( + configIndex.output.includes("Writing to"), + `Config set should succeed. Output was:\n${configIndex.output}` + ); + + // Verify config persisted + const listConfig = await shell.runCommand("pip3 config list"); + assert.ok( + listConfig.output.includes("timeout") && listConfig.output.includes("60"), + `Config should show timeout=60. Output was:\n${listConfig.output}` + ); + assert.ok( + listConfig.output.includes("extra-index-url") && listConfig.output.includes("pypi.org"), + `Config should show extra-index-url. Output was:\n${listConfig.output}` + ); + + // Now download packages with the configured settings + const downloadResult = await shell.runCommand( + "pip3 download -d /tmp/packages requests certifi" + ); + + assert.ok( + downloadResult.output.includes("no malware found."), + `Should scan for malware. Output was:\n${downloadResult.output}` + ); + + // Verify downloads succeeded + assert.ok( + downloadResult.output.includes("Saved") || downloadResult.output.includes("requests"), + `Download should succeed with configured settings. Output was:\n${downloadResult.output}` + ); + assert.ok( + downloadResult.output.includes("certifi"), + `Should download certifi. Output was:\n${downloadResult.output}` + ); + }); }); From b7453c670066cb16895c1ff77d79df568a223122 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Tue, 2 Dec 2025 19:05:05 +0100 Subject: [PATCH 315/345] Add NPM version and downloads badges --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 47f0894..c1ff682 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ # Aikido Safe Chain +[![NPM Version](https://img.shields.io/npm/v/%40aikidosec%2Fsafe-chain?style=flat-square)](https://www.npmjs.com/package/@aikidosec/safe-chain) +[![NPM Downloads](https://img.shields.io/npm/dw/%40aikidosec%2Fsafe-chain?style=flat-square)](https://www.npmjs.com/package/@aikidosec/safe-chain) + - ✅ **Block malware on developer laptops and CI/CD** - ✅ **Supports npm and PyPI** more package managers coming - ✅ **Blocks packages newer than 24 hours** without breaking your build From 31a14a3f1b1e1a06627cddd18abb24f051c9c601 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 10:47:28 +0100 Subject: [PATCH 316/345] Update packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 --- .../src/shell-integration/startup-scripts/init-pwsh.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ddf5aee..0b7f5ee 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 @@ -89,4 +89,4 @@ function npm { } Invoke-WrappedCommand "npm" $args -} \ No newline at end of file +} From 4139275b764d5bafce76436850f82fc0dac5ebff Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 10:06:58 +0100 Subject: [PATCH 317/345] Handle PR comments --- build.js | 11 +- .../include-python/init-fish.fish | 62 +++++----- .../include-python/init-posix.sh | 44 ++++---- .../include-python/init-pwsh.ps1 | 106 +++++++++--------- .../startup-scripts/init-fish.fish | 62 +++++----- .../startup-scripts/init-posix.sh | 44 ++++---- .../startup-scripts/init-pwsh.ps1 | 70 ++++++------ 7 files changed, 204 insertions(+), 195 deletions(-) diff --git a/build.js b/build.js index 24230cd..235968b 100644 --- a/build.js +++ b/build.js @@ -11,12 +11,21 @@ if (!target) { process.exit(1); } -(async function () { +(async function main() { await clearOutputFolder(); + + // Esbuild creates a single safe-chain.cjs with all dependencies included await bundleSafeChain(); + + // Copy assets that need to be included in the binary + // - All shell scripts that are used to setup safe-chain + // - Certifi because it contains static root certs for Python + // - Package.json for its metadata (package name, version, ...) await copyShellScripts(); await copyCertifi(); await copyAndModifyPackageJson(); + + // Creates a single binary with safe-chain.cjs and the copied assets await buildSafeChainBinary(target); })(); diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish index 81e28ef..4c881ba 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-fish.fish @@ -1,36 +1,5 @@ set -gx PATH $PATH $HOME/.safe-chain/bin -function printSafeChainWarning - set original_cmd $argv[1] - - # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" - set_color -b yellow black - printf "Warning:" - set_color normal - printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd - - # Cyan text for the install command - printf "Install safe-chain by using " - set_color cyan - printf "npm install -g @aikidosec/safe-chain" - set_color normal - printf ".\n" -end - -function wrapSafeChainCommand - set original_cmd $argv[1] - set cmd_args $argv[2..-1] - - if type -q safe-chain - # If the safe-chain command is available, just run it with the provided arguments - safe-chain $original_cmd $cmd_args - else - # If the safe-chain command is not available, print a warning and run the original command - printSafeChainWarning $original_cmd - command $original_cmd $cmd_args - end -end - function npx wrapSafeChainCommand "npx" $argv end @@ -92,3 +61,34 @@ end function python3 wrapSafeChainCommand "python3" $argv end + +function printSafeChainWarning + set original_cmd $argv[1] + + # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" + set_color -b yellow black + printf "Warning:" + set_color normal + printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd + + # Cyan text for the install command + printf "Install safe-chain by using " + set_color cyan + printf "npm install -g @aikidosec/safe-chain" + set_color normal + printf ".\n" +end + +function wrapSafeChainCommand + set original_cmd $argv[1] + set cmd_args $argv[2..-1] + + if type -q safe-chain + # If the safe-chain command is available, just run it with the provided arguments + safe-chain $original_cmd $cmd_args + else + # If the safe-chain command is not available, print a warning and run the original command + printSafeChainWarning $original_cmd + command $original_cmd $cmd_args + end +end diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh index fd844fc..af5b18e 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-posix.sh @@ -1,27 +1,5 @@ export PATH="$PATH:$HOME/.safe-chain/bin" -function printSafeChainWarning() { - # \033[43;30m is used to set the background color to yellow and text color to black - # \033[0m is used to reset the text formatting - printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" - # \033[36m is used to set the text color to cyan - printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" -} - -function wrapSafeChainCommand() { - local original_cmd="$1" - - if command -v safe-chain > /dev/null 2>&1; then - # If the aikido command is available, just run it with the provided arguments - safe-chain "$@" - else - # If the aikido command is not available, print a warning and run the original command - printSafeChainWarning "$original_cmd" - - command "$original_cmd" "$@" - fi -} - function npx() { wrapSafeChainCommand "npx" "$@" } @@ -79,3 +57,25 @@ function python() { function python3() { wrapSafeChainCommand "python3" "$@" } + +function printSafeChainWarning() { + # \033[43;30m is used to set the background color to yellow and text color to black + # \033[0m is used to reset the text formatting + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" + # \033[36m is used to set the text color to cyan + printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" +} + +function wrapSafeChainCommand() { + local original_cmd="$1" + + if command -v safe-chain > /dev/null 2>&1; then + # If the aikido command is available, just run it with the provided arguments + safe-chain "$@" + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning "$original_cmd" + + command "$original_cmd" "$@" + fi +} diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 index 50a6d0b..2edc93b 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -3,59 +3,6 @@ $pathSeparator = if ($IsWindows) { ';' } else { ':' } $safeChainBin = Join-Path $HOME '.safe-chain' 'bin' $env:PATH = "$env:PATH$pathSeparator$safeChainBin" -function Write-SafeChainWarning { - param([string]$Command) - - # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" - Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline - Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." - - # Cyan text for the install command - Write-Host "Install safe-chain by using " -NoNewline - Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline - Write-Host "." -} - -function Test-CommandAvailable { - param([string]$Command) - - try { - Get-Command $Command -ErrorAction Stop | Out-Null - return $true - } - catch { - return $false - } -} - -function Invoke-RealCommand { - param( - [string]$Command, - [string[]]$Arguments - ) - - # Find the real executable to avoid calling our wrapped functions - $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 - if ($realCommand) { - & $realCommand.Source @Arguments - } -} - -function Invoke-WrappedCommand { - param( - [string]$OriginalCmd, - [string[]]$Arguments - ) - - if (Test-CommandAvailable "safe-chain") { - & safe-chain $OriginalCmd @Arguments - } - else { - Write-SafeChainWarning $OriginalCmd - Invoke-RealCommand $OriginalCmd $Arguments - } -} - function npx { Invoke-WrappedCommand "npx" $args } @@ -113,3 +60,56 @@ function python3 { Invoke-WrappedCommand 'python3' $args } + +function Write-SafeChainWarning { + param([string]$Command) + + # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:" + Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline + Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it." + + # Cyan text for the install command + Write-Host "Install safe-chain by using " -NoNewline + Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline + Write-Host "." +} + +function Test-CommandAvailable { + param([string]$Command) + + try { + Get-Command $Command -ErrorAction Stop | Out-Null + return $true + } + catch { + return $false + } +} + +function Invoke-RealCommand { + param( + [string]$Command, + [string[]]$Arguments + ) + + # Find the real executable to avoid calling our wrapped functions + $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1 + if ($realCommand) { + & $realCommand.Source @Arguments + } +} + +function Invoke-WrappedCommand { + param( + [string]$OriginalCmd, + [string[]]$Arguments + ) + + if (Test-CommandAvailable "safe-chain") { + & safe-chain $OriginalCmd @Arguments + } + else { + Write-SafeChainWarning $OriginalCmd + Invoke-RealCommand $OriginalCmd $Arguments + } +} 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 f697da2..b18ff96 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 @@ -1,36 +1,5 @@ set -gx PATH $PATH $HOME/.safe-chain/bin -function printSafeChainWarning - set original_cmd $argv[1] - - # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" - set_color -b yellow black - printf "Warning:" - set_color normal - printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd - - # Cyan text for the install command - printf "Install safe-chain by using " - set_color cyan - printf "npm install -g @aikidosec/safe-chain" - set_color normal - printf ".\n" -end - -function wrapSafeChainCommand - set original_cmd $argv[1] - set cmd_args $argv[2..-1] - - if type -q safe-chain - # If the safe-chain command is available, just run it with the provided arguments - safe-chain $original_cmd $cmd_args - else - # If the safe-chain command is not available, print a warning and run the original command - printSafeChainWarning $original_cmd - command $original_cmd $cmd_args - end -end - function npx wrapSafeChainCommand "npx" $argv end @@ -69,3 +38,34 @@ function npm wrapSafeChainCommand "npm" $argv end + +function printSafeChainWarning + set original_cmd $argv[1] + + # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:" + set_color -b yellow black + printf "Warning:" + set_color normal + printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd + + # Cyan text for the install command + printf "Install safe-chain by using " + set_color cyan + printf "npm install -g @aikidosec/safe-chain" + set_color normal + printf ".\n" +end + +function wrapSafeChainCommand + set original_cmd $argv[1] + set cmd_args $argv[2..-1] + + if type -q safe-chain + # If the safe-chain command is available, just run it with the provided arguments + safe-chain $original_cmd $cmd_args + else + # If the safe-chain command is not available, print a warning and run the original command + printSafeChainWarning $original_cmd + command $original_cmd $cmd_args + 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 6d426c5..5c32143 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 @@ -1,27 +1,5 @@ export PATH="$PATH:$HOME/.safe-chain/bin" -function printSafeChainWarning() { - # \033[43;30m is used to set the background color to yellow and text color to black - # \033[0m is used to reset the text formatting - printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" - # \033[36m is used to set the text color to cyan - printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" -} - -function wrapSafeChainCommand() { - local original_cmd="$1" - - if command -v safe-chain > /dev/null 2>&1; then - # If the aikido command is available, just run it with the provided arguments - safe-chain "$@" - else - # If the aikido command is not available, print a warning and run the original command - printSafeChainWarning "$original_cmd" - - command "$original_cmd" "$@" - fi -} - function npx() { wrapSafeChainCommand "npx" "$@" } @@ -56,3 +34,25 @@ function npm() { wrapSafeChainCommand "npm" "$@" } + +function printSafeChainWarning() { + # \033[43;30m is used to set the background color to yellow and text color to black + # \033[0m is used to reset the text formatting + printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1" + # \033[36m is used to set the text color to cyan + printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n" +} + +function wrapSafeChainCommand() { + local original_cmd="$1" + + if command -v safe-chain > /dev/null 2>&1; then + # If the aikido command is available, just run it with the provided arguments + safe-chain "$@" + else + # If the aikido command is not available, print a warning and run the original command + printSafeChainWarning "$original_cmd" + + command "$original_cmd" "$@" + 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 0b7f5ee..4f58406 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 @@ -3,6 +3,41 @@ $pathSeparator = if ($IsWindows) { ';' } else { ':' } $safeChainBin = Join-Path $HOME '.safe-chain' 'bin' $env:PATH = "$env:PATH$pathSeparator$safeChainBin" +function npx { + Invoke-WrappedCommand "npx" $args +} + +function yarn { + Invoke-WrappedCommand "yarn" $args +} + +function pnpm { + Invoke-WrappedCommand "pnpm" $args +} + +function pnpx { + Invoke-WrappedCommand "pnpx" $args +} + +function bun { + Invoke-WrappedCommand "bun" $args +} + +function bunx { + Invoke-WrappedCommand "bunx" $args +} + +function npm { + # If args is just -v or --version and nothing else, just run the npm version command + # This is because nvm uses this to check the version of npm + if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) { + Invoke-RealCommand "npm" $args + return + } + + Invoke-WrappedCommand "npm" $args +} + function Write-SafeChainWarning { param([string]$Command) @@ -55,38 +90,3 @@ function Invoke-WrappedCommand { Invoke-RealCommand $OriginalCmd $Arguments } } - -function npx { - Invoke-WrappedCommand "npx" $args -} - -function yarn { - Invoke-WrappedCommand "yarn" $args -} - -function pnpm { - Invoke-WrappedCommand "pnpm" $args -} - -function pnpx { - Invoke-WrappedCommand "pnpx" $args -} - -function bun { - Invoke-WrappedCommand "bun" $args -} - -function bunx { - Invoke-WrappedCommand "bunx" $args -} - -function npm { - # If args is just -v or --version and nothing else, just run the npm version command - # This is because nvm uses this to check the version of npm - if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) { - Invoke-RealCommand "npm" $args - return - } - - Invoke-WrappedCommand "npm" $args -} From c0076091c26f6a823cb811b9d946aac944385588 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 11:10:47 +0100 Subject: [PATCH 318/345] Update packages/safe-chain/bin/safe-chain.js --- packages/safe-chain/bin/safe-chain.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index f3b790b..2bd47e1 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -134,7 +134,7 @@ async function getVersion() { return json.version; } - return "1.0.0"; + return "0.0.0"; } /** From b366466e1181ef98165f761cba19c9f8eeac9440 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 11:26:11 +0100 Subject: [PATCH 319/345] Modify install scripts --- install-scripts/install-safe-chain.ps1 | 17 ++++++++++++----- install-scripts/install-safe-chain.sh | 23 +++++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index bc3aa23..3400bfc 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -92,12 +92,19 @@ function Remove-VoltaInstallation { # Main installation function Install-SafeChain { - Write-Info "Installing safe-chain $Version..." + # Build installation message + $installMsg = "Installing safe-chain $Version" + if ($includepython) { + $installMsg += " with python" + } + if ($ci) { + $installMsg += " in ci" + } - # Check for existing npm installation + Write-Info $installMsg + + # Check for existing safe-chain installation through npm or volta Remove-NpmInstallation - - # Check for existing Volta installation Remove-VoltaInstallation # Detect platform @@ -120,7 +127,6 @@ function Install-SafeChain { # Download binary $downloadUrl = "$RepoUrl/releases/download/$Version/$binaryName" $tempFile = Join-Path $InstallDir $binaryName - $finalFile = Join-Path $InstallDir "safe-chain.exe" Write-Info "Downloading from: $downloadUrl" @@ -135,6 +141,7 @@ function Install-SafeChain { } # Rename to final location + $finalFile = Join-Path $InstallDir "safe-chain.exe" try { Move-Item -Path $tempFile -Destination $finalFile -Force } diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 3fc5043..9b34e0c 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -69,7 +69,7 @@ download() { } # Check and uninstall npm global package if present -check_npm_installation() { +remove_npm_installation() { if ! command_exists npm; then return fi @@ -89,7 +89,7 @@ check_npm_installation() { } # Check and uninstall Volta-managed package if present -check_volta_installation() { +remove_volta_installation() { if ! command_exists volta; then return fi @@ -135,13 +135,20 @@ main() { # Parse command-line arguments parse_arguments "$@" - info "Installing safe-chain ${VERSION}..." + # Build installation message + INSTALL_MSG="Installing safe-chain ${VERSION}" + if [ "$INCLUDE_PYTHON" = "true" ]; then + INSTALL_MSG="${INSTALL_MSG} with python" + fi + if [ "$USE_CI_SETUP" = "true" ]; then + INSTALL_MSG="${INSTALL_MSG} in ci" + fi - # Check for existing npm installation - check_npm_installation + info "$INSTALL_MSG" - # Check for existing Volta installation - check_volta_installation + # Check for existing safe-chain installation through npm or volta + remove_npm_installation + remove_volta_installation # Detect platform OS=$(detect_os) @@ -159,12 +166,12 @@ main() { # Download binary DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}/${BINARY_NAME}" TEMP_FILE="${INSTALL_DIR}/${BINARY_NAME}" - FINAL_FILE="${INSTALL_DIR}/safe-chain" info "Downloading from: $DOWNLOAD_URL" download "$DOWNLOAD_URL" "$TEMP_FILE" # Rename and make executable + FINAL_FILE="${INSTALL_DIR}/safe-chain" mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE" chmod +x "$FINAL_FILE" || error "Failed to make binary executable" From aa441e74831dbd2575654c482f999db41438d499 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 11:31:35 +0100 Subject: [PATCH 320/345] Add comments for esm vs cjs __dirname implementation --- packages/safe-chain/bin/safe-chain.js | 6 +++++- packages/safe-chain/src/shell-integration/setup-ci.js | 6 +++++- packages/safe-chain/src/shell-integration/setup.js | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 2bd47e1..7a1d6ab 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -20,8 +20,12 @@ import { } from "../src/packagemanager/pip/pipSettings.js"; /** @type {string} */ +// This checks the current file's dirname in a way that's compatible with: +// - Modulejs (import.meta.url) +// - ES modules (__dirname) +// This is needed because safe-chain's npm package is built using ES modules, +// but building the binaries requires commonjs. let dirname; - if (import.meta.url) { const filename = fileURLToPath(import.meta.url); dirname = path.dirname(filename); diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 9228673..bc5c5e6 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -9,8 +9,12 @@ import { includePython } from "../config/cliArguments.js"; import { ECOSYSTEM_PY } from "../config/settings.js"; /** @type {string} */ +// This checks the current file's dirname in a way that's compatible with: +// - Modulejs (import.meta.url) +// - ES modules (__dirname) +// This is needed because safe-chain's npm package is built using ES modules, +// but building the binaries requires commonjs. let dirname; - if (import.meta.url) { const filename = fileURLToPath(import.meta.url); dirname = path.dirname(filename); diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index 45a1fb8..d5c4be9 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -9,8 +9,12 @@ import { includePython } from "../config/cliArguments.js"; import { fileURLToPath } from "url"; /** @type {string} */ +// This checks the current file's dirname in a way that's compatible with: +// - Modulejs (import.meta.url) +// - ES modules (__dirname) +// This is needed because safe-chain's npm package is built using ES modules, +// but building the binaries requires commonjs. let dirname; - if (import.meta.url) { const filename = fileURLToPath(import.meta.url); dirname = path.dirname(filename); From 0fd54b159b61787797e89dd8fdf1e977786f69f4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 11:38:27 +0100 Subject: [PATCH 321/345] Lock down @yao-pkg/pkg dependency --- build.js | 12 +- package-lock.json | 1435 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 3 files changed, 1441 insertions(+), 9 deletions(-) diff --git a/build.js b/build.js index 235968b..7ebc3c4 100644 --- a/build.js +++ b/build.js @@ -103,14 +103,10 @@ async function copyAndModifyPackageJson() { function buildSafeChainBinary(target) { return new Promise((resolve, reject) => { - const pkg = spawn( - "npx", - ["@yao-pkg/pkg", "./build/package.json", "-t", target], - { - stdio: "inherit", - shell: true, - } - ); + const pkg = spawn("./node_modules/.bin/pkg", ["./build/package.json", "-t", target], { + stdio: "inherit", + shell: true, + }); pkg.on("close", (code) => { if (code !== 0) { diff --git a/package-lock.json b/package-lock.json index 16f42c7..3fb6a1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "test/e2e" ], "devDependencies": { + "@yao-pkg/pkg": "6.10.1", "esbuild": "^0.27.0", "oxlint": "^1.22.0" } @@ -24,6 +25,73 @@ "resolved": "test/e2e", "link": true }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", @@ -487,6 +555,58 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@npmcli/agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", @@ -723,6 +843,82 @@ "@types/node": "*" } }, + "node_modules/@yao-pkg/pkg": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.10.1.tgz", + "integrity": "sha512-M/eqDg0Iir2nmyZ06Q9ospIPv1Yk7K1du5iLiaYrfMogQcI6bqf82A026MVYngyLH8jZsquZvjNAbvgbW4Uwkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "@yao-pkg/pkg-fetch": "3.5.30", + "into-stream": "^6.0.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "picocolors": "^1.1.0", + "picomatch": "^4.0.2", + "prebuild-install": "^7.1.1", + "resolve": "^1.22.10", + "stream-meter": "^1.0.4", + "tar": "^7.4.3", + "tinyglobby": "^0.2.11", + "unzipper": "^0.12.3" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@yao-pkg/pkg-fetch": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.30.tgz", + "integrity": "sha512-OrXQlsR3vE/IvwXSk8R5ETYbcxAFtUPmLkeepbG+ArN82TvlIwcUJ65tEWxLG3Tl89VRbmOupuhkXfmuaO05+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "picocolors": "^1.1.0", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^3.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/@yao-pkg/pkg-fetch/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@yao-pkg/pkg-fetch/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -732,12 +928,230 @@ "node": ">= 14" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", + "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "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/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -793,6 +1207,48 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -805,6 +1261,13 @@ "node": ">= 0.8" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -822,6 +1285,32 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -831,6 +1320,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -845,6 +1344,23 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -855,6 +1371,16 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -948,6 +1474,61 @@ "@esbuild/win32-x64": "0.27.0" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -964,6 +1545,39 @@ "node": ">= 6" } }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -985,6 +1599,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1022,6 +1646,13 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", @@ -1051,6 +1682,13 @@ "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==", + "dev": true, + "license": "ISC" + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1147,6 +1785,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "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": "BSD-3-Clause" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1156,6 +1815,13 @@ "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==", + "dev": true, + "license": "ISC" + }, "node_modules/ini": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", @@ -1165,6 +1831,23 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -1174,6 +1857,65 @@ "node": ">= 12" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -1244,6 +1986,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -1259,6 +2014,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -1381,18 +2146,72 @@ "node": ">= 18" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "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", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/nan": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1402,6 +2221,40 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", @@ -1411,6 +2264,13 @@ "node": ">= 6.13.0" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-pty": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", @@ -1455,6 +2315,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oxlint": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.22.0.tgz", @@ -1489,6 +2359,16 @@ } } }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -1501,6 +2381,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", @@ -1517,6 +2404,105 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -1526,6 +2512,23 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -1539,6 +2542,87 @@ "node": ">=10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -1548,6 +2632,13 @@ "node": ">= 4" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1567,6 +2658,53 @@ "node": ">=10" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -1617,6 +2755,190 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1660,6 +2982,37 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-name": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.0.tgz", @@ -1669,12 +3022,94 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-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==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "packages/safe-chain": { "name": "@aikidosec/safe-chain", "version": "1.0.0", diff --git a/package.json b/package.json index 8428fe4..2793f9c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "AGPL-3.0-or-later", "devDependencies": { "oxlint": "^1.22.0", - "esbuild": "^0.27.0" + "esbuild": "^0.27.0", + "@yao-pkg/pkg": "6.10.1" } } From a578ee72137e8049ed377034b1616aba8756d0e6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 11:42:22 +0100 Subject: [PATCH 322/345] Fix windows build --- build.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/build.js b/build.js index 7ebc3c4..4711ce6 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,7 @@ import { build } from "esbuild"; import { mkdir, cp, rm, readFile, writeFile } from "node:fs/promises"; import { spawn } from "node:child_process"; +import { resolve } from "node:path"; const target = process.argv[2]; if (!target) { @@ -102,8 +103,13 @@ async function copyAndModifyPackageJson() { } function buildSafeChainBinary(target) { - return new Promise((resolve, reject) => { - const pkg = spawn("./node_modules/.bin/pkg", ["./build/package.json", "-t", target], { + return new Promise((promiseResolve, reject) => { + // Use .cmd on Windows, resolve to absolute path for cross-platform compatibility + const pkgBin = process.platform === "win32" + ? resolve("node_modules/.bin/pkg.cmd") + : resolve("node_modules/.bin/pkg"); + + const pkg = spawn(pkgBin, ["./build/package.json", "-t", target], { stdio: "inherit", shell: true, }); @@ -112,7 +118,7 @@ function buildSafeChainBinary(target) { if (code !== 0) { reject(new Error(`pkg process exited with code ${code}`)); } else { - resolve(); + promiseResolve(); } }); }); From ac6567ba598e9f12dc37fcbf5931b277c595e2e6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 11:58:33 +0100 Subject: [PATCH 323/345] Make scripts release-proof again --- .github/workflows/build-and-release.yml | 8 ++++---- install-scripts/install-safe-chain.ps1 | 26 +++++++++++++++++++++++- install-scripts/install-safe-chain.sh | 27 ++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 4464c02..95a6c91 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -63,10 +63,10 @@ jobs: cp LICENSE packages/safe-chain/ cp -r docs packages/safe-chain/ - # - name: Publish to npm - # run: | - # echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" - # npm publish --workspace=packages/safe-chain --access public --provenance + - name: Publish to npm + run: | + echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" + npm publish --workspace=packages/safe-chain --access public --provenance - name: Download all binary artifacts uses: actions/download-artifact@v4 diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 3400bfc..230bb11 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -7,7 +7,7 @@ param( [switch]$includepython ) -$Version = "v0.0.7-binaries-beta" +$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set $InstallDir = Join-Path $env:USERPROFILE ".safe-chain\bin" $RepoUrl = "https://github.com/AikidoSec/safe-chain" @@ -31,6 +31,25 @@ function Write-Error-Custom { exit 1 } +# Fetch latest release version tag from GitHub +function Get-LatestVersion { + Write-Info "Fetching latest release version..." + + try { + $response = Invoke-RestMethod -Uri "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" -UseBasicParsing + $latestVersion = $response.tag_name + + if ([string]::IsNullOrWhiteSpace($latestVersion)) { + Write-Error-Custom "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable." + } + + return $latestVersion + } + catch { + Write-Error-Custom "Failed to fetch latest version from GitHub API: $($_.Exception.Message). Please set SAFE_CHAIN_VERSION environment variable." + } +} + # Detect architecture function Get-Architecture { $arch = $env:PROCESSOR_ARCHITECTURE @@ -92,6 +111,11 @@ function Remove-VoltaInstallation { # Main installation function Install-SafeChain { + # Fetch latest version if VERSION is not set + if ([string]::IsNullOrWhiteSpace($script:Version)) { + $script:Version = Get-LatestVersion + } + # Build installation message $installMsg = "Installing safe-chain $Version" if ($includepython) { diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 9b34e0c..0fbbf34 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -7,7 +7,7 @@ set -e # Exit on error # Configuration -VERSION="${SAFE_CHAIN_VERSION:-v0.0.7-binaries-beta}" +VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set INSTALL_DIR="${HOME}/.safe-chain/bin" REPO_URL="https://github.com/AikidoSec/safe-chain" @@ -54,6 +54,26 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +# Fetch latest release version tag from GitHub +fetch_latest_version() { + info "Fetching latest release version..." + + # Try using GitHub API to get the latest release tag + if command_exists curl; then + latest_version=$(curl -fsSL "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + elif command_exists wget; then + latest_version=$(wget -qO- "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + else + error "Neither curl nor wget found. Please install one of them or set SAFE_CHAIN_VERSION environment variable." + fi + + if [ -z "$latest_version" ]; then + error "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable." + fi + + echo "$latest_version" +} + # Download file download() { url="$1" @@ -135,6 +155,11 @@ main() { # Parse command-line arguments parse_arguments "$@" + # Fetch latest version if VERSION is not set + if [ -z "$VERSION" ]; then + VERSION=$(fetch_latest_version) + fi + # Build installation message INSTALL_MSG="Installing safe-chain ${VERSION}" if [ "$INCLUDE_PYTHON" = "true" ]; then From 019d70cc5228efbcb7896b114a613bde82b5a690 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 12:02:19 +0100 Subject: [PATCH 324/345] Fix install scripts --- install-scripts/install-safe-chain.ps1 | 3 +-- install-scripts/install-safe-chain.sh | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 230bb11..c7a6df5 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -33,8 +33,6 @@ function Write-Error-Custom { # Fetch latest release version tag from GitHub function Get-LatestVersion { - Write-Info "Fetching latest release version..." - try { $response = Invoke-RestMethod -Uri "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" -UseBasicParsing $latestVersion = $response.tag_name @@ -113,6 +111,7 @@ function Remove-VoltaInstallation { function Install-SafeChain { # Fetch latest version if VERSION is not set if ([string]::IsNullOrWhiteSpace($script:Version)) { + Write-Info "Fetching latest release version..." $script:Version = Get-LatestVersion } diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 0fbbf34..2afb583 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -56,8 +56,6 @@ command_exists() { # Fetch latest release version tag from GitHub fetch_latest_version() { - info "Fetching latest release version..." - # Try using GitHub API to get the latest release tag if command_exists curl; then latest_version=$(curl -fsSL "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') @@ -157,6 +155,7 @@ main() { # Fetch latest version if VERSION is not set if [ -z "$VERSION" ]; then + info "Fetching latest release version..." VERSION=$(fetch_latest_version) fi From 2085aad0054a838f332b5cc59e9b8401520d6a4c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 13:24:04 +0100 Subject: [PATCH 325/345] Improve logs for MITM handler --- .../src/registryProxy/mitmRequestHandler.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index bfc6c3e..cf2af5b 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -117,14 +117,16 @@ function forwardRequest(req, hostname, res, requestHandler) { proxyReq.on("error", (err) => { ui.writeVerbose( - `Safe-chain: Error occurred while proxying request: ${err.message}` + `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}` ); res.writeHead(502); res.end("Bad Gateway"); }); req.on("error", (err) => { - ui.writeError(`Safe-chain: Error reading client request: ${err.message}`); + ui.writeError( + `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}` + ); proxyReq.destroy(); }); @@ -175,7 +177,7 @@ function createProxyRequest(hostname, req, res, requestHandler) { const proxyReq = https.request(options, (proxyRes) => { proxyRes.on("error", (err) => { ui.writeError( - `Safe-chain: Error reading upstream response: ${err.message}` + `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}` ); if (!res.headersSent) { res.writeHead(502); @@ -184,7 +186,9 @@ function createProxyRequest(hostname, req, res, requestHandler) { }); if (!proxyRes.statusCode) { - ui.writeError("Safe-chain: Proxy response missing status code"); + ui.writeError( + `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}` + ); res.writeHead(500); res.end("Internal Server Error"); return; From aba771e35594f8be170505fef2c1193804ab0bf9 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 14:14:55 +0100 Subject: [PATCH 326/345] add --compress GZip option to build --- build.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.js b/build.js index 4711ce6..870d9f8 100644 --- a/build.js +++ b/build.js @@ -109,7 +109,11 @@ function buildSafeChainBinary(target) { ? resolve("node_modules/.bin/pkg.cmd") : resolve("node_modules/.bin/pkg"); - const pkg = spawn(pkgBin, ["./build/package.json", "-t", target], { + let pkgArgs = ["./build/package.json", "-t", "target"]; + + pkgArgs += ["--compress", "GZip"]; + + const pkg = spawn(pkgBin, pkgArgs, { stdio: "inherit", shell: true, }); From 9bf88dfd142c919377e4fcc953c203305666cb32 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 14:17:07 +0100 Subject: [PATCH 327/345] .gitignore: add .idea folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7c44b34..920883f 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,6 @@ Claude.md # Build files build/ dist/ + +# Jetbrains IDEs +.idea/** From bdddf8f37e3506a0492875be78b5ce2366236cf2 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 15:27:12 +0100 Subject: [PATCH 328/345] Fix scoping in powershell script --- install-scripts/install-safe-chain.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index c7a6df5..081d232 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -110,9 +110,9 @@ function Remove-VoltaInstallation { # Main installation function Install-SafeChain { # Fetch latest version if VERSION is not set - if ([string]::IsNullOrWhiteSpace($script:Version)) { + if ([string]::IsNullOrWhiteSpace($Version)) { Write-Info "Fetching latest release version..." - $script:Version = Get-LatestVersion + $Version = Get-LatestVersion } # Build installation message @@ -166,6 +166,10 @@ function Install-SafeChain { # Rename to final location $finalFile = Join-Path $InstallDir "safe-chain.exe" try { + # Remove existing file if present (Move-Item -Force doesn't overwrite) + if (Test-Path $finalFile) { + Remove-Item -Path $finalFile -Force + } Move-Item -Path $tempFile -Destination $finalFile -Force } catch { From 9da3411cc1db1ba2b5a2d22be02b5acd74c76c9a Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 15:32:51 +0100 Subject: [PATCH 329/345] Add decent logging to build script --- build.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.js b/build.js index 4711ce6..ee3cbe3 100644 --- a/build.js +++ b/build.js @@ -13,10 +13,13 @@ if (!target) { } (async function main() { + const startBuildTime = performance.now(); await clearOutputFolder(); + console.log("- Cleared output folder ✅") // Esbuild creates a single safe-chain.cjs with all dependencies included await bundleSafeChain(); + console.log("- Bundled safe-chain into safe-chain.cjs (es-build) ✅") // Copy assets that need to be included in the binary // - All shell scripts that are used to setup safe-chain @@ -25,9 +28,15 @@ if (!target) { await copyShellScripts(); await copyCertifi(); await copyAndModifyPackageJson(); + console.log("- Copied auxiliary resources (shell, package.json,...) ✅") // Creates a single binary with safe-chain.cjs and the copied assets await buildSafeChainBinary(target); + console.log(`- Built safe-chain binary for ${target} (pkg) ✅`) + + + const endBuildTime = performance.now(); + console.log(`🏁 Finished build in ${((endBuildTime - startBuildTime)/1000).toFixed(2)}s`); })(); async function clearOutputFolder() { From 267a5ab423b34dcbacd38b81e9f6a18d7838b1cd Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 15:33:16 +0100 Subject: [PATCH 330/345] add spacing where necessry in build.js --- build.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build.js b/build.js index ee3cbe3..bd046c3 100644 --- a/build.js +++ b/build.js @@ -14,6 +14,7 @@ if (!target) { (async function main() { const startBuildTime = performance.now(); + await clearOutputFolder(); console.log("- Cleared output folder ✅") From b64d84c252454fe82675bd0b9923f890ed57d3b5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 3 Dec 2025 15:54:03 +0100 Subject: [PATCH 331/345] Hard-code links and remove outdated information from readme --- README.md | 10 ++++------ docs/npm-to-binary-migration.md | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4b001e4..6cbb445 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Aikido Safe Chain](./docs/banner.svg) +![Aikido Safe Chain](https://raw.githubusercontent.com/AikidoSec/safe-chain/main/docs/banner.svg) # Aikido Safe Chain @@ -10,7 +10,7 @@ - ✅ **Blocks packages newer than 24 hours** without breaking your build - ✅ **Tokenless, free, no build data shared** -Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers: +Aikido Safe Chain supports the following package managers: - 📦 **npm** - 📦 **npx** @@ -29,7 +29,7 @@ Aikido Safe Chain works on Node.js version 16 and above and supports the followi Installing the Aikido Safe Chain is easy with our one-line installer. -> ⚠️ **Already installed via npm?** See the [migration guide](docs/npm-to-binary-migration.md) to switch to the binary version. +> ⚠️ **Already installed via npm?** See the [migration guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/npm-to-binary-migration.md) to switch to the binary version. ### Unix/Linux/macOS @@ -111,7 +111,7 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc - ✅ **PowerShell** - ✅ **PowerShell Core** -More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md). +More information about the shell integration can be found in the [shell integration documentation](https://github.com/AikidoSec/safe-chain/blob/main/docs/shell-integration.md). ## Uninstallation @@ -182,8 +182,6 @@ You can set the minimum package age through multiple sources (in order of priori You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. -For optimal protection in CI/CD environments, we recommend using **npm >= 10.4.0** as it provides full dependency tree scanning. Other package managers currently offer limited scanning of install command arguments only. - ## Installation for CI/CD Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases. diff --git a/docs/npm-to-binary-migration.md b/docs/npm-to-binary-migration.md index c0b8f9a..c29a044 100644 --- a/docs/npm-to-binary-migration.md +++ b/docs/npm-to-binary-migration.md @@ -20,7 +20,7 @@ Depending on the version manager you're using, the uninstall process differs: npm uninstall -g @aikidosec/safe-chain ``` -4. **Install the binary version** (see [Installation](../README.md#installation)) +4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation)) ### nvm (Node Version Manager) @@ -51,7 +51,7 @@ Depending on the version manager you're using, the uninstall process differs: Repeat for each Node version where safe-chain was installed. -4. **Install the binary version** (see [Installation](../README.md#installation)) +4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation)) ### Volta @@ -69,7 +69,7 @@ Depending on the version manager you're using, the uninstall process differs: volta uninstall @aikidosec/safe-chain ``` -4. **Install the binary version** (see [Installation](../README.md#installation)) +4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation)) ## Troubleshooting From 0a4c6ed5db937a93310980dd7b9f850dabaa5f29 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 16:10:44 +0100 Subject: [PATCH 332/345] fi pkgArgs build --- build.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.js b/build.js index 2531fb6..91a5215 100644 --- a/build.js +++ b/build.js @@ -119,10 +119,12 @@ function buildSafeChainBinary(target) { ? resolve("node_modules/.bin/pkg.cmd") : resolve("node_modules/.bin/pkg"); - let pkgArgs = ["./build/package.json", "-t", "target"]; + let pkgArgs = []; - pkgArgs += ["--compress", "GZip"]; + // using gzip compression to lower binary size (original is 50MB) + pkgArgs = pkgArgs.concat(["--compress", "GZip"]); + pkgArgs = pkgArgs.concat(["./build/package.json", "-t", target]); const pkg = spawn(pkgBin, pkgArgs, { stdio: "inherit", shell: true, From 7abbd4aee9c92b147bcce350e4b5173bad48928a Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 16:18:24 +0100 Subject: [PATCH 333/345] report total size at the end --- build.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.js b/build.js index 91a5215..0f3621d 100644 --- a/build.js +++ b/build.js @@ -1,5 +1,5 @@ import { build } from "esbuild"; -import { mkdir, cp, rm, readFile, writeFile } from "node:fs/promises"; +import { mkdir, cp, rm, readFile, writeFile, stat } from "node:fs/promises"; import { spawn } from "node:child_process"; import { resolve } from "node:path"; @@ -36,8 +36,9 @@ if (!target) { console.log(`- Built safe-chain binary for ${target} (pkg) ✅`) - const endBuildTime = performance.now(); - console.log(`🏁 Finished build in ${((endBuildTime - startBuildTime)/1000).toFixed(2)}s`); + const totalBuildTime = (performance.now() - startBuildTime)/1000; + const totalSizeInMb = (await stat("./dist/safe-chain")).size / (1024*1024); + console.log(`🏁 Finished build in ${totalBuildTime.toFixed(2)}s, total build size: ${totalSizeInMb.toFixed(2)}MB`); })(); async function clearOutputFolder() { From 3a1d9c25af293950e8d2012b63665bd333d9da37 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 16:25:42 +0100 Subject: [PATCH 334/345] rm --compress for now --- build.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.js b/build.js index 0f3621d..9650b41 100644 --- a/build.js +++ b/build.js @@ -122,9 +122,6 @@ function buildSafeChainBinary(target) { let pkgArgs = []; - // using gzip compression to lower binary size (original is 50MB) - pkgArgs = pkgArgs.concat(["--compress", "GZip"]); - pkgArgs = pkgArgs.concat(["./build/package.json", "-t", target]); const pkg = spawn(pkgBin, pkgArgs, { stdio: "inherit", From 6fa648d6cabf2f82e23de83abf8e48037dfa28a0 Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 16:27:25 +0100 Subject: [PATCH 335/345] make compat with windows: sze reporting --- build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.js b/build.js index 9650b41..43d1ffb 100644 --- a/build.js +++ b/build.js @@ -37,7 +37,7 @@ if (!target) { const totalBuildTime = (performance.now() - startBuildTime)/1000; - const totalSizeInMb = (await stat("./dist/safe-chain")).size / (1024*1024); + const totalSizeInMb = (await stat("./dist/safe-chain" + (process.platform === "win32" ? ".bin" : ""))).size / (1024*1024); console.log(`🏁 Finished build in ${totalBuildTime.toFixed(2)}s, total build size: ${totalSizeInMb.toFixed(2)}MB`); })(); From 75f87678198803addd3482113c1e3d5c600de29c Mon Sep 17 00:00:00 2001 From: bitterpanda Date: Wed, 3 Dec 2025 16:30:19 +0100 Subject: [PATCH 336/345] needs to be safe-chain.exe instead of safe-chain.cmd for size --- build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.js b/build.js index 43d1ffb..81619f4 100644 --- a/build.js +++ b/build.js @@ -37,7 +37,7 @@ if (!target) { const totalBuildTime = (performance.now() - startBuildTime)/1000; - const totalSizeInMb = (await stat("./dist/safe-chain" + (process.platform === "win32" ? ".bin" : ""))).size / (1024*1024); + const totalSizeInMb = (await stat("./dist/safe-chain" + (process.platform === "win32" ? ".exe" : ""))).size / (1024*1024); console.log(`🏁 Finished build in ${totalBuildTime.toFixed(2)}s, total build size: ${totalSizeInMb.toFixed(2)}MB`); })(); From aadd083b9e2a81fdc9b4f84050d65007efdf4b51 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 4 Dec 2025 11:35:32 +0100 Subject: [PATCH 337/345] Fix Join-Path error for Windows Powershell --- .../startup-scripts/include-python/init-pwsh.ps1 | 2 +- .../src/shell-integration/startup-scripts/init-pwsh.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 index 2edc93b..c8d3310 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 @@ -1,6 +1,6 @@ # Use cross-platform path separator (: on Unix, ; on Windows) $pathSeparator = if ($IsWindows) { ';' } else { ':' } -$safeChainBin = Join-Path $HOME '.safe-chain' 'bin' +$safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin' $env:PATH = "$env:PATH$pathSeparator$safeChainBin" function npx { 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 4f58406..78228a0 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 @@ -1,6 +1,6 @@ # Use cross-platform path separator (: on Unix, ; on Windows) $pathSeparator = if ($IsWindows) { ';' } else { ':' } -$safeChainBin = Join-Path $HOME '.safe-chain' 'bin' +$safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin' $env:PATH = "$env:PATH$pathSeparator$safeChainBin" function npx { From 47ea989bbd3a3e38838e2a17deda2d29c5fb2fb4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 4 Dec 2025 15:20:47 +0100 Subject: [PATCH 338/345] Reduce connect timeout for tunnel for known instance metadata hosts --- .../src/registryProxy/tunnelRequestHandler.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 4b756d7..4a08d4b 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -1,6 +1,9 @@ import * as net from "net"; import { ui } from "../environment/userInteraction.js"; +/** @type {string[]} */ +let timedoutEndpoints = []; + /** * @param {import("http").IncomingMessage} req * @param {import("http").ServerResponse} clientSocket @@ -38,6 +41,14 @@ export function tunnelRequest(req, clientSocket, head) { function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); + if (timedoutEndpoints.includes(hostname)) { + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + ui.writeError( + `Safe-chain: Closing connection because previously timedout connect to ${hostname}` + ); + return; + } + const serverSocket = net.connect( Number.parseInt(port) || 443, hostname, @@ -49,6 +60,16 @@ function tunnelRequestToDestination(req, clientSocket, head) { } ); + const connectTimeout = getConnectTimeout(hostname); + serverSocket.setTimeout(connectTimeout); + serverSocket.on("timeout", () => { + timedoutEndpoints.push(hostname); + ui.writeError( + `Safe-chain: connect to ${hostname}:${port} timed out after ${connectTimeout}ms` + ); + clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); + }); + clientSocket.on("error", () => { // This can happen if the client TCP socket sends RST instead of FIN. // Not subscribing to 'error' event will cause node to throw and crash. @@ -145,3 +166,16 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { } }); } + +const imdsEndpoints = [ + "metadata.google.internal", + "metadata.goog", + "169.254.169.254", + "192.0.2.1", +]; +function getConnectTimeout(/** @type {string} */ host) { + if (imdsEndpoints.includes(host)) { + return 3000; + } + return 30000; +} From a9ebec14f68be7420aeeeace3efe9c2e1e9326c5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 4 Dec 2025 15:21:47 +0100 Subject: [PATCH 339/345] Remove 192.0.2.1 --- packages/safe-chain/src/registryProxy/tunnelRequestHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 4a08d4b..96717ba 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -171,7 +171,6 @@ const imdsEndpoints = [ "metadata.google.internal", "metadata.goog", "169.254.169.254", - "192.0.2.1", ]; function getConnectTimeout(/** @type {string} */ host) { if (imdsEndpoints.includes(host)) { From 10a3b63a5f254f7c59a92aadd7a472e1bb6eb794 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 4 Dec 2025 15:54:26 +0100 Subject: [PATCH 340/345] Add --tag to npm publish --- .github/workflows/build-and-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 95a6c91..082ece0 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -66,7 +66,7 @@ jobs: - name: Publish to npm run: | echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance + npm publish --workspace=packages/safe-chain --access public --provenance --tag ${{ steps.get_version.outputs.tag }} - name: Download all binary artifacts uses: actions/download-artifact@v4 From 6d449d63c846f06ab9087f8f7e5a62e28bd52e71 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 4 Dec 2025 16:06:48 +0100 Subject: [PATCH 341/345] Fix version number when publishing to npmjs --- .github/workflows/build-and-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 082ece0..7aa1250 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -65,8 +65,8 @@ jobs: - name: Publish to npm run: | - echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance --tag ${{ steps.get_version.outputs.tag }} + echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" + npm publish --workspace=packages/safe-chain --access public --provenance --tag ${{ needs.set-version.outputs.version }} - name: Download all binary artifacts uses: actions/download-artifact@v4 From 22b93e91f6196c7affe55843ac65deb604e595a8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 4 Dec 2025 16:16:31 +0100 Subject: [PATCH 342/345] Use "beta" as tag --- .github/workflows/build-and-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 7aa1250..c956386 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -66,7 +66,7 @@ jobs: - name: Publish to npm run: | echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance --tag ${{ needs.set-version.outputs.version }} + npm publish --workspace=packages/safe-chain --access public --provenance --tag beta - name: Download all binary artifacts uses: actions/download-artifact@v4 From e211f531c526b4c15f5581001637c73913969c61 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Thu, 4 Dec 2025 12:37:59 -0800 Subject: [PATCH 343/345] Refactor PyPI logic and cleanup --- packages/safe-chain/bin/aikido-pip.js | 7 +- packages/safe-chain/bin/aikido-pip3.js | 8 +- packages/safe-chain/bin/aikido-python.js | 21 +- packages/safe-chain/bin/aikido-python3.js | 21 +- packages/safe-chain/bin/safe-chain.js | 64 +--- .../packagemanager/currentPackageManager.js | 5 +- .../pip/createPackageManager.js | 14 +- .../src/packagemanager/pip/pipSettings.js | 32 +- .../src/packagemanager/pip/runPipCommand.js | 38 ++- test/e2e/pip.e2e.spec.js | 274 ++++++++++++++++++ test/e2e/safe-chain-cli-python.e2e.spec.js | 104 +++++++ 11 files changed, 450 insertions(+), 138 deletions(-) create mode 100644 test/e2e/safe-chain-cli-python.e2e.spec.js diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 006e661..6eb3e4e 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -3,15 +3,12 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; -import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; +import { PIP_PACKAGE_MANAGER, PIP_COMMAND } from "../src/packagemanager/pip/pipSettings.js"; // Set eco system setEcoSystem(ECOSYSTEM_PY); -// Set current invocation -setCurrentPipInvocation(PIP_INVOCATIONS.PIP); - -initializePackageManager(PIP_PACKAGE_MANAGER); +initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP_COMMAND, args: process.argv.slice(2) }); (async () => { // Pass through only user-supplied pip args diff --git a/packages/safe-chain/bin/aikido-pip3.js b/packages/safe-chain/bin/aikido-pip3.js index e831afe..510b688 100755 --- a/packages/safe-chain/bin/aikido-pip3.js +++ b/packages/safe-chain/bin/aikido-pip3.js @@ -3,16 +3,12 @@ import { main } from "../src/main.js"; import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; -import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; +import { PIP_PACKAGE_MANAGER, PIP3_COMMAND } from "../src/packagemanager/pip/pipSettings.js"; // Set eco system setEcoSystem(ECOSYSTEM_PY); -// Set current invocation -setCurrentPipInvocation(PIP_INVOCATIONS.PIP3); - -// Create package manager -initializePackageManager(PIP_PACKAGE_MANAGER); +initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP3_COMMAND, args: process.argv.slice(2) }); (async () => { // Pass through only user-supplied pip args diff --git a/packages/safe-chain/bin/aikido-python.js b/packages/safe-chain/bin/aikido-python.js index 29c38e6..b769b4a 100755 --- a/packages/safe-chain/bin/aikido-python.js +++ b/packages/safe-chain/bin/aikido-python.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; -import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; +import { PIP_PACKAGE_MANAGER, PYTHON_COMMAND } from "../src/packagemanager/pip/pipSettings.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; import { main } from "../src/main.js"; @@ -11,20 +11,9 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); +initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON_COMMAND, args: argv }); + (async () => { - if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { - setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP); - initializePackageManager(PIP_PACKAGE_MANAGER); - - // Strip off the '-m pip' or '-m pip3' from the args - argv = argv.slice(2); - - var exitCode = await main(argv); - process.exit(exitCode); - } else { - // Forward to real python binary for non-pip flows - const { spawn } = await import('child_process'); - spawn('python', argv, { stdio: 'inherit' }); - } + var exitCode = await main(argv); + process.exit(exitCode); })(); diff --git a/packages/safe-chain/bin/aikido-python3.js b/packages/safe-chain/bin/aikido-python3.js index 997a88d..c572a7b 100755 --- a/packages/safe-chain/bin/aikido-python3.js +++ b/packages/safe-chain/bin/aikido-python3.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; -import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js"; +import { PIP_PACKAGE_MANAGER, PYTHON3_COMMAND } from "../src/packagemanager/pip/pipSettings.js"; import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js"; import { main } from "../src/main.js"; @@ -11,20 +11,9 @@ setEcoSystem(ECOSYSTEM_PY); // Strip nodejs and wrapper script from args let argv = process.argv.slice(2); +initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON3_COMMAND, args: argv }); + (async () => { - if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) { - setEcoSystem(ECOSYSTEM_PY); - setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP); - initializePackageManager(PIP_PACKAGE_MANAGER); - - // Strip off the '-m pip' or '-m pip3' from the args - argv = argv.slice(2); - - var exitCode = await main(argv); - process.exit(exitCode); - } else { - // Forward to real python3 binary for non-pip flows - const { spawn } = await import('child_process'); - spawn('python3', argv, { stdio: 'inherit' }); - } + var exitCode = await main(argv); + process.exit(exitCode); })(); diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 7a1d6ab..2793987 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -13,11 +13,6 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import { knownAikidoTools } from "../src/shell-integration/helpers.js"; -import { - PIP_INVOCATIONS, - PIP_PACKAGE_MANAGER, - setCurrentPipInvocation, -} from "../src/packagemanager/pip/pipSettings.js"; /** @type {string} */ // This checks the current file's dirname in a way that's compatible with: @@ -46,15 +41,14 @@ const command = process.argv[2]; const tool = knownAikidoTools.find((tool) => tool.tool === command); -if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) { - (async function () { - await executePip(tool); - })(); -} else if (tool) { +if (tool) { const args = process.argv.slice(3); setEcoSystem(tool.ecoSystem); - initializePackageManager(tool.internalPackageManagerName); + + // Provide tool context to PM (pip uses this; others ignore) + const toolContext = { tool: tool.tool, args }; + initializePackageManager(tool.internalPackageManagerName, toolContext); (async () => { var exitCode = await main(args); @@ -140,51 +134,3 @@ async function getVersion() { return "0.0.0"; } - -/** - * @param {import("../src/shell-integration/helpers.js").AikidoTool} tool - */ -async function executePip(tool) { - // Scanners for pip / pip3 / python / python3 use a slightly different approach: - // - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so - // - It needs to set which tool to run (pip / pip3 / python / python3) - // - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager - // - Python / python3 skips safe-chain if not being run with -m pip or -m pip3 - - let args = process.argv.slice(3); - setEcoSystem(tool.ecoSystem); - initializePackageManager(PIP_PACKAGE_MANAGER); - - let shouldSkip = false; - if (tool.tool === "pip") { - setCurrentPipInvocation(PIP_INVOCATIONS.PIP); - } else if (tool.tool === "pip3") { - setCurrentPipInvocation(PIP_INVOCATIONS.PIP3); - } else if (tool.tool === "python") { - if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) { - setCurrentPipInvocation( - args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP - ); - args = args.slice(2); - } else { - shouldSkip = true; - } - } else if (tool.tool === "python3") { - if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) { - setCurrentPipInvocation( - args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP - ); - args = args.slice(2); - } else { - shouldSkip = true; - } - } - - if (shouldSkip) { - const { spawn } = await import("child_process"); - spawn(tool.tool, args, { stdio: "inherit" }); - } else { - var exitCode = await main(args); - process.exit(exitCode); - } -} diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index c6f4484..a6fad4a 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -35,10 +35,11 @@ const state = { /** * @param {string} packageManagerName + * @param {{ tool: string, args: string[] }} [context] - Optional tool context for package managers like pip * * @return {PackageManager} */ -export function initializePackageManager(packageManagerName) { +export function initializePackageManager(packageManagerName, context) { if (packageManagerName === "npm") { state.packageManagerName = createNpmPackageManager(); } else if (packageManagerName === "npx") { @@ -54,7 +55,7 @@ export function initializePackageManager(packageManagerName) { } else if (packageManagerName === "bunx") { state.packageManagerName = createBunxPackageManager(); } else if (packageManagerName === "pip") { - state.packageManagerName = createPipPackageManager(); + state.packageManagerName = createPipPackageManager(context); } else if (packageManagerName === "uv") { state.packageManagerName = createUvPackageManager(); } else { diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 6ec5d1a..bd78605 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -1,17 +1,21 @@ import { runPip } from "./runPipCommand.js"; -import { getCurrentPipInvocation } from "./pipSettings.js"; +import { PIP_COMMAND } from "./pipSettings.js"; + /** + * @param {{ tool: string, args: string[] }} [context] - Optional context with tool name and args * @returns {import("../currentPackageManager.js").PackageManager} */ -export function createPipPackageManager() { +export function createPipPackageManager(context) { + const tool = context?.tool || PIP_COMMAND; + return { /** * @param {string[]} args */ runCommand: (args) => { - const invocation = getCurrentPipInvocation(); - const fullArgs = [...invocation.args, ...args]; - return runPip(invocation.command, fullArgs); + // Args from main.js are already stripped of --safe-chain-* flags + // We just pass the tool (e.g. "python3") and the args (e.g. ["-m", "pip", "install", ...]) + return runPip(tool, args); }, // For pip, rely solely on MITM proxy to detect/deny downloads from known registries. isSupportedCommand: () => false, diff --git a/packages/safe-chain/src/packagemanager/pip/pipSettings.js b/packages/safe-chain/src/packagemanager/pip/pipSettings.js index 0316b77..1ef6720 100644 --- a/packages/safe-chain/src/packagemanager/pip/pipSettings.js +++ b/packages/safe-chain/src/packagemanager/pip/pipSettings.js @@ -1,30 +1,6 @@ export const PIP_PACKAGE_MANAGER = "pip"; -// All supported python/pip invocations for Safe Chain interception -export const PIP_INVOCATIONS = { - PIP: { command: "pip", args: [] }, - PIP3: { command: "pip3", args: [] }, - PY_PIP: { command: "python", args: ["-m", "pip"] }, - PY3_PIP: { command: "python3", args: ["-m", "pip"] }, - PY_PIP3: { command: "python", args: ["-m", "pip3"] }, - PY3_PIP3: { command: "python3", args: ["-m", "pip3"] } -}; - -/** - * @type {{ command: string, args: string[] }} - */ -let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip - -/** - * @param {{ command: string, args: string[] }} invocation - */ -export function setCurrentPipInvocation(invocation) { - currentInvocation = invocation; -} - -/** - * @returns {{ command: string, args: string[] }} - */ -export function getCurrentPipInvocation() { - return currentInvocation; -} +export const PIP_COMMAND = "pip"; +export const PIP3_COMMAND = "pip3"; +export const PYTHON_COMMAND = "python"; +export const PYTHON3_COMMAND = "python3"; diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index f7050a5..dc9a1ad 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,12 +2,31 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js"; import fs from "node:fs/promises"; import fsSync from "node:fs"; import os from "node:os"; import path from "node:path"; import ini from "ini"; +/** + * Checks if this pip invocation should bypass safe-chain and spawn directly. + * Returns true if the tool is python/python3 but NOT being run with -m pip/pip3. + * @param {string} command - The command executable + * @param {string[]} args - The arguments + * @returns {boolean} + */ +function shouldBypassSafeChain(command, args) { + if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) { + // Check if args start with -m pip + if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) { + return false; + } + return true; + } + return false; +} + /** * Sets fallback CA bundle environment variables used by Python libraries. * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python @@ -49,11 +68,28 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) { * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow * users to read/write persistent config. Only CA environment variables are set for these commands. * - * @param {string} command - The pip command to execute (e.g., 'pip3') + * @param {string} command - The pip command executable (e.g., 'pip3' or 'python3') * @param {string[]} args - Command line arguments to pass to pip * @returns {Promise<{status: number}>} Exit status of the pip command */ export async function runPip(command, args) { + // Check if we should bypass safe-chain (python/python3 without -m pip) + if (shouldBypassSafeChain(command, args)) { + ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`); + // Spawn the ORIGINAL command with ORIGINAL args + const { spawn } = await import("child_process"); + return new Promise((_resolve) => { + const proc = spawn(command, args, { stdio: "inherit" }); + proc.on("exit", (/** @type {number | null} */ code) => { + process.exit(code ?? 0); + }); + proc.on("error", (/** @type {Error} */ err) => { + ui.writeError(`Error executing command: ${err.message}`); + process.exit(1); + }); + }); + } + try { const env = mergeSafeChainProxyEnvironmentVariables(process.env); diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 26ebb78..f4579ab 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -539,4 +539,278 @@ describe("E2E: pip coverage", () => { `Should download certifi. Output was:\n${downloadResult.output}` ); }); + + // Tests for python/python3 bypass (non-pip invocations should go directly without safe-chain) + + it(`python3 --version should bypass safe-chain and work normally`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python3 --version"); + + // Should output Python version + assert.ok( + result.output.match(/Python 3\.\d+\.\d+/), + `Should output Python version. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain proxy + assert.ok( + !result.output.includes("Safe-chain"), + `python3 --version should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python --version should bypass safe-chain and work normally`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python --version"); + + // Should output Python version + assert.ok( + result.output.match(/Python \d+\.\d+\.\d+/), + `Should output Python version. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python --version should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 -c "print('hello')" should bypass safe-chain and execute code`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python3 -c \"print('hello world')\""); + + // Should execute Python code + assert.ok( + result.output.includes("hello world"), + `Should execute Python code. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 -c should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python -c should bypass safe-chain and execute code`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("python -c \"import sys; print(sys.version)\""); + + // Should execute Python code and print version + assert.ok( + result.output.match(/\d+\.\d+\.\d+/), + `Should execute Python code. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python -c should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 script.py should bypass safe-chain and execute script`, async () => { + const shell = await container.openShell("zsh"); + + // Create a simple Python script + await shell.runCommand("echo \"print('script executed')\" > /tmp/test_script.py"); + + const result = await shell.runCommand("python3 /tmp/test_script.py"); + + // Should execute the script + assert.ok( + result.output.includes("script executed"), + `Should execute Python script. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 script.py should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python script.py should bypass safe-chain and execute script`, async () => { + const shell = await container.openShell("zsh"); + + // Create a simple Python script + await shell.runCommand("echo \"print('python2/3 compatible')\" > /tmp/test_script2.py"); + + const result = await shell.runCommand("python /tmp/test_script2.py"); + + // Should execute the script + assert.ok( + result.output.includes("python2/3 compatible"), + `Should execute Python script. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python script.py should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 -m json.tool should bypass safe-chain (module other than pip)`, async () => { + const shell = await container.openShell("zsh"); + + // json.tool is a built-in Python module for formatting JSON + const result = await shell.runCommand("echo '{\"test\": 123}' | python3 -m json.tool"); + + // Should format JSON + assert.ok( + result.output.includes('"test"') && result.output.includes('123'), + `Should format JSON. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 -m json.tool should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 -m http.server should bypass safe-chain (module other than pip)`, async () => { + const shell = await container.openShell("zsh"); + + // Start http.server in background and kill it immediately + // We just want to verify it starts without safe-chain interference + const result = await shell.runCommand("timeout 1 python3 -m http.server 8999 || true"); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 -m http.server should not go through safe-chain. Output was:\n${result.output}` + ); + + // Should either start the server or timeout (both are success for bypass test) + assert.ok( + result.output.includes("Serving HTTP") || result.output === "" || result.exitCode !== undefined, + `Should attempt to start server. Output was:\n${result.output}` + ); + }); + + it(`python3 interactive mode should bypass safe-chain`, async () => { + const shell = await container.openShell("zsh"); + + // Run python3 with a command piped to stdin to simulate interactive mode + const result = await shell.runCommand("echo 'print(2+2)' | python3"); + + // Should execute the command + assert.ok( + result.output.includes("4"), + `Should execute Python interactively. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 interactive should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 with no arguments should bypass safe-chain`, async () => { + const shell = await container.openShell("zsh"); + + // Python with no args goes to interactive REPL, pipe exit command + const result = await shell.runCommand("echo 'exit()' | python3"); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 with no args should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 -m venv should bypass safe-chain (venv module)`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand("python3 -m venv /tmp/test_venv"); + + // Should create venv without safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 -m venv should not go through safe-chain. Output was:\n${result.output}` + ); + + // Verify venv was created + const checkVenv = await shell.runCommand("test -f /tmp/test_venv/bin/python3 && echo 'exists'"); + assert.ok( + checkVenv.output.includes("exists"), + `venv should be created. Output was:\n${checkVenv.output}` + ); + }); + + it(`python3 -m pytest should bypass safe-chain (pytest module)`, async () => { + const shell = await container.openShell("zsh"); + + // pytest may not be installed, but the bypass should work regardless + const result = await shell.runCommand("python3 -m pytest --version 2>&1 || true"); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 -m pytest should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 -m site should bypass safe-chain (site module)`, async () => { + const shell = await container.openShell("zsh"); + + const result = await shell.runCommand("python3 -m site"); + + // Should output site information + assert.ok( + result.output.includes("sys.path") || result.output.includes("USER_BASE"), + `Should output site information. Output was:\n${result.output}` + ); + + // Should NOT go through safe-chain + assert.ok( + !result.output.includes("Safe-chain"), + `python3 -m site should not go through safe-chain. Output was:\n${result.output}` + ); + }); + + // Verify that -m pip* still goes through safe-chain (sanity check) + + it(`python3 -m pip DOES go through safe-chain (sanity check)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "python3 -m pip install --break-system-packages certifi" + ); + + // SHOULD go through safe-chain + assert.ok( + result.output.includes("Safe-chain") || result.output.includes("no malware found"), + `python3 -m pip SHOULD go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python3 -m pip3 DOES go through safe-chain (sanity check)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "python3 -m pip3 install --break-system-packages certifi" + ); + + // SHOULD go through safe-chain + assert.ok( + result.output.includes("Safe-chain") || result.output.includes("no malware found"), + `python3 -m pip3 SHOULD go through safe-chain. Output was:\n${result.output}` + ); + }); + + it(`python -m pip DOES go through safe-chain (sanity check)`, async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "python -m pip install --break-system-packages certifi" + ); + + // SHOULD go through safe-chain + assert.ok( + result.output.includes("Safe-chain") || result.output.includes("no malware found"), + `python -m pip SHOULD go through safe-chain. Output was:\n${result.output}` + ); + }); }); diff --git a/test/e2e/safe-chain-cli-python.e2e.spec.js b/test/e2e/safe-chain-cli-python.e2e.spec.js new file mode 100644 index 0000000..5c84945 --- /dev/null +++ b/test/e2e/safe-chain-cli-python.e2e.spec.js @@ -0,0 +1,104 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: safe-chain CLI python/pip support", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + container = new DockerTestContainer(); + await container.start(); + // Note: We do NOT run 'safe-chain setup' here. + // We want to test the 'safe-chain' CLI command directly. + }); + + afterEach(async () => { + if (container) { + await container.stop(); + container = null; + } + }); + + it("safe-chain pip3 install routes through proxy", async () => { + const shell = await container.openShell("zsh"); + // Invoke safe-chain directly with pip3 command + const result = await shell.runCommand( + "safe-chain pip3 install --break-system-packages requests" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Successfully installed") || + result.output.includes("Requirement already satisfied"), + `Installation failed. Output was:\n${result.output}` + ); + }); + + it("safe-chain python3 -m pip install routes through proxy", async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand( + "safe-chain python3 -m pip install --break-system-packages requests" + ); + + assert.ok( + result.output.includes("no malware found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it("safe-chain python3 script.py bypasses proxy", async () => { + const shell = await container.openShell("zsh"); + + // Create a simple script + await shell.runCommand("echo \"print('direct execution')\" > /tmp/test.py"); + + const result = await shell.runCommand("safe-chain python3 /tmp/test.py"); + + // Should execute the script + assert.ok( + result.output.includes("direct execution"), + `Script execution failed. Output was:\n${result.output}` + ); + + // Should NOT show safe-chain logs + assert.ok( + !result.output.includes("Safe-chain"), + `Should have bypassed safe-chain. Output was:\n${result.output}` + ); + }); + + it("safe-chain python3 --version bypasses proxy", async () => { + const shell = await container.openShell("zsh"); + const result = await shell.runCommand("safe-chain python3 --version"); + + assert.ok( + result.output.match(/Python 3\.\d+\.\d+/), + `Should show python version. Output was:\n${result.output}` + ); + assert.ok( + !result.output.includes("Safe-chain"), + `Should have bypassed safe-chain. Output was:\n${result.output}` + ); + }); + + it("safe-chain blocks malicious package via pip3", async () => { + const shell = await container.openShell("zsh"); + await shell.runCommand("pip3 cache purge"); + + const result = await shell.runCommand( + "safe-chain pip3 install --break-system-packages safe-chain-pi-test" + ); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Should have blocked malware. Output was:\n${result.output}` + ); + }); +}); From 57a0e88fa4f055f38d40effec557efb3182905e5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 5 Dec 2025 12:09:19 +0100 Subject: [PATCH 344/345] Add tests and clarifying comments --- .github/workflows/build-and-release.yml | 2 +- .../src/registryProxy/isImdsEndpoint.js | 13 ++ .../registryProxy.connect-tunnel.spec.js | 150 ++++++++++++++++-- .../src/registryProxy/tunnelRequestHandler.js | 55 +++++-- 4 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/isImdsEndpoint.js diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index c956386..f9ca4da 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -66,7 +66,7 @@ jobs: - name: Publish to npm run: | echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance --tag beta + npm publish --workspace=packages/safe-chain --access public --provenance - name: Download all binary artifacts uses: actions/download-artifact@v4 diff --git a/packages/safe-chain/src/registryProxy/isImdsEndpoint.js b/packages/safe-chain/src/registryProxy/isImdsEndpoint.js new file mode 100644 index 0000000..2bde02a --- /dev/null +++ b/packages/safe-chain/src/registryProxy/isImdsEndpoint.js @@ -0,0 +1,13 @@ +// Instance Metadata Service (IMDS) endpoints used by cloud providers. +// Cloud SDK tools probe these to detect environment and retrieve credentials. +// When outside cloud environments, connections timeout - we reduce timeout (3s vs 30s) +// and suppress error logging since this is expected behavior. +const imdsEndpoints = [ + "metadata.google.internal", + "metadata.goog", + "169.254.169.254", +]; + +export function isImdsEndpoint(/** @type {string} */ host) { + return imdsEndpoints.includes(host); +} 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 45fd96a..b382d3f 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.connect-tunnel.spec.js @@ -1,11 +1,28 @@ -import { before, after, describe, it } from "node:test"; +import { before, after, describe, it, mock } from "node:test"; import assert from "node:assert"; import net from "net"; import tls from "tls"; -import { - createSafeChainProxy, - mergeSafeChainProxyEnvironmentVariables, -} from "./registryProxy.js"; + +// Mock isImdsEndpoint BEFORE any other imports that might use it +// This allows us to use TEST-NET-1 (192.0.2.1) as a test IMDS endpoint +mock.module("./isImdsEndpoint.js", { + namedExports: { + isImdsEndpoint: (host) => { + // 192.0.2.1 is TEST-NET-1, reserved for testing (RFC 5737) + if (host === "192.0.2.1") return true; + // Real IMDS endpoints + return [ + "metadata.google.internal", + "metadata.goog", + "169.254.169.254", + ].includes(host); + }, + }, +}); + +// Use dynamic import AFTER mocking to ensure mock is applied +const { createSafeChainProxy, mergeSafeChainProxyEnvironmentVariables } = + await import("./registryProxy.js"); describe("registryProxy.connectTunnel", () => { let proxy, proxyHost, proxyPort; @@ -62,15 +79,21 @@ describe("registryProxy.connectTunnel", () => { // 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 !== 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}`); + assert.strictEqual( + certInfo.subject.includes("postman-echo.com"), + true, + `Certificate subject should include postman-echo.com, got: ${certInfo.subject}` + ); socket.destroy(); }); @@ -105,7 +128,6 @@ describe("registryProxy.connectTunnel", () => { assert.ok(true); }); - it("should handle socket errors without crashing", async () => { const socket = await connectToProxy(proxyHost, proxyPort); @@ -125,7 +147,94 @@ describe("registryProxy.connectTunnel", () => { // Test passes if no unhandled error crashes the process assert.ok(true); }); + }); + describe("Connection Timeout", () => { + it("should timeout quickly when connecting to IMDS endpoint (3s)", async () => { + // We need to make sure we're not running behind an existing safe-chain installation to allow this test to work + const https_proxy = process.env.HTTPS_PROXY; + delete process.env.HTTPS_PROXY; + const socket = await connectToProxy(proxyHost, proxyPort); + const startTime = Date.now(); + + // 192.0.2.1 is TEST-NET-1 (RFC 5737), guaranteed to never route + const connectRequest = `CONNECT 192.0.2.1:443 HTTP/1.1\r\nHost: 192.0.2.1:443\r\n\r\n`; + socket.write(connectRequest); + + let responseData = ""; + await new Promise((resolve) => { + socket.once("data", (data) => { + responseData += data.toString(); + resolve(); + }); + }); + + const duration = Date.now() - startTime; + + // Should return 502 Bad Gateway + assert.ok( + responseData.includes("HTTP/1.1 502 Bad Gateway"), + "Should return 502 for timeout" + ); + + // Should timeout around 3 seconds for IMDS endpoints (allow some margin) + assert.ok( + duration >= 2800 && duration < 5000, + `IMDS timeout should be ~3s, got ${duration}ms` + ); + + socket.destroy(); + if (https_proxy) { + process.env.HTTPS_PROXY = https_proxy; + } + }); + + it("should cache timed-out endpoints and fail immediately on retry", async () => { + // We need to make sure we're not running behind an existing safe-chain installation to allow this test to work + const https_proxy = process.env.HTTPS_PROXY; + delete process.env.HTTPS_PROXY; + // First connection - will timeout + const socket1 = await connectToProxy(proxyHost, proxyPort); + const connectRequest = `CONNECT 192.0.2.1:80 HTTP/1.1\r\nHost: 192.0.2.1:80\r\n\r\n`; + socket1.write(connectRequest); + + await new Promise((resolve) => { + socket1.once("data", () => resolve()); + }); + socket1.destroy(); + + // Second connection - should fail immediately (cached) + const socket2 = await connectToProxy(proxyHost, proxyPort); + const startTime = Date.now(); + socket2.write(connectRequest); + + let responseData = ""; + await new Promise((resolve) => { + socket2.once("data", (data) => { + responseData += data.toString(); + resolve(); + }); + }); + + const duration = Date.now() - startTime; + + // Should return 502 immediately (cached timeout) + assert.ok( + responseData.includes("HTTP/1.1 502 Bad Gateway"), + "Should return 502 for cached timeout" + ); + + // Should be nearly instant (< 100ms) since it's cached + assert.ok( + duration < 100, + `Cached timeout should be instant, got ${duration}ms` + ); + + socket2.destroy(); + if (https_proxy) { + process.env.HTTPS_PROXY = https_proxy; + } + }); }); }); @@ -167,7 +276,12 @@ function establishHttpsTunnel(socket, targetHost, targetPort) { }); } -function sendHttpsRequestThroughTunnel(socket, verb, url, rejectUnauthorized = false) { +function sendHttpsRequestThroughTunnel( + socket, + verb, + url, + rejectUnauthorized = false +) { return new Promise((resolve, reject) => { const tlsSocket = tls.connect( { @@ -214,12 +328,16 @@ function getTlsCertificateInfo(socket, url) { 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"; + 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 }); diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 96717ba..9288ec3 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -1,5 +1,6 @@ import * as net from "net"; import { ui } from "../environment/userInteraction.js"; +import { isImdsEndpoint } from "./isImdsEndpoint.js"; /** @type {string[]} */ let timedoutEndpoints = []; @@ -40,12 +41,19 @@ export function tunnelRequest(req, clientSocket, head) { */ function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); + const isImds = isImdsEndpoint(hostname); if (timedoutEndpoints.includes(hostname)) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); - ui.writeError( - `Safe-chain: Closing connection because previously timedout connect to ${hostname}` - ); + if (isImds) { + ui.writeVerbose( + `Safe-chain: Closing connection because previously timedout connect to ${hostname}` + ); + } else { + ui.writeError( + `Safe-chain: Closing connection because previously timedout connect to ${hostname}` + ); + } return; } @@ -60,13 +68,24 @@ function tunnelRequestToDestination(req, clientSocket, head) { } ); + // Set explicit connection timeout to avoid waiting for OS default (~2 minutes). + // IMDS endpoints get shorter timeout (3s) since they're commonly unreachable outside cloud environments. const connectTimeout = getConnectTimeout(hostname); serverSocket.setTimeout(connectTimeout); + serverSocket.on("timeout", () => { timedoutEndpoints.push(hostname); - ui.writeError( - `Safe-chain: connect to ${hostname}:${port} timed out after ${connectTimeout}ms` - ); + // Suppress error logging for IMDS endpoints - timeouts are expected when not in cloud + if (isImdsEndpoint(hostname)) { + ui.writeVerbose( + `Safe-chain: connect to ${hostname}:${port || 443} timed out after ${connectTimeout}ms` + ); + } else { + ui.writeError( + `Safe-chain: connect to ${hostname}:${port || 443} timed out after ${connectTimeout}ms` + ); + } + serverSocket.destroy(); // Clean up socket to prevent event loop hanging clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); }); @@ -79,9 +98,15 @@ function tunnelRequestToDestination(req, clientSocket, head) { }); serverSocket.on("error", (err) => { - ui.writeError( - `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` - ); + if (isImdsEndpoint(hostname)) { + ui.writeVerbose( + `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` + ); + } else { + ui.writeError( + `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` + ); + } if (clientSocket.writable) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } @@ -167,13 +192,13 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { }); } -const imdsEndpoints = [ - "metadata.google.internal", - "metadata.goog", - "169.254.169.254", -]; +/** + * Returns appropriate connection timeout for a host. + * - IMDS endpoints: 3s (fail fast when outside cloud, reduce 5min delay to ~20s) + * - Other endpoints: 30s (allow for slow networks while preventing indefinite hangs) + */ function getConnectTimeout(/** @type {string} */ host) { - if (imdsEndpoints.includes(host)) { + if (isImdsEndpoint(host)) { return 3000; } return 30000; From e421414b8a8ff9a5e4ee015ce24aac0e976428a3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 5 Dec 2025 12:12:22 +0100 Subject: [PATCH 345/345] Don't repeatedly call isImdsEndpoint --- .../safe-chain/src/registryProxy/isImdsEndpoint.js | 2 +- .../src/registryProxy/tunnelRequestHandler.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/isImdsEndpoint.js b/packages/safe-chain/src/registryProxy/isImdsEndpoint.js index 2bde02a..deccf10 100644 --- a/packages/safe-chain/src/registryProxy/isImdsEndpoint.js +++ b/packages/safe-chain/src/registryProxy/isImdsEndpoint.js @@ -5,7 +5,7 @@ const imdsEndpoints = [ "metadata.google.internal", "metadata.goog", - "169.254.169.254", + "169.254.169.254", // AWS, Azure, Oracle Cloud, GCP ]; export function isImdsEndpoint(/** @type {string} */ host) { diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 9288ec3..b97799b 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -76,13 +76,17 @@ function tunnelRequestToDestination(req, clientSocket, head) { serverSocket.on("timeout", () => { timedoutEndpoints.push(hostname); // Suppress error logging for IMDS endpoints - timeouts are expected when not in cloud - if (isImdsEndpoint(hostname)) { + if (isImds) { ui.writeVerbose( - `Safe-chain: connect to ${hostname}:${port || 443} timed out after ${connectTimeout}ms` + `Safe-chain: connect to ${hostname}:${ + port || 443 + } timed out after ${connectTimeout}ms` ); } else { ui.writeError( - `Safe-chain: connect to ${hostname}:${port || 443} timed out after ${connectTimeout}ms` + `Safe-chain: connect to ${hostname}:${ + port || 443 + } timed out after ${connectTimeout}ms` ); } serverSocket.destroy(); // Clean up socket to prevent event loop hanging @@ -98,7 +102,7 @@ function tunnelRequestToDestination(req, clientSocket, head) { }); serverSocket.on("error", (err) => { - if (isImdsEndpoint(hostname)) { + if (isImds) { ui.writeVerbose( `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}` );