@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 +54 -5
- package/bin/aikido-pipx.js +16 -0
- 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 +40 -11
- package/src/config/environmentVariables.js +10 -0
- package/src/config/settings.js +45 -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/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,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
|
+

|
|
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,
|
|
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`, `
|
|
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,
|
|
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,
|
|
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 `
|
|
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.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",
|
package/src/config/configFile.js
CHANGED
|
@@ -7,10 +7,14 @@ 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
|
+
*
|
|
15
|
+
* @typedef {Object} SafeChainRegistryConfiguration
|
|
11
16
|
* We cannot trust the input and should add the necessary validations.
|
|
12
|
-
* @property {unknown}
|
|
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
|
+
}
|
package/src/config/settings.js
CHANGED
|
@@ -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 {
|
|
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);
|
|
@@ -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
|