AikidoSec-safe-chain/packages/safe-chain/src/packagemanager/pip/runPipCommand.js
2025-11-13 15:50:14 -08:00

114 lines
4.1 KiB
JavaScript

import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import fs from "node:fs/promises";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import ini from "ini";
/**
* @param {string} command
* @param {string[]} args
*
* @returns {Promise<{status: number}>}
*/
export async function runPip(command, args) {
try {
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots)
// so that any network request made by pip, including those outside explicit CLI args,
// validates correctly under both MITM'd and tunneled HTTPS.
const combinedCaPath = getCombinedCaBundlePath();
// https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the --cert option (which we're providing via INI file)
// will tell pip to use the provided CA bundle for HTTPS verification.
// Proxy settings: prefer GLOBAL_AGENT_HTTP_PROXY, then HTTPS_PROXY, then HTTP_PROXY
const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
const tmpDir = os.tmpdir();
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
if (!env.PIP_CONFIG_FILE) {
// Build pip config INI
/** @type {{ global: { cert: string, proxy?: string } }} */
const configObj = { global: { cert: combinedCaPath } };
if (proxy) {
configObj.global.proxy = proxy;
}
const pipConfig = ini.stringify(configObj);
await fs.writeFile(pipConfigPath, pipConfig);
env.PIP_CONFIG_FILE = pipConfigPath;
} else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
// Existing pip config file present and exists on disk.
// Lets merge in our cert and proxy settings if not already present
const userConfig = env.PIP_CONFIG_FILE;
ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
// Read the existing config without modifying it
let content = await fs.readFile(userConfig, "utf-8");
const parsed = ini.parse(content);
// Ensure [global] section exists
parsed.global = parsed.global || {};
// Adding CERT and PROXY
// If either is already set, there's no neeed to throw an error
// MITM might fail and throw later if the proxy config is invalid
// Cert
if (typeof parsed.global.cert === "undefined") {
ui.writeVerbose("Safe-chain: Adding cert to temporary PIP_CONFIG_FILE.");
parsed.global.cert = combinedCaPath;
}
// Proxy
if (typeof parsed.global.proxy === "undefined") {
if (proxy) {
ui.writeVerbose("Safe-chain: Adding proxy to temporary PIP_CONFIG_FILE.");
parsed.global.proxy = proxy;
}
}
const updated = ini.stringify(parsed);
// Save to a new temp file to avoid overwriting user's original config
await fs.writeFile(pipConfigPath, updated, "utf-8");
env.PIP_CONFIG_FILE = pipConfigPath;
} else {
// The user provided PIP_CONFIG_FILE does not exist on disk
// PIP will handle this as an error and inform the user
}
// REQUESTS_CA_BUNDLE, SSL_CERT_FILE and PIP_CERT as extra safety nets.
if (!env.REQUESTS_CA_BUNDLE) {
env.REQUESTS_CA_BUNDLE = combinedCaPath;
}
if (!env.SSL_CERT_FILE) {
env.SSL_CERT_FILE = combinedCaPath;
}
if (!env.PIP_CERT) {
env.PIP_CERT = combinedCaPath;
}
const result = await safeSpawn(command, args, {
stdio: "inherit",
env,
});
return { status: result.status };
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {
ui.writeError(`Error executing command: ${error.message}`);
ui.writeError(`Is '${command}' installed and available on your system?`);
return { status: 1 };
}
}
}