@aikidosec/safe-chain 0.0.4-connect-timeout-beta
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 +257 -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 +20 -0
- package/bin/aikido-pip3.js +21 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-python.js +30 -0
- package/bin/aikido-python3.js +30 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +190 -0
- package/docs/banner.svg +151 -0
- package/docs/npm-to-binary-migration.md +89 -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/package.json +68 -0
- package/src/api/aikido.js +54 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +138 -0
- package/src/config/configFile.js +192 -0
- package/src/config/environmentVariables.js +7 -0
- package/src/config/settings.js +100 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/main.js +104 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +53 -0
- package/src/packagemanager/currentPackageManager.js +72 -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 +25 -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 +25 -0
- package/src/packagemanager/pip/createPackageManager.js +21 -0
- package/src/packagemanager/pip/pipSettings.js +30 -0
- package/src/packagemanager/pip/runPipCommand.js +175 -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 +36 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +71 -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 +41 -0
- package/src/registryProxy/certBundle.js +95 -0
- package/src/registryProxy/certUtils.js +128 -0
- package/src/registryProxy/http-utils.js +17 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
- package/src/registryProxy/mitmRequestHandler.js +231 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +184 -0
- package/src/registryProxy/tunnelRequestHandler.js +180 -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/shell-integration/helpers.js +213 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +170 -0
- package/src/shell-integration/setup.js +127 -0
- package/src/shell-integration/shellDetection.js +37 -0
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
- package/src/shell-integration/supported-shells/bash.js +134 -0
- package/src/shell-integration/supported-shells/fish.js +77 -0
- package/src/shell-integration/supported-shells/powershell.js +73 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
- package/src/shell-integration/supported-shells/zsh.js +74 -0
- package/src/shell-integration/teardown.js +64 -0
- package/src/utils/safeSpawn.js +137 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
4
|
+
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sets CA bundle environment variables used by Python libraries and uv.
|
|
8
|
+
*
|
|
9
|
+
* @param {NodeJS.ProcessEnv} env - Env object
|
|
10
|
+
* @param {string} combinedCaPath - Path to the combined CA bundle
|
|
11
|
+
*/
|
|
12
|
+
function setUvCaBundleEnvironmentVariables(env, combinedCaPath) {
|
|
13
|
+
// SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients
|
|
14
|
+
if (env.SSL_CERT_FILE) {
|
|
15
|
+
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
|
16
|
+
}
|
|
17
|
+
env.SSL_CERT_FILE = combinedCaPath;
|
|
18
|
+
|
|
19
|
+
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
|
|
20
|
+
if (env.REQUESTS_CA_BUNDLE) {
|
|
21
|
+
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
|
22
|
+
}
|
|
23
|
+
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
|
24
|
+
|
|
25
|
+
// PIP_CERT: Some underlying pip operations may respect this
|
|
26
|
+
if (env.PIP_CERT) {
|
|
27
|
+
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
|
28
|
+
}
|
|
29
|
+
env.PIP_CERT = combinedCaPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runs a uv command with safe-chain's certificate bundle and proxy configuration.
|
|
34
|
+
*
|
|
35
|
+
* uv respects standard environment variables for proxy and TLS configuration:
|
|
36
|
+
* - HTTP_PROXY / HTTPS_PROXY: Proxy settings
|
|
37
|
+
* - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
|
|
38
|
+
*
|
|
39
|
+
* Unlike pip (which requires a temporary config file for cert configuration), uv directly
|
|
40
|
+
* honors environment variables, so no config/ini file is needed.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} command - The uv command to execute (typically 'uv')
|
|
43
|
+
* @param {string[]} args - Command line arguments to pass to uv
|
|
44
|
+
* @returns {Promise<{status: number}>} Exit status of the uv command
|
|
45
|
+
*/
|
|
46
|
+
export async function runUv(command, args) {
|
|
47
|
+
try {
|
|
48
|
+
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
49
|
+
|
|
50
|
+
const combinedCaPath = getCombinedCaBundlePath();
|
|
51
|
+
setUvCaBundleEnvironmentVariables(env, combinedCaPath);
|
|
52
|
+
|
|
53
|
+
// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
|
|
54
|
+
// These are already set by mergeSafeChainProxyEnvironmentVariables
|
|
55
|
+
|
|
56
|
+
const result = await safeSpawn(command, args, {
|
|
57
|
+
stdio: "inherit",
|
|
58
|
+
env,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { status: result.status };
|
|
62
|
+
} catch (/** @type any */ error) {
|
|
63
|
+
if (error.status) {
|
|
64
|
+
return { status: error.status };
|
|
65
|
+
} else {
|
|
66
|
+
ui.writeError(`Error executing command: ${error.message}`);
|
|
67
|
+
ui.writeError(`Is '${command}' installed and available on your system?`);
|
|
68
|
+
return { status: 1 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
|
2
|
+
import { runYarnCommand } from "./runYarnCommand.js";
|
|
3
|
+
|
|
4
|
+
const scanner = commandArgumentScanner();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
8
|
+
*/
|
|
9
|
+
export function createYarnPackageManager() {
|
|
10
|
+
return {
|
|
11
|
+
runCommand: runYarnCommand,
|
|
12
|
+
isSupportedCommand: (args) =>
|
|
13
|
+
matchesCommand(args, "add") ||
|
|
14
|
+
matchesCommand(args, "global", "add") ||
|
|
15
|
+
matchesCommand(args, "install") ||
|
|
16
|
+
matchesCommand(args, "up") ||
|
|
17
|
+
matchesCommand(args, "upgrade") ||
|
|
18
|
+
matchesCommand(args, "global", "upgrade") ||
|
|
19
|
+
matchesCommand(args, "dlx"),
|
|
20
|
+
getDependencyUpdatesForCommand: (args) => scanner.scan(args),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string[]} args
|
|
26
|
+
* @param {...string} commandArgs
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function matchesCommand(args, ...commandArgs) {
|
|
30
|
+
if (args.length < commandArgs.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (var i = 0; i < commandArgs.length; i++) {
|
|
35
|
+
if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
|
2
|
+
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
|
|
6
|
+
*/
|
|
7
|
+
export function commandArgumentScanner() {
|
|
8
|
+
return {
|
|
9
|
+
scan: (args) => scanDependencies(args),
|
|
10
|
+
shouldScan: () => true, // There's no dry run for yarn, so we always scan
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string[]} args
|
|
16
|
+
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
|
|
17
|
+
*/
|
|
18
|
+
async function scanDependencies(args) {
|
|
19
|
+
const changes = [];
|
|
20
|
+
const packageUpdates = parsePackagesFromArguments(args);
|
|
21
|
+
|
|
22
|
+
for (const packageUpdate of packageUpdates) {
|
|
23
|
+
var exactVersion = await resolvePackageVersion(
|
|
24
|
+
packageUpdate.name,
|
|
25
|
+
packageUpdate.version
|
|
26
|
+
);
|
|
27
|
+
if (exactVersion) {
|
|
28
|
+
packageUpdate.version = exactVersion;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
changes.push({ ...packageUpdate, type: "add" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return changes;
|
|
35
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} args
|
|
3
|
+
* @returns {{name: string, version: string}[]}
|
|
4
|
+
*/
|
|
5
|
+
export function parsePackagesFromArguments(args) {
|
|
6
|
+
const changes = [];
|
|
7
|
+
let defaultTag = "latest";
|
|
8
|
+
|
|
9
|
+
for (let i = 1; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
const option = getOption(arg);
|
|
12
|
+
|
|
13
|
+
if (option) {
|
|
14
|
+
// If the option has a parameter, skip the next argument as well
|
|
15
|
+
i += option.numberOfParameters;
|
|
16
|
+
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const packageDetails = parsePackagename(arg, defaultTag);
|
|
21
|
+
if (packageDetails) {
|
|
22
|
+
changes.push(packageDetails);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return changes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} arg
|
|
31
|
+
*
|
|
32
|
+
* @returns {{name: string, numberOfParameters: number} | undefined}
|
|
33
|
+
*/
|
|
34
|
+
function getOption(arg) {
|
|
35
|
+
if (isOptionWithParameter(arg)) {
|
|
36
|
+
return {
|
|
37
|
+
name: arg,
|
|
38
|
+
numberOfParameters: 1,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Arguments starting with "-" or "--" are considered options
|
|
43
|
+
// except for "--package=" which contains the package name
|
|
44
|
+
if (arg.startsWith("-")) {
|
|
45
|
+
return {
|
|
46
|
+
name: arg,
|
|
47
|
+
numberOfParameters: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} arg
|
|
56
|
+
*
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
function isOptionWithParameter(arg) {
|
|
60
|
+
const optionsWithParameters = [
|
|
61
|
+
"--use-yarnrc",
|
|
62
|
+
"--link-folder",
|
|
63
|
+
"--global-folder",
|
|
64
|
+
"--modules-folder",
|
|
65
|
+
"--preferred-cache-folder",
|
|
66
|
+
"--cache-folder",
|
|
67
|
+
"--mutex",
|
|
68
|
+
"--cwd",
|
|
69
|
+
"--proxy",
|
|
70
|
+
"--https-proxy",
|
|
71
|
+
"--registry",
|
|
72
|
+
"--network-concurrency",
|
|
73
|
+
"--network-timeout",
|
|
74
|
+
"--scripts-prepend-node-path",
|
|
75
|
+
"--otp",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
return optionsWithParameters.includes(arg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} arg
|
|
83
|
+
* @param {string} defaultTag
|
|
84
|
+
*
|
|
85
|
+
* @returns {{name: string, version: string}}
|
|
86
|
+
*/
|
|
87
|
+
function parsePackagename(arg, defaultTag) {
|
|
88
|
+
// format can be --package=name@version
|
|
89
|
+
// in that case, we need to remove the --package= part
|
|
90
|
+
if (arg.startsWith("--package=")) {
|
|
91
|
+
arg = arg.slice(10);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
arg = removeAlias(arg);
|
|
95
|
+
|
|
96
|
+
// Split at the last "@" to separate the package name and version
|
|
97
|
+
const lastAtIndex = arg.lastIndexOf("@");
|
|
98
|
+
|
|
99
|
+
let name, version;
|
|
100
|
+
// The index of the last "@" should be greater than 0
|
|
101
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
|
102
|
+
if (lastAtIndex > 0) {
|
|
103
|
+
name = arg.slice(0, lastAtIndex);
|
|
104
|
+
version = arg.slice(lastAtIndex + 1);
|
|
105
|
+
} else {
|
|
106
|
+
name = arg;
|
|
107
|
+
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
version,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} arg
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
function removeAlias(arg) {
|
|
121
|
+
// removes the alias.
|
|
122
|
+
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
|
123
|
+
const aliasIndex = arg.indexOf("@npm:");
|
|
124
|
+
if (aliasIndex !== -1) {
|
|
125
|
+
return arg.slice(aliasIndex + 5);
|
|
126
|
+
}
|
|
127
|
+
return arg;
|
|
128
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string[]} args
|
|
7
|
+
*
|
|
8
|
+
* @returns {Promise<{status: number}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function runYarnCommand(args) {
|
|
11
|
+
try {
|
|
12
|
+
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
13
|
+
await fixYarnProxyEnvironmentVariables(env);
|
|
14
|
+
|
|
15
|
+
const result = await safeSpawn("yarn", args, {
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
env,
|
|
18
|
+
});
|
|
19
|
+
return { status: result.status };
|
|
20
|
+
} catch (/** @type any */ error) {
|
|
21
|
+
if (error.status) {
|
|
22
|
+
return { status: error.status };
|
|
23
|
+
} else {
|
|
24
|
+
ui.writeError("Error executing command:", error.message);
|
|
25
|
+
return { status: 1 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {Record<string, string>} env
|
|
32
|
+
*
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
async function fixYarnProxyEnvironmentVariables(env) {
|
|
36
|
+
// Yarn ignores standard proxy environment variable HTTPS_PROXY
|
|
37
|
+
// It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though.
|
|
38
|
+
// Don't use YARN_HTTPS_CA_FILE_PATH or YARN_CA_FILE_PATH though, it causes yarn to ignore all system CAs
|
|
39
|
+
|
|
40
|
+
env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a PEM string contains only parsable cert blocks.
|
|
12
|
+
* @param {string} pem - PEM-encoded certificate string
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function isParsable(pem) {
|
|
16
|
+
if (!pem || typeof pem !== "string") return false;
|
|
17
|
+
const begin = "-----BEGIN CERTIFICATE-----";
|
|
18
|
+
const end = "-----END CERTIFICATE-----";
|
|
19
|
+
const blocks = [];
|
|
20
|
+
|
|
21
|
+
let idx = 0;
|
|
22
|
+
while (idx < pem.length) {
|
|
23
|
+
const start = pem.indexOf(begin, idx);
|
|
24
|
+
if (start === -1) break;
|
|
25
|
+
const stop = pem.indexOf(end, start + begin.length);
|
|
26
|
+
if (stop === -1) break;
|
|
27
|
+
const blockEnd = stop + end.length;
|
|
28
|
+
blocks.push(pem.slice(start, blockEnd));
|
|
29
|
+
idx = blockEnd;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (blocks.length === 0) return false;
|
|
33
|
+
try {
|
|
34
|
+
for (const b of blocks) {
|
|
35
|
+
// throw if invalid
|
|
36
|
+
new X509Certificate(b);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @type {string | null} */
|
|
45
|
+
let cachedPath = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a combined CA bundle for Python and Node HTTPS flows.
|
|
49
|
+
* - Includes Safe Chain CA (for MITM of known registries)
|
|
50
|
+
* - Includes Mozilla roots via npm `certifi` (public HTTPS)
|
|
51
|
+
* - Includes Node's built-in root certificates as a portable fallback
|
|
52
|
+
* @returns {string} Path to the combined CA bundle PEM file
|
|
53
|
+
*/
|
|
54
|
+
export function getCombinedCaBundlePath() {
|
|
55
|
+
if (cachedPath && fs.existsSync(cachedPath)) return cachedPath;
|
|
56
|
+
|
|
57
|
+
// Concatenate PEM files
|
|
58
|
+
const parts = [];
|
|
59
|
+
|
|
60
|
+
// 1) Safe Chain CA (for MITM'd registries)
|
|
61
|
+
const safeChainPath = getCaCertPath();
|
|
62
|
+
try {
|
|
63
|
+
const safeChainPem = fs.readFileSync(safeChainPath, "utf8");
|
|
64
|
+
if (isParsable(safeChainPem)) parts.push(safeChainPem.trim());
|
|
65
|
+
} catch {
|
|
66
|
+
// Ignore if Safe Chain CA is not available
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2) certifi (Mozilla CA bundle for all public HTTPS)
|
|
70
|
+
try {
|
|
71
|
+
const certifiPem = fs.readFileSync(certifi, "utf8");
|
|
72
|
+
if (isParsable(certifiPem)) parts.push(certifiPem.trim());
|
|
73
|
+
} catch {
|
|
74
|
+
// Ignore if certifi bundle is not available
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3) Node's built-in root certificates
|
|
78
|
+
try {
|
|
79
|
+
const nodeRoots = tls.rootCertificates;
|
|
80
|
+
if (Array.isArray(nodeRoots) && nodeRoots.length) {
|
|
81
|
+
for (const rootPem of nodeRoots) {
|
|
82
|
+
if (typeof rootPem !== "string") continue;
|
|
83
|
+
if (isParsable(rootPem)) parts.push(rootPem.trim());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Ignore if unavailable
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const combined = parts.filter(Boolean).join("\n");
|
|
91
|
+
const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem");
|
|
92
|
+
fs.writeFileSync(target, combined, { encoding: "utf8" });
|
|
93
|
+
cachedPath = target;
|
|
94
|
+
return cachedPath;
|
|
95
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import forge from "node-forge";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
const certFolder = path.join(os.homedir(), ".safe-chain", "certs");
|
|
7
|
+
const ca = loadCa();
|
|
8
|
+
|
|
9
|
+
const certCache = new Map();
|
|
10
|
+
|
|
11
|
+
export function getCaCertPath() {
|
|
12
|
+
return path.join(certFolder, "ca-cert.pem");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} hostname
|
|
17
|
+
* @returns {{privateKey: string, certificate: string}}
|
|
18
|
+
*/
|
|
19
|
+
export function generateCertForHost(hostname) {
|
|
20
|
+
let existingCert = certCache.get(hostname);
|
|
21
|
+
if (existingCert) {
|
|
22
|
+
return existingCert;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
26
|
+
const cert = forge.pki.createCertificate();
|
|
27
|
+
cert.publicKey = keys.publicKey;
|
|
28
|
+
cert.serialNumber = "01";
|
|
29
|
+
cert.validity.notBefore = new Date();
|
|
30
|
+
cert.validity.notAfter = new Date();
|
|
31
|
+
cert.validity.notAfter.setHours(cert.validity.notBefore.getHours() + 1);
|
|
32
|
+
|
|
33
|
+
const attrs = [{ name: "commonName", value: hostname }];
|
|
34
|
+
cert.setSubject(attrs);
|
|
35
|
+
cert.setIssuer(ca.certificate.subject.attributes);
|
|
36
|
+
cert.setExtensions([
|
|
37
|
+
{
|
|
38
|
+
name: "subjectAltName",
|
|
39
|
+
altNames: [
|
|
40
|
+
{
|
|
41
|
+
type: 2, // DNS
|
|
42
|
+
value: hostname,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "keyUsage",
|
|
48
|
+
digitalSignature: true,
|
|
49
|
+
keyEncipherment: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
/*
|
|
53
|
+
extKeyUsage serverAuth is required for TLS server authentication.
|
|
54
|
+
This is especially important for Python venv environments, which use their own
|
|
55
|
+
certificate validation logic and will reject certificates lacking the serverAuth EKU.
|
|
56
|
+
Adding serverAuth does not impact other usages
|
|
57
|
+
*/
|
|
58
|
+
name: "extKeyUsage",
|
|
59
|
+
serverAuth: true,
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
cert.sign(ca.privateKey, forge.md.sha256.create());
|
|
63
|
+
|
|
64
|
+
const result = {
|
|
65
|
+
privateKey: forge.pki.privateKeyToPem(keys.privateKey),
|
|
66
|
+
certificate: forge.pki.certificateToPem(cert),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
certCache.set(hostname, result);
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loadCa() {
|
|
75
|
+
const keyPath = path.join(certFolder, "ca-key.pem");
|
|
76
|
+
const certPath = path.join(certFolder, "ca-cert.pem");
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
79
|
+
const privateKeyPem = fs.readFileSync(keyPath, "utf8");
|
|
80
|
+
const certPem = fs.readFileSync(certPath, "utf8");
|
|
81
|
+
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
|
|
82
|
+
const certificate = forge.pki.certificateFromPem(certPem);
|
|
83
|
+
|
|
84
|
+
// Don't return a cert that is valid for less than 1 hour
|
|
85
|
+
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
|
|
86
|
+
if (certificate.validity.notAfter > oneHourFromNow) {
|
|
87
|
+
return { privateKey, certificate };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { privateKey, certificate } = generateCa();
|
|
92
|
+
fs.mkdirSync(certFolder, { recursive: true });
|
|
93
|
+
fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey));
|
|
94
|
+
fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate));
|
|
95
|
+
return { privateKey, certificate };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function generateCa() {
|
|
99
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
100
|
+
const cert = forge.pki.createCertificate();
|
|
101
|
+
cert.publicKey = keys.publicKey;
|
|
102
|
+
cert.serialNumber = "01";
|
|
103
|
+
cert.validity.notBefore = new Date();
|
|
104
|
+
cert.validity.notAfter = new Date();
|
|
105
|
+
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1);
|
|
106
|
+
|
|
107
|
+
const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
|
|
108
|
+
cert.setSubject(attrs);
|
|
109
|
+
cert.setIssuer(attrs);
|
|
110
|
+
cert.setExtensions([
|
|
111
|
+
{
|
|
112
|
+
name: "basicConstraints",
|
|
113
|
+
cA: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "keyUsage",
|
|
117
|
+
keyCertSign: true,
|
|
118
|
+
digitalSignature: true,
|
|
119
|
+
keyEncipherment: true,
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
privateKey: keys.privateKey,
|
|
126
|
+
certificate: cert,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
@@ -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 "./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
|
+
}
|