@aikidosec/safe-chain 1.1.9 → 1.1.10
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 +50 -19
- package/bin/aikido-uv.js +14 -0
- package/package.json +6 -6
- package/src/config/cliArguments.js +24 -1
- package/src/config/configFile.js +33 -1
- package/src/config/environmentVariables.js +7 -0
- package/src/config/settings.js +46 -0
- package/src/environment/userInteraction.js +0 -57
- package/src/packagemanager/currentPackageManager.js +3 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +71 -0
- package/src/scanning/index.js +11 -30
- package/src/shell-integration/helpers.js +1 -0
- 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 +4 -0
package/README.md
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# Aikido Safe Chain
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- ✅ **Block malware on developer laptops and CI/CD**
|
|
4
|
+
- ✅ **Supports npm and PyPI** more package managers coming
|
|
5
|
+
- ✅ **Blocks packages newer than 24 hours** without breaking your build
|
|
6
|
+
- ✅ **Tokenless, free, no build data shared**
|
|
6
7
|
|
|
7
8
|
Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers:
|
|
8
9
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
10
|
+
- 📦 **npm**
|
|
11
|
+
- 📦 **npx**
|
|
12
|
+
- 📦 **yarn**
|
|
13
|
+
- 📦 **pnpm**
|
|
14
|
+
- 📦 **pnpx**
|
|
15
|
+
- 📦 **bun**
|
|
16
|
+
- 📦 **bunx**
|
|
17
|
+
- 📦 **pip** (beta)
|
|
18
|
+
- 📦 **pip3** (beta)
|
|
19
|
+
- 📦 **uv** (beta)
|
|
18
20
|
|
|
19
21
|
# Usage
|
|
20
22
|
|
|
@@ -32,7 +34,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
|
|
32
34
|
safe-chain setup
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
To enable Python (pip/pip3) support (beta), use the `--include-python` flag:
|
|
37
|
+
To enable Python (pip/pip3/uv) support (beta), use the `--include-python` flag:
|
|
36
38
|
|
|
37
39
|
```shell
|
|
38
40
|
safe-chain setup --include-python
|
|
@@ -58,7 +60,7 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
|
|
58
60
|
|
|
59
61
|
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
|
60
62
|
|
|
61
|
-
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, or `pip3` 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.
|
|
63
|
+
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `uv`, `pip`, or `pip3` 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.
|
|
62
64
|
|
|
63
65
|
You can check the installed version by running:
|
|
64
66
|
|
|
@@ -70,17 +72,17 @@ safe-chain --version
|
|
|
70
72
|
|
|
71
73
|
### Malware Blocking
|
|
72
74
|
|
|
73
|
-
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`, or `pip3` 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.
|
|
75
|
+
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`, or `pip3` 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.
|
|
74
76
|
|
|
75
77
|
### Minimum package age (npm only)
|
|
76
78
|
|
|
77
|
-
For npm packages, Safe Chain temporarily suppresses packages published within the last 24 hours 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 bypass this protection
|
|
79
|
+
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.
|
|
78
80
|
|
|
79
|
-
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to
|
|
81
|
+
⚠️ 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).
|
|
80
82
|
|
|
81
83
|
### Shell Integration
|
|
82
84
|
|
|
83
|
-
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip
|
|
85
|
+
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, bun, bunx, and Python package managers (uv, pip). It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:
|
|
84
86
|
|
|
85
87
|
- ✅ **Bash**
|
|
86
88
|
- ✅ **Zsh**
|
|
@@ -126,6 +128,35 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin
|
|
|
126
128
|
npm install express --safe-chain-logging=verbose
|
|
127
129
|
```
|
|
128
130
|
|
|
131
|
+
## Minimum Package Age
|
|
132
|
+
|
|
133
|
+
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 24 hours old before they can be installed through npm-based package managers.
|
|
134
|
+
|
|
135
|
+
### Configuration Options
|
|
136
|
+
|
|
137
|
+
You can set the minimum package age through multiple sources (in order of priority):
|
|
138
|
+
|
|
139
|
+
1. **CLI Argument** (highest priority):
|
|
140
|
+
|
|
141
|
+
```shell
|
|
142
|
+
npm install express --safe-chain-minimum-package-age-hours=48
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
2. **Environment Variable**:
|
|
146
|
+
|
|
147
|
+
```shell
|
|
148
|
+
export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=48
|
|
149
|
+
npm install express
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
3. **Config File** (`~/.aikido/config.json`):
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"minimumPackageAgeHours": 48
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
129
160
|
# Usage in CI/CD
|
|
130
161
|
|
|
131
162
|
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.
|
|
@@ -140,7 +171,7 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after
|
|
|
140
171
|
safe-chain setup-ci
|
|
141
172
|
```
|
|
142
173
|
|
|
143
|
-
To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag:
|
|
174
|
+
To enable Python (pip/pip3/uv) support (beta) in CI/CD, use the `--include-python` flag:
|
|
144
175
|
|
|
145
176
|
```shell
|
|
146
177
|
safe-chain setup-ci --include-python
|
package/bin/aikido-uv.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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("uv");
|
|
11
|
+
|
|
12
|
+
// Pass through only user-supplied uv args
|
|
13
|
+
var exitCode = await main(process.argv.slice(2));
|
|
14
|
+
process.exit(exitCode);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
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'",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
|
16
16
|
"aikido-bun": "bin/aikido-bun.js",
|
|
17
17
|
"aikido-bunx": "bin/aikido-bunx.js",
|
|
18
|
+
"aikido-uv": "bin/aikido-uv.js",
|
|
18
19
|
"aikido-pip": "bin/aikido-pip.js",
|
|
19
20
|
"aikido-pip3": "bin/aikido-pip3.js",
|
|
20
21
|
"aikido-python": "bin/aikido-python.js",
|
|
@@ -33,25 +34,24 @@
|
|
|
33
34
|
"keywords": [],
|
|
34
35
|
"author": "Aikido Security",
|
|
35
36
|
"license": "AGPL-3.0-or-later",
|
|
36
|
-
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/),
|
|
37
|
+
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.",
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"certifi": "
|
|
39
|
+
"certifi": "14.5.15",
|
|
39
40
|
"chalk": "5.4.1",
|
|
40
41
|
"https-proxy-agent": "7.0.6",
|
|
41
|
-
"ini": "
|
|
42
|
+
"ini": "6.0.0",
|
|
42
43
|
"make-fetch-happen": "14.0.3",
|
|
43
44
|
"node-forge": "1.3.1",
|
|
44
45
|
"npm-registry-fetch": "18.0.2",
|
|
45
|
-
"ora": "8.2.0",
|
|
46
46
|
"semver": "7.7.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/ini": "^4.1.1",
|
|
50
50
|
"@types/make-fetch-happen": "^10.0.4",
|
|
51
51
|
"@types/node": "^18.19.130",
|
|
52
|
+
"@types/node-forge": "^1.3.14",
|
|
52
53
|
"@types/npm-registry-fetch": "^8.0.9",
|
|
53
54
|
"@types/semver": "^7.7.1",
|
|
54
|
-
"@types/node-forge": "^1.3.14",
|
|
55
55
|
"typescript": "^5.9.3"
|
|
56
56
|
},
|
|
57
57
|
"main": "src/main.js",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, includePython: boolean}}
|
|
2
|
+
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, includePython: boolean}}
|
|
3
3
|
*/
|
|
4
4
|
const state = {
|
|
5
5
|
loggingLevel: undefined,
|
|
6
6
|
skipMinimumPackageAge: undefined,
|
|
7
|
+
minimumPackageAgeHours: undefined,
|
|
7
8
|
includePython: false,
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ export function initializeCliArguments(args) {
|
|
|
17
18
|
// Reset state on each call
|
|
18
19
|
state.loggingLevel = undefined;
|
|
19
20
|
state.skipMinimumPackageAge = undefined;
|
|
21
|
+
state.minimumPackageAgeHours = undefined;
|
|
20
22
|
|
|
21
23
|
const safeChainArgs = [];
|
|
22
24
|
const remainingArgs = [];
|
|
@@ -31,6 +33,7 @@ export function initializeCliArguments(args) {
|
|
|
31
33
|
|
|
32
34
|
setLoggingLevel(safeChainArgs);
|
|
33
35
|
setSkipMinimumPackageAge(safeChainArgs);
|
|
36
|
+
setMinimumPackageAgeHours(safeChainArgs);
|
|
34
37
|
setIncludePython(args);
|
|
35
38
|
|
|
36
39
|
return remainingArgs;
|
|
@@ -86,6 +89,26 @@ export function getSkipMinimumPackageAge() {
|
|
|
86
89
|
return state.skipMinimumPackageAge;
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @param {string[]} args
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
function setMinimumPackageAgeHours(args) {
|
|
97
|
+
const argName = SAFE_CHAIN_ARG_PREFIX + "minimum-package-age-hours=";
|
|
98
|
+
|
|
99
|
+
const value = getLastArgEqualsValue(args, argName);
|
|
100
|
+
if (value) {
|
|
101
|
+
state.minimumPackageAgeHours = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @returns {string | undefined}
|
|
107
|
+
*/
|
|
108
|
+
export function getMinimumPackageAgeHours() {
|
|
109
|
+
return state.minimumPackageAgeHours;
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
/**
|
|
90
113
|
* @param {string[]} args
|
|
91
114
|
*/
|
package/src/config/configFile.js
CHANGED
|
@@ -9,7 +9,8 @@ import { getEcoSystem } from "./settings.js";
|
|
|
9
9
|
*
|
|
10
10
|
* This should be a number, but can be anything because it is user-input.
|
|
11
11
|
* We cannot trust the input and should add the necessary validations.
|
|
12
|
-
* @property {
|
|
12
|
+
* @property {unknown} scanTimeout
|
|
13
|
+
* @property {unknown} minimumPackageAgeHours
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -48,6 +49,35 @@ function validateTimeout(value) {
|
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* @param {any} value
|
|
54
|
+
* @returns {number | undefined}
|
|
55
|
+
*/
|
|
56
|
+
function validateMinimumPackageAgeHours(value) {
|
|
57
|
+
const hours = Number(value);
|
|
58
|
+
if (!Number.isNaN(hours)) {
|
|
59
|
+
return hours;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets the minimum package age in hours from config file only
|
|
66
|
+
* @returns {number | undefined}
|
|
67
|
+
*/
|
|
68
|
+
export function getMinimumPackageAgeHours() {
|
|
69
|
+
const config = readConfigFile();
|
|
70
|
+
if (config.minimumPackageAgeHours) {
|
|
71
|
+
const validated = validateMinimumPackageAgeHours(
|
|
72
|
+
config.minimumPackageAgeHours
|
|
73
|
+
);
|
|
74
|
+
if (validated !== undefined) {
|
|
75
|
+
return validated;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
/**
|
|
52
82
|
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
|
53
83
|
* @param {string | number} version
|
|
@@ -111,6 +141,7 @@ function readConfigFile() {
|
|
|
111
141
|
if (!fs.existsSync(configFilePath)) {
|
|
112
142
|
return {
|
|
113
143
|
scanTimeout: undefined,
|
|
144
|
+
minimumPackageAgeHours: undefined,
|
|
114
145
|
};
|
|
115
146
|
}
|
|
116
147
|
|
|
@@ -120,6 +151,7 @@ function readConfigFile() {
|
|
|
120
151
|
} catch {
|
|
121
152
|
return {
|
|
122
153
|
scanTimeout: undefined,
|
|
154
|
+
minimumPackageAgeHours: undefined,
|
|
123
155
|
};
|
|
124
156
|
}
|
|
125
157
|
}
|
package/src/config/settings.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as cliArguments from "./cliArguments.js";
|
|
2
|
+
import * as configFile from "./configFile.js";
|
|
3
|
+
import * as environmentVariables from "./environmentVariables.js";
|
|
2
4
|
|
|
3
5
|
export const LOGGING_SILENT = "silent";
|
|
4
6
|
export const LOGGING_NORMAL = "normal";
|
|
@@ -38,10 +40,54 @@ export function setEcoSystem(setting) {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const defaultMinimumPackageAge = 24;
|
|
43
|
+
/** @returns {number} */
|
|
41
44
|
export function getMinimumPackageAgeHours() {
|
|
45
|
+
// Priority 1: CLI argument
|
|
46
|
+
const cliValue = validateMinimumPackageAgeHours(
|
|
47
|
+
cliArguments.getMinimumPackageAgeHours()
|
|
48
|
+
);
|
|
49
|
+
if (cliValue !== undefined) {
|
|
50
|
+
return cliValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Priority 2: Environment variable
|
|
54
|
+
const envValue = validateMinimumPackageAgeHours(
|
|
55
|
+
environmentVariables.getMinimumPackageAgeHours()
|
|
56
|
+
);
|
|
57
|
+
if (envValue !== undefined) {
|
|
58
|
+
return envValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Priority 3: Config file
|
|
62
|
+
const configValue = configFile.getMinimumPackageAgeHours();
|
|
63
|
+
if (configValue !== undefined) {
|
|
64
|
+
return configValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
return defaultMinimumPackageAge;
|
|
43
68
|
}
|
|
44
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @param {string | undefined} value
|
|
72
|
+
* @returns {number | undefined}
|
|
73
|
+
*/
|
|
74
|
+
function validateMinimumPackageAgeHours(value) {
|
|
75
|
+
if (!value) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const numericValue = Number(value);
|
|
80
|
+
if (Number.isNaN(numericValue)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (numericValue > 0) {
|
|
85
|
+
return numericValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
45
91
|
const defaultSkipMinimumPackageAge = false;
|
|
46
92
|
export function skipMinimumPackageAge() {
|
|
47
93
|
const cliValue = cliArguments.getSkipMinimumPackageAge();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// oxlint-disable no-console
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import ora from "ora";
|
|
4
3
|
import { isCi } from "./environment.js";
|
|
5
4
|
import {
|
|
6
5
|
getLoggingLevel,
|
|
@@ -98,61 +97,6 @@ function writeOrBuffer(messageFunction) {
|
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
/**
|
|
102
|
-
* @typedef {Object} Spinner
|
|
103
|
-
* @property {(message: string) => void} succeed
|
|
104
|
-
* @property {(message: string) => void} fail
|
|
105
|
-
* @property {() => void} stop
|
|
106
|
-
* @property {(message: string) => void} setText
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* @param {string} message
|
|
111
|
-
*
|
|
112
|
-
* @returns {Spinner}
|
|
113
|
-
*/
|
|
114
|
-
function startProcess(message) {
|
|
115
|
-
if (isSilentMode()) {
|
|
116
|
-
return {
|
|
117
|
-
succeed: () => {},
|
|
118
|
-
fail: () => {},
|
|
119
|
-
stop: () => {},
|
|
120
|
-
setText: () => {},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (isCi()) {
|
|
125
|
-
return {
|
|
126
|
-
succeed: (message) => {
|
|
127
|
-
writeInformation(message);
|
|
128
|
-
},
|
|
129
|
-
fail: (message) => {
|
|
130
|
-
writeError(message);
|
|
131
|
-
},
|
|
132
|
-
stop: () => {},
|
|
133
|
-
setText: (message) => {
|
|
134
|
-
writeInformation(message);
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
} else {
|
|
138
|
-
const spinner = ora(message).start();
|
|
139
|
-
return {
|
|
140
|
-
succeed: (message) => {
|
|
141
|
-
spinner.succeed(message);
|
|
142
|
-
},
|
|
143
|
-
fail: (message) => {
|
|
144
|
-
spinner.fail(message);
|
|
145
|
-
},
|
|
146
|
-
stop: () => {
|
|
147
|
-
spinner.stop();
|
|
148
|
-
},
|
|
149
|
-
setText: (message) => {
|
|
150
|
-
spinner.text = message;
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
100
|
function startBufferingLogs() {
|
|
157
101
|
state.bufferOutput = true;
|
|
158
102
|
state.bufferedMessages = [];
|
|
@@ -173,7 +117,6 @@ export const ui = {
|
|
|
173
117
|
writeError,
|
|
174
118
|
writeExitWithoutInstallingMaliciousPackages,
|
|
175
119
|
emptyLine,
|
|
176
|
-
startProcess,
|
|
177
120
|
startBufferingLogs,
|
|
178
121
|
writeBufferedLogsAndStopBuffering,
|
|
179
122
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./pnpm/createPackageManager.js";
|
|
11
11
|
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
|
12
12
|
import { createPipPackageManager } from "./pip/createPackageManager.js";
|
|
13
|
+
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @type {{packageManagerName: PackageManager | null}}
|
|
@@ -54,6 +55,8 @@ export function initializePackageManager(packageManagerName) {
|
|
|
54
55
|
state.packageManagerName = createBunxPackageManager();
|
|
55
56
|
} else if (packageManagerName === "pip") {
|
|
56
57
|
state.packageManagerName = createPipPackageManager();
|
|
58
|
+
} else if (packageManagerName === "uv") {
|
|
59
|
+
state.packageManagerName = createUvPackageManager();
|
|
57
60
|
} else {
|
|
58
61
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
|
59
62
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runUv } from "./runUvCommand.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
5
|
+
*/
|
|
6
|
+
export function createUvPackageManager() {
|
|
7
|
+
return {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
*/
|
|
11
|
+
runCommand: (args) => {
|
|
12
|
+
return runUv("uv", args);
|
|
13
|
+
},
|
|
14
|
+
// For uv, rely solely on MITM
|
|
15
|
+
isSupportedCommand: () => false,
|
|
16
|
+
getDependencyUpdatesForCommand: () => [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 uv.
|
|
8
|
+
*
|
|
9
|
+
* @param {NodeJS.ProcessEnv} env - Env object
|
|
10
|
+
* @param {string} combinedCaPath - Path to the combined CA bundle
|
|
11
|
+
*/
|
|
12
|
+
function setUvCaBundleEnvironmentVariables(env, combinedCaPath) {
|
|
13
|
+
// SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients
|
|
14
|
+
if (env.SSL_CERT_FILE) {
|
|
15
|
+
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
|
16
|
+
}
|
|
17
|
+
env.SSL_CERT_FILE = combinedCaPath;
|
|
18
|
+
|
|
19
|
+
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
|
|
20
|
+
if (env.REQUESTS_CA_BUNDLE) {
|
|
21
|
+
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
|
22
|
+
}
|
|
23
|
+
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
|
24
|
+
|
|
25
|
+
// PIP_CERT: Some underlying pip operations may respect this
|
|
26
|
+
if (env.PIP_CERT) {
|
|
27
|
+
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
|
28
|
+
}
|
|
29
|
+
env.PIP_CERT = combinedCaPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runs a uv command with safe-chain's certificate bundle and proxy configuration.
|
|
34
|
+
*
|
|
35
|
+
* uv respects standard environment variables for proxy and TLS configuration:
|
|
36
|
+
* - HTTP_PROXY / HTTPS_PROXY: Proxy settings
|
|
37
|
+
* - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
|
|
38
|
+
*
|
|
39
|
+
* Unlike pip (which requires a temporary config file for cert configuration), uv directly
|
|
40
|
+
* honors environment variables, so no config/ini file is needed.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} command - The uv command to execute (typically 'uv')
|
|
43
|
+
* @param {string[]} args - Command line arguments to pass to uv
|
|
44
|
+
* @returns {Promise<{status: number}>} Exit status of the uv command
|
|
45
|
+
*/
|
|
46
|
+
export async function runUv(command, args) {
|
|
47
|
+
try {
|
|
48
|
+
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
49
|
+
|
|
50
|
+
const combinedCaPath = getCombinedCaBundlePath();
|
|
51
|
+
setUvCaBundleEnvironmentVariables(env, combinedCaPath);
|
|
52
|
+
|
|
53
|
+
// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
|
|
54
|
+
// These are already set by mergeSafeChainProxyEnvironmentVariables
|
|
55
|
+
|
|
56
|
+
const result = await safeSpawn(command, args, {
|
|
57
|
+
stdio: "inherit",
|
|
58
|
+
env,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { status: result.status };
|
|
62
|
+
} catch (/** @type any */ error) {
|
|
63
|
+
if (error.status) {
|
|
64
|
+
return { status: error.status };
|
|
65
|
+
} else {
|
|
66
|
+
ui.writeError(`Error executing command: ${error.message}`);
|
|
67
|
+
ui.writeError(`Is '${command}' installed and available on your system?`);
|
|
68
|
+
return { status: 1 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/scanning/index.js
CHANGED
|
@@ -29,36 +29,19 @@ export async function scanCommand(args) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
let timedOut = false;
|
|
32
|
-
|
|
33
|
-
const spinner = ui.startProcess(
|
|
34
|
-
"Safe-chain: Scanning for malicious packages..."
|
|
35
|
-
);
|
|
36
32
|
/** @type {import("./audit/index.js").AuditResult | undefined} */
|
|
37
33
|
let audit;
|
|
38
34
|
|
|
39
35
|
await Promise.race([
|
|
40
36
|
(async () => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const changes = await packageManager.getDependencyUpdatesForCommand(
|
|
44
|
-
args
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
if (timedOut) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (changes.length > 0) {
|
|
52
|
-
spinner.setText(
|
|
53
|
-
`Safe-chain: Scanning ${changes.length} package(s)...`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
37
|
+
const packageManager = getPackageManager();
|
|
38
|
+
const changes = await packageManager.getDependencyUpdatesForCommand(args);
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
spinner.fail(`Safe-chain: Error while scanning.`);
|
|
60
|
-
throw error;
|
|
40
|
+
if (timedOut) {
|
|
41
|
+
return;
|
|
61
42
|
}
|
|
43
|
+
|
|
44
|
+
audit = await auditChanges(changes);
|
|
62
45
|
})(),
|
|
63
46
|
setTimeout(getScanTimeout()).then(() => {
|
|
64
47
|
timedOut = true;
|
|
@@ -66,15 +49,13 @@ export async function scanCommand(args) {
|
|
|
66
49
|
]);
|
|
67
50
|
|
|
68
51
|
if (timedOut) {
|
|
69
|
-
spinner.fail("Safe-chain: Timeout exceeded while scanning.");
|
|
70
52
|
throw new Error("Timeout exceeded while scanning npm install command.");
|
|
71
53
|
}
|
|
72
54
|
|
|
73
55
|
if (!audit || audit.isAllowed) {
|
|
74
|
-
spinner.stop();
|
|
75
56
|
return 0;
|
|
76
57
|
} else {
|
|
77
|
-
printMaliciousChanges(audit.disallowedChanges
|
|
58
|
+
printMaliciousChanges(audit.disallowedChanges);
|
|
78
59
|
onMalwareFound();
|
|
79
60
|
return 1;
|
|
80
61
|
}
|
|
@@ -82,12 +63,12 @@ export async function scanCommand(args) {
|
|
|
82
63
|
|
|
83
64
|
/**
|
|
84
65
|
* @param {import("./audit/index.js").PackageChange[]} changes
|
|
85
|
-
* @param spinner {import("../environment/userInteraction.js").Spinner}
|
|
86
|
-
*
|
|
87
66
|
* @return {void}
|
|
88
67
|
*/
|
|
89
|
-
function printMaliciousChanges(changes
|
|
90
|
-
|
|
68
|
+
function printMaliciousChanges(changes) {
|
|
69
|
+
ui.writeInformation(
|
|
70
|
+
chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:")
|
|
71
|
+
);
|
|
91
72
|
|
|
92
73
|
for (const change of changes) {
|
|
93
74
|
ui.writeInformation(` - ${change.name}@${change.version}`);
|
|
@@ -22,6 +22,7 @@ export const knownAikidoTools = [
|
|
|
22
22
|
{ tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
|
|
23
23
|
{ tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS },
|
|
24
24
|
{ tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS },
|
|
25
|
+
{ tool: "uv", aikidoCommand: "aikido-uv", ecoSystem: ECOSYSTEM_PY },
|
|
25
26
|
{ tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY },
|
|
26
27
|
{ tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
|
|
27
28
|
{ tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },
|
|
@@ -77,6 +77,10 @@ function pip3
|
|
|
77
77
|
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
function uv
|
|
81
|
+
wrapSafeChainCommand "uv" "aikido-uv" $argv
|
|
82
|
+
end
|
|
83
|
+
|
|
80
84
|
# `python -m pip`, `python -m pip3`.
|
|
81
85
|
function python
|
|
82
86
|
wrapSafeChainCommand "python" "aikido-python" $argv
|
|
@@ -69,6 +69,10 @@ function pip3() {
|
|
|
69
69
|
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function uv() {
|
|
73
|
+
wrapSafeChainCommand "uv" "aikido-uv" "$@"
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
# `python -m pip`, `python -m pip3`.
|
|
73
77
|
function python() {
|
|
74
78
|
wrapSafeChainCommand "python" "aikido-python" "$@"
|
|
@@ -95,6 +95,10 @@ function pip3 {
|
|
|
95
95
|
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function uv {
|
|
99
|
+
Invoke-WrappedCommand "uv" "aikido-uv" $args
|
|
100
|
+
}
|
|
101
|
+
|
|
98
102
|
# `python -m pip`, `python -m pip3`.
|
|
99
103
|
function python {
|
|
100
104
|
Invoke-WrappedCommand 'python' 'aikido-python' $args
|