@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.
- package/LICENSE +674 -0
- package/README.md +537 -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-uvx.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +147 -0
- package/docs/Release.md +25 -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 +3180 -0
- package/package.json +71 -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/safeChainDir.js +71 -0
- package/src/config/settings.js +247 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/installLocation.js +42 -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 +82 -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/uvx/createUvxPackageManager.js +18 -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 +296 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +37 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +25 -0
- package/src/shell-integration/setup-ci.js +152 -0
- package/src/shell-integration/setup.js +110 -0
- package/src/shell-integration/shellDetection.js +39 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +122 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +112 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +176 -0
- package/src/shell-integration/supported-shells/bash.js +222 -0
- package/src/shell-integration/supported-shells/fish.js +97 -0
- package/src/shell-integration/supported-shells/powershell.js +102 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +102 -0
- package/src/shell-integration/supported-shells/zsh.js +94 -0
- package/src/shell-integration/teardown.js +114 -0
- package/src/utils/safeSpawn.js +153 -0
- 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
|
+
}
|