@aikidosec/safe-chain 1.2.1 → 1.2.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 +20 -15
- package/bin/aikido-pip.js +2 -5
- package/bin/aikido-pip3.js +2 -6
- package/bin/aikido-poetry.js +13 -0
- package/bin/aikido-python.js +5 -16
- package/bin/aikido-python3.js +5 -16
- package/bin/safe-chain.js +7 -60
- package/package.json +2 -1
- package/src/config/configFile.js +1 -1
- package/src/config/settings.js +1 -1
- package/src/main.js +4 -2
- package/src/packagemanager/currentPackageManager.js +6 -2
- package/src/packagemanager/pip/createPackageManager.js +9 -5
- package/src/packagemanager/pip/pipSettings.js +4 -28
- package/src/packagemanager/pip/runPipCommand.js +41 -2
- package/src/packagemanager/poetry/createPoetryPackageManager.js +77 -0
- package/src/registryProxy/certBundle.js +99 -13
- package/src/registryProxy/certUtils.js +55 -5
- package/src/registryProxy/getConnectTimeout.js +13 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +6 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +23 -9
- package/src/registryProxy/isImdsEndpoint.js +13 -0
- package/src/registryProxy/registryProxy.js +15 -7
- package/src/registryProxy/tunnelRequestHandler.js +55 -3
- package/src/shell-integration/helpers.js +20 -0
- package/src/shell-integration/setup-ci.js +2 -2
- package/src/shell-integration/setup.js +3 -4
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +4 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +4 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +5 -1
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +1 -1
- package/src/shell-integration/teardown.js +43 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Aikido Safe Chain supports the following package managers:
|
|
|
22
22
|
- 📦 **pip** (beta)
|
|
23
23
|
- 📦 **pip3** (beta)
|
|
24
24
|
- 📦 **uv** (beta)
|
|
25
|
+
- 📦 **poetry** (beta)
|
|
25
26
|
|
|
26
27
|
# Usage
|
|
27
28
|
|
|
@@ -81,7 +82,7 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst
|
|
|
81
82
|
|
|
82
83
|
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
|
83
84
|
|
|
84
|
-
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `uv`, `pip`, or `
|
|
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.
|
|
85
86
|
|
|
86
87
|
You can check the installed version by running:
|
|
87
88
|
|
|
@@ -93,13 +94,13 @@ safe-chain --version
|
|
|
93
94
|
|
|
94
95
|
### Malware Blocking
|
|
95
96
|
|
|
96
|
-
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,
|
|
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.
|
|
97
98
|
|
|
98
99
|
### Minimum package age (npm only)
|
|
99
100
|
|
|
100
101
|
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.
|
|
101
102
|
|
|
102
|
-
⚠️ 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).
|
|
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).
|
|
103
104
|
|
|
104
105
|
### Shell Integration
|
|
105
106
|
|
|
@@ -115,17 +116,21 @@ More information about the shell integration can be found in the [shell integrat
|
|
|
115
116
|
|
|
116
117
|
## Uninstallation
|
|
117
118
|
|
|
118
|
-
To uninstall the Aikido Safe Chain,
|
|
119
|
+
To uninstall the Aikido Safe Chain, use our one-line uninstaller:
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
### Unix/Linux/macOS
|
|
122
|
+
|
|
123
|
+
```shell
|
|
124
|
+
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/uninstall-safe-chain.sh | sh
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Windows (PowerShell)
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
iex (iwr "https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/uninstall-safe-chain.ps1" -UseBasicParsing)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**❗Restart your terminal** after uninstalling to ensure all aliases are removed.
|
|
129
134
|
|
|
130
135
|
# Configuration
|
|
131
136
|
|
|
@@ -235,7 +240,7 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst
|
|
|
235
240
|
run: npm ci
|
|
236
241
|
```
|
|
237
242
|
|
|
238
|
-
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
|
|
243
|
+
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv/poetry) support.
|
|
239
244
|
|
|
240
245
|
## Azure DevOps Example
|
|
241
246
|
|
|
@@ -252,6 +257,6 @@ iex "& { $(iwr 'https://raw.githubusercontent.com/AikidoSec/safe-chain/main/inst
|
|
|
252
257
|
displayName: "Install dependencies"
|
|
253
258
|
```
|
|
254
259
|
|
|
255
|
-
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv) support.
|
|
260
|
+
> **Note:** Remove `--include-python` if you don't need Python (pip/pip3/uv/poetry) support.
|
|
256
261
|
|
|
257
262
|
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
|
package/bin/aikido-pip.js
CHANGED
|
@@ -3,15 +3,12 @@
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
5
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
|
6
|
-
import {
|
|
6
|
+
import { PIP_PACKAGE_MANAGER, PIP_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
|
7
7
|
|
|
8
8
|
// Set eco system
|
|
9
9
|
setEcoSystem(ECOSYSTEM_PY);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
|
|
13
|
-
|
|
14
|
-
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
11
|
+
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP_COMMAND, args: process.argv.slice(2) });
|
|
15
12
|
|
|
16
13
|
(async () => {
|
|
17
14
|
// Pass through only user-supplied pip args
|
package/bin/aikido-pip3.js
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
5
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
|
6
|
-
import {
|
|
6
|
+
import { PIP_PACKAGE_MANAGER, PIP3_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
|
7
7
|
|
|
8
8
|
// Set eco system
|
|
9
9
|
setEcoSystem(ECOSYSTEM_PY);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
|
|
13
|
-
|
|
14
|
-
// Create package manager
|
|
15
|
-
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
11
|
+
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP3_COMMAND, args: process.argv.slice(2) });
|
|
16
12
|
|
|
17
13
|
(async () => {
|
|
18
14
|
// Pass through only user-supplied pip args
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
8
|
+
initializePackageManager("poetry");
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
var exitCode = await main(process.argv.slice(2));
|
|
12
|
+
process.exit(exitCode);
|
|
13
|
+
})();
|
package/bin/aikido-python.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PIP_PACKAGE_MANAGER, PYTHON_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
|
5
5
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
|
6
6
|
import { main } from "../src/main.js";
|
|
7
7
|
|
|
@@ -11,20 +11,9 @@ setEcoSystem(ECOSYSTEM_PY);
|
|
|
11
11
|
// Strip nodejs and wrapper script from args
|
|
12
12
|
let argv = process.argv.slice(2);
|
|
13
13
|
|
|
14
|
-
(
|
|
15
|
-
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
|
|
16
|
-
setEcoSystem(ECOSYSTEM_PY);
|
|
17
|
-
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
|
|
18
|
-
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
19
|
-
|
|
20
|
-
// Strip off the '-m pip' or '-m pip3' from the args
|
|
21
|
-
argv = argv.slice(2);
|
|
14
|
+
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON_COMMAND, args: argv });
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Forward to real python binary for non-pip flows
|
|
27
|
-
const { spawn } = await import('child_process');
|
|
28
|
-
spawn('python', argv, { stdio: 'inherit' });
|
|
29
|
-
}
|
|
16
|
+
(async () => {
|
|
17
|
+
var exitCode = await main(argv);
|
|
18
|
+
process.exit(exitCode);
|
|
30
19
|
})();
|
package/bin/aikido-python3.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PIP_PACKAGE_MANAGER, PYTHON3_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
|
5
5
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
|
6
6
|
import { main } from "../src/main.js";
|
|
7
7
|
|
|
@@ -11,20 +11,9 @@ setEcoSystem(ECOSYSTEM_PY);
|
|
|
11
11
|
// Strip nodejs and wrapper script from args
|
|
12
12
|
let argv = process.argv.slice(2);
|
|
13
13
|
|
|
14
|
-
(
|
|
15
|
-
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
|
|
16
|
-
setEcoSystem(ECOSYSTEM_PY);
|
|
17
|
-
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
|
|
18
|
-
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
19
|
-
|
|
20
|
-
// Strip off the '-m pip' or '-m pip3' from the args
|
|
21
|
-
argv = argv.slice(2);
|
|
14
|
+
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON3_COMMAND, args: argv });
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Forward to real python3 binary for non-pip flows
|
|
27
|
-
const { spawn } = await import('child_process');
|
|
28
|
-
spawn('python3', argv, { stdio: 'inherit' });
|
|
29
|
-
}
|
|
16
|
+
(async () => {
|
|
17
|
+
var exitCode = await main(argv);
|
|
18
|
+
process.exit(exitCode);
|
|
30
19
|
})();
|
package/bin/safe-chain.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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 { teardown } from "../src/shell-integration/teardown.js";
|
|
6
|
+
import { teardown, teardownDirectories } from "../src/shell-integration/teardown.js";
|
|
7
7
|
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
|
8
8
|
import { initializeCliArguments } from "../src/config/cliArguments.js";
|
|
9
9
|
import { setEcoSystem } from "../src/config/settings.js";
|
|
@@ -13,11 +13,6 @@ import path from "path";
|
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
14
14
|
import fs from "fs";
|
|
15
15
|
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
|
|
16
|
-
import {
|
|
17
|
-
PIP_INVOCATIONS,
|
|
18
|
-
PIP_PACKAGE_MANAGER,
|
|
19
|
-
setCurrentPipInvocation,
|
|
20
|
-
} from "../src/packagemanager/pip/pipSettings.js";
|
|
21
16
|
|
|
22
17
|
/** @type {string} */
|
|
23
18
|
// This checks the current file's dirname in a way that's compatible with:
|
|
@@ -46,15 +41,14 @@ const command = process.argv[2];
|
|
|
46
41
|
|
|
47
42
|
const tool = knownAikidoTools.find((tool) => tool.tool === command);
|
|
48
43
|
|
|
49
|
-
if (tool
|
|
50
|
-
(async function () {
|
|
51
|
-
await executePip(tool);
|
|
52
|
-
})();
|
|
53
|
-
} else if (tool) {
|
|
44
|
+
if (tool) {
|
|
54
45
|
const args = process.argv.slice(3);
|
|
55
46
|
|
|
56
47
|
setEcoSystem(tool.ecoSystem);
|
|
57
|
-
|
|
48
|
+
|
|
49
|
+
// Provide tool context to PM (pip uses this; others ignore)
|
|
50
|
+
const toolContext = { tool: tool.tool, args };
|
|
51
|
+
initializePackageManager(tool.internalPackageManagerName, toolContext);
|
|
58
52
|
|
|
59
53
|
(async () => {
|
|
60
54
|
var exitCode = await main(args);
|
|
@@ -66,6 +60,7 @@ if (tool && tool.internalPackageManagerName === PIP_PACKAGE_MANAGER) {
|
|
|
66
60
|
} else if (command === "setup") {
|
|
67
61
|
setup();
|
|
68
62
|
} else if (command === "teardown") {
|
|
63
|
+
teardownDirectories();
|
|
69
64
|
teardown();
|
|
70
65
|
} else if (command === "setup-ci") {
|
|
71
66
|
setupCi();
|
|
@@ -140,51 +135,3 @@ async function getVersion() {
|
|
|
140
135
|
|
|
141
136
|
return "0.0.0";
|
|
142
137
|
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* @param {import("../src/shell-integration/helpers.js").AikidoTool} tool
|
|
146
|
-
*/
|
|
147
|
-
async function executePip(tool) {
|
|
148
|
-
// Scanners for pip / pip3 / python / python3 use a slightly different approach:
|
|
149
|
-
// - They all use the same PIP_PACKAGE_MANAGER internally, but need some setup to be able to do so
|
|
150
|
-
// - It needs to set which tool to run (pip / pip3 / python / python3)
|
|
151
|
-
// - For python and python3, the -m pip/pip3 args are removed and later added again by the package manager
|
|
152
|
-
// - Python / python3 skips safe-chain if not being run with -m pip or -m pip3
|
|
153
|
-
|
|
154
|
-
let args = process.argv.slice(3);
|
|
155
|
-
setEcoSystem(tool.ecoSystem);
|
|
156
|
-
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
157
|
-
|
|
158
|
-
let shouldSkip = false;
|
|
159
|
-
if (tool.tool === "pip") {
|
|
160
|
-
setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
|
|
161
|
-
} else if (tool.tool === "pip3") {
|
|
162
|
-
setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
|
|
163
|
-
} else if (tool.tool === "python") {
|
|
164
|
-
if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
|
|
165
|
-
setCurrentPipInvocation(
|
|
166
|
-
args[1] === "pip3" ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP
|
|
167
|
-
);
|
|
168
|
-
args = args.slice(2);
|
|
169
|
-
} else {
|
|
170
|
-
shouldSkip = true;
|
|
171
|
-
}
|
|
172
|
-
} else if (tool.tool === "python3") {
|
|
173
|
-
if (args[0] === "-m" && (args[1] === "pip" || args[1] === "pip3")) {
|
|
174
|
-
setCurrentPipInvocation(
|
|
175
|
-
args[1] === "pip3" ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP
|
|
176
|
-
);
|
|
177
|
-
args = args.slice(2);
|
|
178
|
-
} else {
|
|
179
|
-
shouldSkip = true;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (shouldSkip) {
|
|
184
|
-
const { spawn } = await import("child_process");
|
|
185
|
-
spawn(tool.tool, args, { stdio: "inherit" });
|
|
186
|
-
} else {
|
|
187
|
-
var exitCode = await main(args);
|
|
188
|
-
process.exit(exitCode);
|
|
189
|
-
}
|
|
190
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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'",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"aikido-pip3": "bin/aikido-pip3.js",
|
|
21
21
|
"aikido-python": "bin/aikido-python.js",
|
|
22
22
|
"aikido-python3": "bin/aikido-python3.js",
|
|
23
|
+
"aikido-poetry": "bin/aikido-poetry.js",
|
|
23
24
|
"safe-chain": "bin/safe-chain.js"
|
|
24
25
|
},
|
|
25
26
|
"type": "module",
|
package/src/config/configFile.js
CHANGED
|
@@ -67,7 +67,7 @@ function validateMinimumPackageAgeHours(value) {
|
|
|
67
67
|
*/
|
|
68
68
|
export function getMinimumPackageAgeHours() {
|
|
69
69
|
const config = readConfigFile();
|
|
70
|
-
if (config.minimumPackageAgeHours) {
|
|
70
|
+
if (config.minimumPackageAgeHours !== undefined) {
|
|
71
71
|
const validated = validateMinimumPackageAgeHours(
|
|
72
72
|
config.minimumPackageAgeHours
|
|
73
73
|
);
|
package/src/config/settings.js
CHANGED
package/src/main.js
CHANGED
|
@@ -23,6 +23,7 @@ export async function main(args) {
|
|
|
23
23
|
process.on("uncaughtException", (error) => {
|
|
24
24
|
ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`);
|
|
25
25
|
ui.writeVerbose(`Stack trace: ${error.stack}`);
|
|
26
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
26
27
|
process.exit(1);
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -31,6 +32,7 @@ export async function main(args) {
|
|
|
31
32
|
if (reason instanceof Error) {
|
|
32
33
|
ui.writeVerbose(`Stack trace: ${reason.stack}`);
|
|
33
34
|
}
|
|
35
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
34
36
|
process.exit(1);
|
|
35
37
|
});
|
|
36
38
|
|
|
@@ -64,8 +66,7 @@ export async function main(args) {
|
|
|
64
66
|
|
|
65
67
|
const auditStats = getAuditStats();
|
|
66
68
|
if (auditStats.totalPackages > 0) {
|
|
67
|
-
ui.
|
|
68
|
-
ui.writeInformation(
|
|
69
|
+
ui.writeVerbose(
|
|
69
70
|
`${chalk.green("✔")} Safe-chain: Scanned ${
|
|
70
71
|
auditStats.totalPackages
|
|
71
72
|
} packages, no malware found.`
|
|
@@ -90,6 +91,7 @@ export async function main(args) {
|
|
|
90
91
|
return packageManagerResult.status;
|
|
91
92
|
} catch (/** @type any */ error) {
|
|
92
93
|
ui.writeError("Failed to check for malicious packages:", error.message);
|
|
94
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
93
95
|
|
|
94
96
|
// Returning the exit code back to the caller allows the promise
|
|
95
97
|
// to be awaited in the bin files and return the correct exit code
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
|
12
12
|
import { createPipPackageManager } from "./pip/createPackageManager.js";
|
|
13
13
|
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
|
14
|
+
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @type {{packageManagerName: PackageManager | null}}
|
|
@@ -35,10 +36,11 @@ const state = {
|
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* @param {string} packageManagerName
|
|
39
|
+
* @param {{ tool: string, args: string[] }} [context] - Optional tool context for package managers like pip
|
|
38
40
|
*
|
|
39
41
|
* @return {PackageManager}
|
|
40
42
|
*/
|
|
41
|
-
export function initializePackageManager(packageManagerName) {
|
|
43
|
+
export function initializePackageManager(packageManagerName, context) {
|
|
42
44
|
if (packageManagerName === "npm") {
|
|
43
45
|
state.packageManagerName = createNpmPackageManager();
|
|
44
46
|
} else if (packageManagerName === "npx") {
|
|
@@ -54,9 +56,11 @@ export function initializePackageManager(packageManagerName) {
|
|
|
54
56
|
} else if (packageManagerName === "bunx") {
|
|
55
57
|
state.packageManagerName = createBunxPackageManager();
|
|
56
58
|
} else if (packageManagerName === "pip") {
|
|
57
|
-
state.packageManagerName = createPipPackageManager();
|
|
59
|
+
state.packageManagerName = createPipPackageManager(context);
|
|
58
60
|
} else if (packageManagerName === "uv") {
|
|
59
61
|
state.packageManagerName = createUvPackageManager();
|
|
62
|
+
} else if (packageManagerName === "poetry") {
|
|
63
|
+
state.packageManagerName = createPoetryPackageManager();
|
|
60
64
|
} else {
|
|
61
65
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
|
62
66
|
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { runPip } from "./runPipCommand.js";
|
|
2
|
-
import {
|
|
2
|
+
import { PIP_COMMAND } from "./pipSettings.js";
|
|
3
|
+
|
|
3
4
|
/**
|
|
5
|
+
* @param {{ tool: string, args: string[] }} [context] - Optional context with tool name and args
|
|
4
6
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
5
7
|
*/
|
|
6
|
-
export function createPipPackageManager() {
|
|
8
|
+
export function createPipPackageManager(context) {
|
|
9
|
+
const tool = context?.tool || PIP_COMMAND;
|
|
10
|
+
|
|
7
11
|
return {
|
|
8
12
|
/**
|
|
9
13
|
* @param {string[]} args
|
|
10
14
|
*/
|
|
11
15
|
runCommand: (args) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return runPip(
|
|
16
|
+
// Args from main.js are already stripped of --safe-chain-* flags
|
|
17
|
+
// We just pass the tool (e.g. "python3") and the args (e.g. ["-m", "pip", "install", ...])
|
|
18
|
+
return runPip(tool, args);
|
|
15
19
|
},
|
|
16
20
|
// For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
|
|
17
21
|
isSupportedCommand: () => false,
|
|
@@ -1,30 +1,6 @@
|
|
|
1
1
|
export const PIP_PACKAGE_MANAGER = "pip";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
PY_PIP: { command: "python", args: ["-m", "pip"] },
|
|
8
|
-
PY3_PIP: { command: "python3", args: ["-m", "pip"] },
|
|
9
|
-
PY_PIP3: { command: "python", args: ["-m", "pip3"] },
|
|
10
|
-
PY3_PIP3: { command: "python3", args: ["-m", "pip3"] }
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @type {{ command: string, args: string[] }}
|
|
15
|
-
*/
|
|
16
|
-
let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {{ command: string, args: string[] }} invocation
|
|
20
|
-
*/
|
|
21
|
-
export function setCurrentPipInvocation(invocation) {
|
|
22
|
-
currentInvocation = invocation;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @returns {{ command: string, args: string[] }}
|
|
27
|
-
*/
|
|
28
|
-
export function getCurrentPipInvocation() {
|
|
29
|
-
return currentInvocation;
|
|
30
|
-
}
|
|
3
|
+
export const PIP_COMMAND = "pip";
|
|
4
|
+
export const PIP3_COMMAND = "pip3";
|
|
5
|
+
export const PYTHON_COMMAND = "python";
|
|
6
|
+
export const PYTHON3_COMMAND = "python3";
|
|
@@ -2,11 +2,31 @@ import { ui } from "../../environment/userInteraction.js";
|
|
|
2
2
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
3
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
4
4
|
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
|
5
|
+
import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
7
|
import fsSync from "node:fs";
|
|
7
8
|
import os from "node:os";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import ini from "ini";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks if this pip invocation should bypass safe-chain and spawn directly.
|
|
15
|
+
* Returns true if the tool is python/python3 but NOT being run with -m pip/pip3.
|
|
16
|
+
* @param {string} command - The command executable
|
|
17
|
+
* @param {string[]} args - The arguments
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function shouldBypassSafeChain(command, args) {
|
|
21
|
+
if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
|
|
22
|
+
// Check if args start with -m pip
|
|
23
|
+
if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
10
30
|
|
|
11
31
|
/**
|
|
12
32
|
* Sets fallback CA bundle environment variables used by Python libraries.
|
|
@@ -49,15 +69,34 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
|
|
|
49
69
|
* Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
|
|
50
70
|
* users to read/write persistent config. Only CA environment variables are set for these commands.
|
|
51
71
|
*
|
|
52
|
-
* @param {string} command - The pip command
|
|
72
|
+
* @param {string} command - The pip command executable (e.g., 'pip3' or 'python3')
|
|
53
73
|
* @param {string[]} args - Command line arguments to pass to pip
|
|
54
74
|
* @returns {Promise<{status: number}>} Exit status of the pip command
|
|
55
75
|
*/
|
|
56
76
|
export async function runPip(command, args) {
|
|
77
|
+
// Check if we should bypass safe-chain (python/python3 without -m pip)
|
|
78
|
+
if (shouldBypassSafeChain(command, args)) {
|
|
79
|
+
ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
|
|
80
|
+
// Spawn the ORIGINAL command with ORIGINAL args
|
|
81
|
+
return new Promise((_resolve) => {
|
|
82
|
+
const proc = spawn(command, args, { stdio: "inherit" });
|
|
83
|
+
proc.on("exit", (/** @type {number | null} */ code) => {
|
|
84
|
+
ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`);
|
|
85
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
86
|
+
process.exit(code ?? 0);
|
|
87
|
+
});
|
|
88
|
+
proc.on("error", (/** @type {Error} */ err) => {
|
|
89
|
+
ui.writeError(`Error executing command: ${err.message}`);
|
|
90
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
try {
|
|
58
97
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
59
98
|
|
|
60
|
-
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots)
|
|
99
|
+
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs)
|
|
61
100
|
// so that any network request made by pip, including those outside explicit CLI args,
|
|
62
101
|
// validates correctly under both MITM'd and tunneled HTTPS.
|
|
63
102
|
const combinedCaPath = getCombinedCaBundlePath();
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
8
|
+
*/
|
|
9
|
+
export function createPoetryPackageManager() {
|
|
10
|
+
return {
|
|
11
|
+
runCommand: (args) => runPoetryCommand(args),
|
|
12
|
+
|
|
13
|
+
// MITM only approach for Poetry
|
|
14
|
+
isSupportedCommand: () => false,
|
|
15
|
+
getDependencyUpdatesForCommand: () => [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sets CA bundle environment variables used by Poetry and Python libraries.
|
|
21
|
+
* Poetry uses the Python requests library which respects these environment variables.
|
|
22
|
+
*
|
|
23
|
+
* @param {NodeJS.ProcessEnv} env - Environment object to modify
|
|
24
|
+
* @param {string} combinedCaPath - Path to the combined CA bundle
|
|
25
|
+
*/
|
|
26
|
+
function setPoetryCaBundleEnvironmentVariables(env, combinedCaPath) {
|
|
27
|
+
// SSL_CERT_FILE: Used by Python SSL libraries and requests
|
|
28
|
+
if (env.SSL_CERT_FILE) {
|
|
29
|
+
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
|
30
|
+
}
|
|
31
|
+
env.SSL_CERT_FILE = combinedCaPath;
|
|
32
|
+
|
|
33
|
+
// REQUESTS_CA_BUNDLE: Used by the requests library (which Poetry uses)
|
|
34
|
+
if (env.REQUESTS_CA_BUNDLE) {
|
|
35
|
+
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
|
36
|
+
}
|
|
37
|
+
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
|
38
|
+
|
|
39
|
+
// PIP_CERT: Poetry may use pip internally
|
|
40
|
+
if (env.PIP_CERT) {
|
|
41
|
+
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
|
42
|
+
}
|
|
43
|
+
env.PIP_CERT = combinedCaPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs a poetry command with safe-chain's certificate bundle and proxy configuration.
|
|
48
|
+
*
|
|
49
|
+
* Poetry respects standard HTTP_PROXY/HTTPS_PROXY environment variables through
|
|
50
|
+
* the Python requests library.
|
|
51
|
+
*
|
|
52
|
+
* @param {string[]} args - Command line arguments to pass to poetry
|
|
53
|
+
* @returns {Promise<{status: number}>} Exit status of the poetry command
|
|
54
|
+
*/
|
|
55
|
+
async function runPoetryCommand(args) {
|
|
56
|
+
try {
|
|
57
|
+
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
58
|
+
|
|
59
|
+
const combinedCaPath = getCombinedCaBundlePath();
|
|
60
|
+
setPoetryCaBundleEnvironmentVariables(env, combinedCaPath);
|
|
61
|
+
|
|
62
|
+
const result = await safeSpawn("poetry", args, {
|
|
63
|
+
stdio: "inherit",
|
|
64
|
+
env,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { status: result.status };
|
|
68
|
+
} catch (/** @type any */ error) {
|
|
69
|
+
if (error.status) {
|
|
70
|
+
return { status: error.status };
|
|
71
|
+
} else {
|
|
72
|
+
ui.writeError("Error executing command:", error.message);
|
|
73
|
+
ui.writeError("Is 'poetry' installed and available on your system?");
|
|
74
|
+
return { status: 1 };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|