@aikidosec/safe-chain 1.3.2 β†’ 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,9 +23,12 @@ Aikido Safe Chain supports the following package managers:
23
23
  - πŸ“¦ **pip3**
24
24
  - πŸ“¦ **uv**
25
25
  - πŸ“¦ **poetry**
26
+ - πŸ“¦ **pipx**
26
27
 
27
28
  # Usage
28
29
 
30
+ ![Aikido Safe Chain demo](https://raw.githubusercontent.com/AikidoSec/safe-chain/main/docs/safe-package-manager-demo.gif)
31
+
29
32
  ## Installation
30
33
 
31
34
  Installing the Aikido Safe Chain is easy with our one-line installer.
@@ -49,11 +52,13 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/insta
49
52
  To install a specific version instead of the latest, replace `latest` with the version number in the URL (available from version 1.3.2 onwards):
50
53
 
51
54
  **Unix/Linux/macOS:**
55
+
52
56
  ```shell
53
57
  curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/x.x.x/install-safe-chain.sh | sh
54
58
  ```
55
59
 
56
60
  **Windows (PowerShell):**
61
+
57
62
  ```powershell
58
63
  iex (iwr "https://github.com/AikidoSec/safe-chain/releases/download/x.x.x/install-safe-chain.ps1" -UseBasicParsing)
59
64
  ```
@@ -64,7 +69,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
64
69
 
65
70
  1. **❗Restart your terminal** to start using the Aikido Safe Chain.
66
71
 
67
- - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
72
+ - 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.
68
73
 
69
74
  2. **Verify the installation** by running one of the following commands:
70
75
 
@@ -82,7 +87,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
82
87
 
83
88
  - The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
84
89
 
85
- When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `uv`, `pip`, `pip3` or `poetry` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`). If any malware is detected, it will prompt you to exit the command.
90
+ When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry` and `pipx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`). If any malware is detected, it will prompt you to exit the command.
86
91
 
87
92
  You can check the installed version by running:
88
93
 
@@ -94,17 +99,17 @@ safe-chain --version
94
99
 
95
100
  ### Malware Blocking
96
101
 
97
- The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, pip, pip3 or poetry commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
102
+ The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, uv, poetry or pipx commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
98
103
 
99
104
  ### Minimum package age (npm only)
100
105
 
101
106
  For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours (by default) until they have been validated against malware. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can configure this threshold or bypass this protection entirely - see the [Minimum Package Age Configuration](#minimum-package-age) section below.
102
107
 
103
- ⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3, poetry).
108
+ ⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3, poetry, pipx).
104
109
 
105
110
  ### Shell Integration
106
111
 
107
- The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (uv, pip). It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:
112
+ The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (pip, uv, poetry, pipx). It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:
108
113
 
109
114
  - βœ… **Bash**
110
115
  - βœ… **Zsh**
@@ -183,6 +188,30 @@ You can set the minimum package age through multiple sources (in order of priori
183
188
  }
184
189
  ```
185
190
 
191
+ ## Custom NPM Registries
192
+
193
+ Configure Safe Chain to scan packages from custom or private npm registries.
194
+
195
+ ### Configuration Options
196
+
197
+ You can set custom registries through environment variable or config file. Both sources are merged together.
198
+
199
+ 1. **Environment Variable** (comma-separated):
200
+
201
+ ```shell
202
+ export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
203
+ ```
204
+
205
+ 2. **Config File** (`~/.aikido/config.json`):
206
+
207
+ ```json
208
+ {
209
+ "npm": {
210
+ "customRegistries": ["npm.company.com", "registry.internal.net"]
211
+ }
212
+ }
213
+ ```
214
+
186
215
  # Usage in CI/CD
187
216
 
188
217
  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.
@@ -207,6 +236,7 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
207
236
 
208
237
  - βœ… **GitHub Actions**
209
238
  - βœ… **Azure Pipelines**
239
+ - βœ… **CircleCI**
210
240
 
211
241
  ## GitHub Actions Example
212
242
 
@@ -239,4 +269,23 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
239
269
  displayName: "Install dependencies"
240
270
  ```
241
271
 
272
+ ## CircleCI Example
273
+
274
+ ```yaml
275
+ version: 2.1
276
+ jobs:
277
+ build:
278
+ docker:
279
+ - image: cimg/node:lts
280
+ steps:
281
+ - checkout
282
+ - run: |
283
+ curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
284
+ - run: npm ci
285
+ workflows:
286
+ build_and_test:
287
+ jobs:
288
+ - build
289
+ ```
290
+
242
291
  After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+ import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
6
+
7
+ // Set eco system
8
+ setEcoSystem(ECOSYSTEM_PY);
9
+
10
+ initializePackageManager("pipx");
11
+
12
+ (async () => {
13
+ // Pass through only user-supplied pipx args
14
+ var exitCode = await main(process.argv.slice(2));
15
+ process.exit(exitCode);
16
+ })();
Binary file
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
5
+ The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry`, `pipx`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
6
6
 
7
7
  ## Supported Shells
8
8
 
@@ -28,7 +28,7 @@ This command:
28
28
 
29
29
  - Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
30
30
  - Detects all supported shells on your system
31
- - Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3`
31
+ - Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry` and `pipx`
32
32
  - Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` route through Safe Chain when invoked by name
33
33
 
34
34
  ❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly.
@@ -78,7 +78,7 @@ The system modifies the following files to source Safe Chain startup scripts:
78
78
  This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
79
79
 
80
80
  - Make sure Aikido Safe Chain is properly installed on your system
81
- - Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist
81
+ - Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, `aikido-pip3`, `aikido-uv`, `aikido-poetry` and `aikido-pipx` commands exist
82
82
  - Check that these commands are in your system's PATH
83
83
 
84
84
  ### Manual Verification
@@ -121,7 +121,7 @@ npm() {
121
121
  }
122
122
  ```
123
123
 
124
- Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
124
+ Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `poetry` and `pipx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
125
125
 
126
126
  To intercept Python module invocations for pip without altering Python itself, you can add small forwarding functions:
127
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -21,6 +21,7 @@
21
21
  "aikido-python": "bin/aikido-python.js",
22
22
  "aikido-python3": "bin/aikido-python3.js",
23
23
  "aikido-poetry": "bin/aikido-poetry.js",
24
+ "aikido-pipx": "bin/aikido-pipx.js",
24
25
  "safe-chain": "bin/safe-chain.js"
25
26
  },
26
27
  "type": "module",
@@ -7,10 +7,14 @@ import { getEcoSystem } from "./settings.js";
7
7
  /**
8
8
  * @typedef {Object} SafeChainConfig
9
9
  *
10
- * This should be a number, but can be anything because it is user-input.
10
+ * We cannot trust the input and should add the necessary validations
11
+ * @property {unknown | Number} scanTimeout
12
+ * @property {unknown | Number} minimumPackageAgeHours
13
+ * @property {unknown | SafeChainRegistryConfiguration} npm
14
+ *
15
+ * @typedef {Object} SafeChainRegistryConfiguration
11
16
  * We cannot trust the input and should add the necessary validations.
12
- * @property {unknown} scanTimeout
13
- * @property {unknown} minimumPackageAgeHours
17
+ * @property {unknown | string[]} customRegistries
14
18
  */
15
19
 
16
20
  /**
@@ -78,6 +82,28 @@ export function getMinimumPackageAgeHours() {
78
82
  return undefined;
79
83
  }
80
84
 
85
+ /**
86
+ * Gets the custom npm registries from the config file (format parsing only, no validation)
87
+ * @returns {string[]}
88
+ */
89
+ export function getNpmCustomRegistries() {
90
+ const config = readConfigFile();
91
+
92
+ if (!config || !config.npm) {
93
+ return [];
94
+ }
95
+
96
+ // TypeScript needs help understanding that config.npm exists and has customRegistries
97
+ const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
98
+ const customRegistries = npmConfig.customRegistries;
99
+
100
+ if (!Array.isArray(customRegistries)) {
101
+ return [];
102
+ }
103
+
104
+ return customRegistries.filter((item) => typeof item === "string");
105
+ }
106
+
81
107
  /**
82
108
  * @param {import("../api/aikido.js").MalwarePackage[]} data
83
109
  * @param {string | number} version
@@ -136,23 +162,26 @@ export function readDatabaseFromLocalCache() {
136
162
  * @returns {SafeChainConfig}
137
163
  */
138
164
  function readConfigFile() {
165
+ /** @type {SafeChainConfig} */
166
+ const emptyConfig = {
167
+ scanTimeout: undefined,
168
+ minimumPackageAgeHours: undefined,
169
+ npm: {
170
+ customRegistries: undefined,
171
+ },
172
+ };
173
+
139
174
  const configFilePath = getConfigFilePath();
140
175
 
141
176
  if (!fs.existsSync(configFilePath)) {
142
- return {
143
- scanTimeout: undefined,
144
- minimumPackageAgeHours: undefined,
145
- };
177
+ return emptyConfig;
146
178
  }
147
179
 
148
180
  try {
149
181
  const data = fs.readFileSync(configFilePath, "utf8");
150
182
  return JSON.parse(data);
151
183
  } catch {
152
- return {
153
- scanTimeout: undefined,
154
- minimumPackageAgeHours: undefined,
155
- };
184
+ return emptyConfig;
156
185
  }
157
186
  }
158
187
 
@@ -5,3 +5,13 @@
5
5
  export function getMinimumPackageAgeHours() {
6
6
  return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS;
7
7
  }
8
+
9
+ /**
10
+ * Gets the custom npm registries from environment variable
11
+ * Expected format: comma-separated list of registry domains
12
+ * Example: "npm.company.com,registry.internal.net"
13
+ * @returns {string | undefined}
14
+ */
15
+ export function getNpmCustomRegistries() {
16
+ return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
17
+ }
@@ -98,3 +98,48 @@ export function skipMinimumPackageAge() {
98
98
 
99
99
  return defaultSkipMinimumPackageAge;
100
100
  }
101
+
102
+ /**
103
+ * Normalizes a registry URL by removing protocol if present
104
+ * @param {string} registry
105
+ * @returns {string}
106
+ */
107
+ function normalizeRegistry(registry) {
108
+ // Remove protocol (http://, https://) if present
109
+ return registry.replace(/^https?:\/\//, "");
110
+ }
111
+
112
+ /**
113
+ * Parses comma-separated registries from environment variable
114
+ * @param {string | undefined} envValue
115
+ * @returns {string[]}
116
+ */
117
+ function parseRegistriesFromEnv(envValue) {
118
+ if (!envValue || typeof envValue !== "string") {
119
+ return [];
120
+ }
121
+
122
+ // Split by comma and trim whitespace
123
+ return envValue
124
+ .split(",")
125
+ .map((registry) => registry.trim())
126
+ .filter((registry) => registry.length > 0);
127
+ }
128
+
129
+ /**
130
+ * Gets the custom npm registries from both environment variable and config file (merged)
131
+ * @returns {string[]}
132
+ */
133
+ export function getNpmCustomRegistries() {
134
+ const envRegistries = parseRegistriesFromEnv(
135
+ environmentVariables.getNpmCustomRegistries()
136
+ );
137
+ const configRegistries = configFile.getNpmCustomRegistries();
138
+
139
+ // Merge both sources and remove duplicates
140
+ const allRegistries = [...envRegistries, ...configRegistries];
141
+ const uniqueRegistries = [...new Set(allRegistries)];
142
+
143
+ // Normalize each registry (remove protocol if any)
144
+ return uniqueRegistries.map(normalizeRegistry);
145
+ }
@@ -12,6 +12,7 @@ import { createYarnPackageManager } from "./yarn/createPackageManager.js";
12
12
  import { createPipPackageManager } from "./pip/createPackageManager.js";
13
13
  import { createUvPackageManager } from "./uv/createUvPackageManager.js";
14
14
  import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
15
+ import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
15
16
 
16
17
  /**
17
18
  * @type {{packageManagerName: PackageManager | null}}
@@ -61,6 +62,8 @@ export function initializePackageManager(packageManagerName, context) {
61
62
  state.packageManagerName = createUvPackageManager();
62
63
  } else if (packageManagerName === "poetry") {
63
64
  state.packageManagerName = createPoetryPackageManager();
65
+ } else if (packageManagerName === "pipx") {
66
+ state.packageManagerName = createPipXPackageManager();
64
67
  } else {
65
68
  throw new Error("Unsupported package manager: " + packageManagerName);
66
69
  }
@@ -0,0 +1,18 @@
1
+ import { runPipX } from "./runPipXCommand.js";
2
+
3
+ /**
4
+ * @returns {import("../currentPackageManager.js").PackageManager}
5
+ */
6
+ export function createPipXPackageManager() {
7
+ return {
8
+ /**
9
+ * @param {string[]} args
10
+ */
11
+ runCommand: (args) => {
12
+ return runPipX("pipx", args);
13
+ },
14
+ // MITM only
15
+ isSupportedCommand: () => false,
16
+ getDependencyUpdatesForCommand: () => [],
17
+ };
18
+ }
@@ -0,0 +1,65 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
+ import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
5
+
6
+ /**
7
+ * Sets CA bundle environment variables used by Python libraries and pipx.
8
+ *
9
+ * @param {NodeJS.ProcessEnv} env - Env object
10
+ * @param {string} combinedCaPath - Path to the combined CA bundle
11
+ * @return {NodeJS.ProcessEnv} Modified environment object
12
+ */
13
+ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
14
+ let retVal = { ...env };
15
+
16
+ if (env.SSL_CERT_FILE) {
17
+ ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
18
+ }
19
+ retVal.SSL_CERT_FILE = combinedCaPath;
20
+
21
+ if (env.REQUESTS_CA_BUNDLE) {
22
+ ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
23
+ }
24
+ retVal.REQUESTS_CA_BUNDLE = combinedCaPath;
25
+
26
+ if (env.PIP_CERT) {
27
+ ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
28
+ }
29
+ retVal.PIP_CERT = combinedCaPath;
30
+ return retVal;
31
+ }
32
+
33
+ /**
34
+ * Runs a pipx command with safe-chain's certificate bundle and proxy configuration.
35
+ *
36
+ * @param {string} command - The command to execute
37
+ * @param {string[]} args - Command line arguments
38
+ * @returns {Promise<{status: number}>} Exit status of the command
39
+ */
40
+ export async function runPipX(command, args) {
41
+ try {
42
+ const env = mergeSafeChainProxyEnvironmentVariables(process.env);
43
+
44
+ const combinedCaPath = getCombinedCaBundlePath();
45
+ const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath);
46
+
47
+ // Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
48
+ // These are already set by mergeSafeChainProxyEnvironmentVariables
49
+
50
+ const result = await safeSpawn(command, args, {
51
+ stdio: "inherit",
52
+ env: modifiedEnv,
53
+ });
54
+
55
+ return { status: result.status };
56
+ } catch (/** @type any */ error) {
57
+ if (error.status) {
58
+ return { status: error.status };
59
+ } else {
60
+ ui.writeError(`Error executing command: ${error.message}`);
61
+ ui.writeError(`Is '${command}' installed and available on your system?`);
62
+ return { status: 1 };
63
+ }
64
+ }
65
+ }
@@ -1,4 +1,7 @@
1
- import { skipMinimumPackageAge } from "../../../config/settings.js";
1
+ import {
2
+ getNpmCustomRegistries,
3
+ skipMinimumPackageAge,
4
+ } from "../../../config/settings.js";
2
5
  import { isMalwarePackage } from "../../../scanning/audit/index.js";
3
6
  import { interceptRequests } from "../interceptorBuilder.js";
4
7
  import {
@@ -8,14 +11,20 @@ import {
8
11
  } from "./modifyNpmInfo.js";
9
12
  import { parseNpmPackageUrl } from "./parseNpmPackageUrl.js";
10
13
 
11
- const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"];
14
+ const knownJsRegistries = [
15
+ "registry.npmjs.org",
16
+ "registry.yarnpkg.com",
17
+ "registry.npmjs.com",
18
+ ];
12
19
 
13
20
  /**
14
21
  * @param {string} url
15
22
  * @returns {import("../interceptorBuilder.js").Interceptor | undefined}
16
23
  */
17
24
  export function npmInterceptorForUrl(url) {
18
- const registry = knownJsRegistries.find((reg) => url.includes(reg));
25
+ const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find(
26
+ (reg) => url.includes(reg)
27
+ );
19
28
 
20
29
  if (registry) {
21
30
  return buildNpmInterceptor(registry);
@@ -43,6 +43,7 @@ export function tunnelRequest(req, clientSocket, head) {
43
43
  function tunnelRequestToDestination(req, clientSocket, head) {
44
44
  const { port, hostname } = new URL(`http://${req.url}`);
45
45
  const isImds = isImdsEndpoint(hostname);
46
+ const targetPort = Number.parseInt(port) || 443;
46
47
 
47
48
  if (timedoutImdsEndpoints.includes(hostname)) {
48
49
  clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
@@ -58,64 +59,77 @@ function tunnelRequestToDestination(req, clientSocket, head) {
58
59
  return;
59
60
  }
60
61
 
61
- const serverSocket = net.connect(
62
- Number.parseInt(port) || 443,
63
- hostname,
64
- () => {
65
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
66
- serverSocket.write(head);
67
- serverSocket.pipe(clientSocket);
68
- clientSocket.pipe(serverSocket);
69
- }
70
- );
71
-
72
- // Set explicit connection timeout to avoid waiting for OS default (~2 minutes).
73
- // IMDS endpoints get shorter timeout (3s) since they're commonly unreachable outside cloud environments.
74
62
  const connectTimeout = getConnectTimeout(hostname);
75
- serverSocket.setTimeout(connectTimeout);
76
63
 
77
- serverSocket.on("timeout", () => {
78
- // Suppress error logging for IMDS endpoints - timeouts are expected when not in cloud
64
+ // Use JS setTimeout for true connection timeout (not idle timeout).
65
+ // socket.setTimeout() measures inactivity, not time since connection attempt.
66
+ const connectTimer = setTimeout(() => {
79
67
  if (isImds) {
80
68
  timedoutImdsEndpoints.push(hostname);
81
69
  ui.writeVerbose(
82
- `Safe-chain: connect to ${hostname}:${
83
- port || 443
84
- } timed out after ${connectTimeout}ms`
70
+ `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`
85
71
  );
86
72
  } else {
87
73
  ui.writeError(
88
- `Safe-chain: connect to ${hostname}:${
89
- port || 443
90
- } timed out after ${connectTimeout}ms`
74
+ `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`
91
75
  );
92
76
  }
93
- serverSocket.destroy(); // Clean up socket to prevent event loop hanging
94
- clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
77
+ serverSocket.destroy();
78
+ if (clientSocket.writable) {
79
+ clientSocket.end("HTTP/1.1 504 Gateway Timeout\r\n\r\n");
80
+ }
81
+ }, connectTimeout);
82
+
83
+ const serverSocket = net.connect(targetPort, hostname, () => {
84
+ // Clear timer to prevent false timeout errors after successful connection
85
+ clearTimeout(connectTimer);
86
+
87
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
88
+ serverSocket.write(head);
89
+ serverSocket.pipe(clientSocket);
90
+ clientSocket.pipe(serverSocket);
95
91
  });
96
92
 
97
93
  clientSocket.on("error", () => {
98
94
  // This can happen if the client TCP socket sends RST instead of FIN.
99
95
  // Not subscribing to 'error' event will cause node to throw and crash.
96
+ clearTimeout(connectTimer);
97
+ if (serverSocket.writable) {
98
+ serverSocket.end();
99
+ }
100
+ });
101
+
102
+ clientSocket.on("close", () => {
103
+ // Client closed connection - clean up server socket
104
+ clearTimeout(connectTimer);
100
105
  if (serverSocket.writable) {
101
106
  serverSocket.end();
102
107
  }
103
108
  });
104
109
 
105
110
  serverSocket.on("error", (err) => {
111
+ clearTimeout(connectTimer);
106
112
  if (isImds) {
107
113
  ui.writeVerbose(
108
- `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
114
+ `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
109
115
  );
110
116
  } else {
111
117
  ui.writeError(
112
- `Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
118
+ `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
113
119
  );
114
120
  }
115
121
  if (clientSocket.writable) {
116
122
  clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
117
123
  }
118
124
  });
125
+
126
+ serverSocket.on("close", () => {
127
+ // Server closed connection - clean up client socket
128
+ clearTimeout(connectTimer);
129
+ if (clientSocket.writable) {
130
+ clientSocket.end();
131
+ }
132
+ });
119
133
  }
120
134
 
121
135
  /**
@@ -94,6 +94,12 @@ export const knownAikidoTools = [
94
94
  ecoSystem: ECOSYSTEM_PY,
95
95
  internalPackageManagerName: "pip",
96
96
  },
97
+ {
98
+ tool: "pipx",
99
+ aikidoCommand: "aikido-pipx",
100
+ ecoSystem: ECOSYSTEM_PY,
101
+ internalPackageManagerName: "pipx",
102
+ }
97
103
  // When adding a new tool here, also update the documentation for the new tool in the README.md
98
104
  ];
99
105
 
@@ -157,6 +157,14 @@ function modifyPathForCi(shimsDir, binDir) {
157
157
  ui.writeInformation("##vso[task.prependpath]" + shimsDir);
158
158
  ui.writeInformation("##vso[task.prependpath]" + binDir);
159
159
  }
160
+
161
+ if (process.env.BASH_ENV) {
162
+ // In CircleCI, persisting PATH across steps is done by appending shell exports
163
+ // to the file referenced by BASH_ENV. CircleCI sources this file for 'run' each step.
164
+ const exportLine = `export PATH="${shimsDir}:${binDir}:$PATH"` + os.EOL;
165
+ fs.appendFileSync(process.env.BASH_ENV, exportLine, "utf-8");
166
+ ui.writeInformation(`Added shims directory to BASH_ENV for CircleCI.`);
167
+ }
160
168
  }
161
169
 
162
170
  function getToolsToSetup() {
@@ -39,7 +39,6 @@ function npm
39
39
  wrapSafeChainCommand "npm" $argv
40
40
  end
41
41
 
42
-
43
42
  function pip
44
43
  wrapSafeChainCommand "pip" $argv
45
44
  end
@@ -66,6 +65,10 @@ function python3
66
65
  wrapSafeChainCommand "python3" $argv
67
66
  end
68
67
 
68
+ function pipx
69
+ wrapSafeChainCommand "pipx" $argv
70
+ end
71
+
69
72
  function printSafeChainWarning
70
73
  set original_cmd $argv[1]
71
74
 
@@ -35,7 +35,6 @@ function npm() {
35
35
  wrapSafeChainCommand "npm" "$@"
36
36
  }
37
37
 
38
-
39
38
  function pip() {
40
39
  wrapSafeChainCommand "pip" "$@"
41
40
  }
@@ -62,6 +61,10 @@ function python3() {
62
61
  wrapSafeChainCommand "python3" "$@"
63
62
  }
64
63
 
64
+ function pipx() {
65
+ wrapSafeChainCommand "pipx" "$@"
66
+ }
67
+
65
68
  function printSafeChainWarning() {
66
69
  # \033[43;30m is used to set the background color to yellow and text color to black
67
70
  # \033[0m is used to reset the text formatting
@@ -66,6 +66,9 @@ function python3 {
66
66
  Invoke-WrappedCommand 'python3' $args
67
67
  }
68
68
 
69
+ function pipx {
70
+ Invoke-WrappedCommand "pipx" $args
71
+ }
69
72
 
70
73
  function Write-SafeChainWarning {
71
74
  param([string]$Command)