@aikidosec/safe-chain 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -13
- package/bin/safe-chain.js +94 -14
- package/docs/troubleshooting.md +85 -13
- package/package.json +3 -1
- package/src/api/aikido.js +85 -27
- package/src/config/configFile.js +22 -0
- package/src/config/environmentVariables.js +19 -0
- package/src/config/settings.js +43 -6
- package/src/installation/downloadAgent.js +125 -0
- package/src/installation/installOnMacOS.js +155 -0
- package/src/installation/installOnWindows.js +203 -0
- package/src/installation/installUltimate.js +35 -0
- package/src/main.js +5 -5
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +28 -2
- package/src/shell-integration/helpers.js +67 -2
- package/src/shell-integration/setup.js +16 -16
- package/src/shell-integration/shellDetection.js +2 -2
- package/src/shell-integration/startup-scripts/init-fish.fish +16 -2
- package/src/shell-integration/startup-scripts/init-posix.sh +8 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +63 -16
- package/src/shell-integration/supported-shells/powershell.js +14 -5
- package/src/shell-integration/supported-shells/windowsPowershell.js +14 -5
- package/src/ultimate/ultimateTroubleshooting.js +111 -0
- package/src/utils/safeSpawn.js +16 -0
package/README.md
CHANGED
|
@@ -66,7 +66,6 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
|
|
66
66
|
### Verify the installation
|
|
67
67
|
|
|
68
68
|
1. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
|
69
|
-
|
|
70
69
|
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
|
71
70
|
|
|
72
71
|
2. **Verify the installation** by running the verification command:
|
|
@@ -152,23 +151,35 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/unins
|
|
|
152
151
|
|
|
153
152
|
## Logging
|
|
154
153
|
|
|
155
|
-
You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag
|
|
154
|
+
You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag or the `SAFE_CHAIN_LOGGING` environment variable.
|
|
155
|
+
|
|
156
|
+
### Configuration Options
|
|
157
|
+
|
|
158
|
+
You can set the logging level through multiple sources (in order of priority):
|
|
159
|
+
|
|
160
|
+
1. **CLI Argument** (highest priority):
|
|
161
|
+
- `--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.
|
|
162
|
+
|
|
163
|
+
```shell
|
|
164
|
+
npm install express --safe-chain-logging=silent
|
|
165
|
+
```
|
|
156
166
|
|
|
157
|
-
- `--safe-chain-logging=
|
|
167
|
+
- `--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.
|
|
158
168
|
|
|
159
|
-
|
|
169
|
+
```shell
|
|
170
|
+
npm install express --safe-chain-logging=verbose
|
|
171
|
+
```
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
npm install express --safe-chain-logging=silent
|
|
163
|
-
```
|
|
173
|
+
2. **Environment Variable**:
|
|
164
174
|
|
|
165
|
-
|
|
175
|
+
```shell
|
|
176
|
+
export SAFE_CHAIN_LOGGING=verbose
|
|
177
|
+
npm install express
|
|
178
|
+
```
|
|
166
179
|
|
|
167
|
-
|
|
180
|
+
Valid values: `silent`, `normal`, `verbose`
|
|
168
181
|
|
|
169
|
-
|
|
170
|
-
npm install express --safe-chain-logging=verbose
|
|
171
|
-
```
|
|
182
|
+
This is useful for setting a default logging level for all package manager commands in your terminal session or CI/CD environment.
|
|
172
183
|
|
|
173
184
|
## Minimum Package Age
|
|
174
185
|
|
|
@@ -199,6 +210,22 @@ You can set the minimum package age through multiple sources (in order of priori
|
|
|
199
210
|
}
|
|
200
211
|
```
|
|
201
212
|
|
|
213
|
+
### Excluding Packages
|
|
214
|
+
|
|
215
|
+
Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged). Use `@scope/*` to trust all packages from an organization:
|
|
216
|
+
|
|
217
|
+
```shell
|
|
218
|
+
export SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"npm": {
|
|
224
|
+
"minimumPackageAgeExclusions": ["@aikidosec/*"]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
202
229
|
## Custom Registries
|
|
203
230
|
|
|
204
231
|
Configure Safe Chain to scan packages from custom or private registries.
|
|
@@ -258,6 +285,8 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
|
|
|
258
285
|
- ✅ **Azure Pipelines**
|
|
259
286
|
- ✅ **CircleCI**
|
|
260
287
|
- ✅ **Jenkins**
|
|
288
|
+
- ✅ **Bitbucket Pipelines**
|
|
289
|
+
- ✅ **GitLab Pipelines**
|
|
261
290
|
|
|
262
291
|
## GitHub Actions Example
|
|
263
292
|
|
|
@@ -347,8 +376,85 @@ pipeline {
|
|
|
347
376
|
}
|
|
348
377
|
```
|
|
349
378
|
|
|
379
|
+
## Bitbucket Pipelines Example
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
image: node:22
|
|
383
|
+
|
|
384
|
+
steps:
|
|
385
|
+
- step:
|
|
386
|
+
name: Install
|
|
387
|
+
script:
|
|
388
|
+
- curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
|
389
|
+
- export PATH=~/.safe-chain/shims:$PATH
|
|
390
|
+
- npm ci
|
|
391
|
+
```
|
|
392
|
+
|
|
350
393
|
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
|
|
351
394
|
|
|
395
|
+
## GitLab Pipelines Example
|
|
396
|
+
|
|
397
|
+
To add safe-chain in GitLab pipelines, you need to install it in the image running the pipeline. This can be done by:
|
|
398
|
+
|
|
399
|
+
1. Define a dockerfile to run your build
|
|
400
|
+
|
|
401
|
+
```dockerfile
|
|
402
|
+
FROM node:lts
|
|
403
|
+
|
|
404
|
+
# Install safe-chain
|
|
405
|
+
RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
|
406
|
+
|
|
407
|
+
# Add safe-chain to PATH
|
|
408
|
+
ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
2. Build the Docker image in your CI pipeline
|
|
412
|
+
|
|
413
|
+
```yaml
|
|
414
|
+
build-image:
|
|
415
|
+
stage: build-image
|
|
416
|
+
image: docker:latest
|
|
417
|
+
services:
|
|
418
|
+
- docker:dind
|
|
419
|
+
script:
|
|
420
|
+
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
421
|
+
- docker build -t $CI_REGISTRY_IMAGE:latest .
|
|
422
|
+
- docker push $CI_REGISTRY_IMAGE:latest
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
3. Use the image in your pipeline:
|
|
426
|
+
```yaml
|
|
427
|
+
npm-ci:
|
|
428
|
+
stage: install
|
|
429
|
+
image: $CI_REGISTRY_IMAGE:latest
|
|
430
|
+
script:
|
|
431
|
+
- npm ci
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The full pipeline for this example looks like this:
|
|
435
|
+
|
|
436
|
+
```yaml
|
|
437
|
+
stages:
|
|
438
|
+
- build-image
|
|
439
|
+
- install
|
|
440
|
+
|
|
441
|
+
build-image:
|
|
442
|
+
stage: build-image
|
|
443
|
+
image: docker:latest
|
|
444
|
+
services:
|
|
445
|
+
- docker:dind
|
|
446
|
+
script:
|
|
447
|
+
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
448
|
+
- docker build -t $CI_REGISTRY_IMAGE:latest .
|
|
449
|
+
- docker push $CI_REGISTRY_IMAGE:latest
|
|
450
|
+
|
|
451
|
+
npm-ci:
|
|
452
|
+
stage: install
|
|
453
|
+
image: $CI_REGISTRY_IMAGE:latest
|
|
454
|
+
script:
|
|
455
|
+
- npm ci
|
|
456
|
+
```
|
|
457
|
+
|
|
352
458
|
# Troubleshooting
|
|
353
459
|
|
|
354
|
-
Having issues? See the [Troubleshooting Guide](https://
|
|
460
|
+
Having issues? See the [Troubleshooting Guide](https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting) for help with common problems.
|
package/bin/safe-chain.js
CHANGED
|
@@ -16,6 +16,14 @@ import path from "path";
|
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
import fs from "fs";
|
|
18
18
|
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
|
|
19
|
+
import {
|
|
20
|
+
installUltimate,
|
|
21
|
+
uninstallUltimate,
|
|
22
|
+
} from "../src/installation/installUltimate.js";
|
|
23
|
+
import {
|
|
24
|
+
printUltimateLogs,
|
|
25
|
+
troubleshootingExport,
|
|
26
|
+
} from "../src/ultimate/ultimateTroubleshooting.js";
|
|
19
27
|
|
|
20
28
|
/** @type {string} */
|
|
21
29
|
// This checks the current file's dirname in a way that's compatible with:
|
|
@@ -62,9 +70,42 @@ if (tool) {
|
|
|
62
70
|
process.exit(0);
|
|
63
71
|
} else if (command === "setup") {
|
|
64
72
|
setup();
|
|
73
|
+
} else if (command === "ultimate") {
|
|
74
|
+
const cliArgs = initializeCliArguments(process.argv.slice(2));
|
|
75
|
+
const subCommand = cliArgs[1];
|
|
76
|
+
if (subCommand === "uninstall") {
|
|
77
|
+
guardCliArgsMaxLenght(2, cliArgs, "safe-chain ultimate uninstall");
|
|
78
|
+
(async () => {
|
|
79
|
+
await uninstallUltimate();
|
|
80
|
+
})();
|
|
81
|
+
} else if (subCommand === "troubleshooting-logs") {
|
|
82
|
+
guardCliArgsMaxLenght(
|
|
83
|
+
2,
|
|
84
|
+
cliArgs,
|
|
85
|
+
"safe-chain ultimate troubleshooting-logs",
|
|
86
|
+
);
|
|
87
|
+
(async () => {
|
|
88
|
+
await printUltimateLogs();
|
|
89
|
+
})();
|
|
90
|
+
} else if (subCommand === "troubleshooting-export") {
|
|
91
|
+
guardCliArgsMaxLenght(
|
|
92
|
+
2,
|
|
93
|
+
cliArgs,
|
|
94
|
+
"safe-chain ultimate troubleshooting-export",
|
|
95
|
+
);
|
|
96
|
+
(async () => {
|
|
97
|
+
await troubleshootingExport();
|
|
98
|
+
})();
|
|
99
|
+
} else {
|
|
100
|
+
guardCliArgsMaxLenght(1, cliArgs, "safe-chain ultimate");
|
|
101
|
+
// Install command = when no subcommand is provided (safe-chain ultimate)
|
|
102
|
+
(async () => {
|
|
103
|
+
await installUltimate();
|
|
104
|
+
})();
|
|
105
|
+
}
|
|
65
106
|
} else if (command === "teardown") {
|
|
66
|
-
teardownDirectories();
|
|
67
107
|
teardown();
|
|
108
|
+
teardownDirectories();
|
|
68
109
|
} else if (command === "setup-ci") {
|
|
69
110
|
setupCi();
|
|
70
111
|
} else if (command === "--version" || command === "-v" || command === "-v") {
|
|
@@ -80,38 +121,77 @@ if (tool) {
|
|
|
80
121
|
process.exit(1);
|
|
81
122
|
}
|
|
82
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @param {Number} maxLength
|
|
126
|
+
* @param {String[]} args
|
|
127
|
+
* @param {String} command
|
|
128
|
+
*/
|
|
129
|
+
function guardCliArgsMaxLenght(maxLength, args, command) {
|
|
130
|
+
if (args.length > maxLength) {
|
|
131
|
+
ui.writeError(`Unexpected number of arguments for command ${command}.`);
|
|
132
|
+
ui.emptyLine();
|
|
133
|
+
|
|
134
|
+
writeHelp();
|
|
135
|
+
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
83
140
|
function writeHelp() {
|
|
84
141
|
ui.writeInformation(
|
|
85
|
-
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
|
|
142
|
+
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>"),
|
|
86
143
|
);
|
|
87
144
|
ui.emptyLine();
|
|
88
145
|
ui.writeInformation(
|
|
89
146
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
|
90
|
-
"teardown"
|
|
91
|
-
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
|
92
|
-
"--version"
|
|
93
|
-
)}
|
|
147
|
+
"teardown",
|
|
148
|
+
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("ultimate")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
|
149
|
+
"--version",
|
|
150
|
+
)}`,
|
|
94
151
|
);
|
|
95
152
|
ui.emptyLine();
|
|
96
153
|
ui.writeInformation(
|
|
97
154
|
`- ${chalk.cyan(
|
|
98
|
-
"safe-chain setup"
|
|
99
|
-
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3
|
|
155
|
+
"safe-chain setup",
|
|
156
|
+
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`,
|
|
100
157
|
);
|
|
101
158
|
ui.writeInformation(
|
|
102
159
|
`- ${chalk.cyan(
|
|
103
|
-
"safe-chain teardown"
|
|
104
|
-
)}: This will remove safe-chain aliases from your shell configuration
|
|
160
|
+
"safe-chain teardown",
|
|
161
|
+
)}: This will remove safe-chain aliases from your shell configuration.`,
|
|
105
162
|
);
|
|
106
163
|
ui.writeInformation(
|
|
107
164
|
`- ${chalk.cyan(
|
|
108
|
-
"safe-chain setup-ci"
|
|
109
|
-
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH
|
|
165
|
+
"safe-chain setup-ci",
|
|
166
|
+
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`,
|
|
110
167
|
);
|
|
111
168
|
ui.writeInformation(
|
|
112
169
|
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
|
|
113
|
-
"-v"
|
|
114
|
-
)}): Display the current version of safe-chain
|
|
170
|
+
"-v",
|
|
171
|
+
)}): Display the current version of safe-chain.`,
|
|
172
|
+
);
|
|
173
|
+
ui.emptyLine();
|
|
174
|
+
ui.writeInformation(chalk.bold("Ultimate commands:"));
|
|
175
|
+
ui.emptyLine();
|
|
176
|
+
ui.writeInformation(
|
|
177
|
+
`- ${chalk.cyan(
|
|
178
|
+
"safe-chain ultimate",
|
|
179
|
+
)}: Install the ultimate version of safe-chain, enabling protection for more eco-systems.`,
|
|
180
|
+
);
|
|
181
|
+
ui.writeInformation(
|
|
182
|
+
`- ${chalk.cyan(
|
|
183
|
+
"safe-chain ultimate troubleshooting-logs",
|
|
184
|
+
)}: Prints standard and error logs for safe-chain ultimate and it's proxy.`,
|
|
185
|
+
);
|
|
186
|
+
ui.writeInformation(
|
|
187
|
+
`- ${chalk.cyan(
|
|
188
|
+
"safe-chain ultimate troubleshooting-export",
|
|
189
|
+
)}: Creates a zip archive of useful data for troubleshooting safe-chain ultimate, that can be shared with our support team.`,
|
|
190
|
+
);
|
|
191
|
+
ui.writeInformation(
|
|
192
|
+
`- ${chalk.cyan(
|
|
193
|
+
"safe-chain ultimate uninstall",
|
|
194
|
+
)}: Uninstall the ultimate version of safe-chain.`,
|
|
115
195
|
);
|
|
116
196
|
ui.emptyLine();
|
|
117
197
|
}
|
package/docs/troubleshooting.md
CHANGED
|
@@ -44,20 +44,72 @@ pip3 install safe-chain-pi-test
|
|
|
44
44
|
|
|
45
45
|
These test packages are flagged as malware and should be blocked by Safe Chain.
|
|
46
46
|
|
|
47
|
+
**If the test package installs successfully instead of being blocked**, see [Malware Not Being Blocked](#malware-not-being-blocked) below.
|
|
48
|
+
|
|
47
49
|
### Logging Options
|
|
48
50
|
|
|
49
|
-
Use logging flags to get more information:
|
|
51
|
+
Use logging flags or environment variables to get more information:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
# Verbose mode - detailed diagnostic output for troubleshooting
|
|
53
55
|
npm install express --safe-chain-logging=verbose
|
|
54
56
|
|
|
57
|
+
# Or set it globally for all commands in your session
|
|
58
|
+
export SAFE_CHAIN_LOGGING=verbose
|
|
59
|
+
npm install express
|
|
60
|
+
|
|
55
61
|
# Silent mode - suppress all output except malware blocking
|
|
56
62
|
npm install express --safe-chain-logging=silent
|
|
57
63
|
```
|
|
58
64
|
|
|
59
65
|
## Common Issues
|
|
60
66
|
|
|
67
|
+
### Malware Not Being Blocked
|
|
68
|
+
|
|
69
|
+
**Symptom:** Test malware packages (like `safe-chain-test`) install successfully when they should be blocked
|
|
70
|
+
|
|
71
|
+
**Most Common Cause:** The package is cached in your package manager's local store
|
|
72
|
+
|
|
73
|
+
Safe-chain blocks malicious packages by intercepting network requests to package registries using its proxy.
|
|
74
|
+
|
|
75
|
+
When a package is already cached locally, the package manager skips downloading it from the registry, which bypasses the proxy.
|
|
76
|
+
|
|
77
|
+
**Resolution Steps:**
|
|
78
|
+
|
|
79
|
+
1. **Clear your package manager's cache:**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# For npm
|
|
83
|
+
npm cache clean --force
|
|
84
|
+
|
|
85
|
+
# For pnpm
|
|
86
|
+
pnpm store prune
|
|
87
|
+
|
|
88
|
+
# For yarn (classic)
|
|
89
|
+
yarn cache clean
|
|
90
|
+
|
|
91
|
+
# For yarn (berry/v2+)
|
|
92
|
+
yarn cache clean --all
|
|
93
|
+
|
|
94
|
+
# For bun
|
|
95
|
+
bun pm cache rm
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **⚠️ Warning:** Cache clearing is safe but will remove all cached packages. Subsequent installations will need to re-download packages. In CI/CD environments or monorepos, this may affect build times.
|
|
99
|
+
|
|
100
|
+
2. **Clean local installation artifacts:**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Remove node_modules if you want a completely fresh install
|
|
104
|
+
rm -rf node_modules
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
3. **Re-test malware blocking:**
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install safe-chain-test # Should be blocked
|
|
111
|
+
```
|
|
112
|
+
|
|
61
113
|
### Shell Aliases Not Working After Installation
|
|
62
114
|
|
|
63
115
|
**Symptom:** Running `npm` shows regular npm instead of safe-chain wrapped version
|
|
@@ -97,6 +149,37 @@ Should include `~/.safe-chain/bin`
|
|
|
97
149
|
|
|
98
150
|
**If persists:** Re-run the installation script
|
|
99
151
|
|
|
152
|
+
### PowerShell Execution Policy Blocks Scripts (Windows)
|
|
153
|
+
|
|
154
|
+
**Symptom:** When opening PowerShell, you see an error like:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
. : File C:\Users\<username>\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because
|
|
158
|
+
running scripts is disabled on this system.
|
|
159
|
+
CategoryInfo : SecurityError: (:) [], PSSecurityException
|
|
160
|
+
FullyQualifiedErrorId : UnauthorizedAccess
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Cause:** Windows PowerShell's default execution policy (`Restricted`) blocks all script execution, including safe-chain's initialization script that's sourced from your PowerShell profile.
|
|
164
|
+
|
|
165
|
+
**Resolution:**
|
|
166
|
+
|
|
167
|
+
1. **Set the execution policy to allow local scripts:**
|
|
168
|
+
|
|
169
|
+
Open PowerShell as Administrator and run:
|
|
170
|
+
|
|
171
|
+
```powershell
|
|
172
|
+
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This allows:
|
|
176
|
+
- Local scripts (like safe-chain's) to run without signing
|
|
177
|
+
- Downloaded scripts to run only if signed by a trusted publisher
|
|
178
|
+
|
|
179
|
+
2. **Restart PowerShell** and verify the error is resolved.
|
|
180
|
+
|
|
181
|
+
> **Note:** `RemoteSigned` is Microsoft's recommended execution policy for client computers. It provides a good balance between security and usability.
|
|
182
|
+
|
|
100
183
|
### Shell Aliases Persist After Uninstallation
|
|
101
184
|
|
|
102
185
|
**Symptom:** safe-chain commands still active after running uninstall script
|
|
@@ -225,17 +308,6 @@ Look for and remove:
|
|
|
225
308
|
rm -rf ~/.safe-chain
|
|
226
309
|
```
|
|
227
310
|
|
|
228
|
-
## Getting More Information
|
|
229
|
-
|
|
230
|
-
### Enable Verbose Logging
|
|
231
|
-
|
|
232
|
-
Get detailed diagnostic output:
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
npm install express --safe-chain-logging=verbose
|
|
236
|
-
pip install requests --safe-chain-logging=verbose
|
|
237
|
-
```
|
|
238
|
-
|
|
239
311
|
### Report Issues
|
|
240
312
|
|
|
241
313
|
If you encounter problems:
|
|
@@ -246,4 +318,4 @@ If you encounter problems:
|
|
|
246
318
|
- Shell type and version
|
|
247
319
|
- `safe-chain --version` output
|
|
248
320
|
- Output from verification commands
|
|
249
|
-
- Verbose logs of the failing command
|
|
321
|
+
- Verbose logs of the failing command (add the `--safe-chain-logging=verbose` argument)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
6
6
|
"test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"license": "AGPL-3.0-or-later",
|
|
39
39
|
"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.",
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"archiver": "^7.0.1",
|
|
41
42
|
"certifi": "14.5.15",
|
|
42
43
|
"chalk": "5.4.1",
|
|
43
44
|
"https-proxy-agent": "7.0.6",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"semver": "7.7.2"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
52
|
+
"@types/archiver": "^7.0.0",
|
|
51
53
|
"@types/ini": "^4.1.1",
|
|
52
54
|
"@types/make-fetch-happen": "^10.0.4",
|
|
53
55
|
"@types/node": "^18.19.130",
|
package/src/api/aikido.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import fetch from "make-fetch-happen";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getEcoSystem,
|
|
4
|
+
ECOSYSTEM_JS,
|
|
5
|
+
ECOSYSTEM_PY,
|
|
6
|
+
} from "../config/settings.js";
|
|
7
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
8
|
|
|
4
9
|
const malwareDatabaseUrls = {
|
|
5
10
|
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
|
|
@@ -17,38 +22,91 @@ const malwareDatabaseUrls = {
|
|
|
17
22
|
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
|
|
18
23
|
*/
|
|
19
24
|
export async function fetchMalwareDatabase() {
|
|
20
|
-
const
|
|
21
|
-
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
|
22
|
-
const response = await fetch(malwareDatabaseUrl);
|
|
23
|
-
if (!response.ok) {
|
|
24
|
-
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`);
|
|
25
|
-
}
|
|
25
|
+
const numberOfAttempts = 4;
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
return retry(async () => {
|
|
28
|
+
const ecosystem = getEcoSystem();
|
|
29
|
+
const malwareDatabaseUrl =
|
|
30
|
+
malwareDatabaseUrls[
|
|
31
|
+
/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)
|
|
32
|
+
];
|
|
33
|
+
const response = await fetch(malwareDatabaseUrl);
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Error fetching ${ecosystem} malware database: ${response.statusText}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
let malwareDatabase = await response.json();
|
|
42
|
+
return {
|
|
43
|
+
malwareDatabase: malwareDatabase,
|
|
44
|
+
version: response.headers.get("etag") || undefined,
|
|
45
|
+
};
|
|
46
|
+
} catch (/** @type {any} */ error) {
|
|
47
|
+
throw new Error(`Error parsing malware database: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}, numberOfAttempts);
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/**
|
|
39
53
|
* @returns {Promise<string | undefined>}
|
|
40
54
|
*/
|
|
41
55
|
export async function fetchMalwareDatabaseVersion() {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
const numberOfAttempts = 4;
|
|
57
|
+
|
|
58
|
+
return retry(async () => {
|
|
59
|
+
const ecosystem = getEcoSystem();
|
|
60
|
+
const malwareDatabaseUrl =
|
|
61
|
+
malwareDatabaseUrls[
|
|
62
|
+
/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)
|
|
63
|
+
];
|
|
64
|
+
const response = await fetch(malwareDatabaseUrl, {
|
|
65
|
+
method: "HEAD",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return response.headers.get("etag") || undefined;
|
|
74
|
+
}, numberOfAttempts);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Retries an asynchronous function multiple times until it succeeds or exhausts all attempts.
|
|
79
|
+
*
|
|
80
|
+
* @template T
|
|
81
|
+
* @param {() => Promise<T>} func - The asynchronous function to retry
|
|
82
|
+
* @param {number} attempts - The number of attempts
|
|
83
|
+
* @returns {Promise<T>} The return value of the function if successful
|
|
84
|
+
* @throws {Error} The last error encountered if all retry attempts fail
|
|
85
|
+
*/
|
|
86
|
+
async function retry(func, attempts) {
|
|
87
|
+
let lastError;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < attempts; i++) {
|
|
90
|
+
try {
|
|
91
|
+
return await func();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
ui.writeVerbose(
|
|
94
|
+
"An error occurred while trying to download the Aikido Malware database",
|
|
95
|
+
error
|
|
96
|
+
);
|
|
97
|
+
lastError = error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (i < attempts - 1) {
|
|
101
|
+
// When this is not the last try, back-off exponentially:
|
|
102
|
+
// 1st attempt - 500ms delay
|
|
103
|
+
// 2nd attempt - 1000ms delay
|
|
104
|
+
// 3rd attempt - 2000ms delay
|
|
105
|
+
// 4th attempt - 4000ms delay
|
|
106
|
+
// ...
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 500));
|
|
108
|
+
}
|
|
52
109
|
}
|
|
53
|
-
|
|
110
|
+
|
|
111
|
+
throw lastError;
|
|
54
112
|
}
|
package/src/config/configFile.js
CHANGED
|
@@ -16,6 +16,7 @@ import { getEcoSystem } from "./settings.js";
|
|
|
16
16
|
* @typedef {Object} SafeChainRegistryConfiguration
|
|
17
17
|
* We cannot trust the input and should add the necessary validations.
|
|
18
18
|
* @property {unknown | string[]} customRegistries
|
|
19
|
+
* @property {unknown | string[]} minimumPackageAgeExclusions
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -127,6 +128,27 @@ export function getPipCustomRegistries() {
|
|
|
127
128
|
return customRegistries.filter((item) => typeof item === "string");
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Gets the minimum package age exclusions from the config file
|
|
133
|
+
* @returns {string[]}
|
|
134
|
+
*/
|
|
135
|
+
export function getNpmMinimumPackageAgeExclusions() {
|
|
136
|
+
const config = readConfigFile();
|
|
137
|
+
|
|
138
|
+
if (!config || !config.npm) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
|
|
143
|
+
const exclusions = npmConfig.minimumPackageAgeExclusions;
|
|
144
|
+
|
|
145
|
+
if (!Array.isArray(exclusions)) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return exclusions.filter((item) => typeof item === "string");
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
/**
|
|
131
153
|
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
|
132
154
|
* @param {string | number} version
|
|
@@ -25,3 +25,22 @@ export function getNpmCustomRegistries() {
|
|
|
25
25
|
export function getPipCustomRegistries() {
|
|
26
26
|
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Gets the logging level from environment variable
|
|
31
|
+
* Valid values: "silent", "normal", "verbose"
|
|
32
|
+
* @returns {string | undefined}
|
|
33
|
+
*/
|
|
34
|
+
export function getLoggingLevel() {
|
|
35
|
+
return process.env.SAFE_CHAIN_LOGGING;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the minimum package age exclusions from environment variable
|
|
40
|
+
* Expected format: comma-separated list of package names
|
|
41
|
+
* Example: "react,@aikidosec/safe-chain,lodash"
|
|
42
|
+
* @returns {string | undefined}
|
|
43
|
+
*/
|
|
44
|
+
export function getNpmMinimumPackageAgeExclusions() {
|
|
45
|
+
return process.env.SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS;
|
|
46
|
+
}
|