@aikidosec/safe-chain 0.0.1-custom-install-dir

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.
Files changed (116) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +537 -0
  3. package/bin/aikido-bun.js +14 -0
  4. package/bin/aikido-bunx.js +14 -0
  5. package/bin/aikido-npm.js +14 -0
  6. package/bin/aikido-npx.js +14 -0
  7. package/bin/aikido-pip.js +17 -0
  8. package/bin/aikido-pip3.js +17 -0
  9. package/bin/aikido-pipx.js +16 -0
  10. package/bin/aikido-pnpm.js +14 -0
  11. package/bin/aikido-pnpx.js +14 -0
  12. package/bin/aikido-poetry.js +13 -0
  13. package/bin/aikido-python.js +19 -0
  14. package/bin/aikido-python3.js +19 -0
  15. package/bin/aikido-uv.js +16 -0
  16. package/bin/aikido-uvx.js +16 -0
  17. package/bin/aikido-yarn.js +14 -0
  18. package/bin/safe-chain.js +147 -0
  19. package/docs/Release.md +25 -0
  20. package/docs/banner.svg +151 -0
  21. package/docs/safe-package-manager-demo.gif +0 -0
  22. package/docs/safe-package-manager-demo.png +0 -0
  23. package/docs/shell-integration.md +149 -0
  24. package/docs/troubleshooting.md +321 -0
  25. package/npm-shrinkwrap.json +3180 -0
  26. package/package.json +71 -0
  27. package/src/api/aikido.js +187 -0
  28. package/src/api/npmApi.js +71 -0
  29. package/src/config/cliArguments.js +161 -0
  30. package/src/config/configFile.js +327 -0
  31. package/src/config/environmentVariables.js +57 -0
  32. package/src/config/safeChainDir.js +71 -0
  33. package/src/config/settings.js +247 -0
  34. package/src/environment/environment.js +14 -0
  35. package/src/environment/userInteraction.js +122 -0
  36. package/src/installLocation.js +42 -0
  37. package/src/main.js +123 -0
  38. package/src/packagemanager/_shared/commandErrors.js +17 -0
  39. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  40. package/src/packagemanager/bun/createBunPackageManager.js +48 -0
  41. package/src/packagemanager/currentPackageManager.js +82 -0
  42. package/src/packagemanager/npm/createPackageManager.js +72 -0
  43. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  44. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  45. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  46. package/src/packagemanager/npm/runNpmCommand.js +20 -0
  47. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  48. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  49. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  50. package/src/packagemanager/npx/createPackageManager.js +15 -0
  51. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  52. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  53. package/src/packagemanager/npx/runNpxCommand.js +20 -0
  54. package/src/packagemanager/pip/createPackageManager.js +25 -0
  55. package/src/packagemanager/pip/pipSettings.js +6 -0
  56. package/src/packagemanager/pip/runPipCommand.js +209 -0
  57. package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
  58. package/src/packagemanager/pipx/runPipXCommand.js +60 -0
  59. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  60. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  61. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  62. package/src/packagemanager/pnpm/runPnpmCommand.js +32 -0
  63. package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
  64. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  65. package/src/packagemanager/uv/runUvCommand.js +66 -0
  66. package/src/packagemanager/uvx/createUvxPackageManager.js +18 -0
  67. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  68. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  69. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  70. package/src/packagemanager/yarn/runYarnCommand.js +36 -0
  71. package/src/registryProxy/certBundle.js +203 -0
  72. package/src/registryProxy/certUtils.js +178 -0
  73. package/src/registryProxy/getConnectTimeout.js +13 -0
  74. package/src/registryProxy/http-utils.js +80 -0
  75. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  76. package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
  77. package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
  78. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
  79. package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
  80. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
  81. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
  82. package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
  83. package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
  84. package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
  85. package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
  86. package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
  87. package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
  88. package/src/registryProxy/isImdsEndpoint.js +13 -0
  89. package/src/registryProxy/mitmRequestHandler.js +240 -0
  90. package/src/registryProxy/plainHttpProxy.js +95 -0
  91. package/src/registryProxy/registryProxy.js +255 -0
  92. package/src/registryProxy/tunnelRequestHandler.js +213 -0
  93. package/src/scanning/audit/index.js +129 -0
  94. package/src/scanning/index.js +82 -0
  95. package/src/scanning/malwareDatabase.js +131 -0
  96. package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
  97. package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
  98. package/src/scanning/newPackagesListCache.js +126 -0
  99. package/src/scanning/packageNameVariants.js +29 -0
  100. package/src/shell-integration/helpers.js +296 -0
  101. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +37 -0
  102. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +25 -0
  103. package/src/shell-integration/setup-ci.js +152 -0
  104. package/src/shell-integration/setup.js +110 -0
  105. package/src/shell-integration/shellDetection.js +39 -0
  106. package/src/shell-integration/startup-scripts/init-fish.fish +122 -0
  107. package/src/shell-integration/startup-scripts/init-posix.sh +112 -0
  108. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +176 -0
  109. package/src/shell-integration/supported-shells/bash.js +222 -0
  110. package/src/shell-integration/supported-shells/fish.js +97 -0
  111. package/src/shell-integration/supported-shells/powershell.js +102 -0
  112. package/src/shell-integration/supported-shells/windowsPowershell.js +102 -0
  113. package/src/shell-integration/supported-shells/zsh.js +94 -0
  114. package/src/shell-integration/teardown.js +114 -0
  115. package/src/utils/safeSpawn.js +153 -0
  116. package/tsconfig.json +21 -0
@@ -0,0 +1,240 @@
1
+ import https from "https";
2
+ import { generateCertForHost } from "./certUtils.js";
3
+ import { HttpsProxyAgent } from "https-proxy-agent";
4
+ import { ui } from "../environment/userInteraction.js";
5
+ import { gunzipSync } from "zlib";
6
+ import { omitHeaders } from "./http-utils.js";
7
+
8
+ /**
9
+ * @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor
10
+ */
11
+
12
+ /**
13
+ * @param {import("http").IncomingMessage} req
14
+ * @param {import("http").ServerResponse} clientSocket
15
+ * @param {Interceptor} interceptor
16
+ */
17
+ export function mitmConnect(req, clientSocket, interceptor) {
18
+ ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
19
+ const { hostname, port } = new URL(`http://${req.url}`);
20
+
21
+ clientSocket.on("error", (err) => {
22
+ ui.writeVerbose(
23
+ `Safe-chain: Client socket error for ${req.url}: ${err.message}`
24
+ );
25
+ // NO-OP
26
+ // This can happen if the client TCP socket sends RST instead of FIN.
27
+ // Not subscribing to 'close' event will cause node to throw and crash.
28
+ });
29
+
30
+ const server = createHttpsServer(hostname, port, interceptor);
31
+
32
+ server.on("error", (err) => {
33
+ ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
34
+ if (!clientSocket.headersSent) {
35
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
36
+ } else if (clientSocket.writable) {
37
+ clientSocket.end();
38
+ }
39
+ });
40
+
41
+ // Establish the connection
42
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
43
+
44
+ // Hand off the socket to the HTTPS server
45
+ server.emit("connection", clientSocket);
46
+ }
47
+
48
+ /**
49
+ * @param {string} hostname
50
+ * @param {string} port
51
+ * @param {Interceptor} interceptor
52
+ * @returns {import("https").Server}
53
+ */
54
+ function createHttpsServer(hostname, port, interceptor) {
55
+ const cert = generateCertForHost(hostname);
56
+
57
+ /**
58
+ * @param {import("http").IncomingMessage} req
59
+ * @param {import("http").ServerResponse} res
60
+ *
61
+ * @returns {Promise<void>}
62
+ */
63
+ async function handleRequest(req, res) {
64
+ if (!req.url) {
65
+ ui.writeError("Safe-chain: Request missing URL");
66
+ res.writeHead(400, "Bad Request");
67
+ res.end("Bad Request: Missing URL");
68
+ return;
69
+ }
70
+
71
+ const pathAndQuery = getRequestPathAndQuery(req.url);
72
+ const targetUrl = `https://${hostname}${pathAndQuery}`;
73
+
74
+ const requestInterceptor = await interceptor.handleRequest(targetUrl);
75
+ const blockResponse = requestInterceptor.blockResponse;
76
+
77
+ if (blockResponse) {
78
+ ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
79
+ res.writeHead(blockResponse.statusCode, blockResponse.message);
80
+ res.end(blockResponse.message);
81
+ return;
82
+ }
83
+
84
+ // Collect request body
85
+ forwardRequest(req, hostname, port, res, requestInterceptor);
86
+ }
87
+
88
+ const server = https.createServer(
89
+ {
90
+ key: cert.privateKey,
91
+ cert: cert.certificate,
92
+ },
93
+ handleRequest
94
+ );
95
+
96
+ return server;
97
+ }
98
+
99
+ /**
100
+ * @param {string} url
101
+ * @returns {string}
102
+ */
103
+ function getRequestPathAndQuery(url) {
104
+ if (url.startsWith("http://") || url.startsWith("https://")) {
105
+ const parsedUrl = new URL(url);
106
+ return parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
107
+ }
108
+ return url;
109
+ }
110
+
111
+ /**
112
+ * @param {import("http").IncomingMessage} req
113
+ * @param {string} hostname
114
+ * @param {string} port
115
+ * @param {import("http").ServerResponse} res
116
+ * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
117
+ */
118
+ function forwardRequest(req, hostname, port, res, requestHandler) {
119
+ const proxyReq = createProxyRequest(hostname, port, req, res, requestHandler);
120
+
121
+ proxyReq.on("error", (err) => {
122
+ ui.writeVerbose(
123
+ `Safe-chain: Error occurred while proxying request to ${req.url} for ${hostname}: ${err.message}`
124
+ );
125
+ res.writeHead(502);
126
+ res.end("Bad Gateway");
127
+ });
128
+
129
+ req.on("error", (err) => {
130
+ ui.writeError(
131
+ `Safe-chain: Error reading client request to ${req.url} for ${hostname}: ${err.message}`
132
+ );
133
+ proxyReq.destroy();
134
+ });
135
+
136
+ req.on("data", (chunk) => {
137
+ proxyReq.write(chunk);
138
+ });
139
+
140
+ req.on("end", () => {
141
+ ui.writeVerbose(
142
+ `Safe-chain: Finished proxying request to ${req.url} for ${hostname}`
143
+ );
144
+ proxyReq.end();
145
+ });
146
+ }
147
+
148
+ /**
149
+ * @param {string} hostname
150
+ * @param {string} port
151
+ * @param {import("http").IncomingMessage} req
152
+ * @param {import("http").ServerResponse} res
153
+ * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
154
+ *
155
+ * @returns {import("http").ClientRequest}
156
+ */
157
+ function createProxyRequest(hostname, port, req, res, requestHandler) {
158
+ /** @type {NodeJS.Dict<string | string[]> | undefined} */
159
+ let headers = { ...req.headers };
160
+ // Remove the host header from the incoming request before forwarding.
161
+ // Node's http module sets the correct host header for the target hostname automatically.
162
+ if (headers.host) {
163
+ delete headers.host;
164
+ }
165
+ headers = requestHandler.modifyRequestHeaders(headers);
166
+
167
+ /** @type {import("http").RequestOptions} */
168
+ const options = {
169
+ hostname: hostname,
170
+ port: port || 443,
171
+ path: req.url,
172
+ method: req.method,
173
+ headers: { ...headers },
174
+ };
175
+
176
+ const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
177
+ if (httpsProxy) {
178
+ options.agent = new HttpsProxyAgent(httpsProxy);
179
+ }
180
+
181
+ const proxyReq = https.request(options, (proxyRes) => {
182
+ proxyRes.on("error", (err) => {
183
+ ui.writeError(
184
+ `Safe-chain: Error reading upstream response to ${req.url} for ${hostname}: ${err.message}`
185
+ );
186
+ if (!res.headersSent) {
187
+ res.writeHead(502);
188
+ res.end("Bad Gateway");
189
+ }
190
+ });
191
+
192
+ if (!proxyRes.statusCode) {
193
+ ui.writeError(
194
+ `Safe-chain: Proxy response missing status code to ${req.url} for ${hostname}`
195
+ );
196
+ res.writeHead(500);
197
+ res.end("Internal Server Error");
198
+ return;
199
+ }
200
+
201
+ const { statusCode, headers } = proxyRes;
202
+
203
+ if (requestHandler.modifiesResponse()) {
204
+ /** @type {Array<any>} */
205
+ let chunks = [];
206
+
207
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
208
+
209
+ proxyRes.on("end", () => {
210
+ /** @type {Buffer} */
211
+ let buffer = Buffer.concat(chunks);
212
+
213
+ if (proxyRes.headers["content-encoding"] === "gzip") {
214
+ buffer = gunzipSync(buffer);
215
+ }
216
+
217
+ buffer = requestHandler.modifyBody(buffer, headers);
218
+
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);
229
+ res.end(buffer);
230
+ });
231
+ } else {
232
+ // If the response is not being modified, we can
233
+ // just pipe without the need for buffering the output
234
+ res.writeHead(statusCode, headers);
235
+ proxyRes.pipe(res);
236
+ }
237
+ });
238
+
239
+ return proxyReq;
240
+ }
@@ -0,0 +1,95 @@
1
+ import * as http from "http";
2
+ import * as https from "https";
3
+ import { ui } from "../environment/userInteraction.js";
4
+
5
+ /**
6
+ * @param {import("http").IncomingMessage} req
7
+ * @param {import("http").ServerResponse} res
8
+ *
9
+ * @returns {void}
10
+ */
11
+ export function handleHttpProxyRequest(req, res) {
12
+ if (!req.url) {
13
+ ui.writeError("Safe-chain: Request missing URL");
14
+ res.writeHead(400, "Bad Request");
15
+ res.end("Bad Request: Missing URL");
16
+ return;
17
+ }
18
+
19
+ const url = new URL(req.url);
20
+
21
+ // The protocol for the plainHttpProxy should usually only be http:
22
+ // but when the client for some reason sends an https: request directly
23
+ // instead of using the CONNECT method, we should handle it gracefully.
24
+ let protocol;
25
+ if (url.protocol === "http:") {
26
+ protocol = http;
27
+ } else if (url.protocol === "https:") {
28
+ protocol = https;
29
+ } else {
30
+ res.writeHead(502);
31
+ res.end(`Bad Gateway: Unsupported protocol ${url.protocol}`);
32
+ return;
33
+ }
34
+
35
+ const proxyRequest = protocol
36
+ .request(
37
+ req.url,
38
+ { method: req.method, headers: req.headers },
39
+ (proxyRes) => {
40
+ if (!proxyRes.statusCode) {
41
+ ui.writeError("Safe-chain: Proxy response missing status code");
42
+ res.writeHead(500);
43
+ res.end("Internal Server Error");
44
+ return;
45
+ }
46
+
47
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
48
+ proxyRes.pipe(res);
49
+
50
+ proxyRes.on("error", () => {
51
+ // Proxy response stream error
52
+ // Clean up client response stream
53
+ if (res.writable) {
54
+ res.end();
55
+ }
56
+ });
57
+
58
+ proxyRes.on("close", () => {
59
+ // Clean up if the proxy response stream closes
60
+ if (res.writable) {
61
+ res.end();
62
+ }
63
+ });
64
+ }
65
+ )
66
+ .on("error", (err) => {
67
+ if (!res.headersSent) {
68
+ res.writeHead(502);
69
+ res.end(`Bad Gateway: ${err.message}`);
70
+ } else {
71
+ // Headers already sent, just destroy the response
72
+ res.destroy();
73
+ }
74
+ });
75
+
76
+ req.on("error", () => {
77
+ // Client request stream error
78
+ // Abort the proxy request
79
+ proxyRequest.destroy();
80
+ });
81
+
82
+ res.on("error", () => {
83
+ // Client response stream error (client disconnected)
84
+ // Clean up proxy streams
85
+ proxyRequest.destroy();
86
+ });
87
+
88
+ res.on("close", () => {
89
+ // Client disconnected
90
+ // Abort the proxy request to avoid unnecessary work
91
+ proxyRequest.destroy();
92
+ });
93
+
94
+ req.pipe(proxyRequest);
95
+ }
@@ -0,0 +1,255 @@
1
+ import * as http from "http";
2
+ import { tunnelRequest } from "./tunnelRequestHandler.js";
3
+ import { mitmConnect } from "./mitmRequestHandler.js";
4
+ import { handleHttpProxyRequest } from "./plainHttpProxy.js";
5
+ import { getCombinedCaBundlePath, cleanupCertBundle } from "./certBundle.js";
6
+ import { ui } from "../environment/userInteraction.js";
7
+ import chalk from "chalk";
8
+ import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
9
+ import { getHasSuppressedVersions } from "./interceptors/suppressedVersionsState.js";
10
+
11
+ const SERVER_STOP_TIMEOUT_MS = 1000;
12
+ /**
13
+ * @type {{
14
+ * port: number | null,
15
+ * blockedRequests: {packageName: string, version: string, url: string}[],
16
+ * blockedMinimumAgeRequests: {packageName: string, version: string, url: string}[]
17
+ * }}
18
+ */
19
+ const state = {
20
+ port: null,
21
+ blockedRequests: [],
22
+ blockedMinimumAgeRequests: [],
23
+ };
24
+
25
+ export function createSafeChainProxy() {
26
+ const server = createProxyServer();
27
+
28
+ return {
29
+ startServer: () => startServer(server),
30
+ stopServer: () => stopServer(server),
31
+ hasBlockedMaliciousPackages,
32
+ hasBlockedMinimumAgeRequests,
33
+ hasSuppressedVersions: getHasSuppressedVersions,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @returns {Record<string, string>}
39
+ */
40
+ function getSafeChainProxyEnvironmentVariables() {
41
+ if (!state.port) {
42
+ return {};
43
+ }
44
+
45
+ const proxyUrl = `http://localhost:${state.port}`;
46
+ const caCertPath = getCombinedCaBundlePath();
47
+
48
+ return {
49
+ HTTPS_PROXY: proxyUrl,
50
+ GLOBAL_AGENT_HTTP_PROXY: proxyUrl,
51
+ NODE_EXTRA_CA_CERTS: caCertPath,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * @param {Record<string, string | undefined>} env
57
+ *
58
+ * @returns {Record<string, string>}
59
+ */
60
+ export function mergeSafeChainProxyEnvironmentVariables(env) {
61
+ const proxyEnv = getSafeChainProxyEnvironmentVariables();
62
+
63
+ for (const key of Object.keys(env)) {
64
+ // If we were to simply copy all env variables, we might overwrite
65
+ // the proxy settings set by safe-chain when casing varies (e.g. http_proxy vs HTTP_PROXY)
66
+ // So we only copy the variable if it's not already set in a different case
67
+ const upperKey = key.toUpperCase();
68
+
69
+ if (!proxyEnv[upperKey] && env[key]) {
70
+ proxyEnv[key] = env[key];
71
+ }
72
+ }
73
+
74
+ return proxyEnv;
75
+ }
76
+
77
+ function createProxyServer() {
78
+ const server = http.createServer(
79
+ // This handles direct HTTP requests (non-CONNECT requests)
80
+ // This is normally http-only traffic, but we also handle
81
+ // https for clients that don't properly use CONNECT
82
+ handleHttpProxyRequest
83
+ );
84
+
85
+ // This handles HTTPS requests via the CONNECT method
86
+ server.on("connect", handleConnect);
87
+
88
+ return server;
89
+ }
90
+
91
+ /**
92
+ * @param {import("http").Server} server
93
+ *
94
+ * @returns {Promise<void>}
95
+ */
96
+ function startServer(server) {
97
+ return new Promise((resolve, reject) => {
98
+ // Passing port 0 makes the OS assign an available port
99
+ server.listen(0, () => {
100
+ const address = server.address();
101
+ if (address && typeof address === "object") {
102
+ state.port = address.port;
103
+ resolve();
104
+ } else {
105
+ reject(new Error("Failed to start proxy server"));
106
+ }
107
+ });
108
+
109
+ server.on("error", (err) => {
110
+ reject(err);
111
+ });
112
+ });
113
+ }
114
+
115
+ /**
116
+ * @param {import("http").Server} server
117
+ *
118
+ * @returns {Promise<void>}
119
+ */
120
+ function stopServer(server) {
121
+ return new Promise((resolve) => {
122
+ try {
123
+ server.close(() => {
124
+ cleanupCertBundle();
125
+ resolve();
126
+ });
127
+ } catch {
128
+ resolve();
129
+ }
130
+ setTimeout(() => {
131
+ cleanupCertBundle();
132
+ resolve();
133
+ }, SERVER_STOP_TIMEOUT_MS);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * @param {import("http").IncomingMessage} req
139
+ * @param {import("http").ServerResponse} clientSocket
140
+ * @param {Buffer} head
141
+ *
142
+ * @returns {void}
143
+ */
144
+ function handleConnect(req, clientSocket, head) {
145
+ // CONNECT method is used for HTTPS requests
146
+ // It establishes a tunnel to the server identified by the request URL
147
+
148
+ const interceptor = createInterceptorForUrl(req.url || "");
149
+
150
+ if (interceptor) {
151
+ // Subscribe to malware blocked events
152
+ interceptor.on(
153
+ "malwareBlocked",
154
+ (
155
+ /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event
156
+ ) => {
157
+ onMalwareBlocked(event.packageName, event.version, event.targetUrl);
158
+ }
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
+ );
172
+
173
+ mitmConnect(req, clientSocket, interceptor);
174
+ } else {
175
+ // For other hosts, just tunnel the request to the destination tcp socket
176
+ ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
177
+ tunnelRequest(req, clientSocket, head);
178
+ }
179
+ }
180
+
181
+ /**
182
+ *
183
+ * @param {string} packageName
184
+ * @param {string} version
185
+ * @param {string} url
186
+ */
187
+ function onMalwareBlocked(packageName, version, url) {
188
+ state.blockedRequests.push({ packageName, version, url });
189
+ }
190
+
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() {
202
+ if (state.blockedRequests.length === 0) {
203
+ return false;
204
+ }
205
+
206
+ ui.emptyLine();
207
+
208
+ ui.writeInformation(
209
+ `Safe-chain: ${chalk.bold(
210
+ `blocked ${state.blockedRequests.length} malicious package downloads`
211
+ )}:`
212
+ );
213
+
214
+ for (const req of state.blockedRequests) {
215
+ ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`);
216
+ }
217
+
218
+ ui.emptyLine();
219
+ ui.writeExitWithoutInstallingMaliciousPackages();
220
+ ui.emptyLine();
221
+
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;
255
+ }