@aikidosec/safe-chain 1.4.4 → 1.4.8
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/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 +51 -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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {any} file
|
|
5
|
+
* @param {string} metadataUrl
|
|
6
|
+
* @returns {string | undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function getPackageVersionFromMetadataFile(file, metadataUrl) {
|
|
9
|
+
const href = typeof file?.url === "string" ? file.url : undefined;
|
|
10
|
+
const filename = typeof file?.filename === "string" ? file.filename : undefined;
|
|
11
|
+
|
|
12
|
+
if (href) {
|
|
13
|
+
const resolvedHref = new URL(href, metadataUrl).toString();
|
|
14
|
+
return parsePipPackageFromUrl(
|
|
15
|
+
resolvedHref,
|
|
16
|
+
new URL(resolvedHref).host
|
|
17
|
+
).version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (filename) {
|
|
21
|
+
return parsePipPackageFromUrl(
|
|
22
|
+
new URL(filename, metadataUrl).toString(),
|
|
23
|
+
new URL(metadataUrl).host
|
|
24
|
+
).version;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {any} json
|
|
32
|
+
* @param {string} metadataUrl
|
|
33
|
+
* @returns {string[]}
|
|
34
|
+
*/
|
|
35
|
+
export function getAvailableVersionsFromJson(json, metadataUrl) {
|
|
36
|
+
if (json.releases && typeof json.releases === "object") {
|
|
37
|
+
return Object.keys(json.releases);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(json.files)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
...new Set(
|
|
46
|
+
json.files
|
|
47
|
+
.map((/** @type {any} */ file) =>
|
|
48
|
+
getPackageVersionFromMetadataFile(file, metadataUrl)
|
|
49
|
+
)
|
|
50
|
+
.filter(isDefinedString)
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string | undefined} value
|
|
57
|
+
* @returns {value is string}
|
|
58
|
+
*/
|
|
59
|
+
function isDefinedString(value) {
|
|
60
|
+
return typeof value === "string";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string[]} versions
|
|
65
|
+
* @returns {string | undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function calculateLatestVersion(versions) {
|
|
68
|
+
const stableVersions = versions.filter((version) => !isPrerelease(version));
|
|
69
|
+
if (stableVersions.length > 0) {
|
|
70
|
+
return stableVersions.sort(comparePep440ishVersions).at(-1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return versions.sort(comparePep440ishVersions).at(-1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} left
|
|
78
|
+
* @param {string} right
|
|
79
|
+
* @returns {number}
|
|
80
|
+
*/
|
|
81
|
+
function comparePep440ishVersions(left, right) {
|
|
82
|
+
const leftParts = tokenizeVersion(left);
|
|
83
|
+
const rightParts = tokenizeVersion(right);
|
|
84
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
85
|
+
|
|
86
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
87
|
+
const leftPart = leftParts[index];
|
|
88
|
+
const rightPart = rightParts[index];
|
|
89
|
+
|
|
90
|
+
if (leftPart === undefined) return -1;
|
|
91
|
+
if (rightPart === undefined) return 1;
|
|
92
|
+
|
|
93
|
+
if (leftPart === rightPart) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const leftNumeric = typeof leftPart === "number";
|
|
98
|
+
const rightNumeric = typeof rightPart === "number";
|
|
99
|
+
|
|
100
|
+
if (leftNumeric && rightNumeric) {
|
|
101
|
+
return leftPart - rightPart;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (leftNumeric) return 1;
|
|
105
|
+
if (rightNumeric) return -1;
|
|
106
|
+
|
|
107
|
+
return String(leftPart).localeCompare(String(rightPart));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} version
|
|
115
|
+
* @returns {(string | number)[]}
|
|
116
|
+
*/
|
|
117
|
+
function tokenizeVersion(version) {
|
|
118
|
+
return version
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.split(/[^a-z0-9]+/)
|
|
121
|
+
.flatMap((part) => part.match(/[a-z]+|\d+/g) || [])
|
|
122
|
+
.map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} version
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
function isPrerelease(version) {
|
|
130
|
+
return /(a|b|rc|dev)\d+/i.test(version);
|
|
131
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
hasSuppressedVersions: false,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks whether any rewritten metadata response suppressed versions during the
|
|
7
|
+
* current process lifetime. This is intentional shared state used only for the
|
|
8
|
+
* end-of-run summary message exposed through the proxy API.
|
|
9
|
+
*
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
12
|
+
export function recordSuppressedVersion() {
|
|
13
|
+
state.hasSuppressedVersions = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function getHasSuppressedVersions() {
|
|
20
|
+
return state.hasSuppressedVersions;
|
|
21
|
+
}
|
|
@@ -2,7 +2,8 @@ import https from "https";
|
|
|
2
2
|
import { generateCertForHost } from "./certUtils.js";
|
|
3
3
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
4
4
|
import { ui } from "../environment/userInteraction.js";
|
|
5
|
-
import { gunzipSync
|
|
5
|
+
import { gunzipSync } from "zlib";
|
|
6
|
+
import { omitHeaders } from "./http-utils.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor
|
|
@@ -215,11 +216,16 @@ function createProxyRequest(hostname, port, req, res, requestHandler) {
|
|
|
215
216
|
|
|
216
217
|
buffer = requestHandler.modifyBody(buffer, headers);
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
// For rewritten responses, send the final body uncompressed.
|
|
220
|
+
// This avoids mismatches between upstream compression metadata and the
|
|
221
|
+
// rewritten payload on the wire.
|
|
222
|
+
const rewrittenHeaders = omitHeaders(
|
|
223
|
+
headers,
|
|
224
|
+
["content-length", "transfer-encoding", "content-encoding"],
|
|
225
|
+
{ caseInsensitive: true }
|
|
226
|
+
) || {};
|
|
227
|
+
rewrittenHeaders["content-length"] = String(buffer.byteLength);
|
|
228
|
+
res.writeHead(statusCode, rewrittenHeaders);
|
|
223
229
|
res.end(buffer);
|
|
224
230
|
});
|
|
225
231
|
} else {
|
|
@@ -2,19 +2,24 @@ import * as http from "http";
|
|
|
2
2
|
import { tunnelRequest } from "./tunnelRequestHandler.js";
|
|
3
3
|
import { mitmConnect } from "./mitmRequestHandler.js";
|
|
4
4
|
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
|
|
5
|
-
import { getCombinedCaBundlePath } from "./certBundle.js";
|
|
5
|
+
import { getCombinedCaBundlePath, cleanupCertBundle } from "./certBundle.js";
|
|
6
6
|
import { ui } from "../environment/userInteraction.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
|
|
9
|
-
import { getHasSuppressedVersions } from "./interceptors/
|
|
9
|
+
import { getHasSuppressedVersions } from "./interceptors/suppressedVersionsState.js";
|
|
10
10
|
|
|
11
11
|
const SERVER_STOP_TIMEOUT_MS = 1000;
|
|
12
12
|
/**
|
|
13
|
-
* @type {{
|
|
13
|
+
* @type {{
|
|
14
|
+
* port: number | null,
|
|
15
|
+
* blockedRequests: {packageName: string, version: string, url: string}[],
|
|
16
|
+
* blockedMinimumAgeRequests: {packageName: string, version: string, url: string}[]
|
|
17
|
+
* }}
|
|
14
18
|
*/
|
|
15
19
|
const state = {
|
|
16
20
|
port: null,
|
|
17
21
|
blockedRequests: [],
|
|
22
|
+
blockedMinimumAgeRequests: [],
|
|
18
23
|
};
|
|
19
24
|
|
|
20
25
|
export function createSafeChainProxy() {
|
|
@@ -23,7 +28,8 @@ export function createSafeChainProxy() {
|
|
|
23
28
|
return {
|
|
24
29
|
startServer: () => startServer(server),
|
|
25
30
|
stopServer: () => stopServer(server),
|
|
26
|
-
|
|
31
|
+
hasBlockedMaliciousPackages,
|
|
32
|
+
hasBlockedMinimumAgeRequests,
|
|
27
33
|
hasSuppressedVersions: getHasSuppressedVersions,
|
|
28
34
|
};
|
|
29
35
|
}
|
|
@@ -115,12 +121,16 @@ function stopServer(server) {
|
|
|
115
121
|
return new Promise((resolve) => {
|
|
116
122
|
try {
|
|
117
123
|
server.close(() => {
|
|
124
|
+
cleanupCertBundle();
|
|
118
125
|
resolve();
|
|
119
126
|
});
|
|
120
127
|
} catch {
|
|
121
128
|
resolve();
|
|
122
129
|
}
|
|
123
|
-
setTimeout(() =>
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
cleanupCertBundle();
|
|
132
|
+
resolve();
|
|
133
|
+
}, SERVER_STOP_TIMEOUT_MS);
|
|
124
134
|
});
|
|
125
135
|
}
|
|
126
136
|
|
|
@@ -147,6 +157,18 @@ function handleConnect(req, clientSocket, head) {
|
|
|
147
157
|
onMalwareBlocked(event.packageName, event.version, event.targetUrl);
|
|
148
158
|
}
|
|
149
159
|
);
|
|
160
|
+
interceptor.on(
|
|
161
|
+
"minimumAgeRequestBlocked",
|
|
162
|
+
(
|
|
163
|
+
/** @type {import("./interceptors/interceptorBuilder.js").MinimumAgeRequestBlockedEvent} */ event
|
|
164
|
+
) => {
|
|
165
|
+
onMinimumAgeRequestBlocked(
|
|
166
|
+
event.packageName,
|
|
167
|
+
event.version,
|
|
168
|
+
event.targetUrl
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
150
172
|
|
|
151
173
|
mitmConnect(req, clientSocket, interceptor);
|
|
152
174
|
} else {
|
|
@@ -166,10 +188,19 @@ function onMalwareBlocked(packageName, version, url) {
|
|
|
166
188
|
state.blockedRequests.push({ packageName, version, url });
|
|
167
189
|
}
|
|
168
190
|
|
|
169
|
-
|
|
191
|
+
/**
|
|
192
|
+
*
|
|
193
|
+
* @param {string} packageName
|
|
194
|
+
* @param {string} version
|
|
195
|
+
* @param {string} url
|
|
196
|
+
*/
|
|
197
|
+
function onMinimumAgeRequestBlocked(packageName, version, url) {
|
|
198
|
+
state.blockedMinimumAgeRequests.push({ packageName, version, url });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hasBlockedMaliciousPackages() {
|
|
170
202
|
if (state.blockedRequests.length === 0) {
|
|
171
|
-
|
|
172
|
-
return true;
|
|
203
|
+
return false;
|
|
173
204
|
}
|
|
174
205
|
|
|
175
206
|
ui.emptyLine();
|
|
@@ -188,5 +219,37 @@ function verifyNoMaliciousPackages() {
|
|
|
188
219
|
ui.writeExitWithoutInstallingMaliciousPackages();
|
|
189
220
|
ui.emptyLine();
|
|
190
221
|
|
|
191
|
-
return
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasBlockedMinimumAgeRequests() {
|
|
226
|
+
if (state.blockedMinimumAgeRequests.length === 0) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
ui.emptyLine();
|
|
231
|
+
|
|
232
|
+
ui.writeInformation(
|
|
233
|
+
`Safe-chain: ${chalk.bold(
|
|
234
|
+
`blocked ${state.blockedMinimumAgeRequests.length} direct package download request(s) due to minimum package age`
|
|
235
|
+
)}:`
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
for (const req of state.blockedMinimumAgeRequests) {
|
|
239
|
+
ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
ui.writeInformation(
|
|
243
|
+
` To disable this check, use: ${chalk.cyan(
|
|
244
|
+
"--safe-chain-skip-minimum-package-age"
|
|
245
|
+
)}`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
ui.emptyLine();
|
|
249
|
+
ui.writeError(
|
|
250
|
+
"Safe-chain: Exiting without installing packages blocked by the direct download minimum package age check."
|
|
251
|
+
);
|
|
252
|
+
ui.emptyLine();
|
|
253
|
+
|
|
254
|
+
return true;
|
|
192
255
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getMinimumPackageAgeHours,
|
|
3
|
+
getEcoSystem,
|
|
4
|
+
ECOSYSTEM_JS,
|
|
5
|
+
ECOSYSTEM_PY,
|
|
6
|
+
} from "../config/settings.js";
|
|
7
|
+
import { getEquivalentPackageNames } from "./packageNameVariants.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} NewPackagesDatabase
|
|
11
|
+
* @property {function(string | undefined, string | undefined): boolean} isNewlyReleasedPackage
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the ecosystem identifier expected in upstream/core release feeds.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function getCurrentFeedSource() {
|
|
19
|
+
const ecosystem = getEcoSystem();
|
|
20
|
+
|
|
21
|
+
if (ecosystem === ECOSYSTEM_JS) {
|
|
22
|
+
return "npm";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (ecosystem === ECOSYSTEM_PY) {
|
|
26
|
+
return "pypi";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return ecosystem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import("../api/aikido.js").NewPackageEntry[]} newPackagesList
|
|
34
|
+
* @returns {NewPackagesDatabase}
|
|
35
|
+
*/
|
|
36
|
+
export function buildNewPackagesDatabase(newPackagesList) {
|
|
37
|
+
const ecosystem = getEcoSystem();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string | undefined} name
|
|
41
|
+
* @param {string | undefined} version
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function isNewlyReleasedPackage(name, version) {
|
|
45
|
+
if (!name || !version) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cutOff = new Date(
|
|
50
|
+
new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000
|
|
51
|
+
);
|
|
52
|
+
const expectedSource = getCurrentFeedSource();
|
|
53
|
+
const candidateNames = getEquivalentPackageNames(name, ecosystem);
|
|
54
|
+
|
|
55
|
+
const entry = newPackagesList.find(
|
|
56
|
+
(pkg) =>
|
|
57
|
+
(!pkg.source || pkg.source.toLowerCase() === expectedSource) &&
|
|
58
|
+
candidateNames.includes(pkg.package_name) &&
|
|
59
|
+
pkg.version === version
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const releasedOn = new Date(entry.released_on * 1000);
|
|
67
|
+
return releasedOn > cutOff;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { isNewlyReleasedPackage };
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ui } from "../environment/userInteraction.js";
|
|
2
|
+
|
|
3
|
+
let hasWarnedAboutUnavailableNewPackagesDatabase = false;
|
|
4
|
+
|
|
5
|
+
/** @param {Error} error */
|
|
6
|
+
export function warnOnceAboutUnavailableDatabase(error) {
|
|
7
|
+
if (!hasWarnedAboutUnavailableNewPackagesDatabase) {
|
|
8
|
+
ui.writeWarning(
|
|
9
|
+
`Failed to load the new packages list used for direct package download request blocking. Continuing with metadata-based minimum age checks only. ${error.message}`
|
|
10
|
+
);
|
|
11
|
+
hasWarnedAboutUnavailableNewPackagesDatabase = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resetWarningState() {
|
|
16
|
+
hasWarnedAboutUnavailableNewPackagesDatabase = false;
|
|
17
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import {
|
|
3
|
+
fetchNewPackagesList,
|
|
4
|
+
fetchNewPackagesListVersion,
|
|
5
|
+
} from "../api/aikido.js";
|
|
6
|
+
import {
|
|
7
|
+
getNewPackagesListPath,
|
|
8
|
+
getNewPackagesListVersionPath,
|
|
9
|
+
} from "../config/configFile.js";
|
|
10
|
+
import { ui } from "../environment/userInteraction.js";
|
|
11
|
+
import { buildNewPackagesDatabase } from "./newPackagesDatabaseBuilder.js";
|
|
12
|
+
import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import("./newPackagesDatabaseBuilder.js").NewPackagesDatabase} NewPackagesDatabase
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
|
|
19
|
+
/** @type {NewPackagesDatabase | null} */
|
|
20
|
+
let cachedNewPackagesDatabase = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @returns {Promise<NewPackagesDatabase>}
|
|
24
|
+
*/
|
|
25
|
+
export async function openNewPackagesDatabase() {
|
|
26
|
+
if (cachedNewPackagesDatabase) {
|
|
27
|
+
return cachedNewPackagesDatabase;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @type {import("../api/aikido.js").NewPackageEntry[]} */
|
|
31
|
+
let newPackagesList;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
newPackagesList = await getNewPackagesList();
|
|
35
|
+
} catch (/** @type {any} */ error) {
|
|
36
|
+
warnOnceAboutUnavailableDatabase(error);
|
|
37
|
+
cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false };
|
|
38
|
+
return cachedNewPackagesDatabase;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
cachedNewPackagesDatabase = buildNewPackagesDatabase(newPackagesList);
|
|
42
|
+
return cachedNewPackagesDatabase;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {Promise<import("../api/aikido.js").NewPackageEntry[]>}
|
|
47
|
+
*/
|
|
48
|
+
async function getNewPackagesList() {
|
|
49
|
+
const { newPackagesList: cachedList, version: cachedVersion } =
|
|
50
|
+
readNewPackagesListFromLocalCache();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (cachedList) {
|
|
54
|
+
const currentVersion = await fetchNewPackagesListVersion();
|
|
55
|
+
if (cachedVersion === currentVersion) {
|
|
56
|
+
return cachedList;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { newPackagesList, version } = await fetchNewPackagesList();
|
|
61
|
+
|
|
62
|
+
if (version) {
|
|
63
|
+
writeNewPackagesListToLocalCache(newPackagesList, version);
|
|
64
|
+
return newPackagesList;
|
|
65
|
+
} else {
|
|
66
|
+
ui.writeWarning(
|
|
67
|
+
"The new packages list for direct package download request blocking was downloaded, but could not be cached due to a missing version."
|
|
68
|
+
);
|
|
69
|
+
return newPackagesList;
|
|
70
|
+
}
|
|
71
|
+
} catch (/** @type {any} */ error) {
|
|
72
|
+
if (cachedList) {
|
|
73
|
+
ui.writeWarning(
|
|
74
|
+
"Failed to fetch the latest new packages list for direct package download request blocking. Using cached version."
|
|
75
|
+
);
|
|
76
|
+
return cachedList;
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {import("../api/aikido.js").NewPackageEntry[]} data
|
|
84
|
+
* @param {string | number} version
|
|
85
|
+
*
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
export function writeNewPackagesListToLocalCache(data, version) {
|
|
89
|
+
try {
|
|
90
|
+
const listPath = getNewPackagesListPath();
|
|
91
|
+
const versionPath = getNewPackagesListVersionPath();
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(listPath, JSON.stringify(data));
|
|
94
|
+
fs.writeFileSync(versionPath, version.toString());
|
|
95
|
+
} catch {
|
|
96
|
+
ui.writeWarning(
|
|
97
|
+
"Failed to write new packages list to local cache, next time the list will be fetched from the server again."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @returns {{newPackagesList: import("../api/aikido.js").NewPackageEntry[] | null, version: string | null}}
|
|
104
|
+
*/
|
|
105
|
+
export function readNewPackagesListFromLocalCache() {
|
|
106
|
+
try {
|
|
107
|
+
const listPath = getNewPackagesListPath();
|
|
108
|
+
if (!fs.existsSync(listPath)) {
|
|
109
|
+
return { newPackagesList: null, version: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = fs.readFileSync(listPath, "utf8");
|
|
113
|
+
const newPackagesList = JSON.parse(data);
|
|
114
|
+
const versionPath = getNewPackagesListVersionPath();
|
|
115
|
+
let version = null;
|
|
116
|
+
if (fs.existsSync(versionPath)) {
|
|
117
|
+
version = fs.readFileSync(versionPath, "utf8").trim();
|
|
118
|
+
}
|
|
119
|
+
return { newPackagesList, version };
|
|
120
|
+
} catch {
|
|
121
|
+
ui.writeWarning(
|
|
122
|
+
"Failed to read new packages list from local cache. Continuing without local cache."
|
|
123
|
+
);
|
|
124
|
+
return { newPackagesList: null, version: null };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ECOSYSTEM_PY } from "../config/settings.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalises a Python package name per PEP 503: lowercase and collapse any
|
|
5
|
+
* run of `.`, `_`, or `-` into a single hyphen.
|
|
6
|
+
* @param {string} packageName
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function normalizePipPackageName(packageName) {
|
|
10
|
+
return packageName.toLowerCase().replace(/[._-]+/g, "-");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} packageName
|
|
15
|
+
* @param {string} ecosystem
|
|
16
|
+
* @returns {string[]}
|
|
17
|
+
*/
|
|
18
|
+
export function getEquivalentPackageNames(packageName, ecosystem) {
|
|
19
|
+
if (ecosystem !== ECOSYSTEM_PY) {
|
|
20
|
+
return [packageName];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pythonSeparatorPattern = /[._-]/g;
|
|
24
|
+
const hyphenName = packageName.replaceAll(pythonSeparatorPattern, "-");
|
|
25
|
+
const underscoreName = packageName.replaceAll(pythonSeparatorPattern, "_");
|
|
26
|
+
const dotName = packageName.replaceAll(pythonSeparatorPattern, ".");
|
|
27
|
+
|
|
28
|
+
return [...new Set([packageName, hyphenName, underscoreName, dotName])];
|
|
29
|
+
}
|
|
@@ -91,9 +91,7 @@ async function setupShell(shell) {
|
|
|
91
91
|
);
|
|
92
92
|
} else {
|
|
93
93
|
ui.writeError(
|
|
94
|
-
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
|
95
|
-
"Setup failed",
|
|
96
|
-
)}. Please check your ${shell.name} configuration.`,
|
|
94
|
+
`${chalk.bold("- " + shell.name + ":")} ${chalk.red("Setup failed")}`,
|
|
97
95
|
);
|
|
98
96
|
if (error) {
|
|
99
97
|
let message = ` Error: ${error.message}`;
|
|
@@ -102,6 +100,12 @@ async function setupShell(shell) {
|
|
|
102
100
|
}
|
|
103
101
|
ui.writeError(message);
|
|
104
102
|
}
|
|
103
|
+
ui.emptyLine();
|
|
104
|
+
ui.writeInformation(` ${chalk.bold("To set up manually:")}`);
|
|
105
|
+
for (const instruction of shell.getManualSetupInstructions()) {
|
|
106
|
+
ui.writeInformation(` ${instruction}`);
|
|
107
|
+
}
|
|
108
|
+
ui.emptyLine();
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
return success;
|
|
@@ -11,6 +11,8 @@ import { ui } from "../environment/userInteraction.js";
|
|
|
11
11
|
* @property {() => boolean} isInstalled
|
|
12
12
|
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
|
|
13
13
|
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
|
|
14
|
+
* @property {() => string[]} getManualSetupInstructions
|
|
15
|
+
* @property {() => string[]} getManualTeardownInstructions
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -32,7 +32,7 @@ function teardown(tools) {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Removes the line that sources the safe-chain bash initialization script (~/.
|
|
35
|
+
// Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh)
|
|
36
36
|
removeLinesMatchingPattern(
|
|
37
37
|
startupFile,
|
|
38
38
|
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
|
|
@@ -123,6 +123,22 @@ function cygpathw(path) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function getManualTeardownInstructions() {
|
|
127
|
+
return [
|
|
128
|
+
`Remove the following line from your ~/.bashrc file:`,
|
|
129
|
+
` source ~/.safe-chain/scripts/init-posix.sh`,
|
|
130
|
+
`Then restart your terminal or run: source ~/.bashrc`,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getManualSetupInstructions() {
|
|
135
|
+
return [
|
|
136
|
+
`Add the following line to your ~/.bashrc file:`,
|
|
137
|
+
` source ~/.safe-chain/scripts/init-posix.sh`,
|
|
138
|
+
`Then restart your terminal or run: source ~/.bashrc`,
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
/**
|
|
127
143
|
* @type {import("../shellDetection.js").Shell}
|
|
128
144
|
*/
|
|
@@ -131,4 +147,6 @@ export default {
|
|
|
131
147
|
isInstalled,
|
|
132
148
|
setup,
|
|
133
149
|
teardown,
|
|
150
|
+
getManualSetupInstructions,
|
|
151
|
+
getManualTeardownInstructions,
|
|
134
152
|
};
|
|
@@ -66,6 +66,22 @@ function getStartupFile() {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function getManualTeardownInstructions() {
|
|
70
|
+
return [
|
|
71
|
+
`Remove the following line from your ~/.config/fish/config.fish file:`,
|
|
72
|
+
` source ~/.safe-chain/scripts/init-fish.fish`,
|
|
73
|
+
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getManualSetupInstructions() {
|
|
78
|
+
return [
|
|
79
|
+
`Add the following line to your ~/.config/fish/config.fish file:`,
|
|
80
|
+
` source ~/.safe-chain/scripts/init-fish.fish`,
|
|
81
|
+
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
/**
|
|
70
86
|
* @type {import("../shellDetection.js").Shell}
|
|
71
87
|
*/
|
|
@@ -74,4 +90,6 @@ export default {
|
|
|
74
90
|
isInstalled,
|
|
75
91
|
setup,
|
|
76
92
|
teardown,
|
|
93
|
+
getManualSetupInstructions,
|
|
94
|
+
getManualTeardownInstructions,
|
|
77
95
|
};
|
|
@@ -71,6 +71,22 @@ function getStartupFile() {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function getManualTeardownInstructions() {
|
|
75
|
+
return [
|
|
76
|
+
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
|
77
|
+
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
|
78
|
+
`Then restart your terminal or run: . $PROFILE`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getManualSetupInstructions() {
|
|
83
|
+
return [
|
|
84
|
+
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
|
85
|
+
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
|
86
|
+
`Then restart your terminal or run: . $PROFILE`,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
/**
|
|
75
91
|
* @type {import("../shellDetection.js").Shell}
|
|
76
92
|
*/
|
|
@@ -79,4 +95,6 @@ export default {
|
|
|
79
95
|
isInstalled,
|
|
80
96
|
setup,
|
|
81
97
|
teardown,
|
|
98
|
+
getManualSetupInstructions,
|
|
99
|
+
getManualTeardownInstructions,
|
|
82
100
|
};
|