@aikidosec/safe-chain 1.3.2 → 1.3.4
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 +116 -8
- package/bin/aikido-pipx.js +16 -0
- package/bin/safe-chain.js +5 -2
- package/docs/safe-package-manager-demo.gif +0 -0
- package/docs/shell-integration.md +4 -4
- package/package.json +2 -1
- package/src/config/configFile.js +66 -11
- package/src/config/environmentVariables.js +20 -0
- package/src/config/settings.js +63 -0
- package/src/main.js +13 -0
- package/src/packagemanager/currentPackageManager.js +3 -0
- package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
- package/src/packagemanager/pipx/runPipXCommand.js +65 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +12 -3
- package/src/registryProxy/interceptors/pipInterceptor.js +6 -3
- package/src/registryProxy/mitmRequestHandler.js +11 -8
- package/src/registryProxy/tunnelRequestHandler.js +40 -26
- package/src/shell-integration/helpers.js +6 -0
- package/src/shell-integration/setup-ci.js +8 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +4 -1
- package/src/shell-integration/startup-scripts/init-posix.sh +4 -1
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +3 -0
package/README.md
CHANGED
|
@@ -23,15 +23,16 @@ 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
|
+

|
|
31
|
+
|
|
29
32
|
## Installation
|
|
30
33
|
|
|
31
34
|
Installing the Aikido Safe Chain is easy with our one-line installer.
|
|
32
35
|
|
|
33
|
-
> ⚠️ **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.
|
|
34
|
-
|
|
35
36
|
### Unix/Linux/macOS
|
|
36
37
|
|
|
37
38
|
```shell
|
|
@@ -49,11 +50,13 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/insta
|
|
|
49
50
|
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
51
|
|
|
51
52
|
**Unix/Linux/macOS:**
|
|
53
|
+
|
|
52
54
|
```shell
|
|
53
55
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/x.x.x/install-safe-chain.sh | sh
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
**Windows (PowerShell):**
|
|
59
|
+
|
|
57
60
|
```powershell
|
|
58
61
|
iex (iwr "https://github.com/AikidoSec/safe-chain/releases/download/x.x.x/install-safe-chain.ps1" -UseBasicParsing)
|
|
59
62
|
```
|
|
@@ -64,9 +67,22 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
|
|
64
67
|
|
|
65
68
|
1. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
|
66
69
|
|
|
67
|
-
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx,
|
|
70
|
+
- 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.
|
|
71
|
+
|
|
72
|
+
2. **Verify the installation** by running the verification command:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
npm safe-chain-verify
|
|
76
|
+
pnpm safe-chain-verify
|
|
77
|
+
pip safe-chain-verify
|
|
78
|
+
uv safe-chain-verify
|
|
79
|
+
|
|
80
|
+
# Any other supported package manager: {packagemanager} safe-chain-verify
|
|
81
|
+
```
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
- The output should display "OK: Safe-chain works!" confirming that Aikido Safe Chain is properly installed and running.
|
|
84
|
+
|
|
85
|
+
3. **(Optional) Test malware blocking** by attempting to install a test package:
|
|
70
86
|
|
|
71
87
|
For JavaScript/Node.js:
|
|
72
88
|
|
|
@@ -82,7 +98,7 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
|
|
82
98
|
|
|
83
99
|
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
|
84
100
|
|
|
85
|
-
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `
|
|
101
|
+
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
102
|
|
|
87
103
|
You can check the installed version by running:
|
|
88
104
|
|
|
@@ -94,17 +110,17 @@ safe-chain --version
|
|
|
94
110
|
|
|
95
111
|
### Malware Blocking
|
|
96
112
|
|
|
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,
|
|
113
|
+
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
114
|
|
|
99
115
|
### Minimum package age (npm only)
|
|
100
116
|
|
|
101
117
|
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
118
|
|
|
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).
|
|
119
|
+
⚠️ 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
120
|
|
|
105
121
|
### Shell Integration
|
|
106
122
|
|
|
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,
|
|
123
|
+
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
124
|
|
|
109
125
|
- ✅ **Bash**
|
|
110
126
|
- ✅ **Zsh**
|
|
@@ -183,6 +199,39 @@ You can set the minimum package age through multiple sources (in order of priori
|
|
|
183
199
|
}
|
|
184
200
|
```
|
|
185
201
|
|
|
202
|
+
## Custom Registries
|
|
203
|
+
|
|
204
|
+
Configure Safe Chain to scan packages from custom or private registries.
|
|
205
|
+
|
|
206
|
+
Supported ecosystems:
|
|
207
|
+
|
|
208
|
+
- Node.js
|
|
209
|
+
- Python
|
|
210
|
+
|
|
211
|
+
### Configuration Options
|
|
212
|
+
|
|
213
|
+
You can set custom registries through environment variable or config file. Both sources are merged together.
|
|
214
|
+
|
|
215
|
+
1. **Environment Variable** (comma-separated):
|
|
216
|
+
|
|
217
|
+
```shell
|
|
218
|
+
export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
|
|
219
|
+
export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
2. **Config File** (`~/.aikido/config.json`):
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"npm": {
|
|
227
|
+
"customRegistries": ["npm.company.com", "registry.internal.net"]
|
|
228
|
+
},
|
|
229
|
+
"pip": {
|
|
230
|
+
"customRegistries": ["pip.company.com", "registry.internal.net"]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
186
235
|
# Usage in CI/CD
|
|
187
236
|
|
|
188
237
|
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 +256,8 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
|
|
|
207
256
|
|
|
208
257
|
- ✅ **GitHub Actions**
|
|
209
258
|
- ✅ **Azure Pipelines**
|
|
259
|
+
- ✅ **CircleCI**
|
|
260
|
+
- ✅ **Jenkins**
|
|
210
261
|
|
|
211
262
|
## GitHub Actions Example
|
|
212
263
|
|
|
@@ -239,4 +290,61 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
|
|
|
239
290
|
displayName: "Install dependencies"
|
|
240
291
|
```
|
|
241
292
|
|
|
293
|
+
## CircleCI Example
|
|
294
|
+
|
|
295
|
+
```yaml
|
|
296
|
+
version: 2.1
|
|
297
|
+
jobs:
|
|
298
|
+
build:
|
|
299
|
+
docker:
|
|
300
|
+
- image: cimg/node:lts
|
|
301
|
+
steps:
|
|
302
|
+
- checkout
|
|
303
|
+
- run: |
|
|
304
|
+
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
|
|
305
|
+
- run: npm ci
|
|
306
|
+
workflows:
|
|
307
|
+
build_and_test:
|
|
308
|
+
jobs:
|
|
309
|
+
- build
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Jenkins Example
|
|
313
|
+
|
|
314
|
+
Note: This assumes Node.js and npm are installed on the Jenkins agent.
|
|
315
|
+
|
|
316
|
+
```groovy
|
|
317
|
+
pipeline {
|
|
318
|
+
agent any
|
|
319
|
+
|
|
320
|
+
environment {
|
|
321
|
+
// Jenkins does not automatically persist PATH updates from setup-ci,
|
|
322
|
+
// so add the shims + binary directory explicitly for all stages.
|
|
323
|
+
PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
stages {
|
|
327
|
+
stage('Install safe-chain') {
|
|
328
|
+
steps {
|
|
329
|
+
sh '''
|
|
330
|
+
set -euo pipefail
|
|
331
|
+
|
|
332
|
+
# Install Safe Chain for CI
|
|
333
|
+
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
|
334
|
+
'''
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
stage('Install project dependencies etc...') {
|
|
339
|
+
steps {
|
|
340
|
+
sh '''
|
|
341
|
+
set -euo pipefail
|
|
342
|
+
npm ci
|
|
343
|
+
'''
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
242
350
|
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
|
+
})();
|
package/bin/safe-chain.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ui } from "../src/environment/userInteraction.js";
|
|
5
5
|
import { setup } from "../src/shell-integration/setup.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
teardown,
|
|
8
|
+
teardownDirectories,
|
|
9
|
+
} from "../src/shell-integration/teardown.js";
|
|
7
10
|
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
|
8
11
|
import { initializeCliArguments } from "../src/config/cliArguments.js";
|
|
9
12
|
import { setEcoSystem } from "../src/config/settings.js";
|
|
@@ -45,7 +48,7 @@ if (tool) {
|
|
|
45
48
|
const args = process.argv.slice(3);
|
|
46
49
|
|
|
47
50
|
setEcoSystem(tool.ecoSystem);
|
|
48
|
-
|
|
51
|
+
|
|
49
52
|
// Provide tool context to PM (pip uses this; others ignore)
|
|
50
53
|
const toolContext = { tool: tool.tool, args };
|
|
51
54
|
initializePackageManager(tool.internalPackageManagerName, toolContext);
|
|
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 `
|
|
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-
|
|
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 `
|
|
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.
|
|
3
|
+
"version": "1.3.4",
|
|
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",
|
package/src/config/configFile.js
CHANGED
|
@@ -7,10 +7,15 @@ import { getEcoSystem } from "./settings.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {Object} SafeChainConfig
|
|
9
9
|
*
|
|
10
|
-
*
|
|
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
|
+
* @property {unknown | SafeChainRegistryConfiguration} pip
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Object} SafeChainRegistryConfiguration
|
|
11
17
|
* We cannot trust the input and should add the necessary validations.
|
|
12
|
-
* @property {unknown}
|
|
13
|
-
* @property {unknown} minimumPackageAgeHours
|
|
18
|
+
* @property {unknown | string[]} customRegistries
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -78,6 +83,50 @@ export function getMinimumPackageAgeHours() {
|
|
|
78
83
|
return undefined;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
|
88
|
+
* @returns {string[]}
|
|
89
|
+
*/
|
|
90
|
+
export function getNpmCustomRegistries() {
|
|
91
|
+
const config = readConfigFile();
|
|
92
|
+
|
|
93
|
+
if (!config || !config.npm) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// TypeScript needs help understanding that config.npm exists and has customRegistries
|
|
98
|
+
const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
|
|
99
|
+
const customRegistries = npmConfig.customRegistries;
|
|
100
|
+
|
|
101
|
+
if (!Array.isArray(customRegistries)) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return customRegistries.filter((item) => typeof item === "string");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
export function getPipCustomRegistries() {
|
|
113
|
+
const config = readConfigFile();
|
|
114
|
+
|
|
115
|
+
if (!config || !config.pip) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// TypeScript needs help understanding that config.pip exists and has customRegistries
|
|
120
|
+
const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip);
|
|
121
|
+
const customRegistries = pipConfig.customRegistries;
|
|
122
|
+
|
|
123
|
+
if (!Array.isArray(customRegistries)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return customRegistries.filter((item) => typeof item === "string");
|
|
128
|
+
}
|
|
129
|
+
|
|
81
130
|
/**
|
|
82
131
|
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
|
83
132
|
* @param {string | number} version
|
|
@@ -136,23 +185,29 @@ export function readDatabaseFromLocalCache() {
|
|
|
136
185
|
* @returns {SafeChainConfig}
|
|
137
186
|
*/
|
|
138
187
|
function readConfigFile() {
|
|
188
|
+
/** @type {SafeChainConfig} */
|
|
189
|
+
const emptyConfig = {
|
|
190
|
+
scanTimeout: undefined,
|
|
191
|
+
minimumPackageAgeHours: undefined,
|
|
192
|
+
npm: {
|
|
193
|
+
customRegistries: undefined,
|
|
194
|
+
},
|
|
195
|
+
pip: {
|
|
196
|
+
customRegistries: undefined,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
139
200
|
const configFilePath = getConfigFilePath();
|
|
140
201
|
|
|
141
202
|
if (!fs.existsSync(configFilePath)) {
|
|
142
|
-
return
|
|
143
|
-
scanTimeout: undefined,
|
|
144
|
-
minimumPackageAgeHours: undefined,
|
|
145
|
-
};
|
|
203
|
+
return emptyConfig;
|
|
146
204
|
}
|
|
147
205
|
|
|
148
206
|
try {
|
|
149
207
|
const data = fs.readFileSync(configFilePath, "utf8");
|
|
150
208
|
return JSON.parse(data);
|
|
151
209
|
} catch {
|
|
152
|
-
return
|
|
153
|
-
scanTimeout: undefined,
|
|
154
|
-
minimumPackageAgeHours: undefined,
|
|
155
|
-
};
|
|
210
|
+
return emptyConfig;
|
|
156
211
|
}
|
|
157
212
|
}
|
|
158
213
|
|
|
@@ -5,3 +5,23 @@
|
|
|
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
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the custom pip registries from environment variable
|
|
21
|
+
* Expected format: comma-separated list of registry domains
|
|
22
|
+
* Example: "pip.company.com,registry.internal.net"
|
|
23
|
+
* @returns {string | undefined}
|
|
24
|
+
*/
|
|
25
|
+
export function getPipCustomRegistries() {
|
|
26
|
+
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
|
|
27
|
+
}
|
package/src/config/settings.js
CHANGED
|
@@ -98,3 +98,66 @@ 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
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gets the custom npm registries from both environment variable and config file (merged)
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
export function getPipCustomRegistries() {
|
|
152
|
+
const envRegistries = parseRegistriesFromEnv(
|
|
153
|
+
environmentVariables.getPipCustomRegistries()
|
|
154
|
+
);
|
|
155
|
+
const configRegistries = configFile.getPipCustomRegistries();
|
|
156
|
+
|
|
157
|
+
// Merge both sources and remove duplicates
|
|
158
|
+
const allRegistries = [...envRegistries, ...configRegistries];
|
|
159
|
+
const uniqueRegistries = [...new Set(allRegistries)];
|
|
160
|
+
|
|
161
|
+
// Normalize each registry (remove protocol if any)
|
|
162
|
+
return uniqueRegistries.map(normalizeRegistry);
|
|
163
|
+
}
|
package/src/main.js
CHANGED
|
@@ -13,6 +13,10 @@ import { getAuditStats } from "./scanning/audit/index.js";
|
|
|
13
13
|
* @returns {Promise<number>}
|
|
14
14
|
*/
|
|
15
15
|
export async function main(args) {
|
|
16
|
+
if (isSafeChainVerify(args)) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
process.on("SIGINT", handleProcessTermination);
|
|
17
21
|
process.on("SIGTERM", handleProcessTermination);
|
|
18
22
|
|
|
@@ -104,3 +108,12 @@ export async function main(args) {
|
|
|
104
108
|
function handleProcessTermination() {
|
|
105
109
|
ui.writeBufferedLogsAndStopBuffering();
|
|
106
110
|
}
|
|
111
|
+
|
|
112
|
+
/** @param {string[]} args */
|
|
113
|
+
function isSafeChainVerify(args) {
|
|
114
|
+
const safeChainCheckCommand = "safe-chain-verify";
|
|
115
|
+
if (args.length > 0 && args[0] === safeChainCheckCommand) {
|
|
116
|
+
ui.writeInformation("OK: Safe-chain works!");
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -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 {
|
|
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 = [
|
|
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
|
|
25
|
+
const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find(
|
|
26
|
+
(reg) => url.includes(reg)
|
|
27
|
+
);
|
|
19
28
|
|
|
20
29
|
if (registry) {
|
|
21
30
|
return buildNpmInterceptor(registry);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPipCustomRegistries } from "../../config/settings.js";
|
|
1
2
|
import { isMalwarePackage } from "../../scanning/audit/index.js";
|
|
2
3
|
import { interceptRequests } from "./interceptorBuilder.js";
|
|
3
4
|
|
|
@@ -13,7 +14,9 @@ const knownPipRegistries = [
|
|
|
13
14
|
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
|
|
14
15
|
*/
|
|
15
16
|
export function pipInterceptorForUrl(url) {
|
|
16
|
-
const
|
|
17
|
+
const customRegistries = getPipCustomRegistries();
|
|
18
|
+
const registries = [...knownPipRegistries, ...customRegistries];
|
|
19
|
+
const registry = registries.find((reg) => url.includes(reg));
|
|
17
20
|
|
|
18
21
|
if (registry) {
|
|
19
22
|
return buildPipInterceptor(registry);
|
|
@@ -37,8 +40,8 @@ function buildPipInterceptor(registry) {
|
|
|
37
40
|
// Per python, packages that differ only by hyphen vs underscore are considered the same.
|
|
38
41
|
const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName;
|
|
39
42
|
|
|
40
|
-
const isMalicious =
|
|
41
|
-
await isMalwarePackage(packageName, version)
|
|
43
|
+
const isMalicious =
|
|
44
|
+
await isMalwarePackage(packageName, version)
|
|
42
45
|
|| await isMalwarePackage(hyphenName, version);
|
|
43
46
|
|
|
44
47
|
if (isMalicious) {
|
|
@@ -15,7 +15,7 @@ import { gunzipSync, gzipSync } from "zlib";
|
|
|
15
15
|
*/
|
|
16
16
|
export function mitmConnect(req, clientSocket, interceptor) {
|
|
17
17
|
ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
|
|
18
|
-
const { hostname } = new URL(`http://${req.url}`);
|
|
18
|
+
const { hostname, port } = new URL(`http://${req.url}`);
|
|
19
19
|
|
|
20
20
|
clientSocket.on("error", (err) => {
|
|
21
21
|
ui.writeVerbose(
|
|
@@ -26,7 +26,7 @@ export function mitmConnect(req, clientSocket, interceptor) {
|
|
|
26
26
|
// Not subscribing to 'close' event will cause node to throw and crash.
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const server = createHttpsServer(hostname, interceptor);
|
|
29
|
+
const server = createHttpsServer(hostname, port, interceptor);
|
|
30
30
|
|
|
31
31
|
server.on("error", (err) => {
|
|
32
32
|
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
|
|
@@ -46,10 +46,11 @@ export function mitmConnect(req, clientSocket, interceptor) {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* @param {string} hostname
|
|
49
|
+
* @param {string} port
|
|
49
50
|
* @param {Interceptor} interceptor
|
|
50
51
|
* @returns {import("https").Server}
|
|
51
52
|
*/
|
|
52
|
-
function createHttpsServer(hostname, interceptor) {
|
|
53
|
+
function createHttpsServer(hostname, port, interceptor) {
|
|
53
54
|
const cert = generateCertForHost(hostname);
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -80,7 +81,7 @@ function createHttpsServer(hostname, interceptor) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
// Collect request body
|
|
83
|
-
forwardRequest(req, hostname, res, requestInterceptor);
|
|
84
|
+
forwardRequest(req, hostname, port, res, requestInterceptor);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const server = https.createServer(
|
|
@@ -109,11 +110,12 @@ function getRequestPathAndQuery(url) {
|
|
|
109
110
|
/**
|
|
110
111
|
* @param {import("http").IncomingMessage} req
|
|
111
112
|
* @param {string} hostname
|
|
113
|
+
* @param {string} port
|
|
112
114
|
* @param {import("http").ServerResponse} res
|
|
113
115
|
* @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
|
|
114
116
|
*/
|
|
115
|
-
function forwardRequest(req, hostname, res, requestHandler) {
|
|
116
|
-
const proxyReq = createProxyRequest(hostname, req, res, requestHandler);
|
|
117
|
+
function forwardRequest(req, hostname, port, res, requestHandler) {
|
|
118
|
+
const proxyReq = createProxyRequest(hostname, port, req, res, requestHandler);
|
|
117
119
|
|
|
118
120
|
proxyReq.on("error", (err) => {
|
|
119
121
|
ui.writeVerbose(
|
|
@@ -144,13 +146,14 @@ function forwardRequest(req, hostname, res, requestHandler) {
|
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
148
|
* @param {string} hostname
|
|
149
|
+
* @param {string} port
|
|
147
150
|
* @param {import("http").IncomingMessage} req
|
|
148
151
|
* @param {import("http").ServerResponse} res
|
|
149
152
|
* @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
|
|
150
153
|
*
|
|
151
154
|
* @returns {import("http").ClientRequest}
|
|
152
155
|
*/
|
|
153
|
-
function createProxyRequest(hostname, req, res, requestHandler) {
|
|
156
|
+
function createProxyRequest(hostname, port, req, res, requestHandler) {
|
|
154
157
|
/** @type {NodeJS.Dict<string | string[]> | undefined} */
|
|
155
158
|
let headers = { ...req.headers };
|
|
156
159
|
// Remove the host header from the incoming request before forwarding.
|
|
@@ -163,7 +166,7 @@ function createProxyRequest(hostname, req, res, requestHandler) {
|
|
|
163
166
|
/** @type {import("http").RequestOptions} */
|
|
164
167
|
const options = {
|
|
165
168
|
hostname: hostname,
|
|
166
|
-
port: 443,
|
|
169
|
+
port: port || 443,
|
|
167
170
|
path: req.url,
|
|
168
171
|
method: req.method,
|
|
169
172
|
headers: { ...headers },
|
|
@@ -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
|
-
|
|
78
|
-
|
|
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();
|
|
94
|
-
clientSocket.
|
|
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}:${
|
|
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}:${
|
|
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
|