From 833fa285aa84c849cbc4b06badbf1996e651ba85 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 10 Dec 2025 13:27:18 +0100 Subject: [PATCH 01/36] feat: allow python custom registries configuration --- README.md | 19 ++ .../src/config/environmentVariables.js | 8 + packages/safe-chain/src/config/settings.js | 27 +++ .../interceptors/pipInterceptor.js | 9 +- ...pipInterceptor.pipCustomRegistries.spec.js | 199 ++++++++++++++++++ 5 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js diff --git a/README.md b/README.md index def262f..702f8bf 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,25 @@ You can set the minimum package age through multiple sources (in order of priori } ``` +## Custom Registries + +By default, Safe Chain monitors downloads from the official package registries (npm registry, PyPI, etc.). If you use a private or custom package registry, you can configure Safe Chain to also monitor downloads from those registries. + +⚠️ This feature **currently only applies to Python package managers** (pip, pip3, uv, poetry) and does not apply to npm-based package managers. + +### Configuration Options + +You can set custom registries through the following source: + +1. **Environment Variable**: + + ```shell + export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES=my-custom-registry.example.com,private-pypi.internal.com + pip install mypackage + ``` + + Use a comma-separated list of registry hostnames to monitor multiple custom registries. + # 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/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index 5c6056a..fe54732 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -5,3 +5,11 @@ export function getMinimumPackageAgeHours() { return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS; } + +/** + * Gets the custom pip registries from environment variable + * @returns {string | undefined} + */ +export function getPipCustomRegistries() { + return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES; +} diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 7c20358..0480709 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -98,3 +98,30 @@ export function skipMinimumPackageAge() { return defaultSkipMinimumPackageAge; } + +/** @type {string[]} */ +const defaultPipCustomRegistries = []; +/** @returns {string[]} */ +export function getPipCustomRegistries() { + // Priority 1: Environment variable + const envValue = validatePipCustomRegistries( + environmentVariables.getPipCustomRegistries() + ); + if (envValue !== undefined) { + return envValue; + } + + return defaultPipCustomRegistries; +} + +/** + * @param {string | undefined} value + * @returns {string[] | undefined} + */ +function validatePipCustomRegistries(value) { + if (!value) { + return undefined; + } + + return value.split(","); +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 9a122a6..e781e30 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -1,3 +1,4 @@ +import { getPipCustomRegistries } from "../../config/settings.js"; import { isMalwarePackage } from "../../scanning/audit/index.js"; import { interceptRequests } from "./interceptorBuilder.js"; @@ -13,7 +14,9 @@ const knownPipRegistries = [ * @returns {import("./interceptorBuilder.js").Interceptor | undefined} */ export function pipInterceptorForUrl(url) { - const registry = knownPipRegistries.find((reg) => url.includes(reg)); + const customRegistries = getPipCustomRegistries(); + const registries = [...knownPipRegistries, ...customRegistries]; + const registry = registries.find((reg) => url.includes(reg)); if (registry) { return buildPipInterceptor(registry); @@ -37,8 +40,8 @@ function buildPipInterceptor(registry) { // Per python, packages that differ only by hyphen vs underscore are considered the same. const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName; - const isMalicious = - await isMalwarePackage(packageName, version) + const isMalicious = + await isMalwarePackage(packageName, version) || await isMalwarePackage(hyphenName, version); if (isMalicious) { diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js new file mode 100644 index 0000000..fc9c91e --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.pipCustomRegistries.spec.js @@ -0,0 +1,199 @@ +import { describe, it, mock } from "node:test"; +import assert from "node:assert"; + +describe("pipInterceptor custom registries", async () => { + let lastPackage; + let malwareResponse = false; + let customRegistries = []; + + mock.module("../../config/settings.js", { + namedExports: { + getPipCustomRegistries: () => customRegistries, + }, + }); + + mock.module("../../scanning/audit/index.js", { + namedExports: { + isMalwarePackage: async (packageName, version) => { + lastPackage = { packageName, version }; + return malwareResponse; + }, + }, + }); + + const { pipInterceptorForUrl } = await import("./pipInterceptor.js"); + + it("should create interceptor for custom registry", () => { + customRegistries = ["my-custom-registry.example.com"]; + const url = + "https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + + assert.ok( + interceptor, + "Interceptor should be created for custom registry" + ); + }); + + it("should parse package from custom registry URL", async () => { + customRegistries = ["my-custom-registry.example.com"]; + const url = + "https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + assert.ok(interceptor, "Interceptor should be created"); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, { + packageName: "foobar", + version: "1.2.3", + }); + }); + + it("should parse wheel package from custom registry URL", async () => { + customRegistries = ["private-pypi.internal.com"]; + const url = + "https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl"; + + const interceptor = pipInterceptorForUrl(url); + assert.ok(interceptor, "Interceptor should be created"); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, { + packageName: "foo-bar", + version: "2.0.0", + }); + }); + + it("should handle multiple custom registries", async () => { + customRegistries = [ + "registry-one.example.com", + "registry-two.example.com", + ]; + + const url1 = + "https://registry-one.example.com/packages/package1-1.0.0.tar.gz"; + const url2 = + "https://registry-two.example.com/packages/package2-2.0.0.tar.gz"; + + const interceptor1 = pipInterceptorForUrl(url1); + const interceptor2 = pipInterceptorForUrl(url2); + + assert.ok(interceptor1, "Interceptor should be created for first registry"); + assert.ok( + interceptor2, + "Interceptor should be created for second registry" + ); + }); + + it("should block malicious package from custom registry", async () => { + customRegistries = ["my-custom-registry.example.com"]; + malwareResponse = true; + + const url = + "https://my-custom-registry.example.com/packages/malicious_package-1.0.0.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + assert.ok(interceptor, "Interceptor should be created"); + + 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" + ); + + malwareResponse = false; + }); + + it("should still work with known registries when custom registries are set", async () => { + customRegistries = ["my-custom-registry.example.com"]; + + const url = + "https://files.pythonhosted.org/packages/xx/yy/foobar-1.2.3.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + + assert.ok( + interceptor, + "Interceptor should be created for known registry even with custom registries set" + ); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, { + packageName: "foobar", + version: "1.2.3", + }); + }); + + it("should not create interceptor for unknown registry when custom registries are set", () => { + customRegistries = ["my-custom-registry.example.com"]; + const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + + assert.equal( + interceptor, + undefined, + "Interceptor should be undefined for unknown registry" + ); + }); + + it("should handle empty custom registries array", () => { + customRegistries = []; + const url = + "https://my-custom-registry.example.com/packages/foobar-1.0.0.tar.gz"; + + const interceptor = pipInterceptorForUrl(url); + + assert.equal( + interceptor, + undefined, + "Interceptor should be undefined when no custom registries are configured" + ); + }); + + it("should parse .whl.metadata from custom registry", async () => { + customRegistries = ["private-pypi.internal.com"]; + const url = + "https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl.metadata"; + + const interceptor = pipInterceptorForUrl(url); + assert.ok(interceptor, "Interceptor should be created"); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, { + packageName: "foo-bar", + version: "2.0.0", + }); + }); + + it("should parse .tar.gz.metadata from custom registry", async () => { + customRegistries = ["private-pypi.internal.com"]; + const url = + "https://private-pypi.internal.com/packages/foo_bar-2.0.0.tar.gz.metadata"; + + const interceptor = pipInterceptorForUrl(url); + assert.ok(interceptor, "Interceptor should be created"); + + await interceptor.handleRequest(url); + + assert.deepEqual(lastPackage, { + packageName: "foo-bar", + version: "2.0.0", + }); + }); +}); + From b571aad6a0be5b54fe32148d69bad7b5057c250a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 19 Dec 2025 16:18:21 +0100 Subject: [PATCH 02/36] Add command to verify safe-chain is intercepting the package managers commands --- README.md | 15 ++++++++++++++- packages/safe-chain/bin/safe-chain.js | 7 +++++-- packages/safe-chain/src/main.js | 13 +++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 29c6510..0d7866c 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,20 @@ You can find all available versions on the [releases page](https://github.com/Ai - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available. -2. **Verify the installation** by running one of the following commands: +2. **Verify the installation** by running the verification command: + + ```shell + npm safe-chain-verify + pnpm safe-chain-verify + pip safe-chain-verify + uv safe-chain-verify + + # Any other supported package manager: {packagemanager} safe-chain-verify + ``` + + - The output should display "OK: Safe-chain works!" confirming that Aikido Safe Chain is properly installed and running. + +3. **(Optional) Test malware blocking** by attempting to install a test package: For JavaScript/Node.js: diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index aed77f0..841ccee 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -3,7 +3,10 @@ import chalk from "chalk"; import { ui } from "../src/environment/userInteraction.js"; import { setup } from "../src/shell-integration/setup.js"; -import { teardown, teardownDirectories } from "../src/shell-integration/teardown.js"; +import { + teardown, + teardownDirectories, +} from "../src/shell-integration/teardown.js"; import { setupCi } from "../src/shell-integration/setup-ci.js"; import { initializeCliArguments } from "../src/config/cliArguments.js"; import { setEcoSystem } from "../src/config/settings.js"; @@ -45,7 +48,7 @@ if (tool) { const args = process.argv.slice(3); setEcoSystem(tool.ecoSystem); - + // Provide tool context to PM (pip uses this; others ignore) const toolContext = { tool: tool.tool, args }; initializePackageManager(tool.internalPackageManagerName, toolContext); diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 0e895b3..9b7ba53 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -13,6 +13,10 @@ import { getAuditStats } from "./scanning/audit/index.js"; * @returns {Promise} */ export async function main(args) { + if (isSafeChainVerify(args)) { + return 0; + } + process.on("SIGINT", handleProcessTermination); process.on("SIGTERM", handleProcessTermination); @@ -104,3 +108,12 @@ export async function main(args) { function handleProcessTermination() { ui.writeBufferedLogsAndStopBuffering(); } + +/** @param {string[]} args */ +function isSafeChainVerify(args) { + const safeChainCheckCommand = "safe-chain-verify"; + if (args.length > 0 && args[0] === safeChainCheckCommand) { + ui.writeInformation("OK: Safe-chain works!"); + return true; + } +} From c53a7347e22105f0e3304540393447166648e5c8 Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 22 Dec 2025 13:49:45 +0100 Subject: [PATCH 03/36] feat: allow python custom registries configuration through config file --- README.md | 12 +- packages/safe-chain/src/config/configFile.js | 26 ++ .../safe-chain/src/config/configFile.spec.js | 144 +++--- packages/safe-chain/src/config/settings.js | 5 +- .../safe-chain/src/config/settings.spec.js | 421 +++++++++--------- 5 files changed, 325 insertions(+), 283 deletions(-) diff --git a/README.md b/README.md index 29c6510..a55c63b 100644 --- a/README.md +++ b/README.md @@ -188,9 +188,13 @@ You can set the minimum package age through multiple sources (in order of priori } ``` -## Custom NPM Registries +## Custom Registries -Configure Safe Chain to scan packages from custom or private npm registries. +Configure Safe Chain to scan packages from custom or private registries. + +Supported ecosystems: +- Node.js +- Python ### Configuration Options @@ -200,6 +204,7 @@ You can set custom registries through environment variable or config file. Both ```shell export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net" + export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net" ``` 2. **Config File** (`~/.aikido/config.json`): @@ -208,6 +213,9 @@ You can set custom registries through environment variable or config file. Both { "npm": { "customRegistries": ["npm.company.com", "registry.internal.net"] + }, + "pip": { + "customRegistries": ["pip.company.com", "registry.internal.net"] } } ``` diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 1b7525b..a98304e 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -11,6 +11,7 @@ import { getEcoSystem } from "./settings.js"; * @property {unknown | Number} scanTimeout * @property {unknown | Number} minimumPackageAgeHours * @property {unknown | SafeChainRegistryConfiguration} npm + * @property {unknown | SafeChainRegistryConfiguration} pip * * @typedef {Object} SafeChainRegistryConfiguration * We cannot trust the input and should add the necessary validations. @@ -104,6 +105,28 @@ export function getNpmCustomRegistries() { return customRegistries.filter((item) => typeof item === "string"); } +/** + * Gets the custom npm registries from the config file (format parsing only, no validation) + * @returns {string[]} + */ +export function getPipCustomRegistries() { + const config = readConfigFile(); + + if (!config || !config.pip) { + return []; + } + + // TypeScript needs help understanding that config.pip exists and has customRegistries + const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip); + const customRegistries = pipConfig.customRegistries; + + if (!Array.isArray(customRegistries)) { + return []; + } + + return customRegistries.filter((item) => typeof item === "string"); +} + /** * @param {import("../api/aikido.js").MalwarePackage[]} data * @param {string | number} version @@ -169,6 +192,9 @@ function readConfigFile() { npm: { customRegistries: undefined, }, + pip: { + customRegistries: undefined, + }, }; const configFilePath = getConfigFilePath(); diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index f5c6df8..601b0d0 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -232,91 +232,95 @@ describe("getMinimumPackageAgeHours", async () => { }); }); -describe("getNpmCustomRegistries", async () => { - const { getNpmCustomRegistries } = await import("./configFile.js"); +for (const packageManager of ["npm", "pip"]) { + const fnName = `get${packageManager.charAt(0).toUpperCase()}${packageManager.slice(1)}CustomRegistries`; - afterEach(() => { - configFileContent = undefined; - }); + describe(fnName, async () => { + const fn = (await import("./configFile.js"))[fnName]; - it("should return empty array when config file doesn't exist", () => { - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, []); - }); - - it("should return empty array when npm config is not set", () => { - configFileContent = JSON.stringify({ scanTimeout: 5000 }); - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, []); - }); - - it("should return empty array when customRegistries is not an array", () => { - configFileContent = JSON.stringify({ - npm: { customRegistries: "not-an-array" }, + afterEach(() => { + configFileContent = undefined; }); - const registries = getNpmCustomRegistries(); + it("should return empty array when config file doesn't exist", () => { + configFileContent = undefined; - assert.deepStrictEqual(registries, []); - }); + const registries = fn(); - it("should return array of custom registries when set", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: ["npm.company.com", "registry.internal.net"], - }, + assert.deepStrictEqual(registries, []); }); - const registries = getNpmCustomRegistries(); + it(`should return empty array when ${packageManager} config is not set`, () => { + configFileContent = JSON.stringify({ scanTimeout: 5000 }); - assert.deepStrictEqual(registries, [ - "npm.company.com", - "registry.internal.net", - ]); - }); + const registries = fn(); - it("should filter out non-string values", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: [ - "npm.company.com", - 123, - null, - "registry.internal.net", - undefined, - {}, - ], - }, + assert.deepStrictEqual(registries, []); }); - const registries = getNpmCustomRegistries(); + it("should return empty array when customRegistries is not an array", () => { + configFileContent = JSON.stringify({ + [packageManager]: { customRegistries: "not-an-array" }, + }); - assert.deepStrictEqual(registries, [ - "npm.company.com", - "registry.internal.net", - ]); - }); + const registries = fn(); - it("should return empty array for empty customRegistries array", () => { - configFileContent = JSON.stringify({ - npm: { customRegistries: [] }, + assert.deepStrictEqual(registries, []); }); - const registries = getNpmCustomRegistries(); + it("should return array of custom registries when set", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [`${packageManager}.company.com`, "registry.internal.net"], + }, + }); - assert.deepStrictEqual(registries, []); + const registries = fn(); + + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "registry.internal.net", + ]); + }); + + it("should filter out non-string values", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [ + `${packageManager}.company.com`, + 123, + null, + "registry.internal.net", + undefined, + {}, + ], + }, + }); + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "registry.internal.net", + ]); + }); + + it("should return empty array for empty customRegistries array", () => { + configFileContent = JSON.stringify({ + [packageManager]: { customRegistries: [] }, + }); + + const registries = fn(); + + assert.deepStrictEqual(registries, []); + }); + + it("should handle malformed JSON and return empty array", () => { + configFileContent = "{ invalid json"; + + const registries = fn(); + + assert.deepStrictEqual(registries, []); + }); }); - - it("should handle malformed JSON and return empty array", () => { - configFileContent = "{ invalid json"; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, []); - }); -}); +} diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 3a756ea..573c3ab 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -152,11 +152,10 @@ export function getPipCustomRegistries() { const envRegistries = parseRegistriesFromEnv( environmentVariables.getPipCustomRegistries() ); - // const configRegistries = configFile.getPipCustomRegistries(); + const configRegistries = configFile.getPipCustomRegistries(); // Merge both sources and remove duplicates - // const allRegistries = [...envRegistries, ...configRegistries]; - const allRegistries = [...envRegistries]; + const allRegistries = [...envRegistries, ...configRegistries]; const uniqueRegistries = [...new Set(allRegistries)]; // Normalize each registry (remove protocol if any) diff --git a/packages/safe-chain/src/config/settings.spec.js b/packages/safe-chain/src/config/settings.spec.js index 05d698f..778628b 100644 --- a/packages/safe-chain/src/config/settings.spec.js +++ b/packages/safe-chain/src/config/settings.spec.js @@ -11,239 +11,244 @@ mock.module("fs", { }, }); -describe("getNpmCustomRegistries", async () => { - let originalEnv; - const { getNpmCustomRegistries } = await import("./settings.js"); +for (const packageManager of ["npm", "pip"]) { + const fnName = `get${packageManager.charAt(0).toUpperCase()}${packageManager.slice(1)}CustomRegistries`; + const envVarName = `SAFE_CHAIN_${packageManager.toUpperCase()}_CUSTOM_REGISTRIES`; - beforeEach(() => { - originalEnv = process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - }); + describe(fnName, async () => { + let originalEnv; + const fn = (await import("./settings.js"))[fnName]; - afterEach(() => { - if (originalEnv !== undefined) { - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = originalEnv; - } else { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - } - configFileContent = undefined; - }); - - it("should return empty array when no registries configured", () => { - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, []); - }); - - it("should return registries without protocol", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: ["npm.company.com", "registry.internal.net"], - }, + beforeEach(() => { + originalEnv = process.env[envVarName]; }); - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, [ - "npm.company.com", - "registry.internal.net", - ]); - }); - - it("should strip https:// protocol from registries", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: [ - "https://npm.company.com", - "https://registry.internal.net", - ], - }, + afterEach(() => { + if (originalEnv !== undefined) { + process.env[envVarName] = originalEnv; + } else { + delete process.env[envVarName]; + } + configFileContent = undefined; }); - const registries = getNpmCustomRegistries(); + it("should return empty array when no registries configured", () => { + configFileContent = undefined; - assert.deepStrictEqual(registries, [ - "npm.company.com", - "registry.internal.net", - ]); - }); + const registries = fn(); - it("should strip http:// protocol from registries", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: [ - "http://npm.company.com", - "http://registry.internal.net", - ], - }, + assert.deepStrictEqual(registries, []); }); - const registries = getNpmCustomRegistries(); + it("should return registries without protocol", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [`${packageManager}.company.com`, "registry.internal.net"], + }, + }); - assert.deepStrictEqual(registries, [ - "npm.company.com", - "registry.internal.net", - ]); - }); + const registries = fn(); - it("should handle mixed protocols and no protocol", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: [ - "https://npm.company.com", - "registry.internal.net", - "http://private.registry.io", - ], - }, + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "registry.internal.net", + ]); }); - const registries = getNpmCustomRegistries(); + it("should strip https:// protocol from registries", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [ + `https://${packageManager}.company.com`, + "https://registry.internal.net", + ], + }, + }); - assert.deepStrictEqual(registries, [ - "npm.company.com", - "registry.internal.net", - "private.registry.io", - ]); - }); + const registries = fn(); - it("should preserve registry path after stripping protocol", () => { - configFileContent = JSON.stringify({ - npm: { - customRegistries: [ - "https://npm.company.com/custom/path", - "registry.internal.net/npm", - ], - }, + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "registry.internal.net", + ]); }); - const registries = getNpmCustomRegistries(); + it("should strip http:// protocol from registries", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [ + `http://${packageManager}.company.com`, + "http://registry.internal.net", + ], + }, + }); - assert.deepStrictEqual(registries, [ - "npm.company.com/custom/path", - "registry.internal.net/npm", - ]); - }); + const registries = fn(); - it("should parse comma-separated registries from environment variable", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = - "env1.registry.com,env2.registry.net"; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, [ - "env1.registry.com", - "env2.registry.net", - ]); - }); - - it("should trim whitespace from environment variable registries", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = - " env1.registry.com , env2.registry.net "; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, [ - "env1.registry.com", - "env2.registry.net", - ]); - }); - - it("should merge environment variable and config file registries", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "env1.registry.com"; - configFileContent = JSON.stringify({ - npm: { - customRegistries: ["config1.registry.net"], - }, + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "registry.internal.net", + ]); }); - const registries = getNpmCustomRegistries(); + it("should handle mixed protocols and no protocol", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [ + `https://${packageManager}.company.com`, + "registry.internal.net", + "http://private.registry.io", + ], + }, + }); - assert.deepStrictEqual(registries, [ - "env1.registry.com", - "config1.registry.net", - ]); - }); + const registries = fn(); - it("should remove duplicate registries when merging env and config", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = - "npm.company.com,env.registry.com"; - configFileContent = JSON.stringify({ - npm: { - customRegistries: ["npm.company.com", "config.registry.net"], - }, + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "registry.internal.net", + "private.registry.io", + ]); }); - const registries = getNpmCustomRegistries(); + it("should preserve registry path after stripping protocol", () => { + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [ + `https://${packageManager}.company.com/custom/path`, + `registry.internal.net/${packageManager}`, + ], + }, + }); - assert.deepStrictEqual(registries, [ - "npm.company.com", - "env.registry.com", - "config.registry.net", - ]); + const registries = fn(); + + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com/custom/path`, + `registry.internal.net/${packageManager}`, + ]); + }); + + it("should parse comma-separated registries from environment variable", () => { + delete process.env[envVarName]; + process.env[envVarName] = + "env1.registry.com,env2.registry.net"; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + "env1.registry.com", + "env2.registry.net", + ]); + }); + + it("should trim whitespace from environment variable registries", () => { + delete process.env[envVarName]; + process.env[envVarName] = + " env1.registry.com , env2.registry.net "; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + "env1.registry.com", + "env2.registry.net", + ]); + }); + + it("should merge environment variable and config file registries", () => { + delete process.env[envVarName]; + process.env[envVarName] = "env1.registry.com"; + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: ["config1.registry.net"], + }, + }); + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + "env1.registry.com", + "config1.registry.net", + ]); + }); + + it("should remove duplicate registries when merging env and config", () => { + delete process.env[envVarName]; + process.env[envVarName] = + `${packageManager}.company.com,env.registry.com`; + configFileContent = JSON.stringify({ + [packageManager]: { + customRegistries: [`${packageManager}.company.com`, "config.registry.net"], + }, + }); + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + `${packageManager}.company.com`, + "env.registry.com", + "config.registry.net", + ]); + }); + + it("should normalize protocols from environment variable registries", () => { + delete process.env[envVarName]; + process.env[envVarName] = + "https://env1.registry.com,http://env2.registry.net"; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + "env1.registry.com", + "env2.registry.net", + ]); + }); + + it("should handle empty strings in comma-separated list", () => { + delete process.env[envVarName]; + process.env[envVarName] = + "env1.registry.com,,env2.registry.net,"; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, [ + "env1.registry.com", + "env2.registry.net", + ]); + }); + + it("should handle single registry in environment variable", () => { + delete process.env[envVarName]; + process.env[envVarName] = "single.registry.com"; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, ["single.registry.com"]); + }); + + it("should return empty array for empty environment variable", () => { + delete process.env[envVarName]; + process.env[envVarName] = ""; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, []); + }); + + it("should return empty array for whitespace-only environment variable", () => { + delete process.env[envVarName]; + process.env[envVarName] = " , , "; + configFileContent = undefined; + + const registries = fn(); + + assert.deepStrictEqual(registries, []); + }); }); - - it("should normalize protocols from environment variable registries", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = - "https://env1.registry.com,http://env2.registry.net"; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, [ - "env1.registry.com", - "env2.registry.net", - ]); - }); - - it("should handle empty strings in comma-separated list", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = - "env1.registry.com,,env2.registry.net,"; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, [ - "env1.registry.com", - "env2.registry.net", - ]); - }); - - it("should handle single registry in environment variable", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "single.registry.com"; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, ["single.registry.com"]); - }); - - it("should return empty array for empty environment variable", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = ""; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, []); - }); - - it("should return empty array for whitespace-only environment variable", () => { - delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; - process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = " , , "; - configFileContent = undefined; - - const registries = getNpmCustomRegistries(); - - assert.deepStrictEqual(registries, []); - }); -}); +} From a0e19818a095b3e5241494d1c8f277a15658d74c Mon Sep 17 00:00:00 2001 From: Graeme Chapman Date: Wed, 31 Dec 2025 10:18:58 +0000 Subject: [PATCH 04/36] fix: Allow running commands if safe-chain npm package is not installed --- .../src/shell-integration/startup-scripts/init-posix.sh | 4 ++++ 1 file changed, 4 insertions(+) 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 f22f79b..7085465 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 @@ -83,6 +83,10 @@ function wrapSafeChainCommand() { # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" + # Remove the first argument (original_cmd) from $@ + # so that "$@" now contains only the arguments passed to the original command + shift 1 + command "$original_cmd" "$@" fi } From c510d886a95f62070355f3ee49efe1bbee7b2d70 Mon Sep 17 00:00:00 2001 From: Graeme Chapman Date: Wed, 31 Dec 2025 10:57:08 +0000 Subject: [PATCH 05/36] Simplify command execution in init-posix.sh --- .../src/shell-integration/startup-scripts/init-posix.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 7085465..e649909 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 @@ -83,10 +83,6 @@ function wrapSafeChainCommand() { # If the aikido command is not available, print a warning and run the original command printSafeChainWarning "$original_cmd" - # Remove the first argument (original_cmd) from $@ - # so that "$@" now contains only the arguments passed to the original command - shift 1 - - command "$original_cmd" "$@" + command "$@" fi } From b23ba9d9c400f7d0e1a2bb063b3ebc774b91c379 Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 2 Jan 2026 10:39:15 +0100 Subject: [PATCH 06/36] chore: update test parametrization --- .../safe-chain/src/config/configFile.spec.js | 34 +++++++----- .../safe-chain/src/config/settings.spec.js | 52 ++++++++++++------- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index 601b0d0..eff4048 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -232,12 +232,22 @@ describe("getMinimumPackageAgeHours", async () => { }); }); -for (const packageManager of ["npm", "pip"]) { - const fnName = `get${packageManager.charAt(0).toUpperCase()}${packageManager.slice(1)}CustomRegistries`; - - describe(fnName, async () => { - const fn = (await import("./configFile.js"))[fnName]; +const { getNpmCustomRegistries, getPipCustomRegistries } = await import( + "./configFile.js" +); +for (const { packageManager, getCustomRegistries } of [ + { + packageManager: "npm", + getCustomRegistries: getNpmCustomRegistries, + }, + { + packageManager: "pip", + getCustomRegistries: getPipCustomRegistries, + }, +]) +{ + describe(getCustomRegistries.name, async () => { afterEach(() => { configFileContent = undefined; }); @@ -245,7 +255,7 @@ for (const packageManager of ["npm", "pip"]) { it("should return empty array when config file doesn't exist", () => { configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); @@ -253,7 +263,7 @@ for (const packageManager of ["npm", "pip"]) { it(`should return empty array when ${packageManager} config is not set`, () => { configFileContent = JSON.stringify({ scanTimeout: 5000 }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); @@ -263,7 +273,7 @@ for (const packageManager of ["npm", "pip"]) { [packageManager]: { customRegistries: "not-an-array" }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); @@ -275,7 +285,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -297,7 +307,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -310,7 +320,7 @@ for (const packageManager of ["npm", "pip"]) { [packageManager]: { customRegistries: [] }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); @@ -318,7 +328,7 @@ for (const packageManager of ["npm", "pip"]) { it("should handle malformed JSON and return empty array", () => { configFileContent = "{ invalid json"; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); diff --git a/packages/safe-chain/src/config/settings.spec.js b/packages/safe-chain/src/config/settings.spec.js index 778628b..db513f3 100644 --- a/packages/safe-chain/src/config/settings.spec.js +++ b/packages/safe-chain/src/config/settings.spec.js @@ -11,13 +11,25 @@ mock.module("fs", { }, }); -for (const packageManager of ["npm", "pip"]) { - const fnName = `get${packageManager.charAt(0).toUpperCase()}${packageManager.slice(1)}CustomRegistries`; - const envVarName = `SAFE_CHAIN_${packageManager.toUpperCase()}_CUSTOM_REGISTRIES`; +const { getNpmCustomRegistries, getPipCustomRegistries } = await import( + "./settings.js" +); - describe(fnName, async () => { +for (const { packageManager, getCustomRegistries, envVarName } of [ + { + packageManager: "npm", + getCustomRegistries: getNpmCustomRegistries, + envVarName: "SAFE_CHAIN_NPM_CUSTOM_REGISTRIES", + }, + { + packageManager: "pip", + getCustomRegistries: getPipCustomRegistries, + envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES", + }, +]) +{ + describe(getCustomRegistries.name, async () => { let originalEnv; - const fn = (await import("./settings.js"))[fnName]; beforeEach(() => { originalEnv = process.env[envVarName]; @@ -35,7 +47,7 @@ for (const packageManager of ["npm", "pip"]) { it("should return empty array when no registries configured", () => { configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); @@ -47,7 +59,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -65,7 +77,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -83,7 +95,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -102,7 +114,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -121,7 +133,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com/custom/path`, @@ -135,7 +147,7 @@ for (const packageManager of ["npm", "pip"]) { "env1.registry.com,env2.registry.net"; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ "env1.registry.com", @@ -149,7 +161,7 @@ for (const packageManager of ["npm", "pip"]) { " env1.registry.com , env2.registry.net "; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ "env1.registry.com", @@ -166,7 +178,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ "env1.registry.com", @@ -184,7 +196,7 @@ for (const packageManager of ["npm", "pip"]) { }, }); - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ `${packageManager}.company.com`, @@ -199,7 +211,7 @@ for (const packageManager of ["npm", "pip"]) { "https://env1.registry.com,http://env2.registry.net"; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ "env1.registry.com", @@ -213,7 +225,7 @@ for (const packageManager of ["npm", "pip"]) { "env1.registry.com,,env2.registry.net,"; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, [ "env1.registry.com", @@ -226,7 +238,7 @@ for (const packageManager of ["npm", "pip"]) { process.env[envVarName] = "single.registry.com"; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, ["single.registry.com"]); }); @@ -236,7 +248,7 @@ for (const packageManager of ["npm", "pip"]) { process.env[envVarName] = ""; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); @@ -246,7 +258,7 @@ for (const packageManager of ["npm", "pip"]) { process.env[envVarName] = " , , "; configFileContent = undefined; - const registries = fn(); + const registries = getCustomRegistries(); assert.deepStrictEqual(registries, []); }); From a910851422fa9238f0ded30272963e257224fa13 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 5 Jan 2026 14:15:28 +0100 Subject: [PATCH 07/36] Build for linuxstatic and alpine --- .github/workflows/create-artifact.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index d7729fd..5168d6e 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -39,6 +39,26 @@ jobs: runner: ubuntu-24.04-arm target: node20-linux-arm64 extension: "" + - os: linux + arch: x64 + runner: ubuntu-latest + target: node20-linuxstatic-x64 + extension: "" + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + target: node20-linuxstatic-arm64 + extension: "" + - os: linux + arch: x64 + runner: ubuntu-latest + target: node20-alpine-x64 + extension: "" + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + target: node20-alpine-arm64 + extension: "" - os: win arch: x64 runner: windows-latest From 40b8638dddbda703ab3ebe76cb30ec4778a40f4f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 5 Jan 2026 14:24:19 +0100 Subject: [PATCH 08/36] Fix artifact name --- .github/workflows/create-artifact.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 5168d6e..d11447e 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -39,22 +39,22 @@ jobs: runner: ubuntu-24.04-arm target: node20-linux-arm64 extension: "" - - os: linux + - os: linuxstatic arch: x64 runner: ubuntu-latest target: node20-linuxstatic-x64 extension: "" - - os: linux + - os: linuxstatic arch: arm64 runner: ubuntu-24.04-arm target: node20-linuxstatic-arm64 extension: "" - - os: linux + - os: alpine arch: x64 runner: ubuntu-latest target: node20-alpine-x64 extension: "" - - os: linux + - os: alpine arch: arm64 runner: ubuntu-24.04-arm target: node20-alpine-arm64 From 35ca2233f82a257ffe931b37656d41c561508be7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 5 Jan 2026 15:45:57 +0100 Subject: [PATCH 09/36] Use linuxstatic target for linux --- .github/workflows/create-artifact.yml | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index d11447e..bba0d46 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -32,33 +32,13 @@ jobs: - os: linux arch: x64 runner: ubuntu-latest - target: node20-linux-x64 + target: node20-linuxstatic-x64 extension: "" - os: linux - arch: arm64 - runner: ubuntu-24.04-arm - target: node20-linux-arm64 - extension: "" - - os: linuxstatic - arch: x64 - runner: ubuntu-latest - target: node20-linuxstatic-x64 - extension: "" - - os: linuxstatic arch: arm64 runner: ubuntu-24.04-arm target: node20-linuxstatic-arm64 extension: "" - - os: alpine - arch: x64 - runner: ubuntu-latest - target: node20-alpine-x64 - extension: "" - - os: alpine - arch: arm64 - runner: ubuntu-24.04-arm - target: node20-alpine-arm64 - extension: "" - os: win arch: x64 runner: windows-latest From 52a096b7395c0caa05fa74b83d77abaa86f3718d Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 5 Jan 2026 15:47:31 +0100 Subject: [PATCH 10/36] Re-order steps --- .github/workflows/build-and-release.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 83c11d9..93cfb8d 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -55,17 +55,6 @@ jobs: - name: Run tests run: npm run test - - name: Copy documentation files to package - run: | - cp README.md packages/safe-chain/ - cp LICENSE packages/safe-chain/ - cp -r docs packages/safe-chain/ - - - name: Publish to npm - run: | - echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance - - name: Download all binary artifacts uses: actions/download-artifact@v4 with: @@ -107,3 +96,14 @@ jobs: release-artifacts/install-safe-chain.ps1 \ release-artifacts/uninstall-safe-chain.sh \ release-artifacts/uninstall-safe-chain.ps1 + + - name: Copy documentation files to package + run: | + cp README.md packages/safe-chain/ + cp LICENSE packages/safe-chain/ + cp -r docs packages/safe-chain/ + + - name: Publish to npm + run: | + echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" + npm publish --workspace=packages/safe-chain --access public --provenance From d530b9a1de6d75286c82e83fa5c8501c78a210c8 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 08:17:35 +0100 Subject: [PATCH 11/36] Run tests with 0.0.1-docker-linux-exec-beta --- .github/workflows/build-and-release.yml | 2 +- .github/workflows/create-artifact.yml | 4 ++-- .github/workflows/test-on-pr.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 83c11d9..fe180b3 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -44,7 +44,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Set the version in safe-chain package run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index bba0d46..f5bc9f8 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -61,12 +61,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 9e4a5ec..bff7e51 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -24,12 +24,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts @@ -114,7 +114,7 @@ jobs: node-version: "lts/*" - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain/install-safe-chain.sh | sh -s -- --ci - name: Install dependencies (root) run: npm ci From ff4618602a00ff3a28c534defa0cdbef3acdd9ae Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 09:02:22 +0100 Subject: [PATCH 12/36] Add extra artifact for linuxstatic, change install script to use it. --- .github/workflows/create-artifact.yml | 12 +++++++++++- install-scripts/install-safe-chain.sh | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index f5bc9f8..b9a538e 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -32,9 +32,19 @@ jobs: - os: linux arch: x64 runner: ubuntu-latest - target: node20-linuxstatic-x64 + target: node20-linux-x64 extension: "" - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + target: node20-linux-arm64 + extension: "" + - os: linuxstatic + arch: x64 + runner: ubuntu-latest + target: node20-linuxstatic-x64 + extension: "" + - os: linuxstatic arch: arm64 runner: ubuntu-24.04-arm target: node20-linuxstatic-arm64 diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 94a9b55..1de2d23 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -34,7 +34,7 @@ error() { # Detect OS detect_os() { case "$(uname -s)" in - Linux*) echo "linux" ;; + Linux*) echo "linuxstatic" ;; Darwin*) echo "macos" ;; *) error "Unsupported operating system: $(uname -s)" ;; esac From 50f20cc30dcef460fc17041043c51d7fe764d542 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 08:17:35 +0100 Subject: [PATCH 13/36] Run tests with 0.0.1-docker-linux-exec-beta --- .github/workflows/build-and-release.yml | 2 +- .github/workflows/create-artifact.yml | 4 ++-- .github/workflows/test-on-pr.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 93cfb8d..fcb010a 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -44,7 +44,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Set the version in safe-chain package run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index bba0d46..f5bc9f8 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -61,12 +61,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 9e4a5ec..bff7e51 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -24,12 +24,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts @@ -114,7 +114,7 @@ jobs: node-version: "lts/*" - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain/install-safe-chain.sh | sh -s -- --ci - name: Install dependencies (root) run: npm ci From eb32da49aad4632f2f172392327807714ee322e9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 09:02:22 +0100 Subject: [PATCH 14/36] Add extra artifact for linuxstatic, change install script to use it. --- .github/workflows/create-artifact.yml | 12 +++++++++++- install-scripts/install-safe-chain.sh | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index f5bc9f8..b9a538e 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -32,9 +32,19 @@ jobs: - os: linux arch: x64 runner: ubuntu-latest - target: node20-linuxstatic-x64 + target: node20-linux-x64 extension: "" - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + target: node20-linux-arm64 + extension: "" + - os: linuxstatic + arch: x64 + runner: ubuntu-latest + target: node20-linuxstatic-x64 + extension: "" + - os: linuxstatic arch: arm64 runner: ubuntu-24.04-arm target: node20-linuxstatic-arm64 diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 94a9b55..1de2d23 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -34,7 +34,7 @@ error() { # Detect OS detect_os() { case "$(uname -s)" in - Linux*) echo "linux" ;; + Linux*) echo "linuxstatic" ;; Darwin*) echo "macos" ;; *) error "Unsupported operating system: $(uname -s)" ;; esac From 24230da4a7a9e706a3b625b4baf8132689524b59 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:05:52 +0100 Subject: [PATCH 15/36] Add nvm safe-chain uninstallation in install script --- install-scripts/install-safe-chain.sh | 57 ++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 94a9b55..6f0dd26 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -159,6 +159,57 @@ remove_volta_installation() { fi } +# Check and uninstall nvm-managed package if present across all Node versions +remove_nvm_installation() { + # Check if nvm is available as a command + if ! command_exists nvm; then + return + fi + + # Get list of installed Node versions + nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "") + + if [ -z "$nvm_versions" ]; then + return + fi + + # Track if we found any installations + found_installation=false + uninstall_failed=false + current_version=$(nvm current 2>/dev/null || echo "") + + # Check each version for safe-chain installation + for version in $nvm_versions; do + # Check if this version has safe-chain installed + # Use nvm exec to run npm list in the context of that Node version + if nvm exec "$version" npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then + if [ "$found_installation" = false ]; then + info "Detected nvm installation(s) of @aikidosec/safe-chain" + info "Uninstalling from all Node versions..." + found_installation=true + fi + + info " Removing from Node $version..." + if nvm exec "$version" npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then + info " Successfully uninstalled from Node $version" + else + warn " Failed to uninstall from Node $version" + uninstall_failed=true + fi + fi + done + + # Restore original Node version if it was set + if [ -n "$current_version" ] && [ "$current_version" != "none" ] && [ "$current_version" != "system" ]; then + nvm use "$current_version" >/dev/null 2>&1 || true + fi + + # If any uninstall failed, error out instead of continuing + if [ "$uninstall_failed" = true ]; then + error "Failed to uninstall @aikidosec/safe-chain from all nvm Node versions. Please uninstall manually and try again." + fi +} + # Parse command-line arguments parse_arguments() { for arg in "$@"; do @@ -204,9 +255,11 @@ main() { info "$INSTALL_MSG" - # Check for existing safe-chain installation through npm or volta - remove_npm_installation + # Check for existing safe-chain installation through nvm, volta, or npm + # nvm must be checked first as it manages multiple Node versions + remove_nvm_installation remove_volta_installation + remove_npm_installation # Detect platform OS=$(detect_os) From efe3b24ab9906482fb36982ef7cdb1e1745ac8ff Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:07:40 +0100 Subject: [PATCH 16/36] Comment npm publish step --- .github/workflows/build-and-release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 83c11d9..1c05824 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -61,10 +61,10 @@ jobs: cp LICENSE packages/safe-chain/ cp -r docs packages/safe-chain/ - - name: Publish to npm - run: | - echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance + # - name: Publish to npm + # run: | + # echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" + # npm publish --workspace=packages/safe-chain --access public --provenance - name: Download all binary artifacts uses: actions/download-artifact@v4 From 6bbd3f59558b1ccfddb10854a75161c969d6cd9f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:35:10 +0100 Subject: [PATCH 17/36] Add nvm detection to uninstall script --- install-scripts/uninstall-safe-chain.sh | 56 ++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index 4b2d7ec..8d1fbdf 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -75,6 +75,58 @@ remove_volta_installation() { fi } +# Check and uninstall nvm-managed package if present across all Node versions +remove_nvm_installation() { + # Check if nvm is available as a command + if ! command_exists nvm; then + return + fi + + # Get list of installed Node versions + nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "") + + if [ -z "$nvm_versions" ]; then + return + fi + + # Track if we found any installations + found_installation=false + uninstall_failed=false + current_version=$(nvm current 2>/dev/null || echo "") + + # Check each version for safe-chain installation + for version in $nvm_versions; do + # Check if this version has safe-chain installed + # Use nvm exec to run npm list in the context of that Node version + if nvm exec "$version" npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then + if [ "$found_installation" = false ]; then + info "Detected nvm installation(s) of @aikidosec/safe-chain" + info "Uninstalling from all Node versions..." + found_installation=true + fi + + info " Removing from Node $version..." + if nvm exec "$version" npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then + info " Successfully uninstalled from Node $version" + else + warn " Failed to uninstall from Node $version" + uninstall_failed=true + fi + fi + done + + # Restore original Node version if it was set + if [ -n "$current_version" ] && [ "$current_version" != "none" ] && [ "$current_version" != "system" ]; then + nvm use "$current_version" >/dev/null 2>&1 || true + fi + + # Show warning if any uninstall failed (but don't error out during uninstall) + if [ "$uninstall_failed" = true ]; then + warn "Failed to uninstall @aikidosec/safe-chain from some nvm Node versions" + warn "You may need to manually run: nvm exec npm uninstall -g @aikidosec/safe-chain" + fi +} + # Main uninstallation main() { SAFE_CHAIN_LOCATION="$INSTALL_DIR/safe-chain" @@ -89,8 +141,10 @@ main() { warn "safe-chain command not found. Proceeding with uninstallation." fi - remove_npm_installation + # Remove npm-based installations (nvm must be checked first) + remove_nvm_installation remove_volta_installation + remove_npm_installation # Remove install dir recursively if it exists if [ -d "$INSTALL_DIR" ]; then From 10a2407b3227a67c9cd9ec36e85037a154b9fad4 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:43:15 +0100 Subject: [PATCH 18/36] Source nvm in script --- install-scripts/install-safe-chain.sh | 10 +++++++++- install-scripts/uninstall-safe-chain.sh | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 6f0dd26..63e622e 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -161,7 +161,15 @@ remove_volta_installation() { # Check and uninstall nvm-managed package if present across all Node versions remove_nvm_installation() { - # Check if nvm is available as a command + # nvm is a shell function, not a binary, so we need to source it first + if [ -s "$HOME/.nvm/nvm.sh" ]; then + # Source nvm to make it available in this script + . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1 + elif [ -s "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 + fi + + # Check if nvm is now available if ! command_exists nvm; then return fi diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index 8d1fbdf..15c4f96 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -77,7 +77,15 @@ remove_volta_installation() { # Check and uninstall nvm-managed package if present across all Node versions remove_nvm_installation() { - # Check if nvm is available as a command + # nvm is a shell function, not a binary, so we need to source it first + if [ -s "$HOME/.nvm/nvm.sh" ]; then + # Source nvm to make it available in this script + . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1 + elif [ -s "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 + fi + + # Check if nvm is now available if ! command_exists nvm; then return fi From 5a28d6646f28394eb1018d345b4c158f41cb639f Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:53:24 +0100 Subject: [PATCH 19/36] Update comments --- install-scripts/install-safe-chain.sh | 5 +++-- install-scripts/uninstall-safe-chain.sh | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 63e622e..8e184da 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -161,7 +161,9 @@ remove_volta_installation() { # Check and uninstall nvm-managed package if present across all Node versions remove_nvm_installation() { - # nvm is a shell function, not a binary, so we need to source it first + # This script is run in sh shell for greatest compatibility. + # Because nvm is usually setup in bash/zsh/fish startup scripts, we need to source it. + # Otherwise it won't be available in sh. if [ -s "$HOME/.nvm/nvm.sh" ]; then # Source nvm to make it available in this script . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1 @@ -174,7 +176,6 @@ remove_nvm_installation() { return fi - # Get list of installed Node versions nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "") if [ -z "$nvm_versions" ]; then diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index 15c4f96..7b226a5 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -77,7 +77,9 @@ remove_volta_installation() { # Check and uninstall nvm-managed package if present across all Node versions remove_nvm_installation() { - # nvm is a shell function, not a binary, so we need to source it first + # This script is run in sh shell for greatest compatibility. + # Because nvm is usually setup in bash/zsh/fish startup scripts, we need to source it. + # Otherwise it won't be available in sh. if [ -s "$HOME/.nvm/nvm.sh" ]; then # Source nvm to make it available in this script . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1 From d7d5bacd2158ffed87171519148d7cb54915419e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:53:32 +0100 Subject: [PATCH 20/36] Remove warning from readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index a13395c..f08daad 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,6 @@ Aikido Safe Chain supports the following package managers: Installing the Aikido Safe Chain is easy with our one-line installer. -> ⚠️ **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 ```shell @@ -206,6 +204,7 @@ You can set the minimum package age through multiple sources (in order of priori Configure Safe Chain to scan packages from custom or private registries. Supported ecosystems: + - Node.js - Python @@ -348,5 +347,4 @@ pipeline { } ``` - After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. From 4aca6ef86a9f564c7bf0e18b44079e0cac4f9180 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 10:54:34 +0100 Subject: [PATCH 21/36] Restore publish script --- .github/workflows/build-and-release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 1c05824..83c11d9 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -61,10 +61,10 @@ jobs: cp LICENSE packages/safe-chain/ cp -r docs packages/safe-chain/ - # - name: Publish to npm - # run: | - # echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" - # npm publish --workspace=packages/safe-chain --access public --provenance + - name: Publish to npm + run: | + echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" + npm publish --workspace=packages/safe-chain --access public --provenance - name: Download all binary artifacts uses: actions/download-artifact@v4 From 4e098bcff746f3ed0c0904e357be5671dd88ea16 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 11:23:47 +0100 Subject: [PATCH 22/36] Change order of removal for npm-based installations --- install-scripts/install-safe-chain.sh | 5 ++--- install-scripts/uninstall-safe-chain.sh | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 8e184da..80e4493 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -265,10 +265,9 @@ main() { info "$INSTALL_MSG" # Check for existing safe-chain installation through nvm, volta, or npm - # nvm must be checked first as it manages multiple Node versions - remove_nvm_installation - remove_volta_installation remove_npm_installation + remove_volta_installation + remove_nvm_installation # Detect platform OS=$(detect_os) diff --git a/install-scripts/uninstall-safe-chain.sh b/install-scripts/uninstall-safe-chain.sh index 7b226a5..e208319 100755 --- a/install-scripts/uninstall-safe-chain.sh +++ b/install-scripts/uninstall-safe-chain.sh @@ -151,10 +151,10 @@ main() { warn "safe-chain command not found. Proceeding with uninstallation." fi - # Remove npm-based installations (nvm must be checked first) - remove_nvm_installation - remove_volta_installation + # Check for existing safe-chain installation through nvm, volta, or npm remove_npm_installation + remove_volta_installation + remove_nvm_installation # Remove install dir recursively if it exists if [ -d "$INSTALL_DIR" ]; then From 66c1da0f1e36ebe1845db9ca7e54816f5c788092 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 11:48:06 +0100 Subject: [PATCH 23/36] Rework release workflow (split npm and github release), and skip npm publish for prereleases --- .github/workflows/build-and-release.yml | 86 ++++++++++++++++--------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 83c11d9..c0256a9 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -11,9 +11,11 @@ permissions: jobs: set-version: + name: Set version number runs-on: ubuntu-latest outputs: version: ${{ steps.get_version.outputs.tag }} + is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} steps: - name: Set version number id: get_version @@ -21,13 +23,23 @@ jobs: version="${{ github.ref_name }}" echo "tag=$version" >> $GITHUB_OUTPUT + - name: Check if pre-release + id: check_prerelease + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + IS_PRERELEASE=$(gh release view ${{ steps.get_version.outputs.tag }} --json isPrerelease --jq '.isPrerelease') + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + echo "Release ${{ steps.get_version.outputs.tag }} is pre-release: $IS_PRERELEASE" + create-binaries: needs: set-version uses: ./.github/workflows/create-artifact.yml with: version: ${{ needs.set-version.outputs.version }} - build: + publish-binaries: + name: Publish to GitHub release needs: [set-version, create-binaries] runs-on: ubuntu-latest @@ -35,37 +47,6 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: "lts/*" - registry-url: "https://registry.npmjs.org/" - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - - - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - - - name: Set the version in safe-chain package - run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm run test - - - name: Copy documentation files to package - run: | - cp README.md packages/safe-chain/ - cp LICENSE packages/safe-chain/ - cp -r docs packages/safe-chain/ - - - name: Publish to npm - run: | - echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" - npm publish --workspace=packages/safe-chain --access public --provenance - - name: Download all binary artifacts uses: actions/download-artifact@v4 with: @@ -107,3 +88,44 @@ jobs: release-artifacts/install-safe-chain.ps1 \ release-artifacts/uninstall-safe-chain.sh \ release-artifacts/uninstall-safe-chain.ps1 + + publish-npm: + name: Publish to npm + needs: [set-version, create-binaries] + if: needs.set-version.outputs.is_prerelease != 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "lts/*" + registry-url: "https://registry.npmjs.org/" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + + - name: Setup safe-chain + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + + - name: Set the version in safe-chain package + run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test + + - name: Copy documentation files to package + run: | + cp README.md packages/safe-chain/ + cp LICENSE packages/safe-chain/ + cp -r docs packages/safe-chain/ + + - name: Publish to npm + run: | + echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM" + npm publish --workspace=packages/safe-chain --access public --provenance From 1f4e50df9db9dbf63aa5f9182b10a99a6f01d8e9 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 11:51:01 +0100 Subject: [PATCH 24/36] Checkout code in set version --- .github/workflows/build-and-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index c0256a9..a372e1e 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -17,6 +17,9 @@ jobs: version: ${{ steps.get_version.outputs.tag }} is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set version number id: get_version run: | From e8f993623bceeb11032015cca37be03db6fcb6d6 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 15:48:15 +0100 Subject: [PATCH 25/36] Add troubleshooting docs --- README.md | 4 + docs/npm-to-binary-migration.md | 89 ------------ docs/troubleshooting.md | 248 ++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 89 deletions(-) delete mode 100644 docs/npm-to-binary-migration.md create mode 100644 docs/troubleshooting.md diff --git a/README.md b/README.md index f08daad..14dc26c 100644 --- a/README.md +++ b/README.md @@ -348,3 +348,7 @@ pipeline { ``` After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection. + +# Troubleshooting + +Having issues? See the [Troubleshooting Guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md) for help with common problems. diff --git a/docs/npm-to-binary-migration.md b/docs/npm-to-binary-migration.md deleted file mode 100644 index c29a044..0000000 --- a/docs/npm-to-binary-migration.md +++ /dev/null @@ -1,89 +0,0 @@ -# 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](https://github.com/AikidoSec/safe-chain/blob/main/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](https://github.com/AikidoSec/safe-chain/blob/main/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](https://github.com/AikidoSec/safe-chain/blob/main/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. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..0e95f56 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,248 @@ +# Troubleshooting + +This guide helps you diagnose and resolve common issues with Aikido Safe Chain. + +## Verification & Diagnostics + +### Check Installation + +```bash +# Check version +safe-chain --version +``` + +### Verify Shell Integration + +Run the verification command for your package manager: + +```bash +npm safe-chain-verify +pnpm safe-chain-verify +pip safe-chain-verify +uv safe-chain-verify + +# Any other supported package manager: {packagemanager} safe-chain-verify +``` + +Expected output: `OK: Safe-chain works!` + +### Test Malware Blocking + +Verify that malware detection is working: + +**For JavaScript/Node.js:** + +```bash +npm install safe-chain-test +``` + +**For Python:** + +```bash +pip3 install safe-chain-pi-test +``` + +These test packages are flagged as malware and should be blocked by Safe Chain. + +### Logging Options + +Use logging flags to get more information: + +```bash +# Verbose mode - detailed diagnostic output for troubleshooting +npm install express --safe-chain-logging=verbose + +# Silent mode - suppress all output except malware blocking +npm install express --safe-chain-logging=silent +``` + +## Common Issues + +### Shell Aliases Not Working After Installation + +**Symptom:** Running `npm` shows regular npm instead of safe-chain wrapped version + +**First step:** Restart your terminal (most common fix) + +**Verify it's working:** + +```bash +type npm +``` + +Should show: `npm is a function` + +**If still not working:** + +Check that your startup file sources safe-chain scripts from `~/.safe-chain/scripts/`: + +- Bash: `~/.bashrc` +- Zsh: `~/.zshrc` +- Fish: `~/.config/fish/config.fish` +- PowerShell: `$PROFILE` + +### "Command Not Found: safe-chain" + +**Symptom:** Binary not found in PATH + +**First step:** Restart your terminal + +**Check PATH:** + +```bash +echo $PATH +``` + +Should include `~/.safe-chain/bin` + +**If persists:** Re-run the installation script + +### Shell Aliases Persist After Uninstallation + +**Symptom:** safe-chain commands still active after running uninstall script + +**Steps:** + +1. Run `safe-chain teardown` (if binary still exists) +2. Restart your terminal +3. If still present, manually edit shell config files: + - Bash: `~/.bashrc` + - Zsh: `~/.zshrc` + - Fish: `~/.config/fish/config.fish` + - PowerShell: `$PROFILE` +4. Remove lines that source scripts from `~/.safe-chain/scripts/` +5. Restart terminal again + +## Manual Verification Steps + +### Check Installation Status + +```bash +# Check installation location (helps identify if installed via npm or as standalone binary) +which safe-chain + +# Verify binary exists +ls ~/.safe-chain/bin/safe-chain + +# Check version +safe-chain --version + +# Test shell integration +type npm +type pip +``` + +**Expected `which` output:** +- Standalone binary (correct): `~/.safe-chain/bin/safe-chain` or `/Users//.safe-chain/bin/safe-chain` +- npm global (outdated): path containing `node_modules` or nvm version paths + +If `which` shows an npm installation, see [Check for Conflicting Installations](#check-for-conflicting-installations). + +### Check Shell Integration + +```bash +# Which shell you're using +echo $SHELL + +# Check if startup file sources safe-chain +# For Bash: +grep safe-chain ~/.bashrc + +# For Zsh: +grep safe-chain ~/.zshrc + +# For Fish: +grep safe-chain ~/.config/fish/config.fish + +# Verify scripts exist +ls ~/.safe-chain/scripts/ +``` + +### Check for Conflicting Installations + +The install/uninstall scripts automatically detect and remove conflicting installations, but you can manually check: + +```bash +# Check npm global +npm list -g @aikidosec/safe-chain + +# Check Volta +volta list safe-chain + +# Check nvm (all versions) +for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do + nvm exec "$version" npm list -g @aikidosec/safe-chain 2>/dev/null && echo "Found in $version" +done +``` + +## Manual Cleanup + +> **Note:** The install and uninstall scripts automatically handle these cleanup steps. Use these manual commands only if automatic cleanup fails. + +### Remove npm Global Installation + +```bash +npm uninstall -g @aikidosec/safe-chain +``` + +### Remove Volta Installation + +```bash +volta uninstall @aikidosec/safe-chain +``` + +### Remove nvm Installations (All Versions) + +```bash +# Automated approach +for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do + nvm exec "$version" npm uninstall -g @aikidosec/safe-chain +done + +# Or manual per version +nvm use +npm uninstall -g @aikidosec/safe-chain +``` + +### Clean Shell Configuration Files + +Manually remove safe-chain entries from: + +- Bash: `~/.bashrc` +- Zsh: `~/.zshrc` +- Fish: `~/.config/fish/config.fish` +- PowerShell: `$PROFILE` + +Look for and remove: + +- Lines sourcing from `~/.safe-chain/scripts/` +- Any safe-chain related function definitions + +### Remove Installation Directory + +```bash +rm -rf ~/.safe-chain +``` + +## Getting More Information + +### Enable Verbose Logging + +Get detailed diagnostic output: + +```bash +npm install express --safe-chain-logging=verbose +pip install requests --safe-chain-logging=verbose +``` + +### Report Issues + +If you encounter problems: + +1. Visit [GitHub Issues](https://github.com/AikidoSec/safe-chain/issues) +2. Include: + - Operating system and version + - Shell type and version + - `safe-chain --version` output + - Output from verification commands + - Verbose logs of the failing command From 504b3ca596ae50f747088e0bab524c7824ce1169 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 6 Jan 2026 16:04:15 +0100 Subject: [PATCH 26/36] Update Conflicting Installations note --- docs/troubleshooting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0e95f56..398ef4a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -133,6 +133,7 @@ type pip ``` **Expected `which` output:** + - Standalone binary (correct): `~/.safe-chain/bin/safe-chain` or `/Users//.safe-chain/bin/safe-chain` - npm global (outdated): path containing `node_modules` or nvm version paths @@ -160,7 +161,7 @@ ls ~/.safe-chain/scripts/ ### Check for Conflicting Installations -The install/uninstall scripts automatically detect and remove conflicting installations, but you can manually check: +> **Note:** The install/uninstall scripts automatically detect and remove conflicting installations, but you can manually check: ```bash # Check npm global From b19d67f8539b33b0a5f6623e4a1136fea740abcf Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 7 Jan 2026 08:55:20 +0100 Subject: [PATCH 27/36] Add linuxstatic artifact to release --- .github/workflows/build-and-release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index a372e1e..a752eb8 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -64,6 +64,8 @@ jobs: mv binaries/safe-chain-macos-arm64/safe-chain release-artifacts/safe-chain-macos-arm64 mv binaries/safe-chain-linux-x64/safe-chain release-artifacts/safe-chain-linux-x64 mv binaries/safe-chain-linux-arm64/safe-chain release-artifacts/safe-chain-linux-arm64 + mv binaries/safe-chain-linuxstatic-x64/safe-chain release-artifacts/safe-chain-linuxstatic-x64 + mv binaries/safe-chain-linuxstatic-arm64/safe-chain release-artifacts/safe-chain-linuxstatic-arm64 mv binaries/safe-chain-win-x64/safe-chain.exe release-artifacts/safe-chain-win-x64.exe mv binaries/safe-chain-win-arm64/safe-chain.exe release-artifacts/safe-chain-win-arm64.exe @@ -85,6 +87,8 @@ jobs: release-artifacts/safe-chain-macos-arm64 \ release-artifacts/safe-chain-linux-x64 \ release-artifacts/safe-chain-linux-arm64 \ + release-artifacts/safe-chain-linuxstatic-x64 \ + release-artifacts/safe-chain-linuxstatic-arm64 \ release-artifacts/safe-chain-win-x64.exe \ release-artifacts/safe-chain-win-arm64.exe \ release-artifacts/install-safe-chain.sh \ From 7a4b7057bc5b015463464ee6cc4fc098be274c8a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 7 Jan 2026 09:40:40 +0100 Subject: [PATCH 28/36] Test on gh actions --- .github/workflows/build-and-release.yml | 2 +- .github/workflows/create-artifact.yml | 4 ++-- .github/workflows/test-on-pr.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index a752eb8..e64bc4a 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -115,7 +115,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Set the version in safe-chain package run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index b9a538e..5486401 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -71,12 +71,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index bff7e51..2b37deb 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -24,12 +24,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts @@ -114,7 +114,7 @@ jobs: node-version: "lts/*" - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.1-docker-linux-exec-beta/install-safe-chain/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain/install-safe-chain.sh | sh -s -- --ci - name: Install dependencies (root) run: npm ci From b2a5336556d2ff08ba595199a8b01ae271af36a7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 7 Jan 2026 11:39:22 +0100 Subject: [PATCH 29/36] Use latest build of safe-chain in CI again --- .github/workflows/build-and-release.yml | 2 +- .github/workflows/create-artifact.yml | 4 ++-- .github/workflows/test-on-pr.yml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index e64bc4a..a752eb8 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -115,7 +115,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - name: Set the version in safe-chain package run: npm --no-git-tag-version version ${{ needs.set-version.outputs.version }} --workspace=packages/safe-chain diff --git a/.github/workflows/create-artifact.yml b/.github/workflows/create-artifact.yml index 5486401..00fc58a 100644 --- a/.github/workflows/create-artifact.yml +++ b/.github/workflows/create-artifact.yml @@ -71,12 +71,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 2b37deb..9e4a5ec 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -24,12 +24,12 @@ jobs: - name: Setup safe-chain (Mac/Linux) if: runner.os != 'Windows' - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - name: Setup safe-chain (Windows) if: runner.os == 'Windows' shell: pwsh - run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain.ps1' -UseBasicParsing) } -ci" + run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci" - name: Install dependencies run: npm ci --ignore-scripts @@ -114,7 +114,7 @@ jobs: node-version: "lts/*" - name: Setup safe-chain - run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/0.0.4-docker-linux-exec-beta/install-safe-chain/install-safe-chain.sh | sh -s -- --ci + run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - name: Install dependencies (root) run: npm ci From 6820e1e76c5003347381e7dda767113f459ecba5 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 7 Jan 2026 14:09:18 +0100 Subject: [PATCH 30/36] Fix broken compatibility in install --- install-scripts/install-safe-chain.sh | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/install-scripts/install-safe-chain.sh b/install-scripts/install-safe-chain.sh index 88cabe7..7ee07c2 100755 --- a/install-scripts/install-safe-chain.sh +++ b/install-scripts/install-safe-chain.sh @@ -32,9 +32,16 @@ error() { } # Detect OS +# For legacy versions (when SAFE_CHAIN_VERSION is set), use 'linux' instead of 'linuxstatic' detect_os() { case "$(uname -s)" in - Linux*) echo "linuxstatic" ;; + Linux*) + if [ -n "$SAFE_CHAIN_VERSION" ]; then + echo "linux" + else + echo "linuxstatic" + fi + ;; Darwin*) echo "macos" ;; *) error "Unsupported operating system: $(uname -s)" ;; esac @@ -244,6 +251,20 @@ main() { # Parse command-line arguments parse_arguments "$@" + # Show deprecation warning if SAFE_CHAIN_VERSION is set + if [ -n "$SAFE_CHAIN_VERSION" ]; then + warn "SAFE_CHAIN_VERSION environment variable is deprecated." + warn "" + warn "Please use direct download URLs for version pinning instead:" + warn "" + if [ "$USE_CI_SETUP" = "true" ]; then + warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh -s -- --ci" + else + warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh" + fi + warn "" + fi + # Fetch latest version if VERSION is not set if [ -z "$VERSION" ]; then info "Fetching latest release version..." From 43eda4fadf46035ebf65bf4202b61f5d4bcab441 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Wed, 7 Jan 2026 14:20:16 +0100 Subject: [PATCH 31/36] Add deprecation message to powershell version as well --- install-scripts/install-safe-chain.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/install-scripts/install-safe-chain.ps1 b/install-scripts/install-safe-chain.ps1 index 51d15ba..ffe2505 100644 --- a/install-scripts/install-safe-chain.ps1 +++ b/install-scripts/install-safe-chain.ps1 @@ -149,6 +149,20 @@ function Remove-VoltaInstallation { # Main installation function Install-SafeChain { + # Show deprecation warning if SAFE_CHAIN_VERSION is set + if (-not [string]::IsNullOrWhiteSpace($env:SAFE_CHAIN_VERSION)) { + Write-Warn "SAFE_CHAIN_VERSION environment variable is deprecated." + Write-Warn "" + Write-Warn "Please use direct download URLs for version pinning instead:" + Write-Warn "" + if ($ci) { + Write-Warn " iex `"& { `$(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1' -UseBasicParsing) } -ci`"" + } else { + Write-Warn " iex (iwr `"https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1`" -UseBasicParsing)" + } + Write-Warn "" + } + # Fetch latest version if VERSION is not set if ([string]::IsNullOrWhiteSpace($Version)) { Write-Info "Fetching latest release version..." From 59f8b55bdac485f4cebbf651f711fbd741f598cf Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 8 Jan 2026 08:00:26 +0100 Subject: [PATCH 32/36] Add a section about troubleshooting when the package is already in the cache --- docs/troubleshooting.md | 50 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 398ef4a..34b2099 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -44,6 +44,8 @@ pip3 install safe-chain-pi-test These test packages are flagged as malware and should be blocked by Safe Chain. +**If the test package installs successfully instead of being blocked**, see [Malware Not Being Blocked](#malware-not-being-blocked) below. + ### Logging Options Use logging flags to get more information: @@ -58,6 +60,52 @@ npm install express --safe-chain-logging=silent ## Common Issues +### Malware Not Being Blocked + +**Symptom:** Test malware packages (like `safe-chain-test`) install successfully when they should be blocked + +**Most Common Cause:** The package is cached in your package manager's local store + +Safe-chain blocks malicious packages by intercepting network requests to package registries using its proxy. + +When a package is already cached locally, the package manager skips downloading it from the registry, which bypasses the proxy. + +**Resolution Steps:** + +1. **Clear your package manager's cache:** + + ```bash + # For npm + npm cache clean --force + + # For pnpm + pnpm store prune + + # For yarn (classic) + yarn cache clean + + # For yarn (berry/v2+) + yarn cache clean --all + + # For bun + bun pm cache rm + ``` + + > **⚠️ Warning:** Cache clearing is safe but will remove all cached packages. Subsequent installations will need to re-download packages. In CI/CD environments or monorepos, this may affect build times. + +2. **Clean local installation artifacts (optional):** + + ```bash + # Remove node_modules if you want a completely fresh install + rm -rf node_modules + ``` + +3. **Re-test malware blocking:** + + ```bash + npm install safe-chain-test # Should be blocked + ``` + ### Shell Aliases Not Working After Installation **Symptom:** Running `npm` shows regular npm instead of safe-chain wrapped version @@ -246,4 +294,4 @@ If you encounter problems: - Shell type and version - `safe-chain --version` output - Output from verification commands - - Verbose logs of the failing command + - Verbose logs of the failing command (add the `--safe-chain-logging=verbose` argument) From 6a70898e7b0f52e2d19d65ab4ce373c3e1b114d3 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 8 Jan 2026 08:01:48 +0100 Subject: [PATCH 33/36] Remove "optional" from "Clean local installation artifacts" --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 34b2099..8c32bee 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -93,7 +93,7 @@ When a package is already cached locally, the package manager skips downloading > **⚠️ Warning:** Cache clearing is safe but will remove all cached packages. Subsequent installations will need to re-download packages. In CI/CD environments or monorepos, this may affect build times. -2. **Clean local installation artifacts (optional):** +2. **Clean local installation artifacts:** ```bash # Remove node_modules if you want a completely fresh install From 3573ef2bc5959839ef55b6f48925a7a1231218f7 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 10:50:06 +0100 Subject: [PATCH 34/36] Allow to configure loglevel through an env variable --- .../src/config/environmentVariables.js | 9 ++ packages/safe-chain/src/config/settings.js | 18 ++- .../safe-chain/src/config/settings.spec.js | 131 ++++++++++++++++-- 3 files changed, 137 insertions(+), 21 deletions(-) diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index 64da107..1b85ed7 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -25,3 +25,12 @@ export function getNpmCustomRegistries() { export function getPipCustomRegistries() { return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES; } + +/** + * Gets the logging level from environment variable + * Valid values: "silent", "normal", "verbose" + * @returns {string | undefined} + */ +export function getLoggingLevel() { + return process.env.SAFE_CHAIN_LOGGING; +} diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 573c3ab..7a287ab 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -7,14 +7,20 @@ export const LOGGING_NORMAL = "normal"; export const LOGGING_VERBOSE = "verbose"; export function getLoggingLevel() { - const level = cliArguments.getLoggingLevel(); - - if (level === LOGGING_SILENT) { - return LOGGING_SILENT; + // Priority 1: CLI argument + const cliLevel = cliArguments.getLoggingLevel(); + if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) { + return cliLevel; + } + if (cliLevel) { + // CLI arg was set but invalid, default to normal + return LOGGING_NORMAL; } - if (level === LOGGING_VERBOSE) { - return LOGGING_VERBOSE; + // Priority 2: Environment variable + const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase(); + if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) { + return envLevel; } return LOGGING_NORMAL; diff --git a/packages/safe-chain/src/config/settings.spec.js b/packages/safe-chain/src/config/settings.spec.js index db513f3..314fac0 100644 --- a/packages/safe-chain/src/config/settings.spec.js +++ b/packages/safe-chain/src/config/settings.spec.js @@ -11,9 +11,15 @@ mock.module("fs", { }, }); -const { getNpmCustomRegistries, getPipCustomRegistries } = await import( - "./settings.js" -); +const { + getNpmCustomRegistries, + getPipCustomRegistries, + getLoggingLevel, + LOGGING_SILENT, + LOGGING_NORMAL, + LOGGING_VERBOSE, +} = await import("./settings.js"); +const { initializeCliArguments } = await import("./cliArguments.js"); for (const { packageManager, getCustomRegistries, envVarName } of [ { @@ -26,8 +32,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ getCustomRegistries: getPipCustomRegistries, envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES", }, -]) -{ +]) { describe(getCustomRegistries.name, async () => { let originalEnv; @@ -55,7 +60,10 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should return registries without protocol", () => { configFileContent = JSON.stringify({ [packageManager]: { - customRegistries: [`${packageManager}.company.com`, "registry.internal.net"], + customRegistries: [ + `${packageManager}.company.com`, + "registry.internal.net", + ], }, }); @@ -143,8 +151,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should parse comma-separated registries from environment variable", () => { delete process.env[envVarName]; - process.env[envVarName] = - "env1.registry.com,env2.registry.net"; + process.env[envVarName] = "env1.registry.com,env2.registry.net"; configFileContent = undefined; const registries = getCustomRegistries(); @@ -157,8 +164,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should trim whitespace from environment variable registries", () => { delete process.env[envVarName]; - process.env[envVarName] = - " env1.registry.com , env2.registry.net "; + process.env[envVarName] = " env1.registry.com , env2.registry.net "; configFileContent = undefined; const registries = getCustomRegistries(); @@ -188,11 +194,15 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should remove duplicate registries when merging env and config", () => { delete process.env[envVarName]; - process.env[envVarName] = - `${packageManager}.company.com,env.registry.com`; + process.env[ + envVarName + ] = `${packageManager}.company.com,env.registry.com`; configFileContent = JSON.stringify({ [packageManager]: { - customRegistries: [`${packageManager}.company.com`, "config.registry.net"], + customRegistries: [ + `${packageManager}.company.com`, + "config.registry.net", + ], }, }); @@ -221,8 +231,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should handle empty strings in comma-separated list", () => { delete process.env[envVarName]; - process.env[envVarName] = - "env1.registry.com,,env2.registry.net,"; + process.env[envVarName] = "env1.registry.com,,env2.registry.net,"; configFileContent = undefined; const registries = getCustomRegistries(); @@ -264,3 +273,95 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ }); }); } + +describe("getLoggingLevel", () => { + let originalEnv; + + beforeEach(() => { + originalEnv = process.env.SAFE_CHAIN_LOGGING; + delete process.env.SAFE_CHAIN_LOGGING; + // Reset CLI arguments state + initializeCliArguments([]); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.SAFE_CHAIN_LOGGING = originalEnv; + } else { + delete process.env.SAFE_CHAIN_LOGGING; + } + }); + + it("should return normal by default when nothing is configured", () => { + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_NORMAL); + }); + + it("should return silent from environment variable", () => { + process.env.SAFE_CHAIN_LOGGING = "silent"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should return verbose from environment variable", () => { + process.env.SAFE_CHAIN_LOGGING = "verbose"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_VERBOSE); + }); + + it("should handle uppercase environment variable values", () => { + process.env.SAFE_CHAIN_LOGGING = "VERBOSE"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_VERBOSE); + }); + + it("should handle mixed case environment variable values", () => { + process.env.SAFE_CHAIN_LOGGING = "Silent"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should return normal for invalid environment variable values", () => { + process.env.SAFE_CHAIN_LOGGING = "invalid"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_NORMAL); + }); + + it("should prioritize CLI argument over environment variable", () => { + process.env.SAFE_CHAIN_LOGGING = "verbose"; + initializeCliArguments(["--safe-chain-logging=silent"]); + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should use environment variable when CLI argument is not set", () => { + process.env.SAFE_CHAIN_LOGGING = "silent"; + initializeCliArguments(["install", "express"]); + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should return normal when CLI argument is invalid (even if env var is valid)", () => { + process.env.SAFE_CHAIN_LOGGING = "verbose"; + initializeCliArguments(["--safe-chain-logging=invalid"]); + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_NORMAL); + }); +}); From 20994c1834d3a272bae0eae6e7447834b88c8236 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 11:01:54 +0100 Subject: [PATCH 35/36] Document to configure loglevel through env variables. --- README.md | 35 ++++++++++++++++++++++++----------- docs/troubleshooting.md | 13 +++++++++++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 14dc26c..57d1bf4 100644 --- a/README.md +++ b/README.md @@ -152,23 +152,36 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/unins ## Logging -You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag: +You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag or the `SAFE_CHAIN_LOGGING` environment variable. -- `--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. +### Configuration Options - Example usage: +You can set the logging level through multiple sources (in order of priority): - ```shell - npm install express --safe-chain-logging=silent - ``` +1. **CLI Argument** (highest priority): -- `--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. + - `--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 + ``` - ```shell - npm install express --safe-chain-logging=verbose - ``` + - `--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. + + ```shell + npm install express --safe-chain-logging=verbose + ``` + +2. **Environment Variable**: + + ```shell + export SAFE_CHAIN_LOGGING=verbose + npm install express + ``` + + Valid values: `silent`, `normal`, `verbose` + + This is useful for setting a default logging level for all package manager commands in your terminal session or CI/CD environment. ## Minimum Package Age diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8c32bee..0cd6098 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -48,12 +48,16 @@ These test packages are flagged as malware and should be blocked by Safe Chain. ### Logging Options -Use logging flags to get more information: +Use logging flags or environment variables to get more information: ```bash # Verbose mode - detailed diagnostic output for troubleshooting npm install express --safe-chain-logging=verbose +# Or set it globally for all commands in your session +export SAFE_CHAIN_LOGGING=verbose +npm install express + # Silent mode - suppress all output except malware blocking npm install express --safe-chain-logging=silent ``` @@ -277,11 +281,16 @@ rm -rf ~/.safe-chain ### Enable Verbose Logging -Get detailed diagnostic output: +Get detailed diagnostic output using a CLI flag or environment variable: ```bash +# Using CLI flag npm install express --safe-chain-logging=verbose pip install requests --safe-chain-logging=verbose + +# Using environment variable (applies to all commands) +export SAFE_CHAIN_LOGGING=verbose +npm install express ``` ### Report Issues From 595f269f6268542ae7103d74777f9f05e0566e31 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 12 Jan 2026 11:20:25 +0100 Subject: [PATCH 36/36] Add comment about backwards compat. --- packages/safe-chain/src/config/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 7a287ab..6910fe3 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -13,7 +13,7 @@ export function getLoggingLevel() { return cliLevel; } if (cliLevel) { - // CLI arg was set but invalid, default to normal + // CLI arg was set but invalid, default to normal for backwards compatibility. return LOGGING_NORMAL; }