@aikidosec/safe-chain 1.4.3 → 1.4.7
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 +65 -8
- package/bin/safe-chain.js +1 -81
- package/package.json +1 -1
- package/src/api/aikido.js +93 -18
- package/src/config/cliArguments.js +24 -1
- package/src/config/configFile.js +64 -6
- package/src/config/environmentVariables.js +13 -2
- package/src/config/settings.js +53 -4
- package/src/main.js +6 -2
- package/src/packagemanager/_shared/commandErrors.js +17 -0
- package/src/packagemanager/bun/createBunPackageManager.js +2 -7
- package/src/packagemanager/npm/runNpmCommand.js +2 -7
- package/src/packagemanager/npx/runNpxCommand.js +2 -7
- package/src/packagemanager/pip/runPipCommand.js +2 -7
- package/src/packagemanager/pipx/runPipXCommand.js +2 -7
- package/src/packagemanager/pnpm/runPnpmCommand.js +3 -7
- package/src/packagemanager/poetry/createPoetryPackageManager.js +2 -7
- package/src/packagemanager/uv/runUvCommand.js +2 -7
- package/src/packagemanager/yarn/runYarnCommand.js +2 -7
- package/src/registryProxy/certBundle.js +25 -3
- package/src/registryProxy/http-utils.js +63 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +1 -1
- package/src/registryProxy/interceptors/interceptorBuilder.js +37 -4
- package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +18 -41
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -2
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +20 -3
- package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
- package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
- package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
- package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
- package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
- package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
- package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
- package/src/registryProxy/mitmRequestHandler.js +12 -6
- package/src/registryProxy/registryProxy.js +72 -9
- package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
- package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
- package/src/scanning/newPackagesListCache.js +126 -0
- package/src/scanning/packageNameVariants.js +29 -0
- package/src/shell-integration/setup.js +7 -3
- package/src/shell-integration/shellDetection.js +2 -0
- package/src/shell-integration/supported-shells/bash.js +19 -1
- package/src/shell-integration/supported-shells/fish.js +18 -0
- package/src/shell-integration/supported-shells/powershell.js +18 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +18 -0
- package/src/shell-integration/supported-shells/zsh.js +19 -1
- package/src/shell-integration/teardown.js +7 -1
- package/src/ultimate/ultimateTroubleshooting.js +1 -1
- package/src/installation/downloadAgent.js +0 -125
- package/src/installation/installOnMacOS.js +0 -155
- package/src/installation/installOnWindows.js +0 -203
- package/src/installation/installUltimate.js +0 -35
- package/src/registryProxy/interceptors/pipInterceptor.js +0 -132
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
- ✅ **Block malware on developer laptops and CI/CD**
|
|
9
9
|
- ✅ **Supports npm and PyPI** more package managers coming
|
|
10
|
-
- ✅ **Blocks packages newer than
|
|
10
|
+
- ✅ **Blocks packages newer than 48 hours** without breaking your build
|
|
11
11
|
- ✅ **Tokenless, free, no build data shared**
|
|
12
12
|
|
|
13
13
|
Aikido Safe Chain supports the following package managers:
|
|
@@ -111,11 +111,20 @@ safe-chain --version
|
|
|
111
111
|
|
|
112
112
|
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, pip3, uv, poetry or pipx 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.
|
|
113
113
|
|
|
114
|
-
### Minimum package age
|
|
114
|
+
### Minimum package age
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Safe Chain applies minimum package age checks to supported ecosystems.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Current enforcement differs by ecosystem:
|
|
119
|
+
|
|
120
|
+
- npm-based package managers:
|
|
121
|
+
- during normal package resolution, Safe Chain suppresses versions that are newer than the configured minimum age from the package metadata returned by the registry
|
|
122
|
+
- for direct package download requests that bypass that metadata flow, Safe Chain can block the request itself using a cached list of newly released packages
|
|
123
|
+
- Python package managers:
|
|
124
|
+
- during package resolution, Safe Chain suppresses too-young files and releases from PyPI metadata responses
|
|
125
|
+
- for direct package download requests that bypass that metadata flow, Safe Chain can block the request itself using a cached list of newly released packages
|
|
126
|
+
|
|
127
|
+
By default, the minimum package age is 48 hours. 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.
|
|
119
128
|
|
|
120
129
|
### Shell Integration
|
|
121
130
|
|
|
@@ -183,7 +192,17 @@ You can set the logging level through multiple sources (in order of priority):
|
|
|
183
192
|
|
|
184
193
|
## Minimum Package Age
|
|
185
194
|
|
|
186
|
-
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least
|
|
195
|
+
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 48 hours old before they can be installed.
|
|
196
|
+
|
|
197
|
+
For npm-based package managers, this check currently has two enforcement modes:
|
|
198
|
+
|
|
199
|
+
- Safe Chain suppresses too-young versions from package metadata during normal dependency resolution.
|
|
200
|
+
- Safe Chain blocks direct package download requests when they are matched against the cached newly released packages list.
|
|
201
|
+
|
|
202
|
+
For Python package managers, this check currently has two enforcement modes:
|
|
203
|
+
|
|
204
|
+
- Safe Chain suppresses too-young files and releases from PyPI metadata during dependency resolution.
|
|
205
|
+
- Safe Chain blocks direct package download requests when they are matched against the cached newly released packages list.
|
|
187
206
|
|
|
188
207
|
### Configuration Options
|
|
189
208
|
|
|
@@ -202,7 +221,7 @@ You can set the minimum package age through multiple sources (in order of priori
|
|
|
202
221
|
npm install express
|
|
203
222
|
```
|
|
204
223
|
|
|
205
|
-
3. **Config File** (`~/.
|
|
224
|
+
3. **Config File** (`~/.safe-chain/config.json`):
|
|
206
225
|
|
|
207
226
|
```json
|
|
208
227
|
{
|
|
@@ -215,13 +234,16 @@ You can set the minimum package age through multiple sources (in order of priori
|
|
|
215
234
|
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
235
|
|
|
217
236
|
```shell
|
|
218
|
-
export
|
|
237
|
+
export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*"
|
|
219
238
|
```
|
|
220
239
|
|
|
221
240
|
```json
|
|
222
241
|
{
|
|
223
242
|
"npm": {
|
|
224
243
|
"minimumPackageAgeExclusions": ["@aikidosec/*"]
|
|
244
|
+
},
|
|
245
|
+
"pip": {
|
|
246
|
+
"minimumPackageAgeExclusions": ["requests"]
|
|
225
247
|
}
|
|
226
248
|
}
|
|
227
249
|
```
|
|
@@ -246,7 +268,7 @@ You can set custom registries through environment variable or config file. Both
|
|
|
246
268
|
export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
|
|
247
269
|
```
|
|
248
270
|
|
|
249
|
-
2. **Config File** (`~/.
|
|
271
|
+
2. **Config File** (`~/.safe-chain/config.json`):
|
|
250
272
|
|
|
251
273
|
```json
|
|
252
274
|
{
|
|
@@ -259,6 +281,41 @@ You can set custom registries through environment variable or config file. Both
|
|
|
259
281
|
}
|
|
260
282
|
```
|
|
261
283
|
|
|
284
|
+
## Malware List Base URL
|
|
285
|
+
|
|
286
|
+
Configure Safe Chain to fetch malware databases and new packages lists from a custom mirror URL. This allows you to host your own copy of the Aikido malware database.
|
|
287
|
+
|
|
288
|
+
### Configuration Options
|
|
289
|
+
|
|
290
|
+
You can set the malware list base URL through multiple sources (in order of priority):
|
|
291
|
+
|
|
292
|
+
1. **CLI Argument** (highest priority):
|
|
293
|
+
|
|
294
|
+
```shell
|
|
295
|
+
npm install express --safe-chain-malware-list-base-url=https://your-mirror.com
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
2. **Environment Variable**:
|
|
299
|
+
|
|
300
|
+
```shell
|
|
301
|
+
export SAFE_CHAIN_MALWARE_LIST_BASE_URL=https://your-mirror.com
|
|
302
|
+
npm install express
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
3. **Config File** (`~/.safe-chain/config.json`):
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"malwareListBaseUrl": "https://your-mirror.com"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The base URL should point to a server that mirrors the structure of `https://malware-list.aikido.dev/`, including the following paths:
|
|
314
|
+
- `/malware_predictions.json` (JavaScript ecosystem malware database)
|
|
315
|
+
- `/malware_pypi.json` (Python ecosystem malware database)
|
|
316
|
+
- `/releases/npm.json` (JavaScript new packages list)
|
|
317
|
+
- `/releases/pypi.json` (Python new packages list)
|
|
318
|
+
|
|
262
319
|
# Usage in CI/CD
|
|
263
320
|
|
|
264
321
|
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.
|
package/bin/safe-chain.js
CHANGED
|
@@ -16,14 +16,6 @@ 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";
|
|
27
19
|
|
|
28
20
|
/** @type {string} */
|
|
29
21
|
// This checks the current file's dirname in a way that's compatible with:
|
|
@@ -70,39 +62,6 @@ if (tool) {
|
|
|
70
62
|
process.exit(0);
|
|
71
63
|
} else if (command === "setup") {
|
|
72
64
|
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
|
-
}
|
|
106
65
|
} else if (command === "teardown") {
|
|
107
66
|
teardown();
|
|
108
67
|
teardownDirectories();
|
|
@@ -121,22 +80,6 @@ if (tool) {
|
|
|
121
80
|
process.exit(1);
|
|
122
81
|
}
|
|
123
82
|
|
|
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
|
-
|
|
140
83
|
function writeHelp() {
|
|
141
84
|
ui.writeInformation(
|
|
142
85
|
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>"),
|
|
@@ -145,7 +88,7 @@ function writeHelp() {
|
|
|
145
88
|
ui.writeInformation(
|
|
146
89
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
|
147
90
|
"teardown",
|
|
148
|
-
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("
|
|
91
|
+
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
|
149
92
|
"--version",
|
|
150
93
|
)}`,
|
|
151
94
|
);
|
|
@@ -171,29 +114,6 @@ function writeHelp() {
|
|
|
171
114
|
)}): Display the current version of safe-chain.`,
|
|
172
115
|
);
|
|
173
116
|
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.`,
|
|
195
|
-
);
|
|
196
|
-
ui.emptyLine();
|
|
197
117
|
}
|
|
198
118
|
|
|
199
119
|
async function getVersion() {
|
package/package.json
CHANGED
package/src/api/aikido.js
CHANGED
|
@@ -3,14 +3,22 @@ import {
|
|
|
3
3
|
getEcoSystem,
|
|
4
4
|
ECOSYSTEM_JS,
|
|
5
5
|
ECOSYSTEM_PY,
|
|
6
|
+
getMalwareListBaseUrl,
|
|
6
7
|
} from "../config/settings.js";
|
|
7
8
|
import { ui } from "../environment/userInteraction.js";
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
[ECOSYSTEM_JS]: "
|
|
11
|
-
[ECOSYSTEM_PY]: "
|
|
10
|
+
const malwareDatabasePaths = {
|
|
11
|
+
[ECOSYSTEM_JS]: "malware_predictions.json",
|
|
12
|
+
[ECOSYSTEM_PY]: "malware_pypi.json",
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
const newPackagesListPaths = {
|
|
16
|
+
[ECOSYSTEM_JS]: "releases/npm.json",
|
|
17
|
+
[ECOSYSTEM_PY]: "releases/pypi.json",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_FETCH_RETRY_ATTEMPTS = 4;
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* @typedef {Object} MalwarePackage
|
|
16
24
|
* @property {string} package_name
|
|
@@ -18,18 +26,26 @@ const malwareDatabaseUrls = {
|
|
|
18
26
|
* @property {string} reason
|
|
19
27
|
*/
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} NewPackageEntry
|
|
31
|
+
* @property {string} [source]
|
|
32
|
+
* @property {string} package_name
|
|
33
|
+
* @property {string} version
|
|
34
|
+
* @property {number} released_on - Unix timestamp (seconds)
|
|
35
|
+
* @property {number} scraped_on - Unix timestamp (seconds)
|
|
36
|
+
*/
|
|
37
|
+
|
|
21
38
|
/**
|
|
22
39
|
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
|
|
23
40
|
*/
|
|
24
41
|
export async function fetchMalwareDatabase() {
|
|
25
|
-
const numberOfAttempts = 4;
|
|
26
|
-
|
|
27
42
|
return retry(async () => {
|
|
28
43
|
const ecosystem = getEcoSystem();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const baseUrl = getMalwareListBaseUrl();
|
|
45
|
+
const path = malwareDatabasePaths[
|
|
46
|
+
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
|
47
|
+
];
|
|
48
|
+
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
|
33
49
|
const response = await fetch(malwareDatabaseUrl);
|
|
34
50
|
if (!response.ok) {
|
|
35
51
|
throw new Error(
|
|
@@ -46,21 +62,20 @@ export async function fetchMalwareDatabase() {
|
|
|
46
62
|
} catch (/** @type {any} */ error) {
|
|
47
63
|
throw new Error(`Error parsing malware database: ${error.message}`);
|
|
48
64
|
}
|
|
49
|
-
},
|
|
65
|
+
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
/**
|
|
53
69
|
* @returns {Promise<string | undefined>}
|
|
54
70
|
*/
|
|
55
71
|
export async function fetchMalwareDatabaseVersion() {
|
|
56
|
-
const numberOfAttempts = 4;
|
|
57
|
-
|
|
58
72
|
return retry(async () => {
|
|
59
73
|
const ecosystem = getEcoSystem();
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
const baseUrl = getMalwareListBaseUrl();
|
|
75
|
+
const path = malwareDatabasePaths[
|
|
76
|
+
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
|
77
|
+
];
|
|
78
|
+
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
|
64
79
|
const response = await fetch(malwareDatabaseUrl, {
|
|
65
80
|
method: "HEAD",
|
|
66
81
|
});
|
|
@@ -71,7 +86,67 @@ export async function fetchMalwareDatabaseVersion() {
|
|
|
71
86
|
);
|
|
72
87
|
}
|
|
73
88
|
return response.headers.get("etag") || undefined;
|
|
74
|
-
},
|
|
89
|
+
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @returns {Promise<{newPackagesList: NewPackageEntry[], version: string | undefined}>}
|
|
94
|
+
*/
|
|
95
|
+
export async function fetchNewPackagesList() {
|
|
96
|
+
return retry(async () => {
|
|
97
|
+
const ecosystem = getEcoSystem();
|
|
98
|
+
const baseUrl = getMalwareListBaseUrl();
|
|
99
|
+
const path = newPackagesListPaths[/** @type {keyof typeof newPackagesListPaths} */ (ecosystem)];
|
|
100
|
+
|
|
101
|
+
if (!path) {
|
|
102
|
+
return { newPackagesList: [], version: undefined };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const url = `${baseUrl}/${path}`;
|
|
106
|
+
|
|
107
|
+
const response = await fetch(url);
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Error fetching ${ecosystem} new packages list: ${response.statusText}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const newPackagesList = await response.json();
|
|
116
|
+
return {
|
|
117
|
+
newPackagesList,
|
|
118
|
+
version: response.headers.get("etag") || undefined,
|
|
119
|
+
};
|
|
120
|
+
} catch (/** @type {any} */ error) {
|
|
121
|
+
throw new Error(`Error parsing new packages list: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @returns {Promise<string | undefined>}
|
|
128
|
+
*/
|
|
129
|
+
export async function fetchNewPackagesListVersion() {
|
|
130
|
+
return retry(async () => {
|
|
131
|
+
const ecosystem = getEcoSystem();
|
|
132
|
+
const baseUrl = getMalwareListBaseUrl();
|
|
133
|
+
const path = newPackagesListPaths[/** @type {keyof typeof newPackagesListPaths} */ (ecosystem)];
|
|
134
|
+
|
|
135
|
+
if (!path) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const url = `${baseUrl}/${path}`;
|
|
140
|
+
|
|
141
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Error fetching ${ecosystem} new packages list version: ${response.statusText}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return response.headers.get("etag") || undefined;
|
|
149
|
+
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
|
75
150
|
}
|
|
76
151
|
|
|
77
152
|
/**
|
|
@@ -91,7 +166,7 @@ async function retry(func, attempts) {
|
|
|
91
166
|
return await func();
|
|
92
167
|
} catch (error) {
|
|
93
168
|
ui.writeVerbose(
|
|
94
|
-
"An error occurred while trying to download
|
|
169
|
+
"An error occurred while trying to download Aikido data",
|
|
95
170
|
error
|
|
96
171
|
);
|
|
97
172
|
lastError = error;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ui } from "../environment/userInteraction.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined}}
|
|
4
|
+
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, malwareListBaseUrl: string | undefined}}
|
|
5
5
|
*/
|
|
6
6
|
const state = {
|
|
7
7
|
loggingLevel: undefined,
|
|
8
8
|
skipMinimumPackageAge: undefined,
|
|
9
9
|
minimumPackageAgeHours: undefined,
|
|
10
|
+
malwareListBaseUrl: undefined,
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
|
@@ -20,6 +21,7 @@ export function initializeCliArguments(args) {
|
|
|
20
21
|
state.loggingLevel = undefined;
|
|
21
22
|
state.skipMinimumPackageAge = undefined;
|
|
22
23
|
state.minimumPackageAgeHours = undefined;
|
|
24
|
+
state.malwareListBaseUrl = undefined;
|
|
23
25
|
|
|
24
26
|
const safeChainArgs = [];
|
|
25
27
|
const remainingArgs = [];
|
|
@@ -35,6 +37,7 @@ export function initializeCliArguments(args) {
|
|
|
35
37
|
setLoggingLevel(safeChainArgs);
|
|
36
38
|
setSkipMinimumPackageAge(safeChainArgs);
|
|
37
39
|
setMinimumPackageAgeHours(safeChainArgs);
|
|
40
|
+
setMalwareListBaseUrl(safeChainArgs);
|
|
38
41
|
checkDeprecatedPythonFlag(args);
|
|
39
42
|
return remainingArgs;
|
|
40
43
|
}
|
|
@@ -109,6 +112,26 @@ export function getMinimumPackageAgeHours() {
|
|
|
109
112
|
return state.minimumPackageAgeHours;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/**
|
|
116
|
+
* @param {string[]} args
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
function setMalwareListBaseUrl(args) {
|
|
120
|
+
const argName = SAFE_CHAIN_ARG_PREFIX + "malware-list-base-url=";
|
|
121
|
+
|
|
122
|
+
const value = getLastArgEqualsValue(args, argName);
|
|
123
|
+
if (value) {
|
|
124
|
+
state.malwareListBaseUrl = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @returns {string | undefined}
|
|
130
|
+
*/
|
|
131
|
+
export function getMalwareListBaseUrl() {
|
|
132
|
+
return state.malwareListBaseUrl;
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
/**
|
|
113
136
|
* @param {string[]} args
|
|
114
137
|
* @param {string} flagName
|
package/src/config/configFile.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getEcoSystem } from "./settings.js";
|
|
|
10
10
|
* We cannot trust the input and should add the necessary validations
|
|
11
11
|
* @property {unknown | Number} scanTimeout
|
|
12
12
|
* @property {unknown | Number} minimumPackageAgeHours
|
|
13
|
+
* @property {unknown | string} malwareListBaseUrl
|
|
13
14
|
* @property {unknown | SafeChainRegistryConfiguration} npm
|
|
14
15
|
* @property {unknown | SafeChainRegistryConfiguration} pip
|
|
15
16
|
*
|
|
@@ -84,6 +85,18 @@ export function getMinimumPackageAgeHours() {
|
|
|
84
85
|
return undefined;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Gets the malware list base URL from config file only
|
|
90
|
+
* @returns {string | undefined}
|
|
91
|
+
*/
|
|
92
|
+
export function getMalwareListBaseUrl() {
|
|
93
|
+
const config = readConfigFile();
|
|
94
|
+
if (config.malwareListBaseUrl && typeof config.malwareListBaseUrl === "string") {
|
|
95
|
+
return config.malwareListBaseUrl;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
/**
|
|
88
101
|
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
|
89
102
|
* @returns {string[]}
|
|
@@ -129,18 +142,21 @@ export function getPipCustomRegistries() {
|
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
/**
|
|
132
|
-
* Gets the minimum package age exclusions from the config file
|
|
145
|
+
* Gets the minimum package age exclusions from the config file for the current ecosystem
|
|
133
146
|
* @returns {string[]}
|
|
134
147
|
*/
|
|
135
|
-
export function
|
|
148
|
+
export function getMinimumPackageAgeExclusions() {
|
|
136
149
|
const config = readConfigFile();
|
|
150
|
+
const ecosystem = getEcoSystem();
|
|
151
|
+
const registryConfig = ecosystem === "py" ? config.pip : config.npm;
|
|
137
152
|
|
|
138
|
-
if (!config || !
|
|
153
|
+
if (!config || !registryConfig) {
|
|
139
154
|
return [];
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
const
|
|
143
|
-
|
|
157
|
+
const typedRegistryConfig =
|
|
158
|
+
/** @type {SafeChainRegistryConfiguration} */ (registryConfig);
|
|
159
|
+
const exclusions = typedRegistryConfig.minimumPackageAgeExclusions;
|
|
144
160
|
|
|
145
161
|
if (!Array.isArray(exclusions)) {
|
|
146
162
|
return [];
|
|
@@ -211,6 +227,7 @@ function readConfigFile() {
|
|
|
211
227
|
const emptyConfig = {
|
|
212
228
|
scanTimeout: undefined,
|
|
213
229
|
minimumPackageAgeHours: undefined,
|
|
230
|
+
malwareListBaseUrl: undefined,
|
|
214
231
|
npm: {
|
|
215
232
|
customRegistries: undefined,
|
|
216
233
|
},
|
|
@@ -248,11 +265,52 @@ function getDatabaseVersionPath() {
|
|
|
248
265
|
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
|
249
266
|
}
|
|
250
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @returns {string}
|
|
270
|
+
*/
|
|
271
|
+
export function getNewPackagesListPath() {
|
|
272
|
+
const safeChainDir = getSafeChainDirectory();
|
|
273
|
+
const ecosystem = getEcoSystem();
|
|
274
|
+
return path.join(safeChainDir, `newPackagesList_${ecosystem}.json`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
export function getNewPackagesListVersionPath() {
|
|
281
|
+
const safeChainDir = getSafeChainDirectory();
|
|
282
|
+
const ecosystem = getEcoSystem();
|
|
283
|
+
return path.join(safeChainDir, `newPackagesList_version_${ecosystem}.txt`);
|
|
284
|
+
}
|
|
285
|
+
|
|
251
286
|
/**
|
|
252
287
|
* @returns {string}
|
|
253
288
|
*/
|
|
254
289
|
function getConfigFilePath() {
|
|
255
|
-
|
|
290
|
+
const primaryPath = path.join(getSafeChainDirectory(), "config.json");
|
|
291
|
+
if (fs.existsSync(primaryPath)) {
|
|
292
|
+
return primaryPath;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const legacyPath = path.join(getAikidoDirectory(), "config.json");
|
|
296
|
+
if (fs.existsSync(legacyPath)) {
|
|
297
|
+
return legacyPath;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return primaryPath;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
export function getSafeChainDirectory() {
|
|
307
|
+
const homeDir = os.homedir();
|
|
308
|
+
const safeChainDir = path.join(homeDir, ".safe-chain");
|
|
309
|
+
|
|
310
|
+
if (!fs.existsSync(safeChainDir)) {
|
|
311
|
+
fs.mkdirSync(safeChainDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
return safeChainDir;
|
|
256
314
|
}
|
|
257
315
|
|
|
258
316
|
/**
|
|
@@ -41,6 +41,17 @@ export function getLoggingLevel() {
|
|
|
41
41
|
* Example: "react,@aikidosec/safe-chain,lodash"
|
|
42
42
|
* @returns {string | undefined}
|
|
43
43
|
*/
|
|
44
|
-
export function
|
|
45
|
-
return process.env.
|
|
44
|
+
export function getMinimumPackageAgeExclusions() {
|
|
45
|
+
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS ||
|
|
46
|
+
process.env.SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Gets the malware list base URL from environment variable
|
|
51
|
+
* Expected format: full URL without trailing slash
|
|
52
|
+
* Example: "https://malware-list.aikido.dev"
|
|
53
|
+
* @returns {string | undefined}
|
|
54
|
+
*/
|
|
55
|
+
export function getMalwareListBaseUrl() {
|
|
56
|
+
return process.env.SAFE_CHAIN_MALWARE_LIST_BASE_URL;
|
|
46
57
|
}
|
package/src/config/settings.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as cliArguments from "./cliArguments.js";
|
|
2
2
|
import * as configFile from "./configFile.js";
|
|
3
3
|
import * as environmentVariables from "./environmentVariables.js";
|
|
4
|
+
import { ui } from "../environment/userInteraction.js";
|
|
4
5
|
|
|
5
6
|
export const LOGGING_SILENT = "silent";
|
|
6
7
|
export const LOGGING_NORMAL = "normal";
|
|
@@ -45,7 +46,7 @@ export function setEcoSystem(setting) {
|
|
|
45
46
|
ecosystemSettings.ecoSystem = setting;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const defaultMinimumPackageAge =
|
|
49
|
+
const defaultMinimumPackageAge = 48;
|
|
49
50
|
/** @returns {number} */
|
|
50
51
|
export function getMinimumPackageAgeHours() {
|
|
51
52
|
// Priority 1: CLI argument
|
|
@@ -188,13 +189,61 @@ function parseExclusionsFromEnv(envValue) {
|
|
|
188
189
|
* Gets the minimum package age exclusions from both environment variable and config file (merged)
|
|
189
190
|
* @returns {string[]}
|
|
190
191
|
*/
|
|
191
|
-
export function
|
|
192
|
+
export function getMinimumPackageAgeExclusions() {
|
|
192
193
|
const envExclusions = parseExclusionsFromEnv(
|
|
193
|
-
environmentVariables.
|
|
194
|
+
environmentVariables.getMinimumPackageAgeExclusions()
|
|
194
195
|
);
|
|
195
|
-
const configExclusions = configFile.
|
|
196
|
+
const configExclusions = configFile.getMinimumPackageAgeExclusions();
|
|
196
197
|
|
|
197
198
|
// Merge both sources and remove duplicates
|
|
198
199
|
const allExclusions = [...envExclusions, ...configExclusions];
|
|
199
200
|
return [...new Set(allExclusions)];
|
|
200
201
|
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Gets the malware list base URL with priority: CLI argument > environment variable > config file > default
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
export function getMalwareListBaseUrl() {
|
|
208
|
+
// Priority 1: CLI argument
|
|
209
|
+
const cliValue = cliArguments.getMalwareListBaseUrl();
|
|
210
|
+
if (cliValue) {
|
|
211
|
+
const url = removeTrailingSlashes(cliValue);
|
|
212
|
+
ui.writeInformation(`Fetching malware lists from ${url} as defined by CLI argument --safe-chain-malware-list-base-url`);
|
|
213
|
+
return url;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Priority 2: Environment variable
|
|
217
|
+
const envValue = environmentVariables.getMalwareListBaseUrl();
|
|
218
|
+
if (envValue) {
|
|
219
|
+
const url = removeTrailingSlashes(envValue);
|
|
220
|
+
ui.writeInformation(`Fetching malware lists from ${url} as defined by environment variable SAFE_CHAIN_MALWARE_LIST_BASE_URL`);
|
|
221
|
+
return url;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Priority 3: Config file
|
|
225
|
+
const configValue = configFile.getMalwareListBaseUrl();
|
|
226
|
+
if (configValue) {
|
|
227
|
+
const url = removeTrailingSlashes(configValue);
|
|
228
|
+
ui.writeInformation(`Fetching malware lists from ${url} as defined by config file (malwareListBaseUrl)`);
|
|
229
|
+
return url;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Default
|
|
233
|
+
const url = removeTrailingSlashes("https://malware-list.aikido.dev");
|
|
234
|
+
ui.writeInformation(`Fetching malware lists from ${url} (default)`);
|
|
235
|
+
return url;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Removes trailing slashes from a URL-like string.
|
|
240
|
+
* @param {string} value
|
|
241
|
+
* @returns {string}
|
|
242
|
+
*/
|
|
243
|
+
function removeTrailingSlashes(value) {
|
|
244
|
+
if (!value || typeof value !== "string") {
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return value.replace(/\/+$/, "");
|
|
249
|
+
}
|