@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
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aikidosec/safe-chain",
|
|
3
|
+
"version": "0.0.1-immutable-releases-beta",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
6
|
+
"test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
7
|
+
"lint": "oxlint --deny-warnings",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"aikido-npm": "bin/aikido-npm.js",
|
|
12
|
+
"aikido-npx": "bin/aikido-npx.js",
|
|
13
|
+
"aikido-yarn": "bin/aikido-yarn.js",
|
|
14
|
+
"aikido-pnpm": "bin/aikido-pnpm.js",
|
|
15
|
+
"aikido-pnpx": "bin/aikido-pnpx.js",
|
|
16
|
+
"aikido-bun": "bin/aikido-bun.js",
|
|
17
|
+
"aikido-bunx": "bin/aikido-bunx.js",
|
|
18
|
+
"aikido-uv": "bin/aikido-uv.js",
|
|
19
|
+
"aikido-pip": "bin/aikido-pip.js",
|
|
20
|
+
"aikido-pip3": "bin/aikido-pip3.js",
|
|
21
|
+
"aikido-python": "bin/aikido-python.js",
|
|
22
|
+
"aikido-python3": "bin/aikido-python3.js",
|
|
23
|
+
"aikido-poetry": "bin/aikido-poetry.js",
|
|
24
|
+
"aikido-pipx": "bin/aikido-pipx.js",
|
|
25
|
+
"safe-chain": "bin/safe-chain.js"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"default": "./src/main.js"
|
|
31
|
+
},
|
|
32
|
+
"./scanning": {
|
|
33
|
+
"default": "./src/scanning/audit/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"keywords": [],
|
|
37
|
+
"author": "Aikido Security",
|
|
38
|
+
"license": "AGPL-3.0-or-later",
|
|
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
|
+
"dependencies": {
|
|
41
|
+
"archiver": "^7.0.1",
|
|
42
|
+
"certifi": "14.5.15",
|
|
43
|
+
"chalk": "5.4.1",
|
|
44
|
+
"https-proxy-agent": "7.0.6",
|
|
45
|
+
"ini": "6.0.0",
|
|
46
|
+
"make-fetch-happen": "15.0.3",
|
|
47
|
+
"node-forge": "1.3.2",
|
|
48
|
+
"npm-registry-fetch": "19.1.1",
|
|
49
|
+
"semver": "7.7.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/archiver": "^7.0.0",
|
|
53
|
+
"@types/ini": "^4.1.1",
|
|
54
|
+
"@types/make-fetch-happen": "^10.0.4",
|
|
55
|
+
"@types/node": "^18.19.130",
|
|
56
|
+
"@types/node-forge": "^1.3.14",
|
|
57
|
+
"@types/npm-registry-fetch": "^8.0.9",
|
|
58
|
+
"@types/semver": "^7.7.1",
|
|
59
|
+
"esbuild": "^0.27.0",
|
|
60
|
+
"typescript": "^5.9.3"
|
|
61
|
+
},
|
|
62
|
+
"main": "src/main.js",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/AikidoSec/safe-chain/issues"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://github.com/AikidoSec/safe-chain#readme",
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/AikidoSec/safe-chain.git",
|
|
70
|
+
"directory": "packages/safe-chain"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fetch from "make-fetch-happen";
|
|
2
|
+
import {
|
|
3
|
+
getEcoSystem,
|
|
4
|
+
ECOSYSTEM_JS,
|
|
5
|
+
ECOSYSTEM_PY,
|
|
6
|
+
getMalwareListBaseUrl,
|
|
7
|
+
} from "../config/settings.js";
|
|
8
|
+
import { ui } from "../environment/userInteraction.js";
|
|
9
|
+
|
|
10
|
+
const malwareDatabasePaths = {
|
|
11
|
+
[ECOSYSTEM_JS]: "malware_predictions.json",
|
|
12
|
+
[ECOSYSTEM_PY]: "malware_pypi.json",
|
|
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
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} MalwarePackage
|
|
24
|
+
* @property {string} package_name
|
|
25
|
+
* @property {string} version
|
|
26
|
+
* @property {string} reason
|
|
27
|
+
*/
|
|
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
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function fetchMalwareDatabase() {
|
|
42
|
+
return retry(async () => {
|
|
43
|
+
const ecosystem = getEcoSystem();
|
|
44
|
+
const baseUrl = getMalwareListBaseUrl();
|
|
45
|
+
const path = malwareDatabasePaths[
|
|
46
|
+
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
|
47
|
+
];
|
|
48
|
+
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
|
49
|
+
const response = await fetch(malwareDatabaseUrl);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Error fetching ${ecosystem} malware database: ${response.statusText}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
let malwareDatabase = await response.json();
|
|
58
|
+
return {
|
|
59
|
+
malwareDatabase: malwareDatabase,
|
|
60
|
+
version: response.headers.get("etag") || undefined,
|
|
61
|
+
};
|
|
62
|
+
} catch (/** @type {any} */ error) {
|
|
63
|
+
throw new Error(`Error parsing malware database: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @returns {Promise<string | undefined>}
|
|
70
|
+
*/
|
|
71
|
+
export async function fetchMalwareDatabaseVersion() {
|
|
72
|
+
return retry(async () => {
|
|
73
|
+
const ecosystem = getEcoSystem();
|
|
74
|
+
const baseUrl = getMalwareListBaseUrl();
|
|
75
|
+
const path = malwareDatabasePaths[
|
|
76
|
+
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
|
77
|
+
];
|
|
78
|
+
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
|
79
|
+
const response = await fetch(malwareDatabaseUrl, {
|
|
80
|
+
method: "HEAD",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return response.headers.get("etag") || undefined;
|
|
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);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Retries an asynchronous function multiple times until it succeeds or exhausts all attempts.
|
|
154
|
+
*
|
|
155
|
+
* @template T
|
|
156
|
+
* @param {() => Promise<T>} func - The asynchronous function to retry
|
|
157
|
+
* @param {number} attempts - The number of attempts
|
|
158
|
+
* @returns {Promise<T>} The return value of the function if successful
|
|
159
|
+
* @throws {Error} The last error encountered if all retry attempts fail
|
|
160
|
+
*/
|
|
161
|
+
async function retry(func, attempts) {
|
|
162
|
+
let lastError;
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < attempts; i++) {
|
|
165
|
+
try {
|
|
166
|
+
return await func();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
ui.writeVerbose(
|
|
169
|
+
"An error occurred while trying to download Aikido data",
|
|
170
|
+
error
|
|
171
|
+
);
|
|
172
|
+
lastError = error;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (i < attempts - 1) {
|
|
176
|
+
// When this is not the last try, back-off exponentially:
|
|
177
|
+
// 1st attempt - 500ms delay
|
|
178
|
+
// 2nd attempt - 1000ms delay
|
|
179
|
+
// 3rd attempt - 2000ms delay
|
|
180
|
+
// 4th attempt - 4000ms delay
|
|
181
|
+
// ...
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 500));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw lastError;
|
|
187
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as semver from "semver";
|
|
2
|
+
import * as npmFetch from "npm-registry-fetch";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} packageName
|
|
6
|
+
* @param {string | null} [versionRange]
|
|
7
|
+
* @returns {Promise<string | null>}
|
|
8
|
+
*/
|
|
9
|
+
export async function resolvePackageVersion(packageName, versionRange) {
|
|
10
|
+
if (!versionRange) {
|
|
11
|
+
versionRange = "latest";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (semver.valid(versionRange)) {
|
|
15
|
+
// The version is a fixed version, no need to resolve
|
|
16
|
+
return versionRange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const packageInfo = (
|
|
20
|
+
/** @type {{"dist-tags"?: Record<string, string>, versions?: Record<string, unknown>} | null} */
|
|
21
|
+
await getPackageInfo(packageName)
|
|
22
|
+
);
|
|
23
|
+
if (!packageInfo) {
|
|
24
|
+
// It is possible that no version is found (could be a private package, or a package that doesn't exist)
|
|
25
|
+
// In this case, we return null to indicate that we couldn't resolve the version
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const distTags = packageInfo["dist-tags"];
|
|
30
|
+
if (distTags && isDistTags(distTags) && distTags[versionRange]) {
|
|
31
|
+
// If the version range is a dist-tag, return the version associated with that tag
|
|
32
|
+
// e.g., "latest", "next", etc.
|
|
33
|
+
return distTags[versionRange];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!packageInfo.versions) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If the version range is not a dist-tag, we need to resolve the highest version matching the range.
|
|
41
|
+
// This is useful for ranges like "^1.0.0" or "~2.3.4".
|
|
42
|
+
const availableVersions = Object.keys(packageInfo.versions);
|
|
43
|
+
const resolvedVersion = semver.maxSatisfying(availableVersions, versionRange);
|
|
44
|
+
if (resolvedVersion) {
|
|
45
|
+
return resolvedVersion;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Nothing matched the range, return null
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
* @param {unknown} distTags
|
|
55
|
+
* @returns {distTags is Record<string, string>}
|
|
56
|
+
*/
|
|
57
|
+
function isDistTags(distTags) {
|
|
58
|
+
return typeof distTags === "object";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} packageName
|
|
63
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
64
|
+
*/
|
|
65
|
+
async function getPackageInfo(packageName) {
|
|
66
|
+
try {
|
|
67
|
+
return await npmFetch.json(packageName);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ui } from "../environment/userInteraction.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, malwareListBaseUrl: string | undefined}}
|
|
5
|
+
*/
|
|
6
|
+
const state = {
|
|
7
|
+
loggingLevel: undefined,
|
|
8
|
+
skipMinimumPackageAge: undefined,
|
|
9
|
+
minimumPackageAgeHours: undefined,
|
|
10
|
+
malwareListBaseUrl: undefined,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string[]} args
|
|
17
|
+
* @returns {string[]}
|
|
18
|
+
*/
|
|
19
|
+
export function initializeCliArguments(args) {
|
|
20
|
+
// Reset state on each call
|
|
21
|
+
state.loggingLevel = undefined;
|
|
22
|
+
state.skipMinimumPackageAge = undefined;
|
|
23
|
+
state.minimumPackageAgeHours = undefined;
|
|
24
|
+
state.malwareListBaseUrl = undefined;
|
|
25
|
+
|
|
26
|
+
const safeChainArgs = [];
|
|
27
|
+
const remainingArgs = [];
|
|
28
|
+
|
|
29
|
+
for (const arg of args) {
|
|
30
|
+
if (arg.toLowerCase().startsWith(SAFE_CHAIN_ARG_PREFIX)) {
|
|
31
|
+
safeChainArgs.push(arg);
|
|
32
|
+
} else {
|
|
33
|
+
remainingArgs.push(arg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setLoggingLevel(safeChainArgs);
|
|
38
|
+
setSkipMinimumPackageAge(safeChainArgs);
|
|
39
|
+
setMinimumPackageAgeHours(safeChainArgs);
|
|
40
|
+
setMalwareListBaseUrl(safeChainArgs);
|
|
41
|
+
checkDeprecatedPythonFlag(args);
|
|
42
|
+
return remainingArgs;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string[]} args
|
|
47
|
+
* @param {string} prefix
|
|
48
|
+
* @returns {string | undefined}
|
|
49
|
+
*/
|
|
50
|
+
function getLastArgEqualsValue(args, prefix) {
|
|
51
|
+
for (var i = args.length - 1; i >= 0; i--) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg.toLowerCase().startsWith(prefix)) {
|
|
54
|
+
return arg.substring(prefix.length);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string[]} args
|
|
63
|
+
* @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
function setLoggingLevel(args) {
|
|
66
|
+
const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging=";
|
|
67
|
+
|
|
68
|
+
const level = getLastArgEqualsValue(args, safeChainLoggingArg);
|
|
69
|
+
if (!level) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
state.loggingLevel = level.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getLoggingLevel() {
|
|
76
|
+
return state.loggingLevel;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string[]} args
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function setSkipMinimumPackageAge(args) {
|
|
84
|
+
const flagName = SAFE_CHAIN_ARG_PREFIX + "skip-minimum-package-age";
|
|
85
|
+
|
|
86
|
+
if (hasFlagArg(args, flagName)) {
|
|
87
|
+
state.skipMinimumPackageAge = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getSkipMinimumPackageAge() {
|
|
92
|
+
return state.skipMinimumPackageAge;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string[]} args
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
function setMinimumPackageAgeHours(args) {
|
|
100
|
+
const argName = SAFE_CHAIN_ARG_PREFIX + "minimum-package-age-hours=";
|
|
101
|
+
|
|
102
|
+
const value = getLastArgEqualsValue(args, argName);
|
|
103
|
+
if (value) {
|
|
104
|
+
state.minimumPackageAgeHours = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @returns {string | undefined}
|
|
110
|
+
*/
|
|
111
|
+
export function getMinimumPackageAgeHours() {
|
|
112
|
+
return state.minimumPackageAgeHours;
|
|
113
|
+
}
|
|
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
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string[]} args
|
|
137
|
+
* @param {string} flagName
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function hasFlagArg(args, flagName) {
|
|
141
|
+
for (const arg of args) {
|
|
142
|
+
if (arg.toLowerCase() === flagName.toLowerCase()) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Emits a deprecation warning for legacy --include-python flag
|
|
151
|
+
*
|
|
152
|
+
* @param {string[]} args
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
export function checkDeprecatedPythonFlag(args) {
|
|
156
|
+
if (hasFlagArg(args, "--include-python")) {
|
|
157
|
+
ui.writeWarning(
|
|
158
|
+
"--include-python is deprecated and ignored. Python tooling is included by default."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|