@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 CHANGED
@@ -1,20 +1,22 @@
1
1
  # Aikido Safe Chain
2
2
 
3
- The Aikido Safe Chain **prevents developers from installing malware** on their workstations while developing in the Javascript ecosystem (through npm, npx, yarn, pnpm, pnpx, bun and bunx). It's **free** to use and does not require any token.
4
-
5
- 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), 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, or pip/pip3 from downloading or running the malware.
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
- - **npm**
10
- - **npx**
11
- - **yarn**
12
- - **pnpm**
13
- - **pnpx**
14
- - **bun**
15
- - **bunx**
16
- - **pip** (beta)
17
- - **pip3** (beta)
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 for specific installs using the `--safe-chain-skip-minimum-package-age` flag.
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 PyPI/pip.
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 commands. 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:
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
@@ -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.9",
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/), and [bunx](https://bun.sh/docs/cli/bunx) 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, or bunx from downloading or running the malware.",
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": "^14.5.15",
39
+ "certifi": "14.5.15",
39
40
  "chalk": "5.4.1",
40
41
  "https-proxy-agent": "7.0.6",
41
- "ini": "^6.0.0",
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
  */
@@ -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 {any} scanTimeout
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
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Gets the minimum package age in hours from environment variable
3
+ * @returns {string | undefined}
4
+ */
5
+ export function getMinimumPackageAgeHours() {
6
+ return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS;
7
+ }
@@ -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
+ }
@@ -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
- try {
42
- const packageManager = getPackageManager();
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
- audit = await auditChanges(changes);
58
- } catch (/** @type any */ error) {
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, spinner);
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, spinner) {
90
- spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:"));
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