@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,203 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ // @ts-ignore - certifi has no type definitions
5
+ import certifi from "certifi";
6
+ import tls from "node:tls";
7
+ import { X509Certificate } from "node:crypto";
8
+ import { getCaCertPath } from "./certUtils.js";
9
+ import { ui } from "../environment/userInteraction.js";
10
+
11
+ /** @type {string | null} */
12
+ let bundlePath = null;
13
+
14
+ /**
15
+ * Check if a PEM string contains only parsable cert blocks.
16
+ * @param {string} pem - PEM-encoded certificate string
17
+ * @returns {boolean}
18
+ */
19
+ function isParsable(pem) {
20
+ if (!pem || typeof pem !== "string") return false;
21
+ pem = normalizeLineEndings(pem);
22
+ const begin = "-----BEGIN CERTIFICATE-----";
23
+ const end = "-----END CERTIFICATE-----";
24
+ const blocks = [];
25
+
26
+ let idx = 0;
27
+ while (idx < pem.length) {
28
+ const start = pem.indexOf(begin, idx);
29
+ if (start === -1) break;
30
+ const stop = pem.indexOf(end, start + begin.length);
31
+ if (stop === -1) break;
32
+ const blockEnd = stop + end.length;
33
+ blocks.push(pem.slice(start, blockEnd));
34
+ idx = blockEnd;
35
+ }
36
+
37
+ if (blocks.length === 0) return false;
38
+ try {
39
+ for (const b of blocks) {
40
+ // throw if invalid
41
+ new X509Certificate(b);
42
+ }
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Build a combined CA bundle.
51
+ * Automatically includes:
52
+ * - Safe Chain CA (for MITM of known registries)
53
+ * - Mozilla roots via certifi (for public HTTPS)
54
+ * - Node's built-in root certificates (fallback)
55
+ * - User's custom certificates (if NODE_EXTRA_CA_CERTS environment variable is set)
56
+ *
57
+ * @returns {string} Path to the combined CA bundle PEM file
58
+ */
59
+ export function getCombinedCaBundlePath() {
60
+ if (bundlePath)
61
+ {
62
+ return bundlePath;
63
+ }
64
+
65
+ const parts = [];
66
+
67
+ // 1) Safe Chain CA (for MITM'd registries)
68
+ const safeChainPath = getCaCertPath();
69
+ try {
70
+ const safeChainPem = fs.readFileSync(safeChainPath, "utf8");
71
+ if (isParsable(safeChainPem)) parts.push(safeChainPem.trim());
72
+ } catch {
73
+ // Ignore if Safe Chain CA is not available
74
+ }
75
+
76
+ // 2) certifi (Mozilla CA bundle for all public HTTPS)
77
+ try {
78
+ const certifiPem = fs.readFileSync(certifi, "utf8");
79
+ if (isParsable(certifiPem)) parts.push(certifiPem.trim());
80
+ } catch {
81
+ // Ignore if certifi bundle is not available
82
+ }
83
+
84
+ // 3) Node's built-in root certificates
85
+ try {
86
+ const nodeRoots = tls.rootCertificates;
87
+ if (Array.isArray(nodeRoots) && nodeRoots.length) {
88
+ for (const rootPem of nodeRoots) {
89
+ if (typeof rootPem !== "string") continue;
90
+ if (isParsable(rootPem)) parts.push(rootPem.trim());
91
+ }
92
+ }
93
+ } catch {
94
+ // Ignore if unavailable
95
+ }
96
+
97
+ // 4) User's NODE_EXTRA_CA_CERTS (if set)
98
+ const userCertPath = process.env.NODE_EXTRA_CA_CERTS;
99
+ if (userCertPath) {
100
+ const userPem = readUserCertificateFile(userCertPath);
101
+ if (userPem) {
102
+ parts.push(userPem.trim());
103
+ ui.writeVerbose(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
104
+ } else {
105
+ ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
106
+ }
107
+ }
108
+
109
+ const combined = parts.filter(Boolean).join("\n");
110
+ bundlePath = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`);
111
+ fs.writeFileSync(bundlePath, combined, { encoding: "utf8" });
112
+ return bundlePath;
113
+ }
114
+
115
+ /**
116
+ * Remove the generated CA bundle file from disk.
117
+ */
118
+ export function cleanupCertBundle() {
119
+ if (bundlePath) {
120
+ try {
121
+ fs.unlinkSync(bundlePath);
122
+ } catch (err) {
123
+ ui.writeVerbose(`Failed to cleanup the create bundle at ${bundlePath}`, err)
124
+ }
125
+ bundlePath = null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Normalize path
131
+ * @param {string} p - Path to normalize
132
+ * @returns {string}
133
+ */
134
+ function normalizePathF(p) {
135
+ return p.replace(/\\/g, "/");
136
+ }
137
+
138
+ /**
139
+ * Normalize line endings to LF
140
+ * @param {string} text - Text with mixed line endings
141
+ * @returns {string}
142
+ */
143
+ function normalizeLineEndings(text) {
144
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
145
+ }
146
+
147
+ /**
148
+ * Read and validate user certificate file
149
+ * @param {string} certPath - Path to certificate file
150
+ * @returns {string | null} Certificate PEM content or null if invalid/unreadable
151
+ */
152
+ function readUserCertificateFile(certPath) {
153
+ try {
154
+ // 1) Basic validation
155
+ if (typeof certPath !== "string" || certPath.trim().length === 0) {
156
+ return null;
157
+ }
158
+
159
+ // 2) Reject path traversal attempts (normalize backslashes first for Windows paths)
160
+ const normalizedPath = normalizePathF(certPath);
161
+ if (normalizedPath.includes("..")) {
162
+ return null;
163
+ }
164
+
165
+ // 3) Check if file exists and is not a directory or symlink
166
+ let stats;
167
+ try {
168
+ stats = fs.lstatSync(certPath);
169
+ } catch {
170
+ // File doesn't exist or can't be accessed
171
+ return null;
172
+ }
173
+
174
+ if (!stats.isFile()) {
175
+ // Reject directories and symlinks
176
+ return null;
177
+ }
178
+
179
+ // 4) Read file content
180
+ let content;
181
+ try {
182
+ content = fs.readFileSync(certPath, "utf8");
183
+ } catch {
184
+ return null;
185
+ }
186
+
187
+ if (!content || typeof content !== "string") {
188
+ return null;
189
+ }
190
+
191
+ // 5) Validate PEM format
192
+ if (!isParsable(content)) {
193
+ return null;
194
+ }
195
+
196
+ return content;
197
+ } catch {
198
+ // Silently fail on any errors
199
+ return null;
200
+ }
201
+ }
202
+
203
+
@@ -0,0 +1,178 @@
1
+ import forge from "node-forge";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { getCertsDir } from "../config/safeChainDir.js";
5
+
6
+ const ca = loadCa();
7
+
8
+ const certCache = new Map();
9
+
10
+ /**
11
+ * @param {forge.pki.PublicKey} publicKey
12
+ * @returns {string}
13
+ */
14
+ function createKeyIdentifier(publicKey) {
15
+ return forge.pki.getPublicKeyFingerprint(publicKey, {
16
+ encoding: "binary",
17
+ md: forge.md.sha1.create(),
18
+ });
19
+ }
20
+
21
+ export function getCaCertPath() {
22
+ return path.join(getCertsDir(), "ca-cert.pem");
23
+ }
24
+
25
+ /**
26
+ * @param {string} hostname
27
+ * @returns {{privateKey: string, certificate: string}}
28
+ */
29
+ export function generateCertForHost(hostname) {
30
+ let existingCert = certCache.get(hostname);
31
+ if (existingCert) {
32
+ return existingCert;
33
+ }
34
+
35
+ const keys = forge.pki.rsa.generateKeyPair(2048);
36
+ const cert = forge.pki.createCertificate();
37
+ cert.publicKey = keys.publicKey;
38
+ cert.serialNumber = "01";
39
+ cert.validity.notBefore = new Date();
40
+ cert.validity.notAfter = new Date();
41
+ cert.validity.notAfter.setHours(cert.validity.notBefore.getHours() + 1);
42
+
43
+ const attrs = [{ name: "commonName", value: hostname }];
44
+ cert.setSubject(attrs);
45
+ cert.setIssuer(ca.certificate.subject.attributes);
46
+ const authorityKeyIdentifier = createKeyIdentifier(ca.certificate.publicKey);
47
+ cert.setExtensions([
48
+ {
49
+ name: "subjectAltName",
50
+ altNames: [
51
+ {
52
+ type: 2, // DNS
53
+ value: hostname,
54
+ },
55
+ ],
56
+ },
57
+ {
58
+ name: "keyUsage",
59
+ digitalSignature: true,
60
+ keyEncipherment: true,
61
+ },
62
+ {
63
+ /*
64
+ Extended Key Usage (EKU) serverAuth extension
65
+
66
+ Needed for TLS server authentication. This extension indicates the certificate's
67
+ public key may be used for TLS WWW server authentication.
68
+ Python virtualenv environments (like pipx-installed Poetry) enforce this strictly
69
+ https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12
70
+ */
71
+ name: "extKeyUsage",
72
+ serverAuth: true,
73
+ },
74
+ {
75
+ /*
76
+ Subject Key Identifier (SKI)
77
+
78
+ Needed for Python virtualenv SSL validation and certificate chain building.
79
+ This extension provides a means of identifying certificates containing a particular public key.
80
+ Python virtualenv environments require this for proper certificate chain validation.
81
+ System Python installations may be more lenient.
82
+ https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2
83
+ */
84
+ name: "subjectKeyIdentifier",
85
+ subjectKeyIdentifier: createKeyIdentifier(cert.publicKey),
86
+ },
87
+ {
88
+ /*
89
+ Authority Key Identifier (AKI)
90
+
91
+ Needed for Python virtualenv SSL validation and certificate path validation.
92
+ This extension identifies the public key corresponding to the private key used to sign
93
+ this certificate. It links this certificate to its issuing CA certificate.
94
+ Without this, Python virtualenv certificate validation might fail (for instance for Poetry)
95
+ https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1
96
+ */
97
+ name: "authorityKeyIdentifier",
98
+ keyIdentifier: authorityKeyIdentifier,
99
+ },
100
+ ]);
101
+ cert.sign(ca.privateKey, forge.md.sha256.create());
102
+
103
+ const result = {
104
+ privateKey: forge.pki.privateKeyToPem(keys.privateKey),
105
+ certificate: forge.pki.certificateToPem(cert),
106
+ };
107
+
108
+ certCache.set(hostname, result);
109
+
110
+ return result;
111
+ }
112
+
113
+ function loadCa() {
114
+ const certFolder = getCertsDir();
115
+ const keyPath = path.join(certFolder, "ca-key.pem");
116
+ const certPath = path.join(certFolder, "ca-cert.pem");
117
+
118
+ if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
119
+ const privateKeyPem = fs.readFileSync(keyPath, "utf8");
120
+ const certPem = fs.readFileSync(certPath, "utf8");
121
+ const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
122
+ const certificate = forge.pki.certificateFromPem(certPem);
123
+
124
+ // Don't return a cert that is valid for less than 1 hour
125
+ const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
126
+ if (certificate.validity.notAfter > oneHourFromNow) {
127
+ return { privateKey, certificate };
128
+ }
129
+ }
130
+
131
+ const { privateKey, certificate } = generateCa();
132
+ fs.mkdirSync(certFolder, { recursive: true });
133
+ fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey));
134
+ fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate));
135
+ return { privateKey, certificate };
136
+ }
137
+
138
+ function generateCa() {
139
+ const keys = forge.pki.rsa.generateKeyPair(2048);
140
+ const cert = forge.pki.createCertificate();
141
+ cert.publicKey = keys.publicKey;
142
+ cert.serialNumber = "01";
143
+ cert.validity.notBefore = new Date();
144
+ cert.validity.notAfter = new Date();
145
+ cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1);
146
+
147
+ const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
148
+ cert.setSubject(attrs);
149
+ cert.setIssuer(attrs); // Self-signed: issuer === subject
150
+ const keyIdentifier = createKeyIdentifier(cert.publicKey);
151
+ cert.setExtensions([
152
+ {
153
+ name: "basicConstraints",
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
156
+ },
157
+ {
158
+ name: "keyUsage",
159
+ keyCertSign: true,
160
+ digitalSignature: true,
161
+ keyEncipherment: true,
162
+ },
163
+ {
164
+ name: "subjectKeyIdentifier",
165
+ subjectKeyIdentifier: keyIdentifier,
166
+ },
167
+ {
168
+ name: "authorityKeyIdentifier",
169
+ keyIdentifier,
170
+ },
171
+ ]);
172
+ cert.sign(keys.privateKey, forge.md.sha256.create());
173
+
174
+ return {
175
+ privateKey: keys.privateKey,
176
+ certificate: cert,
177
+ };
178
+ }
@@ -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
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
3
+ * @param {string} headerName
4
+ */
5
+ export function getHeaderValueAsString(headers, headerName) {
6
+ if (!headers) {
7
+ return undefined;
8
+ }
9
+
10
+ let header = headers[headerName];
11
+
12
+ if (Array.isArray(header)) {
13
+ return header.join(", ");
14
+ }
15
+
16
+ return header;
17
+ }
18
+
19
+ /**
20
+ * Returns a copy of headers without the provided header names, matched
21
+ * either exactly or case-insensitively.
22
+ *
23
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
24
+ * @param {string[]} headerNames
25
+ * @param {{ caseInsensitive?: boolean }} [options]
26
+ * @returns {NodeJS.Dict<string | string[]> | undefined}
27
+ */
28
+ export function omitHeaders(headers, headerNames, options = {}) {
29
+ if (!headers) {
30
+ return headers;
31
+ }
32
+
33
+ const omittedHeaderNames = new Set(
34
+ options.caseInsensitive
35
+ ? headerNames.map((name) => name.toLowerCase())
36
+ : headerNames
37
+ );
38
+ /** @type {NodeJS.Dict<string | string[]>} */
39
+ const filteredHeaders = {};
40
+
41
+ for (const [headerName, value] of Object.entries(headers)) {
42
+ const comparableHeaderName = options.caseInsensitive
43
+ ? headerName.toLowerCase()
44
+ : headerName;
45
+ if (!omittedHeaderNames.has(comparableHeaderName)) {
46
+ filteredHeaders[headerName] = value;
47
+ }
48
+ }
49
+
50
+ return filteredHeaders;
51
+ }
52
+
53
+ /**
54
+ * Remove headers that become stale when the response body is modified.
55
+ *
56
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
57
+ * @returns {void}
58
+ */
59
+ export function clearCachingHeaders(headers) {
60
+ if (!headers) {
61
+ return;
62
+ }
63
+
64
+ const filteredHeaders = omitHeaders(headers, [
65
+ "etag",
66
+ "last-modified",
67
+ "cache-control",
68
+ "content-length",
69
+ ]);
70
+
71
+ if (!filteredHeaders) {
72
+ return;
73
+ }
74
+
75
+ for (const key of Object.keys(headers)) {
76
+ delete headers[key];
77
+ }
78
+
79
+ Object.assign(headers, filteredHeaders);
80
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ ECOSYSTEM_JS,
3
+ ECOSYSTEM_PY,
4
+ getEcoSystem,
5
+ } from "../../config/settings.js";
6
+ import { npmInterceptorForUrl } from "./npm/npmInterceptor.js";
7
+ import { pipInterceptorForUrl } from "./pip/pipInterceptor.js";
8
+
9
+ /**
10
+ * @param {string} url
11
+ * @returns {import("./interceptorBuilder.js").Interceptor | undefined}
12
+ */
13
+ export function createInterceptorForUrl(url) {
14
+ const ecosystem = getEcoSystem();
15
+
16
+ if (ecosystem === ECOSYSTEM_JS) {
17
+ return npmInterceptorForUrl(url);
18
+ }
19
+
20
+ if (ecosystem === ECOSYSTEM_PY) {
21
+ return pipInterceptorForUrl(url);
22
+ }
23
+
24
+ return undefined;
25
+ }
@@ -0,0 +1,179 @@
1
+ import { EventEmitter } from "events";
2
+
3
+ /**
4
+ * @typedef {Object} Interceptor
5
+ * @property {(targetUrl: string) => Promise<RequestInterceptionHandler>} handleRequest
6
+ * @property {(event: string, listener: (...args: any[]) => void) => Interceptor} on
7
+ * @property {(event: string, ...args: any[]) => boolean} emit
8
+ *
9
+ *
10
+ * @typedef {Object} RequestInterceptionContext
11
+ * @property {string} targetUrl
12
+ * @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware
13
+ * @property {(packageName: string, version: string, message: string) => void} blockMinimumAgeRequest
14
+ * @property {(modificationFunc: (headers: NodeJS.Dict<string | string[]>) => NodeJS.Dict<string | string[]>) => void} modifyRequestHeaders
15
+ * @property {(modificationFunc: (body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer) => void} modifyBody
16
+ * @property {() => RequestInterceptionHandler} build
17
+ *
18
+ *
19
+ * @typedef {Object} RequestInterceptionHandler
20
+ * @property {{statusCode: number, message: string} | undefined} blockResponse
21
+ * @property {(headers: NodeJS.Dict<string | string[]> | undefined) => NodeJS.Dict<string | string[]> | undefined} modifyRequestHeaders
22
+ * @property {() => boolean} modifiesResponse
23
+ * @property {(body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer} modifyBody
24
+ *
25
+ * @typedef {Object} MalwareBlockedEvent
26
+ * @property {string} packageName
27
+ * @property {string} version
28
+ * @property {string} targetUrl
29
+ * @property {number} timestamp
30
+ *
31
+ * @typedef {Object} MinimumAgeRequestBlockedEvent
32
+ * @property {string} packageName
33
+ * @property {string} version
34
+ * @property {string} targetUrl
35
+ * @property {number} timestamp
36
+ */
37
+
38
+ /**
39
+ * @param {(requestHandlerBuilder: RequestInterceptionContext) => Promise<void>} requestInterceptionFunc
40
+ * @returns {Interceptor}
41
+ */
42
+ export function interceptRequests(requestInterceptionFunc) {
43
+ return buildInterceptor([requestInterceptionFunc]);
44
+ }
45
+
46
+ /**
47
+ * @param {Array<(requestHandlerBuilder: RequestInterceptionContext) => Promise<void>>} requestHandlers
48
+ * @returns {Interceptor}
49
+ */
50
+ function buildInterceptor(requestHandlers) {
51
+ const eventEmitter = new EventEmitter();
52
+
53
+ return {
54
+ async handleRequest(targetUrl) {
55
+ const requestContext = createRequestContext(targetUrl, eventEmitter);
56
+
57
+ for (const handler of requestHandlers) {
58
+ await handler(requestContext);
59
+ }
60
+
61
+ return requestContext.build();
62
+ },
63
+ on(event, listener) {
64
+ eventEmitter.on(event, listener);
65
+ return this;
66
+ },
67
+ emit(event, ...args) {
68
+ return eventEmitter.emit(event, ...args);
69
+ },
70
+ };
71
+ }
72
+
73
+ /**
74
+ * @param {string} targetUrl
75
+ * @param {import('events').EventEmitter} eventEmitter
76
+ * @returns {RequestInterceptionContext}
77
+ */
78
+ function createRequestContext(targetUrl, eventEmitter) {
79
+ /** @type {{statusCode: number, message: string} | undefined} */
80
+ let blockResponse = undefined;
81
+ /** @type {Array<(headers: NodeJS.Dict<string | string[]>) => NodeJS.Dict<string | string[]>>} */
82
+ let reqheaderModificationFuncs = [];
83
+ /** @type {Array<(body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer>} */
84
+ let modifyBodyFuncs = [];
85
+
86
+ /**
87
+ * @param {string | undefined} packageName
88
+ * @param {string | undefined} version
89
+ */
90
+ function blockMalwareSetup(packageName, version) {
91
+ blockResponse = createBlockResponse("Forbidden - blocked by safe-chain");
92
+
93
+ // Emit the malwareBlocked event
94
+ eventEmitter.emit("malwareBlocked", {
95
+ packageName,
96
+ version,
97
+ targetUrl,
98
+ timestamp: Date.now(),
99
+ });
100
+ }
101
+
102
+ /**
103
+ * @param {string} message
104
+ */
105
+ function blockMinimumAgeRequestSetup(
106
+ /** @type {string} */ packageName,
107
+ /** @type {string} */ version,
108
+ /** @type {string} */ message
109
+ ) {
110
+ blockResponse = createBlockResponse(message);
111
+ eventEmitter.emit("minimumAgeRequestBlocked", {
112
+ packageName,
113
+ version,
114
+ targetUrl,
115
+ timestamp: Date.now(),
116
+ });
117
+ }
118
+
119
+ /**
120
+ * @param {string} message
121
+ * @returns {{statusCode: number, message: string}}
122
+ */
123
+ function createBlockResponse(message) {
124
+ return {
125
+ statusCode: 403,
126
+ message,
127
+ };
128
+ }
129
+
130
+ /** @returns {RequestInterceptionHandler} */
131
+ function build() {
132
+ /**
133
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
134
+ * @returns {NodeJS.Dict<string | string[]> | undefined}
135
+ */
136
+ function modifyRequestHeaders(headers) {
137
+ if (headers) {
138
+ for (const func of reqheaderModificationFuncs) {
139
+ func(headers);
140
+ }
141
+ }
142
+
143
+ return headers;
144
+ }
145
+
146
+ /**
147
+ * @param {Buffer} body
148
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
149
+ * @returns {Buffer}
150
+ */
151
+ function modifyBody(body, headers) {
152
+ let modifiedBody = body;
153
+
154
+ for (var func of modifyBodyFuncs) {
155
+ modifiedBody = func(body, headers);
156
+ }
157
+
158
+ return modifiedBody;
159
+ }
160
+
161
+ // These functions are invoked in the proxy, allowing to apply the configured modifications
162
+ return {
163
+ blockResponse,
164
+ modifyRequestHeaders: modifyRequestHeaders,
165
+ modifiesResponse: () => modifyBodyFuncs.length > 0,
166
+ modifyBody,
167
+ };
168
+ }
169
+
170
+ // These functions are used to setup the modifications
171
+ return {
172
+ targetUrl,
173
+ blockMalware: blockMalwareSetup,
174
+ blockMinimumAgeRequest: blockMinimumAgeRequestSetup,
175
+ modifyRequestHeaders: (func) => reqheaderModificationFuncs.push(func),
176
+ modifyBody: (func) => modifyBodyFuncs.push(func),
177
+ build,
178
+ };
179
+ }