@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 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=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.
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
- Example usage:
169
+ ```shell
170
+ npm install express --safe-chain-logging=verbose
171
+ ```
160
172
 
161
- ```shell
162
- npm install express --safe-chain-logging=silent
163
- ```
173
+ 2. **Environment Variable**:
164
174
 
165
- - `--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.
175
+ ```shell
176
+ export SAFE_CHAIN_LOGGING=verbose
177
+ npm install express
178
+ ```
166
179
 
167
- Example usage:
180
+ Valid values: `silent`, `normal`, `verbose`
168
181
 
169
- ```shell
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://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md) for help with common problems.
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
  }
@@ -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.1",
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 { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
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 ecosystem = getEcoSystem();
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
- try {
28
- let malwareDatabase = await response.json();
29
- return {
30
- malwareDatabase: malwareDatabase,
31
- version: response.headers.get("etag") || undefined,
32
- };
33
- } catch (/** @type {any} */ error) {
34
- throw new Error(`Error parsing malware database: ${error.message}`);
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 ecosystem = getEcoSystem();
43
- const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
44
- const response = await fetch(malwareDatabaseUrl, {
45
- method: "HEAD",
46
- });
47
-
48
- if (!response.ok) {
49
- throw new Error(
50
- `Error fetching ${ecosystem} malware database version: ${response.statusText}`
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
- return response.headers.get("etag") || undefined;
110
+
111
+ throw lastError;
54
112
  }
@@ -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
+ }