@aikidosec/safe-chain 1.2.1 → 1.2.4
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 +20 -15
- package/bin/aikido-pip.js +2 -5
- package/bin/aikido-pip3.js +2 -6
- package/bin/aikido-poetry.js +13 -0
- package/bin/aikido-python.js +5 -16
- package/bin/aikido-python3.js +5 -16
- package/bin/safe-chain.js +7 -60
- package/package.json +2 -1
- package/src/config/configFile.js +1 -1
- package/src/config/settings.js +1 -1
- package/src/main.js +4 -2
- package/src/packagemanager/currentPackageManager.js +6 -2
- package/src/packagemanager/pip/createPackageManager.js +9 -5
- package/src/packagemanager/pip/pipSettings.js +4 -28
- package/src/packagemanager/pip/runPipCommand.js +41 -2
- package/src/packagemanager/poetry/createPoetryPackageManager.js +77 -0
- package/src/registryProxy/certBundle.js +99 -13
- package/src/registryProxy/certUtils.js +55 -5
- package/src/registryProxy/getConnectTimeout.js +13 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +6 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +23 -9
- package/src/registryProxy/isImdsEndpoint.js +13 -0
- package/src/registryProxy/registryProxy.js +15 -7
- package/src/registryProxy/tunnelRequestHandler.js +55 -3
- package/src/shell-integration/helpers.js +20 -0
- package/src/shell-integration/setup-ci.js +2 -2
- package/src/shell-integration/setup.js +3 -4
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +4 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +4 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +5 -1
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +1 -1
- package/src/shell-integration/teardown.js +43 -1
|
@@ -6,6 +6,7 @@ import certifi from "certifi";
|
|
|
6
6
|
import tls from "node:tls";
|
|
7
7
|
import { X509Certificate } from "node:crypto";
|
|
8
8
|
import { getCaCertPath } from "./certUtils.js";
|
|
9
|
+
import { ui } from "../environment/userInteraction.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Check if a PEM string contains only parsable cert blocks.
|
|
@@ -14,6 +15,7 @@ import { getCaCertPath } from "./certUtils.js";
|
|
|
14
15
|
*/
|
|
15
16
|
function isParsable(pem) {
|
|
16
17
|
if (!pem || typeof pem !== "string") return false;
|
|
18
|
+
pem = normalizeLineEndings(pem);
|
|
17
19
|
const begin = "-----BEGIN CERTIFICATE-----";
|
|
18
20
|
const end = "-----END CERTIFICATE-----";
|
|
19
21
|
const blocks = [];
|
|
@@ -41,20 +43,17 @@ function isParsable(pem) {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
/** @type {string | null} */
|
|
45
|
-
let cachedPath = null;
|
|
46
|
-
|
|
47
46
|
/**
|
|
48
|
-
* Build a combined CA bundle
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
* -
|
|
47
|
+
* Build a combined CA bundle.
|
|
48
|
+
* Automatically includes:
|
|
49
|
+
* - Safe Chain CA (for MITM of known registries)
|
|
50
|
+
* - Mozilla roots via certifi (for public HTTPS)
|
|
51
|
+
* - Node's built-in root certificates (fallback)
|
|
52
|
+
* - User's custom certificates (if NODE_EXTRA_CA_CERTS environment variable is set)
|
|
53
|
+
*
|
|
52
54
|
* @returns {string} Path to the combined CA bundle PEM file
|
|
53
55
|
*/
|
|
54
56
|
export function getCombinedCaBundlePath() {
|
|
55
|
-
if (cachedPath && fs.existsSync(cachedPath)) return cachedPath;
|
|
56
|
-
|
|
57
|
-
// Concatenate PEM files
|
|
58
57
|
const parts = [];
|
|
59
58
|
|
|
60
59
|
// 1) Safe Chain CA (for MITM'd registries)
|
|
@@ -87,9 +86,96 @@ export function getCombinedCaBundlePath() {
|
|
|
87
86
|
// Ignore if unavailable
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
// 4) User's NODE_EXTRA_CA_CERTS (if set)
|
|
90
|
+
const userCertPath = process.env.NODE_EXTRA_CA_CERTS;
|
|
91
|
+
if (userCertPath) {
|
|
92
|
+
const userPem = readUserCertificateFile(userCertPath);
|
|
93
|
+
if (userPem) {
|
|
94
|
+
parts.push(userPem.trim());
|
|
95
|
+
ui.writeVerbose(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
|
|
96
|
+
} else {
|
|
97
|
+
ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
const combined = parts.filter(Boolean).join("\n");
|
|
91
|
-
const target = path.join(os.tmpdir(),
|
|
102
|
+
const target = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`);
|
|
92
103
|
fs.writeFileSync(target, combined, { encoding: "utf8" });
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
return target;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize path
|
|
109
|
+
* @param {string} p - Path to normalize
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function normalizePathF(p) {
|
|
113
|
+
return p.replace(/\\/g, "/");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalize line endings to LF
|
|
118
|
+
* @param {string} text - Text with mixed line endings
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
function normalizeLineEndings(text) {
|
|
122
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read and validate user certificate file
|
|
127
|
+
* @param {string} certPath - Path to certificate file
|
|
128
|
+
* @returns {string | null} Certificate PEM content or null if invalid/unreadable
|
|
129
|
+
*/
|
|
130
|
+
function readUserCertificateFile(certPath) {
|
|
131
|
+
try {
|
|
132
|
+
// 1) Basic validation
|
|
133
|
+
if (typeof certPath !== "string" || certPath.trim().length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2) Reject path traversal attempts (normalize backslashes first for Windows paths)
|
|
138
|
+
const normalizedPath = normalizePathF(certPath);
|
|
139
|
+
if (normalizedPath.includes("..")) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3) Check if file exists and is not a directory or symlink
|
|
144
|
+
let stats;
|
|
145
|
+
try {
|
|
146
|
+
stats = fs.lstatSync(certPath);
|
|
147
|
+
} catch {
|
|
148
|
+
// File doesn't exist or can't be accessed
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!stats.isFile()) {
|
|
153
|
+
// Reject directories and symlinks
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4) Read file content
|
|
158
|
+
let content;
|
|
159
|
+
try {
|
|
160
|
+
content = fs.readFileSync(certPath, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!content || typeof content !== "string") {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 5) Validate PEM format
|
|
170
|
+
if (!isParsable(content)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return content;
|
|
175
|
+
} catch {
|
|
176
|
+
// Silently fail on any errors
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
95
179
|
}
|
|
180
|
+
|
|
181
|
+
|
|
@@ -8,6 +8,17 @@ const ca = loadCa();
|
|
|
8
8
|
|
|
9
9
|
const certCache = new Map();
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @param {forge.pki.PublicKey} publicKey
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function createKeyIdentifier(publicKey) {
|
|
16
|
+
return forge.pki.getPublicKeyFingerprint(publicKey, {
|
|
17
|
+
encoding: "binary",
|
|
18
|
+
md: forge.md.sha1.create(),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export function getCaCertPath() {
|
|
12
23
|
return path.join(certFolder, "ca-cert.pem");
|
|
13
24
|
}
|
|
@@ -33,6 +44,7 @@ export function generateCertForHost(hostname) {
|
|
|
33
44
|
const attrs = [{ name: "commonName", value: hostname }];
|
|
34
45
|
cert.setSubject(attrs);
|
|
35
46
|
cert.setIssuer(ca.certificate.subject.attributes);
|
|
47
|
+
const authorityKeyIdentifier = createKeyIdentifier(ca.certificate.publicKey);
|
|
36
48
|
cert.setExtensions([
|
|
37
49
|
{
|
|
38
50
|
name: "subjectAltName",
|
|
@@ -50,14 +62,42 @@ export function generateCertForHost(hostname) {
|
|
|
50
62
|
},
|
|
51
63
|
{
|
|
52
64
|
/*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
Extended Key Usage (EKU) serverAuth extension
|
|
66
|
+
|
|
67
|
+
Needed for TLS server authentication. This extension indicates the certificate's
|
|
68
|
+
public key may be used for TLS WWW server authentication.
|
|
69
|
+
Python virtualenv environments (like pipx-installed Poetry) enforce this strictly
|
|
70
|
+
https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12
|
|
57
71
|
*/
|
|
58
72
|
name: "extKeyUsage",
|
|
59
73
|
serverAuth: true,
|
|
60
74
|
},
|
|
75
|
+
{
|
|
76
|
+
/*
|
|
77
|
+
Subject Key Identifier (SKI)
|
|
78
|
+
|
|
79
|
+
Needed for Python virtualenv SSL validation and certificate chain building.
|
|
80
|
+
This extension provides a means of identifying certificates containing a particular public key.
|
|
81
|
+
Python virtualenv environments require this for proper certificate chain validation.
|
|
82
|
+
System Python installations may be more lenient.
|
|
83
|
+
https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2
|
|
84
|
+
*/
|
|
85
|
+
name: "subjectKeyIdentifier",
|
|
86
|
+
subjectKeyIdentifier: createKeyIdentifier(cert.publicKey),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
/*
|
|
90
|
+
Authority Key Identifier (AKI)
|
|
91
|
+
|
|
92
|
+
Needed for Python virtualenv SSL validation and certificate path validation.
|
|
93
|
+
This extension identifies the public key corresponding to the private key used to sign
|
|
94
|
+
this certificate. It links this certificate to its issuing CA certificate.
|
|
95
|
+
Without this, Python virtualenv certificate validation might fail (for instance for Poetry)
|
|
96
|
+
https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1
|
|
97
|
+
*/
|
|
98
|
+
name: "authorityKeyIdentifier",
|
|
99
|
+
keyIdentifier: authorityKeyIdentifier,
|
|
100
|
+
},
|
|
61
101
|
]);
|
|
62
102
|
cert.sign(ca.privateKey, forge.md.sha256.create());
|
|
63
103
|
|
|
@@ -106,11 +146,13 @@ function generateCa() {
|
|
|
106
146
|
|
|
107
147
|
const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
|
|
108
148
|
cert.setSubject(attrs);
|
|
109
|
-
cert.setIssuer(attrs);
|
|
149
|
+
cert.setIssuer(attrs); // Self-signed: issuer === subject
|
|
150
|
+
const keyIdentifier = createKeyIdentifier(cert.publicKey);
|
|
110
151
|
cert.setExtensions([
|
|
111
152
|
{
|
|
112
153
|
name: "basicConstraints",
|
|
113
154
|
cA: true,
|
|
155
|
+
critical: true, // Marking basicConstraints as critical is required for CA certificates so clients must process it to trust the cert as a CA
|
|
114
156
|
},
|
|
115
157
|
{
|
|
116
158
|
name: "keyUsage",
|
|
@@ -118,6 +160,14 @@ function generateCa() {
|
|
|
118
160
|
digitalSignature: true,
|
|
119
161
|
keyEncipherment: true,
|
|
120
162
|
},
|
|
163
|
+
{
|
|
164
|
+
name: "subjectKeyIdentifier",
|
|
165
|
+
subjectKeyIdentifier: keyIdentifier,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "authorityKeyIdentifier",
|
|
169
|
+
keyIdentifier,
|
|
170
|
+
},
|
|
121
171
|
]);
|
|
122
172
|
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
123
173
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isImdsEndpoint } from "./isImdsEndpoint.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns appropriate connection timeout for a host.
|
|
5
|
+
* - IMDS endpoints: 3s (fail fast when outside cloud, reduce 5min delay to ~20s)
|
|
6
|
+
* - Other endpoints: 30s (allow for slow networks while preventing indefinite hangs)
|
|
7
|
+
*/
|
|
8
|
+
export function getConnectTimeout(/** @type {string} */ host) {
|
|
9
|
+
if (isImdsEndpoint(host)) {
|
|
10
|
+
return 3000;
|
|
11
|
+
}
|
|
12
|
+
return 30000;
|
|
13
|
+
}
|
|
@@ -20,6 +20,12 @@ import { EventEmitter } from "events";
|
|
|
20
20
|
* @property {(headers: NodeJS.Dict<string | string[]> | undefined) => NodeJS.Dict<string | string[]> | undefined} modifyRequestHeaders
|
|
21
21
|
* @property {() => boolean} modifiesResponse
|
|
22
22
|
* @property {(body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer} modifyBody
|
|
23
|
+
*
|
|
24
|
+
* @typedef {Object} MalwareBlockedEvent
|
|
25
|
+
* @property {string} packageName
|
|
26
|
+
* @property {string} version
|
|
27
|
+
* @property {string} targetUrl
|
|
28
|
+
* @property {number} timestamp
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
31
|
/**
|
|
@@ -32,7 +32,16 @@ function buildPipInterceptor(registry) {
|
|
|
32
32
|
reqContext.targetUrl,
|
|
33
33
|
registry
|
|
34
34
|
);
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
// Normalize underscores to hyphens for DB matching, as PyPI allows underscores in distribution names.
|
|
37
|
+
// Per python, packages that differ only by hyphen vs underscore are considered the same.
|
|
38
|
+
const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName;
|
|
39
|
+
|
|
40
|
+
const isMalicious =
|
|
41
|
+
await isMalwarePackage(packageName, version)
|
|
42
|
+
|| await isMalwarePackage(hyphenName, version);
|
|
43
|
+
|
|
44
|
+
if (isMalicious) {
|
|
36
45
|
reqContext.blockMalware(packageName, version);
|
|
37
46
|
}
|
|
38
47
|
});
|
|
@@ -71,16 +80,21 @@ function parsePipPackageFromUrl(url, registry) {
|
|
|
71
80
|
// Example wheel: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1-py3-none-any.whl
|
|
72
81
|
// Example sdist: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1.tar.gz
|
|
73
82
|
|
|
74
|
-
// Wheel (.whl)
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
// Wheel (.whl) and Poetry's preflight metadata (.whl.metadata)
|
|
84
|
+
// Examples:
|
|
85
|
+
// foo_bar-2.0.0-py3-none-any.whl
|
|
86
|
+
// foo_bar-2.0.0-py3-none-any.whl.metadata
|
|
87
|
+
const wheelExtRe = /\.whl(?:\.metadata)?$/;
|
|
88
|
+
const wheelExtMatch = filename.match(wheelExtRe);
|
|
89
|
+
if (wheelExtMatch) {
|
|
90
|
+
const base = filename.replace(wheelExtRe, "");
|
|
77
91
|
const firstDash = base.indexOf("-");
|
|
78
92
|
if (firstDash > 0) {
|
|
79
93
|
const dist = base.slice(0, firstDash); // may contain underscores
|
|
80
94
|
const rest = base.slice(firstDash + 1); // version + the rest of tags
|
|
81
95
|
const secondDash = rest.indexOf("-");
|
|
82
96
|
const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest;
|
|
83
|
-
packageName = dist;
|
|
97
|
+
packageName = dist;
|
|
84
98
|
version = rawVersion;
|
|
85
99
|
// Reject "latest" as it's a placeholder, not a real version
|
|
86
100
|
// When version is "latest", this signals the URL doesn't contain actual version info
|
|
@@ -92,10 +106,11 @@ function parsePipPackageFromUrl(url, registry) {
|
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
// Source dist (sdist)
|
|
96
|
-
const
|
|
109
|
+
// Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata)
|
|
110
|
+
const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
|
|
111
|
+
const sdistExtMatch = filename.match(sdistExtWithMetadataRe);
|
|
97
112
|
if (sdistExtMatch) {
|
|
98
|
-
const base = filename.
|
|
113
|
+
const base = filename.replace(sdistExtWithMetadataRe, "");
|
|
99
114
|
const lastDash = base.lastIndexOf("-");
|
|
100
115
|
if (lastDash > 0 && lastDash < base.length - 1) {
|
|
101
116
|
packageName = base.slice(0, lastDash);
|
|
@@ -109,7 +124,6 @@ function parsePipPackageFromUrl(url, registry) {
|
|
|
109
124
|
return { packageName, version };
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
|
-
|
|
113
127
|
// Unknown file type or invalid
|
|
114
128
|
return { packageName: undefined, version: undefined };
|
|
115
129
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Instance Metadata Service (IMDS) endpoints used by cloud providers.
|
|
2
|
+
// Cloud SDK tools probe these to detect environment and retrieve credentials.
|
|
3
|
+
// When outside cloud environments, connections timeout - we reduce timeout (3s vs 30s)
|
|
4
|
+
// and suppress error logging since this is expected behavior.
|
|
5
|
+
const imdsEndpoints = [
|
|
6
|
+
"metadata.google.internal",
|
|
7
|
+
"metadata.goog",
|
|
8
|
+
"169.254.169.254", // AWS, Azure, Oracle Cloud, GCP
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function isImdsEndpoint(/** @type {string} */ host) {
|
|
12
|
+
return imdsEndpoints.includes(host);
|
|
13
|
+
}
|
|
@@ -2,7 +2,7 @@ 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 {
|
|
5
|
+
import { getCombinedCaBundlePath } 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";
|
|
@@ -36,10 +36,13 @@ function getSafeChainProxyEnvironmentVariables() {
|
|
|
36
36
|
return {};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const proxyUrl = `http://localhost:${state.port}`;
|
|
40
|
+
const caCertPath = getCombinedCaBundlePath();
|
|
41
|
+
|
|
39
42
|
return {
|
|
40
|
-
HTTPS_PROXY:
|
|
41
|
-
GLOBAL_AGENT_HTTP_PROXY:
|
|
42
|
-
NODE_EXTRA_CA_CERTS:
|
|
43
|
+
HTTPS_PROXY: proxyUrl,
|
|
44
|
+
GLOBAL_AGENT_HTTP_PROXY: proxyUrl,
|
|
45
|
+
NODE_EXTRA_CA_CERTS: caCertPath,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -136,9 +139,14 @@ function handleConnect(req, clientSocket, head) {
|
|
|
136
139
|
|
|
137
140
|
if (interceptor) {
|
|
138
141
|
// Subscribe to malware blocked events
|
|
139
|
-
interceptor.on(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
interceptor.on(
|
|
143
|
+
"malwareBlocked",
|
|
144
|
+
(
|
|
145
|
+
/** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event
|
|
146
|
+
) => {
|
|
147
|
+
onMalwareBlocked(event.packageName, event.version, event.targetUrl);
|
|
148
|
+
}
|
|
149
|
+
);
|
|
142
150
|
|
|
143
151
|
mitmConnect(req, clientSocket, interceptor);
|
|
144
152
|
} else {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as net from "net";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
|
+
import { isImdsEndpoint } from "./isImdsEndpoint.js";
|
|
4
|
+
import { getConnectTimeout } from "./getConnectTimeout.js";
|
|
5
|
+
|
|
6
|
+
/** @type {string[]} */
|
|
7
|
+
let timedoutImdsEndpoints = [];
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* @param {import("http").IncomingMessage} req
|
|
@@ -37,6 +42,21 @@ export function tunnelRequest(req, clientSocket, head) {
|
|
|
37
42
|
*/
|
|
38
43
|
function tunnelRequestToDestination(req, clientSocket, head) {
|
|
39
44
|
const { port, hostname } = new URL(`http://${req.url}`);
|
|
45
|
+
const isImds = isImdsEndpoint(hostname);
|
|
46
|
+
|
|
47
|
+
if (timedoutImdsEndpoints.includes(hostname)) {
|
|
48
|
+
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
49
|
+
if (isImds) {
|
|
50
|
+
ui.writeVerbose(
|
|
51
|
+
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
ui.writeError(
|
|
55
|
+
`Safe-chain: Closing connection because previously timedout connect to ${hostname}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
40
60
|
|
|
41
61
|
const serverSocket = net.connect(
|
|
42
62
|
Number.parseInt(port) || 443,
|
|
@@ -49,6 +69,31 @@ function tunnelRequestToDestination(req, clientSocket, head) {
|
|
|
49
69
|
}
|
|
50
70
|
);
|
|
51
71
|
|
|
72
|
+
// Set explicit connection timeout to avoid waiting for OS default (~2 minutes).
|
|
73
|
+
// IMDS endpoints get shorter timeout (3s) since they're commonly unreachable outside cloud environments.
|
|
74
|
+
const connectTimeout = getConnectTimeout(hostname);
|
|
75
|
+
serverSocket.setTimeout(connectTimeout);
|
|
76
|
+
|
|
77
|
+
serverSocket.on("timeout", () => {
|
|
78
|
+
// Suppress error logging for IMDS endpoints - timeouts are expected when not in cloud
|
|
79
|
+
if (isImds) {
|
|
80
|
+
timedoutImdsEndpoints.push(hostname);
|
|
81
|
+
ui.writeVerbose(
|
|
82
|
+
`Safe-chain: connect to ${hostname}:${
|
|
83
|
+
port || 443
|
|
84
|
+
} timed out after ${connectTimeout}ms`
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
ui.writeError(
|
|
88
|
+
`Safe-chain: connect to ${hostname}:${
|
|
89
|
+
port || 443
|
|
90
|
+
} timed out after ${connectTimeout}ms`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
serverSocket.destroy(); // Clean up socket to prevent event loop hanging
|
|
94
|
+
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
95
|
+
});
|
|
96
|
+
|
|
52
97
|
clientSocket.on("error", () => {
|
|
53
98
|
// This can happen if the client TCP socket sends RST instead of FIN.
|
|
54
99
|
// Not subscribing to 'error' event will cause node to throw and crash.
|
|
@@ -58,9 +103,15 @@ function tunnelRequestToDestination(req, clientSocket, head) {
|
|
|
58
103
|
});
|
|
59
104
|
|
|
60
105
|
serverSocket.on("error", (err) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
106
|
+
if (isImds) {
|
|
107
|
+
ui.writeVerbose(
|
|
108
|
+
`Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
ui.writeError(
|
|
112
|
+
`Safe-chain: error connecting to ${hostname}:${port} - ${err.message}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
64
115
|
if (clientSocket.writable) {
|
|
65
116
|
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
66
117
|
}
|
|
@@ -145,3 +196,4 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
|
|
|
145
196
|
}
|
|
146
197
|
});
|
|
147
198
|
}
|
|
199
|
+
|
|
@@ -76,6 +76,12 @@ export const knownAikidoTools = [
|
|
|
76
76
|
ecoSystem: ECOSYSTEM_PY,
|
|
77
77
|
internalPackageManagerName: "pip",
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
tool: "poetry",
|
|
81
|
+
aikidoCommand: "aikido-poetry",
|
|
82
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
83
|
+
internalPackageManagerName: "poetry",
|
|
84
|
+
},
|
|
79
85
|
{
|
|
80
86
|
tool: "python",
|
|
81
87
|
aikidoCommand: "aikido-python",
|
|
@@ -107,6 +113,20 @@ export function getPackageManagerList() {
|
|
|
107
113
|
return `${tools.join(", ")}, and ${lastTool} commands`;
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function getShimsDir() {
|
|
120
|
+
return path.join(os.homedir(), ".safe-chain", "shims");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
export function getScriptsDir() {
|
|
127
|
+
return path.join(os.homedir(), ".safe-chain", "scripts");
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
/**
|
|
111
131
|
* @param {string} executableName
|
|
112
132
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
|
-
import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
|
|
3
|
+
import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import os from "os";
|
|
6
6
|
import path from "path";
|
|
@@ -32,7 +32,7 @@ export async function setupCi() {
|
|
|
32
32
|
);
|
|
33
33
|
ui.emptyLine();
|
|
34
34
|
|
|
35
|
-
const shimsDir =
|
|
35
|
+
const shimsDir = getShimsDir();
|
|
36
36
|
const binDir = path.join(os.homedir(), ".safe-chain", "bin");
|
|
37
37
|
// Create the shims directory if it doesn't exist
|
|
38
38
|
if (!fs.existsSync(shimsDir)) {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
3
|
import { detectShells } from "./shellDetection.js";
|
|
4
|
-
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
|
4
|
+
import { knownAikidoTools, getPackageManagerList, getScriptsDir } from "./helpers.js";
|
|
5
5
|
import fs from "fs";
|
|
6
|
-
import os from "os";
|
|
7
6
|
import path from "path";
|
|
8
7
|
import { includePython } from "../config/cliArguments.js";
|
|
9
8
|
import { fileURLToPath } from "url";
|
|
@@ -107,10 +106,10 @@ function setupShell(shell) {
|
|
|
107
106
|
|
|
108
107
|
function copyStartupFiles() {
|
|
109
108
|
const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
|
|
109
|
+
const targetDir = getScriptsDir();
|
|
110
110
|
|
|
111
111
|
for (const file of startupFiles) {
|
|
112
|
-
const
|
|
113
|
-
const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file);
|
|
112
|
+
const targetPath = path.join(targetDir, file);
|
|
114
113
|
|
|
115
114
|
if (!fs.existsSync(targetDir)) {
|
|
116
115
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Use cross-platform path separator (: on Unix, ; on Windows)
|
|
2
2
|
$pathSeparator = if ($IsWindows) { ';' } else { ':' }
|
|
3
|
-
$safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
|
|
3
|
+
$safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
|
|
4
4
|
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
|
|
5
5
|
|
|
6
6
|
function npx {
|
|
@@ -50,6 +50,10 @@ function uv {
|
|
|
50
50
|
Invoke-WrappedCommand "uv" $args
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function poetry {
|
|
54
|
+
Invoke-WrappedCommand "poetry" $args
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
# `python -m pip`, `python -m pip3`.
|
|
54
58
|
function python {
|
|
55
59
|
Invoke-WrappedCommand 'python' $args
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Use cross-platform path separator (: on Unix, ; on Windows)
|
|
2
2
|
$pathSeparator = if ($IsWindows) { ';' } else { ':' }
|
|
3
|
-
$safeChainBin = Join-Path $HOME '.safe-chain' 'bin'
|
|
3
|
+
$safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
|
|
4
4
|
$env:PATH = "$env:PATH$pathSeparator$safeChainBin"
|
|
5
5
|
|
|
6
6
|
function npx {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
3
|
import { detectShells } from "./shellDetection.js";
|
|
4
|
-
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
|
4
|
+
import { knownAikidoTools, getPackageManagerList, getShimsDir, getScriptsDir } from "./helpers.js";
|
|
5
|
+
import fs from "fs";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @returns {Promise<void>}
|
|
@@ -62,3 +63,44 @@ export async function teardown() {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Removes directories created by setup-ci and setup commands
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
*/
|
|
71
|
+
export async function teardownDirectories() {
|
|
72
|
+
const shimsDir = getShimsDir();
|
|
73
|
+
const scriptsDir = getScriptsDir();
|
|
74
|
+
|
|
75
|
+
// Remove CI shims directory
|
|
76
|
+
if (fs.existsSync(shimsDir)) {
|
|
77
|
+
try {
|
|
78
|
+
fs.rmSync(shimsDir, { recursive: true, force: true });
|
|
79
|
+
ui.writeInformation(
|
|
80
|
+
`${chalk.bold("- CI Shims:")} ${chalk.green("Removed successfully")}`
|
|
81
|
+
);
|
|
82
|
+
} catch (/** @type {any} */ error) {
|
|
83
|
+
ui.writeError(
|
|
84
|
+
`${chalk.bold("- CI Shims:")} ${chalk.red(
|
|
85
|
+
"Failed to remove"
|
|
86
|
+
)}. Error: ${error.message}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Remove scripts directory
|
|
92
|
+
if (fs.existsSync(scriptsDir)) {
|
|
93
|
+
try {
|
|
94
|
+
fs.rmSync(scriptsDir, { recursive: true, force: true });
|
|
95
|
+
ui.writeInformation(
|
|
96
|
+
`${chalk.bold("- Scripts:")} ${chalk.green("Removed successfully")}`
|
|
97
|
+
);
|
|
98
|
+
} catch (/** @type {any} */ error) {
|
|
99
|
+
ui.writeError(
|
|
100
|
+
`${chalk.bold("- Scripts:")} ${chalk.red(
|
|
101
|
+
"Failed to remove"
|
|
102
|
+
)}. Error: ${error.message}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|