@aikidosec/safe-chain 1.1.7 → 1.1.8
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 +59 -15
- package/bin/aikido-bun.js +2 -0
- package/bin/aikido-bunx.js +2 -0
- package/bin/aikido-npm.js +2 -0
- package/bin/aikido-npx.js +2 -0
- package/bin/aikido-pip.js +18 -0
- package/bin/aikido-pip3.js +19 -0
- package/bin/aikido-pnpm.js +2 -0
- package/bin/aikido-pnpx.js +2 -0
- package/bin/aikido-python.js +28 -0
- package/bin/aikido-python3.js +28 -0
- package/bin/aikido-yarn.js +2 -0
- package/bin/safe-chain.js +17 -4
- package/docs/shell-integration.md +30 -4
- package/package.json +18 -2
- package/src/api/aikido.js +26 -5
- package/src/api/npmApi.js +23 -2
- package/src/config/cliArguments.js +65 -0
- package/src/config/configFile.js +77 -8
- package/src/config/settings.js +42 -2
- package/src/environment/userInteraction.js +88 -5
- package/src/main.js +43 -7
- package/src/packagemanager/_shared/matchesCommand.js +5 -0
- package/src/packagemanager/bun/createBunPackageManager.js +12 -1
- package/src/packagemanager/currentPackageManager.js +25 -0
- package/src/packagemanager/npm/createPackageManager.js +23 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +37 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +3 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +35 -2
- package/src/packagemanager/npm/runNpmCommand.js +6 -30
- package/src/packagemanager/npm/utils/abbrevs-generated.js +1 -0
- package/src/packagemanager/npm/utils/cmd-list.js +5 -0
- package/src/packagemanager/npm/utils/npmCommands.js +8 -0
- package/src/packagemanager/npx/createPackageManager.js +3 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +12 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +22 -0
- package/src/packagemanager/npx/runNpxCommand.js +6 -1
- package/src/packagemanager/pip/createPackageManager.js +21 -0
- package/src/packagemanager/pip/pipSettings.js +30 -0
- package/src/packagemanager/pip/runPipCommand.js +150 -0
- package/src/packagemanager/pnpm/createPackageManager.js +11 -0
- package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +7 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +21 -0
- package/src/packagemanager/pnpm/runPnpmCommand.js +6 -1
- package/src/packagemanager/yarn/createPackageManager.js +8 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +7 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +24 -0
- package/src/packagemanager/yarn/runYarnCommand.js +13 -25
- package/src/registryProxy/certBundle.js +95 -0
- package/src/registryProxy/certUtils.js +14 -0
- package/src/registryProxy/http-utils.js +17 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +174 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
- package/src/registryProxy/{parsePackageFromUrl.js → interceptors/npm/parseNpmPackageUrl.js} +7 -12
- package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
- package/src/registryProxy/mitmRequestHandler.js +123 -19
- package/src/registryProxy/plainHttpProxy.js +21 -0
- package/src/registryProxy/registryProxy.js +50 -26
- package/src/registryProxy/tunnelRequestHandler.js +31 -7
- package/src/scanning/audit/index.js +73 -0
- package/src/scanning/index.js +19 -2
- package/src/scanning/malwareDatabase.js +57 -7
- package/src/shell-integration/helpers.js +52 -7
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +1 -1
- package/src/shell-integration/setup-ci.js +40 -10
- package/src/shell-integration/setup.js +9 -3
- package/src/shell-integration/shellDetection.js +12 -1
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +88 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +80 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +107 -0
- package/src/shell-integration/supported-shells/bash.js +19 -1
- package/src/shell-integration/supported-shells/fish.js +9 -1
- package/src/shell-integration/supported-shells/powershell.js +9 -1
- package/src/shell-integration/supported-shells/windowsPowershell.js +9 -1
- package/src/shell-integration/supported-shells/zsh.js +6 -1
- package/src/shell-integration/teardown.js +4 -1
- package/src/utils/safeSpawn.js +38 -1
- package/tsconfig.json +21 -0
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# Aikido Safe Chain
|
|
2
2
|
|
|
3
|
-
The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm, pnpx, bun
|
|
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
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/),
|
|
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.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers:
|
|
7
|
+
Aikido Safe Chain works on Node.js version 16 and above and supports the following package managers:
|
|
10
8
|
|
|
11
9
|
- ✅ **npm**
|
|
12
10
|
- ✅ **npx**
|
|
@@ -15,6 +13,8 @@ Aikido Safe Chain works on Node.js version 18 and above and supports the followi
|
|
|
15
13
|
- ✅ **pnpx**
|
|
16
14
|
- ✅ **bun**
|
|
17
15
|
- ✅ **bunx**
|
|
16
|
+
- ✅ **pip** (beta)
|
|
17
|
+
- ✅ **pip3** (beta)
|
|
18
18
|
|
|
19
19
|
# Usage
|
|
20
20
|
|
|
@@ -27,18 +27,38 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
|
|
27
27
|
npm install -g @aikidosec/safe-chain
|
|
28
28
|
```
|
|
29
29
|
2. **Setup the shell integration** by running:
|
|
30
|
+
|
|
30
31
|
```shell
|
|
31
32
|
safe-chain setup
|
|
32
33
|
```
|
|
34
|
+
|
|
35
|
+
To enable Python (pip/pip3) support (beta), use the `--include-python` flag:
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
safe-chain setup --include-python
|
|
39
|
+
```
|
|
40
|
+
|
|
33
41
|
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
|
|
43
|
+
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
|
44
|
+
|
|
45
|
+
4. **Verify the installation** by running one of the following commands:
|
|
46
|
+
|
|
47
|
+
For JavaScript/Node.js:
|
|
48
|
+
|
|
36
49
|
```shell
|
|
37
50
|
npm install safe-chain-test
|
|
38
51
|
```
|
|
39
|
-
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
For Python (beta):
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
pip3 install safe-chain-pi-test
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
|
60
|
+
|
|
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.
|
|
42
62
|
|
|
43
63
|
You can check the installed version by running:
|
|
44
64
|
|
|
@@ -48,9 +68,19 @@ safe-chain --version
|
|
|
48
68
|
|
|
49
69
|
## How it works
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
### Malware Blocking
|
|
72
|
+
|
|
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.
|
|
74
|
+
|
|
75
|
+
### Minimum package age (npm only)
|
|
76
|
+
|
|
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.
|
|
52
78
|
|
|
53
|
-
|
|
79
|
+
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to PyPI/pip.
|
|
80
|
+
|
|
81
|
+
### Shell Integration
|
|
82
|
+
|
|
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:
|
|
54
84
|
|
|
55
85
|
- ✅ **Bash**
|
|
56
86
|
- ✅ **Zsh**
|
|
@@ -82,11 +112,19 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin
|
|
|
82
112
|
|
|
83
113
|
- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit.
|
|
84
114
|
|
|
85
|
-
Example usage:
|
|
115
|
+
Example usage:
|
|
86
116
|
|
|
87
|
-
```shell
|
|
88
|
-
npm install express --safe-chain-logging=silent
|
|
89
|
-
```
|
|
117
|
+
```shell
|
|
118
|
+
npm install express --safe-chain-logging=silent
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes.
|
|
122
|
+
|
|
123
|
+
Example usage:
|
|
124
|
+
|
|
125
|
+
```shell
|
|
126
|
+
npm install express --safe-chain-logging=verbose
|
|
127
|
+
```
|
|
90
128
|
|
|
91
129
|
# Usage in CI/CD
|
|
92
130
|
|
|
@@ -102,6 +140,12 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after
|
|
|
102
140
|
safe-chain setup-ci
|
|
103
141
|
```
|
|
104
142
|
|
|
143
|
+
To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag:
|
|
144
|
+
|
|
145
|
+
```shell
|
|
146
|
+
safe-chain setup-ci --include-python
|
|
147
|
+
```
|
|
148
|
+
|
|
105
149
|
This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands.
|
|
106
150
|
|
|
107
151
|
## Supported Platforms
|
package/bin/aikido-bun.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "bun";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
package/bin/aikido-bunx.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "bunx";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
package/bin/aikido-npm.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "npm";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
package/bin/aikido-npx.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "npx";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
|
7
|
+
|
|
8
|
+
// Set eco system
|
|
9
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
10
|
+
|
|
11
|
+
// Set current invocation
|
|
12
|
+
setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
|
|
13
|
+
|
|
14
|
+
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
15
|
+
|
|
16
|
+
// Pass through only user-supplied pip args
|
|
17
|
+
var exitCode = await main(process.argv.slice(2));
|
|
18
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
|
7
|
+
|
|
8
|
+
// Set eco system
|
|
9
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
10
|
+
|
|
11
|
+
// Set current invocation
|
|
12
|
+
setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
|
|
13
|
+
|
|
14
|
+
// Create package manager
|
|
15
|
+
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
16
|
+
|
|
17
|
+
// Pass through only user-supplied pip args
|
|
18
|
+
var exitCode = await main(process.argv.slice(2));
|
|
19
|
+
process.exit(exitCode);
|
package/bin/aikido-pnpm.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "pnpm";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
package/bin/aikido-pnpx.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "pnpx";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
4
|
+
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
|
6
|
+
import { main } from "../src/main.js";
|
|
7
|
+
|
|
8
|
+
// Set eco system
|
|
9
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
10
|
+
|
|
11
|
+
// Strip nodejs and wrapper script from args
|
|
12
|
+
let argv = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
|
|
15
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
16
|
+
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
|
|
17
|
+
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
18
|
+
|
|
19
|
+
// Strip off the '-m pip' or '-m pip3' from the args
|
|
20
|
+
argv = argv.slice(2);
|
|
21
|
+
|
|
22
|
+
var exitCode = await main(argv);
|
|
23
|
+
process.exit(exitCode);
|
|
24
|
+
} else {
|
|
25
|
+
// Forward to real python binary for non-pip flows
|
|
26
|
+
const { spawn } = await import('child_process');
|
|
27
|
+
spawn('python', argv, { stdio: 'inherit' });
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
4
|
+
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
|
6
|
+
import { main } from "../src/main.js";
|
|
7
|
+
|
|
8
|
+
// Set eco system
|
|
9
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
10
|
+
|
|
11
|
+
// Strip nodejs and wrapper script from args
|
|
12
|
+
let argv = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
|
|
15
|
+
setEcoSystem(ECOSYSTEM_PY);
|
|
16
|
+
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
|
|
17
|
+
initializePackageManager(PIP_PACKAGE_MANAGER);
|
|
18
|
+
|
|
19
|
+
// Strip off the '-m pip' or '-m pip3' from the args
|
|
20
|
+
argv = argv.slice(2);
|
|
21
|
+
|
|
22
|
+
var exitCode = await main(argv);
|
|
23
|
+
process.exit(exitCode);
|
|
24
|
+
} else {
|
|
25
|
+
// Forward to real python3 binary for non-pip flows
|
|
26
|
+
const { spawn } = await import('child_process');
|
|
27
|
+
spawn('python3', argv, { stdio: 'inherit' });
|
|
28
|
+
}
|
package/bin/aikido-yarn.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/main.js";
|
|
4
4
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
|
5
|
+
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
|
5
6
|
|
|
7
|
+
setEcoSystem(ECOSYSTEM_JS);
|
|
6
8
|
const packageManagerName = "yarn";
|
|
7
9
|
initializePackageManager(packageManagerName);
|
|
8
10
|
var exitCode = await main(process.argv.slice(2));
|
package/bin/safe-chain.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ui } from "../src/environment/userInteraction.js";
|
|
|
6
6
|
import { setup } from "../src/shell-integration/setup.js";
|
|
7
7
|
import { teardown } from "../src/shell-integration/teardown.js";
|
|
8
8
|
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
|
9
|
+
import { initializeCliArguments } from "../src/config/cliArguments.js";
|
|
9
10
|
|
|
10
11
|
if (process.argv.length < 3) {
|
|
11
12
|
ui.writeError("No command provided. Please provide a command to execute.");
|
|
@@ -14,6 +15,8 @@ if (process.argv.length < 3) {
|
|
|
14
15
|
process.exit(1);
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
initializeCliArguments(process.argv);
|
|
19
|
+
|
|
17
20
|
const command = process.argv[2];
|
|
18
21
|
|
|
19
22
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
@@ -54,7 +57,12 @@ function writeHelp() {
|
|
|
54
57
|
ui.writeInformation(
|
|
55
58
|
`- ${chalk.cyan(
|
|
56
59
|
"safe-chain setup"
|
|
57
|
-
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun and
|
|
60
|
+
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`
|
|
61
|
+
);
|
|
62
|
+
ui.writeInformation(
|
|
63
|
+
` ${chalk.yellow(
|
|
64
|
+
"--include-python"
|
|
65
|
+
)}: Experimental: include Python package managers (pip, pip3) in the setup.`
|
|
58
66
|
);
|
|
59
67
|
ui.writeInformation(
|
|
60
68
|
`- ${chalk.cyan(
|
|
@@ -67,9 +75,14 @@ function writeHelp() {
|
|
|
67
75
|
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
|
|
68
76
|
);
|
|
69
77
|
ui.writeInformation(
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
)} (
|
|
78
|
+
` ${chalk.yellow(
|
|
79
|
+
"--include-python"
|
|
80
|
+
)}: Experimental: include Python package managers (pip, pip3) in the setup.`
|
|
81
|
+
);
|
|
82
|
+
ui.writeInformation(
|
|
83
|
+
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
|
|
84
|
+
"-v"
|
|
85
|
+
)}): Display the current version of safe-chain.`
|
|
73
86
|
);
|
|
74
87
|
ui.emptyLine();
|
|
75
88
|
}
|
|
@@ -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`) with Aikido's security scanning functionality. 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`) 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,8 @@ 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`, and `
|
|
31
|
+
- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3`
|
|
32
|
+
- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` route through Safe Chain when invoked by name
|
|
32
33
|
|
|
33
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.
|
|
34
35
|
|
|
@@ -77,7 +78,7 @@ The system modifies the following files to source Safe Chain startup scripts:
|
|
|
77
78
|
This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
|
|
78
79
|
|
|
79
80
|
- Make sure Aikido Safe Chain is properly installed on your system
|
|
80
|
-
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, and `aikido-
|
|
81
|
+
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, and `aikido-pip3` commands exist
|
|
81
82
|
- Check that these commands are in your system's PATH
|
|
82
83
|
|
|
83
84
|
### Manual Verification
|
|
@@ -120,4 +121,29 @@ npm() {
|
|
|
120
121
|
}
|
|
121
122
|
```
|
|
122
123
|
|
|
123
|
-
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, and `
|
|
124
|
+
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `bun`, `bunx`, `pip`, and `pip3` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
|
|
125
|
+
|
|
126
|
+
To intercept Python module invocations for pip without altering Python itself, you can add small forwarding functions:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Example for Bash/Zsh
|
|
130
|
+
python() {
|
|
131
|
+
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
|
132
|
+
local mod="$2"; shift 2
|
|
133
|
+
if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi
|
|
134
|
+
else
|
|
135
|
+
command python "$@"
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
python3() {
|
|
140
|
+
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
|
141
|
+
local mod="$2"; shift 2
|
|
142
|
+
if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi
|
|
143
|
+
else
|
|
144
|
+
command python3 "$@"
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Limitations: these only apply when invoking `python`/`python3` by name. Absolute paths (e.g., `/usr/bin/python -m pip`) bypass shell functions.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
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'",
|
|
7
|
-
"lint": "oxlint --deny-warnings"
|
|
7
|
+
"lint": "oxlint --deny-warnings",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
8
9
|
},
|
|
9
10
|
"bin": {
|
|
10
11
|
"aikido-npm": "bin/aikido-npm.js",
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
|
15
16
|
"aikido-bun": "bin/aikido-bun.js",
|
|
16
17
|
"aikido-bunx": "bin/aikido-bunx.js",
|
|
18
|
+
"aikido-pip": "bin/aikido-pip.js",
|
|
19
|
+
"aikido-pip3": "bin/aikido-pip3.js",
|
|
20
|
+
"aikido-python": "bin/aikido-python.js",
|
|
21
|
+
"aikido-python3": "bin/aikido-python3.js",
|
|
17
22
|
"safe-chain": "bin/safe-chain.js"
|
|
18
23
|
},
|
|
19
24
|
"type": "module",
|
|
@@ -30,14 +35,25 @@
|
|
|
30
35
|
"license": "AGPL-3.0-or-later",
|
|
31
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.",
|
|
32
37
|
"dependencies": {
|
|
38
|
+
"certifi": "^14.5.15",
|
|
33
39
|
"chalk": "5.4.1",
|
|
34
40
|
"https-proxy-agent": "7.0.6",
|
|
41
|
+
"ini": "^6.0.0",
|
|
35
42
|
"make-fetch-happen": "14.0.3",
|
|
36
43
|
"node-forge": "1.3.1",
|
|
37
44
|
"npm-registry-fetch": "18.0.2",
|
|
38
45
|
"ora": "8.2.0",
|
|
39
46
|
"semver": "7.7.2"
|
|
40
47
|
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/ini": "^4.1.1",
|
|
50
|
+
"@types/make-fetch-happen": "^10.0.4",
|
|
51
|
+
"@types/node": "^18.19.130",
|
|
52
|
+
"@types/npm-registry-fetch": "^8.0.9",
|
|
53
|
+
"@types/semver": "^7.7.1",
|
|
54
|
+
"@types/node-forge": "^1.3.14",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
},
|
|
41
57
|
"main": "src/main.js",
|
|
42
58
|
"bugs": {
|
|
43
59
|
"url": "https://github.com/AikidoSec/safe-chain/issues"
|
package/src/api/aikido.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import fetch from "make-fetch-happen";
|
|
2
|
+
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
"https://malware-list.aikido.dev/malware_predictions.json"
|
|
4
|
+
const malwareDatabaseUrls = {
|
|
5
|
+
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
|
|
6
|
+
[ECOSYSTEM_PY]: "https://malware-list.aikido.dev/malware_pypi.json",
|
|
7
|
+
};
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} MalwarePackage
|
|
11
|
+
* @property {string} package_name
|
|
12
|
+
* @property {string} version
|
|
13
|
+
* @property {string} reason
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
|
|
18
|
+
*/
|
|
6
19
|
export async function fetchMalwareDatabase() {
|
|
20
|
+
const ecosystem = getEcoSystem();
|
|
21
|
+
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
|
7
22
|
const response = await fetch(malwareDatabaseUrl);
|
|
8
23
|
if (!response.ok) {
|
|
9
|
-
throw new Error(`Error fetching malware database: ${response.statusText}`);
|
|
24
|
+
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`);
|
|
10
25
|
}
|
|
11
26
|
|
|
12
27
|
try {
|
|
@@ -15,18 +30,24 @@ export async function fetchMalwareDatabase() {
|
|
|
15
30
|
malwareDatabase: malwareDatabase,
|
|
16
31
|
version: response.headers.get("etag") || undefined,
|
|
17
32
|
};
|
|
18
|
-
} catch (error) {
|
|
33
|
+
} catch (/** @type {any} */ error) {
|
|
19
34
|
throw new Error(`Error parsing malware database: ${error.message}`);
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @returns {Promise<string | undefined>}
|
|
40
|
+
*/
|
|
23
41
|
export async function fetchMalwareDatabaseVersion() {
|
|
42
|
+
const ecosystem = getEcoSystem();
|
|
43
|
+
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
|
24
44
|
const response = await fetch(malwareDatabaseUrl, {
|
|
25
45
|
method: "HEAD",
|
|
26
46
|
});
|
|
47
|
+
|
|
27
48
|
if (!response.ok) {
|
|
28
49
|
throw new Error(
|
|
29
|
-
`Error fetching malware database version: ${response.statusText}`
|
|
50
|
+
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
|
30
51
|
);
|
|
31
52
|
}
|
|
32
53
|
return response.headers.get("etag") || undefined;
|
package/src/api/npmApi.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as semver from "semver";
|
|
2
2
|
import * as npmFetch from "npm-registry-fetch";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} packageName
|
|
6
|
+
* @param {string | null} [versionRange]
|
|
7
|
+
* @returns {Promise<string | null>}
|
|
8
|
+
*/
|
|
4
9
|
export async function resolvePackageVersion(packageName, versionRange) {
|
|
5
10
|
if (!versionRange) {
|
|
6
11
|
versionRange = "latest";
|
|
@@ -11,7 +16,10 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
|
|
11
16
|
return versionRange;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
const packageInfo =
|
|
19
|
+
const packageInfo = (
|
|
20
|
+
/** @type {{"dist-tags"?: Record<string, string>, versions?: Record<string, unknown>} | null} */
|
|
21
|
+
await getPackageInfo(packageName)
|
|
22
|
+
);
|
|
15
23
|
if (!packageInfo) {
|
|
16
24
|
// It is possible that no version is found (could be a private package, or a package that doesn't exist)
|
|
17
25
|
// In this case, we return null to indicate that we couldn't resolve the version
|
|
@@ -19,7 +27,7 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
const distTags = packageInfo["dist-tags"];
|
|
22
|
-
if (distTags && distTags[versionRange]) {
|
|
30
|
+
if (distTags && isDistTags(distTags) && distTags[versionRange]) {
|
|
23
31
|
// If the version range is a dist-tag, return the version associated with that tag
|
|
24
32
|
// e.g., "latest", "next", etc.
|
|
25
33
|
return distTags[versionRange];
|
|
@@ -41,6 +49,19 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
|
|
41
49
|
return null;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
* @param {unknown} distTags
|
|
55
|
+
* @returns {distTags is Record<string, string>}
|
|
56
|
+
*/
|
|
57
|
+
function isDistTags(distTags) {
|
|
58
|
+
return typeof distTags === "object";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} packageName
|
|
63
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
64
|
+
*/
|
|
44
65
|
async function getPackageInfo(packageName) {
|
|
45
66
|
try {
|
|
46
67
|
return await npmFetch.json(packageName);
|