@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,34 @@
1
+ import { deref } from "./cmd-list.js";
2
+
3
+ /**
4
+ * @param {string[]} args
5
+ * @returns {string | null}
6
+ */
7
+ export function getNpmCommandForArgs(args) {
8
+ if (args.length === 0) {
9
+ return null;
10
+ }
11
+
12
+ const argCommand = deref(args[0]);
13
+ if (!argCommand) {
14
+ return null;
15
+ }
16
+
17
+ return argCommand;
18
+ }
19
+
20
+ /**
21
+ * @param {string[]} args
22
+ * @returns {boolean}
23
+ */
24
+ export function hasDryRunArg(args) {
25
+ return args.some((arg) => arg === "--dry-run");
26
+ }
27
+
28
+ export const npmInstallCommand = "install";
29
+ export const npmCiCommand = "ci";
30
+ export const npmInstallTestCommand = "install-test";
31
+ export const npmInstallCiTestCommand = "install-ci-test";
32
+ export const npmUpdateCommand = "update";
33
+ export const npmAuditCommand = "audit";
34
+ export const npmExecCommand = "exec";
@@ -0,0 +1,15 @@
1
+ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
2
+ import { runNpx } from "./runNpxCommand.js";
3
+
4
+ /**
5
+ * @returns {import("../currentPackageManager.js").PackageManager}
6
+ */
7
+ export function createNpxPackageManager() {
8
+ const scanner = commandArgumentScanner();
9
+
10
+ return {
11
+ runCommand: runNpx,
12
+ isSupportedCommand: (args) => scanner.shouldScan(args),
13
+ getDependencyUpdatesForCommand: (args) => scanner.scan(args),
14
+ };
15
+ }
@@ -0,0 +1,43 @@
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, // all npx commands need to be scanned, npx doesn't have dry-run
11
+ };
12
+ }
13
+
14
+ /**
15
+ * @param {string[]} args
16
+ * @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
17
+ */
18
+ function scanDependencies(args) {
19
+ return checkChangesFromArgs(args);
20
+ }
21
+
22
+ /**
23
+ * @param {string[]} args
24
+ * @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
25
+ */
26
+ export async function checkChangesFromArgs(args) {
27
+ const changes = [];
28
+ const packageUpdates = parsePackagesFromArguments(args);
29
+
30
+ for (const packageUpdate of packageUpdates) {
31
+ var exactVersion = await resolvePackageVersion(
32
+ packageUpdate.name,
33
+ packageUpdate.version
34
+ );
35
+ if (exactVersion) {
36
+ packageUpdate.version = exactVersion;
37
+ }
38
+
39
+ changes.push({ ...packageUpdate, type: "add" });
40
+ }
41
+
42
+ return changes;
43
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @param {string[]} args
3
+ *
4
+ * @returns {{name: string, version: string}[]}
5
+ */
6
+ export function parsePackagesFromArguments(args) {
7
+ let defaultTag = "latest";
8
+
9
+ for (let i = 0; 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
+ return [packageDetails];
23
+ }
24
+ }
25
+
26
+ return [];
27
+ }
28
+
29
+ /**
30
+ * @param {string} arg
31
+ * @returns {{name: string, numberOfParameters: number} | undefined}
32
+ */
33
+ function getOption(arg) {
34
+ if (isOptionWithParameter(arg)) {
35
+ return {
36
+ name: arg,
37
+ numberOfParameters: 1,
38
+ };
39
+ }
40
+
41
+ // Arguments starting with "-" or "--" are considered options
42
+ // except for "--package=" which contains the package name
43
+ if (arg.startsWith("-") && !arg.startsWith("--package=")) {
44
+ return {
45
+ name: arg,
46
+ numberOfParameters: 0,
47
+ };
48
+ }
49
+
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * @param {string} arg
55
+ * @returns {boolean}
56
+ */
57
+ function isOptionWithParameter(arg) {
58
+ const optionsWithParameters = [
59
+ "--access",
60
+ "--auth-type",
61
+ "--cache",
62
+ "--fetch-retries",
63
+ "--fetch-retry-mintimeout",
64
+ "--fetch-retry-maxtimeout",
65
+ "--fetch-retry-factor",
66
+ "--fetch-timeout",
67
+ "--https-proxy",
68
+ "--include",
69
+ "--location",
70
+ "--lockfile-version",
71
+ "--loglevel",
72
+ "--omit",
73
+ "--proxy",
74
+ "--registry",
75
+ "--replace-registry-host",
76
+ "--tag",
77
+ "--user-config",
78
+ "--workspace",
79
+ ];
80
+
81
+ return optionsWithParameters.includes(arg);
82
+ }
83
+
84
+ /**
85
+ * @param {string} arg
86
+ * @param {string} defaultTag
87
+ * @returns {{name: string, version: string}}
88
+ */
89
+ function parsePackagename(arg, defaultTag) {
90
+ // format can be --package=name@version
91
+ // in that case, we need to remove the --package= part
92
+ if (arg.startsWith("--package=")) {
93
+ arg = arg.slice(10);
94
+ }
95
+
96
+ arg = removeAlias(arg);
97
+
98
+ // Split at the last "@" to separate the package name and version
99
+ const lastAtIndex = arg.lastIndexOf("@");
100
+
101
+ let name, version;
102
+ // The index of the last "@" should be greater than 0
103
+ // If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
104
+ if (lastAtIndex > 0) {
105
+ name = arg.slice(0, lastAtIndex);
106
+ version = arg.slice(lastAtIndex + 1);
107
+ } else {
108
+ name = arg;
109
+ version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
110
+ }
111
+
112
+ return {
113
+ name,
114
+ version,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * @param {string} arg
120
+ * @returns {string}
121
+ */
122
+ function removeAlias(arg) {
123
+ // removes the alias.
124
+ // Eg.: server@npm:http-server@latest becomes http-server@latest
125
+ const aliasIndex = arg.indexOf("@npm:");
126
+ if (aliasIndex !== -1) {
127
+ return arg.slice(aliasIndex + 5);
128
+ }
129
+ return arg;
130
+ }
@@ -0,0 +1,20 @@
1
+ import { safeSpawn } from "../../utils/safeSpawn.js";
2
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
3
+ import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
4
+
5
+ /**
6
+ * @param {string[]} args
7
+ *
8
+ * @returns {Promise<{status: number}>}
9
+ */
10
+ export async function runNpx(args) {
11
+ try {
12
+ const result = await safeSpawn("npx", args, {
13
+ stdio: "inherit",
14
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
15
+ });
16
+ return { status: result.status };
17
+ } catch (/** @type any */ error) {
18
+ return reportCommandExecutionFailure(error, "npx");
19
+ }
20
+ }
@@ -0,0 +1,25 @@
1
+ import { runPip } from "./runPipCommand.js";
2
+ import { PIP_COMMAND } from "./pipSettings.js";
3
+
4
+ /**
5
+ * @param {{ tool: string, args: string[] }} [context] - Optional context with tool name and args
6
+ * @returns {import("../currentPackageManager.js").PackageManager}
7
+ */
8
+ export function createPipPackageManager(context) {
9
+ const tool = context?.tool || PIP_COMMAND;
10
+
11
+ return {
12
+ /**
13
+ * @param {string[]} args
14
+ */
15
+ runCommand: (args) => {
16
+ // Args from main.js are already stripped of --safe-chain-* flags
17
+ // We just pass the tool (e.g. "python3") and the args (e.g. ["-m", "pip", "install", ...])
18
+ return runPip(tool, args);
19
+ },
20
+ // For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
21
+ isSupportedCommand: () => false,
22
+ getDependencyUpdatesForCommand: () => [],
23
+ };
24
+ }
25
+
@@ -0,0 +1,6 @@
1
+ export const PIP_PACKAGE_MANAGER = "pip";
2
+
3
+ export const PIP_COMMAND = "pip";
4
+ export const PIP3_COMMAND = "pip3";
5
+ export const PYTHON_COMMAND = "python";
6
+ export const PYTHON3_COMMAND = "python3";
@@ -0,0 +1,209 @@
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
+ import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
6
+ import fs from "node:fs/promises";
7
+ import fsSync from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import ini from "ini";
11
+ import { spawn } from "child_process";
12
+ import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
13
+
14
+ /**
15
+ * Checks if this pip invocation should bypass safe-chain and spawn directly.
16
+ * Returns true if the tool is python/python3 but NOT being run with -m pip/pip3.
17
+ * @param {string} command - The command executable
18
+ * @param {string[]} args - The arguments
19
+ * @returns {boolean}
20
+ */
21
+ export function shouldBypassSafeChain(command, args) {
22
+ if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
23
+ // Check if args start with -m pip
24
+ if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ /**
33
+ * Sets fallback CA bundle environment variables used by Python libraries.
34
+ * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
35
+ * network libraries respect the combined CA bundle, even if they don't read pip's config.
36
+ *
37
+ * @param {NodeJS.ProcessEnv} env - Environment object to modify
38
+ * @param {string} combinedCaPath - Path to the combined CA bundle
39
+ */
40
+ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
41
+ // REQUESTS_CA_BUNDLE: Used by the popular 'requests' library
42
+ if (env.REQUESTS_CA_BUNDLE) {
43
+ ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
44
+ }
45
+ env.REQUESTS_CA_BUNDLE = combinedCaPath;
46
+
47
+ // SSL_CERT_FILE: Used by some Python SSL libraries and urllib
48
+ if (env.SSL_CERT_FILE) {
49
+ ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
50
+ }
51
+ env.SSL_CERT_FILE = combinedCaPath;
52
+
53
+ // PIP_CERT: Pip's own environment variable for certificate verification
54
+ if (env.PIP_CERT) {
55
+ ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
56
+ }
57
+ env.PIP_CERT = combinedCaPath;
58
+ }
59
+
60
+ /**
61
+ * Runs a pip command with safe-chain's certificate bundle and proxy configuration.
62
+ *
63
+ * Creates a temporary pip config file to configure:
64
+ * - Cert bundle for HTTPS verification
65
+ * - Proxy settings
66
+ *
67
+ * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
68
+ * their settings with safe-chain's, leaving the original file unchanged.
69
+ *
70
+ * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
71
+ * users to read/write persistent config. Only CA environment variables are set for these commands.
72
+ *
73
+ * @param {string} command - The pip command executable (e.g., 'pip3' or 'python3')
74
+ * @param {string[]} args - Command line arguments to pass to pip
75
+ * @returns {Promise<{status: number}>} Exit status of the pip command
76
+ */
77
+ export async function runPip(command, args) {
78
+ // Check if we should bypass safe-chain (python/python3 without -m pip)
79
+ if (shouldBypassSafeChain(command, args)) {
80
+ ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
81
+ // Spawn the ORIGINAL command with ORIGINAL args
82
+ return new Promise((_resolve) => {
83
+ const proc = spawn(command, args, { stdio: "inherit" });
84
+ proc.on("exit", (/** @type {number | null} */ code) => {
85
+ ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`);
86
+ ui.writeBufferedLogsAndStopBuffering();
87
+ process.exit(code ?? 0);
88
+ });
89
+ proc.on("error", (/** @type {Error} */ err) => {
90
+ ui.writeError(`Error executing command: ${err.message}`);
91
+ ui.writeBufferedLogsAndStopBuffering();
92
+ process.exit(1);
93
+ });
94
+ });
95
+ }
96
+
97
+ try {
98
+ const env = mergeSafeChainProxyEnvironmentVariables(process.env);
99
+
100
+ // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs)
101
+ // so that any network request made by pip, including those outside explicit CLI args,
102
+ // validates correctly under both MITM'd and tunneled HTTPS.
103
+ const combinedCaPath = getCombinedCaBundlePath();
104
+
105
+ // Commands that need access to persistent config/cache/state files
106
+ // These should not have PIP_CONFIG_FILE overridden as it would prevent them from
107
+ // reading/writing to the user's actual pip configuration and cache directories
108
+ const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
109
+ const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
110
+
111
+ // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
112
+ // will tell pip to use the provided CA bundle for HTTPS verification.
113
+
114
+ // Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active),
115
+ // otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables
116
+ const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
117
+
118
+ const tmpDir = os.tmpdir();
119
+ const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
120
+ let cleanupConfigPath = null; // Track temp file for cleanup
121
+
122
+ if (isConfigRelatedCommand) {
123
+ ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
124
+
125
+ // Still set the fallback CA bundle environment variables to avoid edge cases where a
126
+ // plugin or extension triggers a network call during config introspection
127
+ // This can do no harm
128
+ setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
129
+
130
+ const result = await safeSpawn(command, args, {
131
+ stdio: "inherit",
132
+ env,
133
+ });
134
+
135
+ return { status: result.status };
136
+ }
137
+
138
+ // Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
139
+ if (!env.PIP_CONFIG_FILE) {
140
+ /** @type {{ global: { cert: string, proxy?: string } }} */
141
+ const configObj = { global: { cert: combinedCaPath } };
142
+ if (proxy) {
143
+ configObj.global.proxy = proxy;
144
+ }
145
+ const pipConfig = ini.stringify(configObj);
146
+ await fs.writeFile(pipConfigPath, pipConfig);
147
+ env.PIP_CONFIG_FILE = pipConfigPath;
148
+ cleanupConfigPath = pipConfigPath;
149
+
150
+ } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
151
+ ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
152
+ const userConfig = env.PIP_CONFIG_FILE;
153
+
154
+ // Read the existing config without modifying it
155
+ let content = await fs.readFile(userConfig, "utf-8");
156
+ const parsed = ini.parse(content);
157
+
158
+ // Ensure [global] section exists
159
+ parsed.global = parsed.global || {};
160
+
161
+ // Cert
162
+ if (typeof parsed.global.cert !== "undefined") {
163
+ ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
164
+ }
165
+ parsed.global.cert = combinedCaPath;
166
+
167
+ // Proxy
168
+ if (typeof parsed.global.proxy !== "undefined") {
169
+ ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
170
+ }
171
+ if (proxy) {
172
+ parsed.global.proxy = proxy;
173
+ }
174
+
175
+ const updated = ini.stringify(parsed);
176
+
177
+ // Save to a new temp file to avoid overwriting user's original config
178
+ await fs.writeFile(pipConfigPath, updated, "utf-8");
179
+ env.PIP_CONFIG_FILE = pipConfigPath;
180
+ cleanupConfigPath = pipConfigPath;
181
+
182
+ } else {
183
+ // The user provided PIP_CONFIG_FILE does not exist on disk
184
+ // PIP will handle this as an error and inform the user
185
+ }
186
+
187
+ // Set fallback CA bundle environment variables for Python libraries that don't read pip config
188
+ setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
189
+
190
+ const result = await safeSpawn(command, args, {
191
+ stdio: "inherit",
192
+ env,
193
+ });
194
+
195
+ // Cleanup temporary config file if we created one
196
+ if (cleanupConfigPath) {
197
+ try {
198
+ await fs.unlink(cleanupConfigPath);
199
+ } catch {
200
+ // Ignore cleanup errors - the file may have already been deleted or is inaccessible
201
+ // Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform
202
+ }
203
+ }
204
+
205
+ return { status: result.status };
206
+ } catch (/** @type any */ error) {
207
+ return reportCommandExecutionFailure(error, command);
208
+ }
209
+ }
@@ -0,0 +1,18 @@
1
+ import { runPipX } from "./runPipXCommand.js";
2
+
3
+ /**
4
+ * @returns {import("../currentPackageManager.js").PackageManager}
5
+ */
6
+ export function createPipXPackageManager() {
7
+ return {
8
+ /**
9
+ * @param {string[]} args
10
+ */
11
+ runCommand: (args) => {
12
+ return runPipX("pipx", args);
13
+ },
14
+ // MITM only
15
+ isSupportedCommand: () => false,
16
+ getDependencyUpdatesForCommand: () => [],
17
+ };
18
+ }
@@ -0,0 +1,60 @@
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
+ import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
6
+
7
+ /**
8
+ * Sets CA bundle environment variables used by Python libraries and pipx.
9
+ *
10
+ * @param {NodeJS.ProcessEnv} env - Env object
11
+ * @param {string} combinedCaPath - Path to the combined CA bundle
12
+ * @return {NodeJS.ProcessEnv} Modified environment object
13
+ */
14
+ function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
15
+ let retVal = { ...env };
16
+
17
+ if (env.SSL_CERT_FILE) {
18
+ ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
19
+ }
20
+ retVal.SSL_CERT_FILE = combinedCaPath;
21
+
22
+ if (env.REQUESTS_CA_BUNDLE) {
23
+ ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
24
+ }
25
+ retVal.REQUESTS_CA_BUNDLE = combinedCaPath;
26
+
27
+ if (env.PIP_CERT) {
28
+ ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
29
+ }
30
+ retVal.PIP_CERT = combinedCaPath;
31
+ return retVal;
32
+ }
33
+
34
+ /**
35
+ * Runs a pipx command with safe-chain's certificate bundle and proxy configuration.
36
+ *
37
+ * @param {string} command - The command to execute
38
+ * @param {string[]} args - Command line arguments
39
+ * @returns {Promise<{status: number}>} Exit status of the command
40
+ */
41
+ export async function runPipX(command, args) {
42
+ try {
43
+ const env = mergeSafeChainProxyEnvironmentVariables(process.env);
44
+
45
+ const combinedCaPath = getCombinedCaBundlePath();
46
+ const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath);
47
+
48
+ // Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
49
+ // These are already set by mergeSafeChainProxyEnvironmentVariables
50
+
51
+ const result = await safeSpawn(command, args, {
52
+ stdio: "inherit",
53
+ env: modifiedEnv,
54
+ });
55
+
56
+ return { status: result.status };
57
+ } catch (/** @type any */ error) {
58
+ return reportCommandExecutionFailure(error, command);
59
+ }
60
+ }
@@ -0,0 +1,57 @@
1
+ import { matchesCommand } from "../_shared/matchesCommand.js";
2
+ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
3
+ import { runPnpmCommand } from "./runPnpmCommand.js";
4
+
5
+ const scanner = commandArgumentScanner();
6
+
7
+ /**
8
+ * @returns {import("../currentPackageManager.js").PackageManager}
9
+ */
10
+ export function createPnpmPackageManager() {
11
+ return {
12
+ runCommand: (args) => runPnpmCommand(args, "pnpm"),
13
+ isSupportedCommand: (args) =>
14
+ matchesCommand(args, "add") ||
15
+ matchesCommand(args, "update") ||
16
+ matchesCommand(args, "upgrade") ||
17
+ matchesCommand(args, "up") ||
18
+ matchesCommand(args, "install") ||
19
+ matchesCommand(args, "i") ||
20
+ // dlx does not always come in the first position
21
+ // eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
22
+ // documentation: https://pnpm.io/cli/dlx#--package-name
23
+ args.includes("dlx"),
24
+ getDependencyUpdatesForCommand: (args) =>
25
+ getDependencyUpdatesForCommand(args, false),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * @returns {import("../currentPackageManager.js").PackageManager}
31
+ */
32
+ export function createPnpxPackageManager() {
33
+ return {
34
+ runCommand: (args) => runPnpmCommand(args, "pnpx"),
35
+ isSupportedCommand: () => true,
36
+ getDependencyUpdatesForCommand: (args) =>
37
+ getDependencyUpdatesForCommand(args, true),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * @param {string[]} args
43
+ * @param {boolean} isPnpx
44
+ * @returns {ReturnType<import("../currentPackageManager.js").PackageManager["getDependencyUpdatesForCommand"]>}
45
+ */
46
+ function getDependencyUpdatesForCommand(args, isPnpx) {
47
+ if (isPnpx) {
48
+ return scanner.scan(args);
49
+ }
50
+ if (args.includes("dlx")) {
51
+ // dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`)
52
+ // so we need to filter it out instead of slicing the array
53
+ // documentation: https://pnpm.io/cli/dlx#--package-name
54
+ return scanner.scan(args.filter((arg) => arg !== "dlx"));
55
+ }
56
+ return scanner.scan(args.slice(1));
57
+ }
@@ -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 pnpm, 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
+ }