mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Another iteration
This commit is contained in:
parent
f400c5576a
commit
28d24bb6ea
12 changed files with 134 additions and 107 deletions
|
|
@ -1,19 +1,19 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
// Defaults
|
|
||||||
let packageManagerName = "pip";
|
|
||||||
// Pass through user args as-is
|
|
||||||
const argv = process.argv.slice(2);
|
|
||||||
|
|
||||||
// Set eco system
|
// Set eco system
|
||||||
// This can be used in other parts of the code to determine which eco system we are working with
|
|
||||||
setEcoSystem(ECOSYSTEM_PY);
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
initializePackageManager(packageManagerName);
|
// Set current invocation
|
||||||
var exitCode = await main(argv);
|
setCurrentPipInvocation(PIP_INVOCATIONS.PIP);
|
||||||
|
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER);
|
||||||
|
|
||||||
|
// Pass through only user-supplied pip args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,17 @@
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
|
|
||||||
// Explicit pip3 entrypoint
|
// Set eco system
|
||||||
const packageManagerName = "pip3";
|
|
||||||
|
|
||||||
// Copy argv as-is
|
|
||||||
const argv = process.argv.slice(2);
|
|
||||||
|
|
||||||
// Set ecosystem to Python
|
|
||||||
setEcoSystem(ECOSYSTEM_PY);
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
initializePackageManager(packageManagerName);
|
// Set current invocation
|
||||||
var exitCode = await main(argv);
|
setCurrentPipInvocation(PIP_INVOCATIONS.PIP3);
|
||||||
|
|
||||||
|
// Create package manager
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER);
|
||||||
|
|
||||||
|
// Pass through only user-supplied pip args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
|
|
||||||
const argv = process.argv.slice(2);
|
// Set eco system
|
||||||
|
|
||||||
const supportedArgs = ["pip", "pip3"];
|
|
||||||
|
|
||||||
if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) {
|
|
||||||
setEcoSystem(ECOSYSTEM_PY);
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
initializePackageManager(argv[1]);
|
|
||||||
var exitCode = await main(argv.slice(2));
|
// Strip '-m pip' or '-m pip3' from args if present
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
if (argv[0] === '-m' && argv[1] === 'pip') {
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
setCurrentPipInvocation(PIP_INVOCATIONS.PY_PIP);
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER);
|
||||||
|
argv = argv.slice(2);
|
||||||
|
var exitCode = await main(argv);
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: run the real python
|
// Forward to real python binary for non-pip flows
|
||||||
const { spawn } = await import("child_process");
|
const { spawn } = await import('child_process');
|
||||||
spawn("python", argv, { stdio: "inherit" });
|
spawn('python', argv, { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setCurrentPipInvocation, PIP_INVOCATIONS, PIP_PACKAGE_MANAGER } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
|
|
||||||
const argv = process.argv.slice(2);
|
// Set eco system
|
||||||
|
|
||||||
const supportedArgs = ["pip", "pip3"];
|
|
||||||
|
|
||||||
if (argv[0] === "-m" && argv[1] && supportedArgs.includes(argv[1])) {
|
|
||||||
setEcoSystem(ECOSYSTEM_PY);
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
// python3 -m pip or python3 -m pip3: always use pip3 package manager
|
|
||||||
initializePackageManager("pip3");
|
// Strip nodejs and wrapper script from args
|
||||||
var exitCode = await main(argv.slice(2));
|
let argv = process.argv.slice(2);
|
||||||
|
if (argv[0] === '-m' && argv[1] === 'pip') {
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
setCurrentPipInvocation(PIP_INVOCATIONS.PY3_PIP);
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER);
|
||||||
|
// Strip '-m pip' or '-m pip3' from args if present
|
||||||
|
argv = argv.slice(2);
|
||||||
|
var exitCode = await main(argv);
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: run the real python3
|
// Forward to real python3 binary for non-pip flows
|
||||||
const { spawn } = await import("child_process");
|
const { spawn } = await import('child_process');
|
||||||
spawn("python3", argv, { stdio: "inherit" });
|
spawn('python3', argv, { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ export function initializePackageManager(packageManagerName) {
|
||||||
state.packageManagerName = createBunPackageManager();
|
state.packageManagerName = createBunPackageManager();
|
||||||
} else if (packageManagerName === "bunx") {
|
} else if (packageManagerName === "bunx") {
|
||||||
state.packageManagerName = createBunxPackageManager();
|
state.packageManagerName = createBunxPackageManager();
|
||||||
} else if (packageManagerName === "pip" || packageManagerName === "pip3") {
|
} else if (packageManagerName === "pip") {
|
||||||
state.packageManagerName = createPipPackageManager(packageManagerName);
|
state.packageManagerName = createPipPackageManager();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
import { runPip } from "./runPipCommand.js";
|
import { runPip } from "./runPipCommand.js";
|
||||||
|
import { getCurrentPipInvocation } from "./pipSettings.js";
|
||||||
/**
|
/**
|
||||||
* @param {string} [command]
|
|
||||||
* @returns {import("../currentPackageManager.js").PackageManager}
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
*/
|
*/
|
||||||
export function createPipPackageManager(command = "pip") {
|
export function createPipPackageManager() {
|
||||||
return {
|
return {
|
||||||
runCommand: /** @param {string[]} args */ (args) => runPip(command, args),
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*/
|
||||||
|
runCommand: (args) => {
|
||||||
|
const invocation = getCurrentPipInvocation();
|
||||||
|
const fullArgs = [...invocation.args, ...args];
|
||||||
|
console.debug('[safe-chain debug] runCommand:', invocation.command, fullArgs);
|
||||||
|
return runPip(invocation.command, fullArgs);
|
||||||
|
},
|
||||||
// For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
|
// For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
|
||||||
isSupportedCommand: () => false,
|
isSupportedCommand: () => false,
|
||||||
getDependencyUpdatesForCommand: () => [],
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
|
|
||||||
31
packages/safe-chain/src/packagemanager/pip/pipSettings.js
Normal file
31
packages/safe-chain/src/packagemanager/pip/pipSettings.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Constant for pip package manager name
|
||||||
|
export const PIP_PACKAGE_MANAGER = "pip";
|
||||||
|
|
||||||
|
// Enum of possible Python/pip invocations for Safe Chain interception
|
||||||
|
export const PIP_INVOCATIONS = {
|
||||||
|
PIP: { command: "pip", args: [] },
|
||||||
|
PIP3: { command: "pip3", args: [] },
|
||||||
|
PY_PIP: { command: "python", args: ["-m", "pip"] },
|
||||||
|
PY3_PIP: { command: "python3", args: ["-m", "pip"] }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{ command: string, args: string[] }}
|
||||||
|
*/
|
||||||
|
let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ command: string, args: string[] }} invocation
|
||||||
|
*/
|
||||||
|
export function setCurrentPipInvocation(invocation) {
|
||||||
|
console.debug('[safe-chain debug] setCurrentPipInvocation:', invocation);
|
||||||
|
currentInvocation = invocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {{ command: string, args: string[] }}
|
||||||
|
*/
|
||||||
|
export function getCurrentPipInvocation() {
|
||||||
|
console.debug('[safe-chain debug] getCurrentPipInvocation:', currentInvocation);
|
||||||
|
return currentInvocation;
|
||||||
|
}
|
||||||
|
|
@ -26,10 +26,10 @@ export async function runPip(command, args) {
|
||||||
});
|
});
|
||||||
return { status: result.status };
|
return { status: result.status };
|
||||||
} catch (/** @type any */ error) {
|
} catch (/** @type any */ error) {
|
||||||
|
ui.writeError("Error executing command:", error.message);
|
||||||
if (error.status) {
|
if (error.status) {
|
||||||
return { status: error.status };
|
return { status: error.status };
|
||||||
} else {
|
} else {
|
||||||
ui.writeError("Error executing command:", error.message);
|
|
||||||
return { status: 1 };
|
return { status: 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,10 +142,12 @@ function handleConnect(req, clientSocket, head) {
|
||||||
isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg));
|
isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: log CONNECT request URL and MITM/tunnel decision
|
||||||
|
ui.writeVerbose(`[Safe-chain debug] CONNECT request: url=${url}, ecosystem=${ecosystem}, isKnownRegistry=${isKnownRegistry}`);
|
||||||
|
|
||||||
if (isKnownRegistry) {
|
if (isKnownRegistry) {
|
||||||
mitmConnect(req, clientSocket, isAllowedUrl);
|
mitmConnect(req, clientSocket, isAllowedUrl);
|
||||||
} else {
|
} else {
|
||||||
// For other hosts, just tunnel the request to the destination tcp socket
|
|
||||||
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
|
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
|
||||||
tunnelRequest(req, clientSocket, head);
|
tunnelRequest(req, clientSocket, head);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ RUN curl -fsSL https://bun.sh/install | bash
|
||||||
# Install Python and pip (pip3)
|
# Install Python and pip (pip3)
|
||||||
RUN apt-get update && apt-get install -y python${PYTHON_VERSION} python3-pip && \
|
RUN apt-get update && apt-get install -y python${PYTHON_VERSION} python3-pip && \
|
||||||
ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 && \
|
ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 && \
|
||||||
|
ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python && \
|
||||||
ln -sf /usr/bin/pip3 /usr/local/bin/pip3
|
ln -sf /usr/bin/pip3 /usr/local/bin/pip3
|
||||||
|
|
||||||
# Copy and install Safe chain
|
# Copy and install Safe chain
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,22 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
|
||||||
assert.ok(result.output.includes("hello"), `Output was: ${result.output}`);
|
assert.ok(result.output.includes("hello"), `Output was: ${result.output}`);
|
||||||
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command");
|
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not intercept python3 test.py", async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py");
|
||||||
|
const result = await shell.runCommand("python3 test.py");
|
||||||
|
assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`);
|
||||||
|
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 script execution");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not intercept python test.py", async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py");
|
||||||
|
const result = await shell.runCommand("python test.py");
|
||||||
|
assert.ok(result.output.includes("Hello from test.py!"), `Output was: ${result.output}`);
|
||||||
|
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python script execution");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let shell of ["bash", "zsh"]) {
|
for (let shell of ["bash", "zsh"]) {
|
||||||
|
|
@ -89,27 +105,6 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`setup-ci routes python -m pip3 through safe-chain for ${shell}`, async () => {
|
|
||||||
const installationShell = await container.openShell(shell);
|
|
||||||
await installationShell.runCommand("safe-chain setup-ci");
|
|
||||||
await installationShell.runCommand(
|
|
||||||
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
|
|
||||||
);
|
|
||||||
await installationShell.runCommand(
|
|
||||||
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc"
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectShell = await container.openShell(shell);
|
|
||||||
const result = await projectShell.runCommand(
|
|
||||||
"python -m pip3 install --break-system-packages certifi"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
result.output.includes("no malware found."),
|
|
||||||
`Output did not contain scan message. Output was:\n${result.output}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => {
|
it(`setup-ci routes python3 -m pip through safe-chain for ${shell}`, async () => {
|
||||||
const installationShell = await container.openShell(shell);
|
const installationShell = await container.openShell(shell);
|
||||||
await installationShell.runCommand("safe-chain setup-ci");
|
await installationShell.runCommand("safe-chain setup-ci");
|
||||||
|
|
@ -131,7 +126,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`setup-ci routes python3 -m pip3 through safe-chain for ${shell}`, async () => {
|
it(`setup-ci routes pip through safe-chain for ${shell}`, async () => {
|
||||||
const installationShell = await container.openShell(shell);
|
const installationShell = await container.openShell(shell);
|
||||||
await installationShell.runCommand("safe-chain setup-ci");
|
await installationShell.runCommand("safe-chain setup-ci");
|
||||||
await installationShell.runCommand(
|
await installationShell.runCommand(
|
||||||
|
|
@ -143,7 +138,28 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
|
||||||
|
|
||||||
const projectShell = await container.openShell(shell);
|
const projectShell = await container.openShell(shell);
|
||||||
const result = await projectShell.runCommand(
|
const result = await projectShell.runCommand(
|
||||||
"python3 -m pip3 install --break-system-packages certifi"
|
"pip install --break-system-packages certifi"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not contain scan message. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => {
|
||||||
|
const installationShell = await container.openShell(shell);
|
||||||
|
await installationShell.runCommand("safe-chain setup-ci");
|
||||||
|
await installationShell.runCommand(
|
||||||
|
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
|
||||||
|
);
|
||||||
|
await installationShell.runCommand(
|
||||||
|
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.bashrc"
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectShell = await container.openShell(shell);
|
||||||
|
const result = await projectShell.runCommand(
|
||||||
|
"pip3 install --break-system-packages certifi"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
|
|
@ -161,24 +161,6 @@ describe("E2E: pip coverage", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`python -m pip3 routes to aikido-pip3 (uses pip3 command)`, async () => {
|
|
||||||
const shell = await container.openShell("zsh");
|
|
||||||
const result = await shell.runCommand(
|
|
||||||
"python -m pip3 install --break-system-packages requests"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
result.output.includes("no malware found."),
|
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
|
||||||
);
|
|
||||||
// Verify it completed successfully (would fail if routing was incorrect)
|
|
||||||
assert.ok(
|
|
||||||
result.output.includes("Successfully installed") ||
|
|
||||||
result.output.includes("Requirement already satisfied"),
|
|
||||||
`Installation did not succeed. Output was:\n${result.output}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`python3 -m pip routes to aikido-pip3 (uses pip3 command)`, async () => {
|
it(`python3 -m pip routes to aikido-pip3 (uses pip3 command)`, async () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
const result = await shell.runCommand(
|
const result = await shell.runCommand(
|
||||||
|
|
@ -197,24 +179,6 @@ describe("E2E: pip coverage", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`python3 -m pip3 routes to aikido-pip3 (uses pip3 command)`, async () => {
|
|
||||||
const shell = await container.openShell("zsh");
|
|
||||||
const result = await shell.runCommand(
|
|
||||||
"python3 -m pip3 install --break-system-packages requests"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
result.output.includes("no malware found."),
|
|
||||||
`Output did not include expected text. Output was:\n${result.output}`
|
|
||||||
);
|
|
||||||
// Verify it completed successfully (would fail if routing was incorrect)
|
|
||||||
assert.ok(
|
|
||||||
result.output.includes("Successfully installed") ||
|
|
||||||
result.output.includes("Requirement already satisfied"),
|
|
||||||
`Installation did not succeed. Output was:\n${result.output}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`pip3 can install from GitHub URL using the CA bundle`, async () => {
|
it(`pip3 can install from GitHub URL using the CA bundle`, async () => {
|
||||||
const shell = await container.openShell("zsh");
|
const shell = await container.openShell("zsh");
|
||||||
// Install a simple package from GitHub - this should use TCP tunnel, not MITM
|
// Install a simple package from GitHub - this should use TCP tunnel, not MITM
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue