@aikidosec/safe-chain 0.0.1-immutable-releases-beta
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/LICENSE +674 -0
- package/README.md +517 -0
- package/bin/aikido-bun.js +14 -0
- package/bin/aikido-bunx.js +14 -0
- package/bin/aikido-npm.js +14 -0
- package/bin/aikido-npx.js +14 -0
- package/bin/aikido-pip.js +17 -0
- package/bin/aikido-pip3.js +17 -0
- package/bin/aikido-pipx.js +16 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-poetry.js +13 -0
- package/bin/aikido-python.js +19 -0
- package/bin/aikido-python3.js +19 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +130 -0
- package/docs/banner.svg +151 -0
- package/docs/safe-package-manager-demo.gif +0 -0
- package/docs/safe-package-manager-demo.png +0 -0
- package/docs/shell-integration.md +149 -0
- package/docs/troubleshooting.md +321 -0
- package/npm-shrinkwrap.json +4069 -0
- package/package.json +72 -0
- package/src/api/aikido.js +187 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +161 -0
- package/src/config/configFile.js +327 -0
- package/src/config/environmentVariables.js +57 -0
- package/src/config/settings.js +247 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/main.js +123 -0
- package/src/packagemanager/_shared/commandErrors.js +17 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +48 -0
- package/src/packagemanager/currentPackageManager.js +79 -0
- package/src/packagemanager/npm/createPackageManager.js +72 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
- package/src/packagemanager/npm/runNpmCommand.js +20 -0
- package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
- package/src/packagemanager/npm/utils/cmd-list.js +174 -0
- package/src/packagemanager/npm/utils/npmCommands.js +34 -0
- package/src/packagemanager/npx/createPackageManager.js +15 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
- package/src/packagemanager/npx/runNpxCommand.js +20 -0
- package/src/packagemanager/pip/createPackageManager.js +25 -0
- package/src/packagemanager/pip/pipSettings.js +6 -0
- package/src/packagemanager/pip/runPipCommand.js +209 -0
- package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
- package/src/packagemanager/pipx/runPipXCommand.js +60 -0
- package/src/packagemanager/pnpm/createPackageManager.js +57 -0
- package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
- package/src/packagemanager/pnpm/runPnpmCommand.js +32 -0
- package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +66 -0
- package/src/packagemanager/yarn/createPackageManager.js +41 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
- package/src/packagemanager/yarn/runYarnCommand.js +36 -0
- package/src/registryProxy/certBundle.js +203 -0
- package/src/registryProxy/certUtils.js +178 -0
- package/src/registryProxy/getConnectTimeout.js +13 -0
- package/src/registryProxy/http-utils.js +80 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
- package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
- 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/isImdsEndpoint.js +13 -0
- package/src/registryProxy/mitmRequestHandler.js +240 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +255 -0
- package/src/registryProxy/tunnelRequestHandler.js +213 -0
- package/src/scanning/audit/index.js +129 -0
- package/src/scanning/index.js +82 -0
- package/src/scanning/malwareDatabase.js +131 -0
- 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/helpers.js +304 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +172 -0
- package/src/shell-integration/setup.js +129 -0
- package/src/shell-integration/shellDetection.js +39 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +115 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +96 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +171 -0
- package/src/shell-integration/supported-shells/bash.js +152 -0
- package/src/shell-integration/supported-shells/fish.js +95 -0
- package/src/shell-integration/supported-shells/powershell.js +100 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +100 -0
- package/src/shell-integration/supported-shells/zsh.js +92 -0
- package/src/shell-integration/teardown.js +112 -0
- package/src/ultimate/ultimateTroubleshooting.js +111 -0
- package/src/utils/safeSpawn.js +153 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { ui } from "../environment/userInteraction.js";
|
|
5
|
+
import { getEcoSystem } from "./settings.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} SafeChainConfig
|
|
9
|
+
*
|
|
10
|
+
* We cannot trust the input and should add the necessary validations
|
|
11
|
+
* @property {unknown | Number} scanTimeout
|
|
12
|
+
* @property {unknown | Number} minimumPackageAgeHours
|
|
13
|
+
* @property {unknown | string} malwareListBaseUrl
|
|
14
|
+
* @property {unknown | SafeChainRegistryConfiguration} npm
|
|
15
|
+
* @property {unknown | SafeChainRegistryConfiguration} pip
|
|
16
|
+
*
|
|
17
|
+
* @typedef {Object} SafeChainRegistryConfiguration
|
|
18
|
+
* We cannot trust the input and should add the necessary validations.
|
|
19
|
+
* @property {unknown | string[]} customRegistries
|
|
20
|
+
* @property {unknown | string[]} minimumPackageAgeExclusions
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @returns {number}
|
|
25
|
+
*/
|
|
26
|
+
export function getScanTimeout() {
|
|
27
|
+
const config = readConfigFile();
|
|
28
|
+
|
|
29
|
+
if (process.env.AIKIDO_SCAN_TIMEOUT_MS) {
|
|
30
|
+
const scanTimeout = validateTimeout(process.env.AIKIDO_SCAN_TIMEOUT_MS);
|
|
31
|
+
if (scanTimeout != null) {
|
|
32
|
+
return scanTimeout;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.scanTimeout) {
|
|
37
|
+
const scanTimeout = validateTimeout(config.scanTimeout);
|
|
38
|
+
if (scanTimeout != null) {
|
|
39
|
+
return scanTimeout;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return 10000; // Default to 10 seconds
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @param {any} value
|
|
49
|
+
* @returns {number?}
|
|
50
|
+
*/
|
|
51
|
+
function validateTimeout(value) {
|
|
52
|
+
const timeout = Number(value);
|
|
53
|
+
if (!Number.isNaN(timeout) && timeout > 0) {
|
|
54
|
+
return timeout;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {any} value
|
|
61
|
+
* @returns {number | undefined}
|
|
62
|
+
*/
|
|
63
|
+
function validateMinimumPackageAgeHours(value) {
|
|
64
|
+
const hours = Number(value);
|
|
65
|
+
if (!Number.isNaN(hours)) {
|
|
66
|
+
return hours;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets the minimum package age in hours from config file only
|
|
73
|
+
* @returns {number | undefined}
|
|
74
|
+
*/
|
|
75
|
+
export function getMinimumPackageAgeHours() {
|
|
76
|
+
const config = readConfigFile();
|
|
77
|
+
if (config.minimumPackageAgeHours !== undefined) {
|
|
78
|
+
const validated = validateMinimumPackageAgeHours(
|
|
79
|
+
config.minimumPackageAgeHours
|
|
80
|
+
);
|
|
81
|
+
if (validated !== undefined) {
|
|
82
|
+
return validated;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
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
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
export function getNpmCustomRegistries() {
|
|
105
|
+
const config = readConfigFile();
|
|
106
|
+
|
|
107
|
+
if (!config || !config.npm) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// TypeScript needs help understanding that config.npm exists and has customRegistries
|
|
112
|
+
const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
|
|
113
|
+
const customRegistries = npmConfig.customRegistries;
|
|
114
|
+
|
|
115
|
+
if (!Array.isArray(customRegistries)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return customRegistries.filter((item) => typeof item === "string");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
|
124
|
+
* @returns {string[]}
|
|
125
|
+
*/
|
|
126
|
+
export function getPipCustomRegistries() {
|
|
127
|
+
const config = readConfigFile();
|
|
128
|
+
|
|
129
|
+
if (!config || !config.pip) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// TypeScript needs help understanding that config.pip exists and has customRegistries
|
|
134
|
+
const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip);
|
|
135
|
+
const customRegistries = pipConfig.customRegistries;
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(customRegistries)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return customRegistries.filter((item) => typeof item === "string");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Gets the minimum package age exclusions from the config file for the current ecosystem
|
|
146
|
+
* @returns {string[]}
|
|
147
|
+
*/
|
|
148
|
+
export function getMinimumPackageAgeExclusions() {
|
|
149
|
+
const config = readConfigFile();
|
|
150
|
+
const ecosystem = getEcoSystem();
|
|
151
|
+
const registryConfig = ecosystem === "py" ? config.pip : config.npm;
|
|
152
|
+
|
|
153
|
+
if (!config || !registryConfig) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const typedRegistryConfig =
|
|
158
|
+
/** @type {SafeChainRegistryConfiguration} */ (registryConfig);
|
|
159
|
+
const exclusions = typedRegistryConfig.minimumPackageAgeExclusions;
|
|
160
|
+
|
|
161
|
+
if (!Array.isArray(exclusions)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return exclusions.filter((item) => typeof item === "string");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
|
170
|
+
* @param {string | number} version
|
|
171
|
+
*
|
|
172
|
+
* @returns {void}
|
|
173
|
+
*/
|
|
174
|
+
export function writeDatabaseToLocalCache(data, version) {
|
|
175
|
+
try {
|
|
176
|
+
const databasePath = getDatabasePath();
|
|
177
|
+
const versionPath = getDatabaseVersionPath();
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(databasePath, JSON.stringify(data));
|
|
180
|
+
fs.writeFileSync(versionPath, version.toString());
|
|
181
|
+
} catch {
|
|
182
|
+
ui.writeWarning(
|
|
183
|
+
"Failed to write malware database to local cache, next time the database will be fetched from the server again."
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}}
|
|
190
|
+
*/
|
|
191
|
+
export function readDatabaseFromLocalCache() {
|
|
192
|
+
try {
|
|
193
|
+
const databasePath = getDatabasePath();
|
|
194
|
+
if (!fs.existsSync(databasePath)) {
|
|
195
|
+
return {
|
|
196
|
+
malwareDatabase: null,
|
|
197
|
+
version: null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const data = fs.readFileSync(databasePath, "utf8");
|
|
201
|
+
const malwareDatabase = JSON.parse(data);
|
|
202
|
+
const versionPath = getDatabaseVersionPath();
|
|
203
|
+
let version = null;
|
|
204
|
+
if (fs.existsSync(versionPath)) {
|
|
205
|
+
version = fs.readFileSync(versionPath, "utf8").trim();
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
malwareDatabase: malwareDatabase,
|
|
209
|
+
version: version,
|
|
210
|
+
};
|
|
211
|
+
} catch {
|
|
212
|
+
ui.writeWarning(
|
|
213
|
+
"Failed to read malware database from local cache. Continuing without local cache."
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
malwareDatabase: null,
|
|
217
|
+
version: null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @returns {SafeChainConfig}
|
|
224
|
+
*/
|
|
225
|
+
function readConfigFile() {
|
|
226
|
+
/** @type {SafeChainConfig} */
|
|
227
|
+
const emptyConfig = {
|
|
228
|
+
scanTimeout: undefined,
|
|
229
|
+
minimumPackageAgeHours: undefined,
|
|
230
|
+
malwareListBaseUrl: undefined,
|
|
231
|
+
npm: {
|
|
232
|
+
customRegistries: undefined,
|
|
233
|
+
},
|
|
234
|
+
pip: {
|
|
235
|
+
customRegistries: undefined,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const configFilePath = getConfigFilePath();
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(configFilePath)) {
|
|
242
|
+
return emptyConfig;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const data = fs.readFileSync(configFilePath, "utf8");
|
|
247
|
+
return JSON.parse(data);
|
|
248
|
+
} catch {
|
|
249
|
+
return emptyConfig;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
function getDatabasePath() {
|
|
257
|
+
const aikidoDir = getAikidoDirectory();
|
|
258
|
+
const ecosystem = getEcoSystem();
|
|
259
|
+
return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getDatabaseVersionPath() {
|
|
263
|
+
const aikidoDir = getAikidoDirectory();
|
|
264
|
+
const ecosystem = getEcoSystem();
|
|
265
|
+
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
|
266
|
+
}
|
|
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
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
function getConfigFilePath() {
|
|
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;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
319
|
+
function getAikidoDirectory() {
|
|
320
|
+
const homeDir = os.homedir();
|
|
321
|
+
const aikidoDir = path.join(homeDir, ".aikido");
|
|
322
|
+
|
|
323
|
+
if (!fs.existsSync(aikidoDir)) {
|
|
324
|
+
fs.mkdirSync(aikidoDir, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
return aikidoDir;
|
|
327
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the minimum package age in hours from environment variable
|
|
3
|
+
* @returns {string | undefined}
|
|
4
|
+
*/
|
|
5
|
+
export function getMinimumPackageAgeHours() {
|
|
6
|
+
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets the custom npm registries from environment variable
|
|
11
|
+
* Expected format: comma-separated list of registry domains
|
|
12
|
+
* Example: "npm.company.com,registry.internal.net"
|
|
13
|
+
* @returns {string | undefined}
|
|
14
|
+
*/
|
|
15
|
+
export function getNpmCustomRegistries() {
|
|
16
|
+
return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets the custom pip registries from environment variable
|
|
21
|
+
* Expected format: comma-separated list of registry domains
|
|
22
|
+
* Example: "pip.company.com,registry.internal.net"
|
|
23
|
+
* @returns {string | undefined}
|
|
24
|
+
*/
|
|
25
|
+
export function getPipCustomRegistries() {
|
|
26
|
+
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
|
|
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 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;
|
|
57
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import * as cliArguments from "./cliArguments.js";
|
|
2
|
+
import * as configFile from "./configFile.js";
|
|
3
|
+
import * as environmentVariables from "./environmentVariables.js";
|
|
4
|
+
import { ui } from "../environment/userInteraction.js";
|
|
5
|
+
|
|
6
|
+
export const LOGGING_SILENT = "silent";
|
|
7
|
+
export const LOGGING_NORMAL = "normal";
|
|
8
|
+
export const LOGGING_VERBOSE = "verbose";
|
|
9
|
+
|
|
10
|
+
export function getLoggingLevel() {
|
|
11
|
+
// Priority 1: CLI argument
|
|
12
|
+
const cliLevel = cliArguments.getLoggingLevel();
|
|
13
|
+
if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) {
|
|
14
|
+
return cliLevel;
|
|
15
|
+
}
|
|
16
|
+
if (cliLevel) {
|
|
17
|
+
// CLI arg was set but invalid, default to normal for backwards compatibility.
|
|
18
|
+
return LOGGING_NORMAL;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Priority 2: Environment variable
|
|
22
|
+
const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase();
|
|
23
|
+
if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) {
|
|
24
|
+
return envLevel;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return LOGGING_NORMAL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ECOSYSTEM_JS = "js";
|
|
31
|
+
export const ECOSYSTEM_PY = "py";
|
|
32
|
+
|
|
33
|
+
// Default to JavaScript ecosystem
|
|
34
|
+
const ecosystemSettings = {
|
|
35
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** @returns {string} - The current ecosystem setting (ECOSYSTEM_JS or ECOSYSTEM_PY) */
|
|
39
|
+
export function getEcoSystem() {
|
|
40
|
+
return ecosystemSettings.ecoSystem;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} setting - The ecosystem to set (ECOSYSTEM_JS or ECOSYSTEM_PY)
|
|
44
|
+
*/
|
|
45
|
+
export function setEcoSystem(setting) {
|
|
46
|
+
ecosystemSettings.ecoSystem = setting;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultMinimumPackageAge = 48;
|
|
50
|
+
/** @returns {number} */
|
|
51
|
+
export function getMinimumPackageAgeHours() {
|
|
52
|
+
// Priority 1: CLI argument
|
|
53
|
+
const cliValue = validateMinimumPackageAgeHours(
|
|
54
|
+
cliArguments.getMinimumPackageAgeHours()
|
|
55
|
+
);
|
|
56
|
+
if (cliValue !== undefined) {
|
|
57
|
+
return cliValue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Priority 2: Environment variable
|
|
61
|
+
const envValue = validateMinimumPackageAgeHours(
|
|
62
|
+
environmentVariables.getMinimumPackageAgeHours()
|
|
63
|
+
);
|
|
64
|
+
if (envValue !== undefined) {
|
|
65
|
+
return envValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Priority 3: Config file
|
|
69
|
+
const configValue = configFile.getMinimumPackageAgeHours();
|
|
70
|
+
if (configValue !== undefined) {
|
|
71
|
+
return configValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return defaultMinimumPackageAge;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string | undefined} value
|
|
79
|
+
* @returns {number | undefined}
|
|
80
|
+
*/
|
|
81
|
+
function validateMinimumPackageAgeHours(value) {
|
|
82
|
+
if (!value) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const numericValue = Number(value);
|
|
87
|
+
if (Number.isNaN(numericValue)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (numericValue >= 0) {
|
|
92
|
+
return numericValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const defaultSkipMinimumPackageAge = false;
|
|
99
|
+
export function skipMinimumPackageAge() {
|
|
100
|
+
const cliValue = cliArguments.getSkipMinimumPackageAge();
|
|
101
|
+
|
|
102
|
+
if (cliValue === true) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return defaultSkipMinimumPackageAge;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Normalizes a registry URL by removing protocol if present
|
|
111
|
+
* @param {string} registry
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function normalizeRegistry(registry) {
|
|
115
|
+
// Remove protocol (http://, https://) if present
|
|
116
|
+
return registry.replace(/^https?:\/\//, "");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parses comma-separated registries from environment variable
|
|
121
|
+
* @param {string | undefined} envValue
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
function parseRegistriesFromEnv(envValue) {
|
|
125
|
+
if (!envValue || typeof envValue !== "string") {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Split by comma and trim whitespace
|
|
130
|
+
return envValue
|
|
131
|
+
.split(",")
|
|
132
|
+
.map((registry) => registry.trim())
|
|
133
|
+
.filter((registry) => registry.length > 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Gets the custom npm registries from both environment variable and config file (merged)
|
|
138
|
+
* @returns {string[]}
|
|
139
|
+
*/
|
|
140
|
+
export function getNpmCustomRegistries() {
|
|
141
|
+
const envRegistries = parseRegistriesFromEnv(
|
|
142
|
+
environmentVariables.getNpmCustomRegistries()
|
|
143
|
+
);
|
|
144
|
+
const configRegistries = configFile.getNpmCustomRegistries();
|
|
145
|
+
|
|
146
|
+
// Merge both sources and remove duplicates
|
|
147
|
+
const allRegistries = [...envRegistries, ...configRegistries];
|
|
148
|
+
const uniqueRegistries = [...new Set(allRegistries)];
|
|
149
|
+
|
|
150
|
+
// Normalize each registry (remove protocol if any)
|
|
151
|
+
return uniqueRegistries.map(normalizeRegistry);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets the custom npm registries from both environment variable and config file (merged)
|
|
156
|
+
* @returns {string[]}
|
|
157
|
+
*/
|
|
158
|
+
export function getPipCustomRegistries() {
|
|
159
|
+
const envRegistries = parseRegistriesFromEnv(
|
|
160
|
+
environmentVariables.getPipCustomRegistries()
|
|
161
|
+
);
|
|
162
|
+
const configRegistries = configFile.getPipCustomRegistries();
|
|
163
|
+
|
|
164
|
+
// Merge both sources and remove duplicates
|
|
165
|
+
const allRegistries = [...envRegistries, ...configRegistries];
|
|
166
|
+
const uniqueRegistries = [...new Set(allRegistries)];
|
|
167
|
+
|
|
168
|
+
// Normalize each registry (remove protocol if any)
|
|
169
|
+
return uniqueRegistries.map(normalizeRegistry);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parses comma-separated exclusions from environment variable
|
|
174
|
+
* @param {string | undefined} envValue
|
|
175
|
+
* @returns {string[]}
|
|
176
|
+
*/
|
|
177
|
+
function parseExclusionsFromEnv(envValue) {
|
|
178
|
+
if (!envValue || typeof envValue !== "string") {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return envValue
|
|
183
|
+
.split(",")
|
|
184
|
+
.map((exclusion) => exclusion.trim())
|
|
185
|
+
.filter((exclusion) => exclusion.length > 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets the minimum package age exclusions from both environment variable and config file (merged)
|
|
190
|
+
* @returns {string[]}
|
|
191
|
+
*/
|
|
192
|
+
export function getMinimumPackageAgeExclusions() {
|
|
193
|
+
const envExclusions = parseExclusionsFromEnv(
|
|
194
|
+
environmentVariables.getMinimumPackageAgeExclusions()
|
|
195
|
+
);
|
|
196
|
+
const configExclusions = configFile.getMinimumPackageAgeExclusions();
|
|
197
|
+
|
|
198
|
+
// Merge both sources and remove duplicates
|
|
199
|
+
const allExclusions = [...envExclusions, ...configExclusions];
|
|
200
|
+
return [...new Set(allExclusions)];
|
|
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.writeVerbose(`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.writeVerbose(`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.writeVerbose(`Fetching malware lists from ${url} as defined by config file (malwareListBaseUrl)`);
|
|
229
|
+
return url;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Default
|
|
233
|
+
return removeTrailingSlashes("https://malware-list.aikido.dev");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Removes trailing slashes from a URL-like string.
|
|
238
|
+
* @param {string} value
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function removeTrailingSlashes(value) {
|
|
242
|
+
if (!value || typeof value !== "string") {
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return value.replace(/\/+$/, "");
|
|
247
|
+
}
|