mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Merge pull request #142 from AikidoSec/feature/pypi-ci
[PYPI] Add CI Shims
This commit is contained in:
commit
76acf43128
21 changed files with 325 additions and 94 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -2096,6 +2096,8 @@
|
||||||
"aikido-npx": "bin/aikido-npx.js",
|
"aikido-npx": "bin/aikido-npx.js",
|
||||||
"aikido-pip": "bin/aikido-pip.js",
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
"aikido-pip3": "bin/aikido-pip3.js",
|
"aikido-pip3": "bin/aikido-pip3.js",
|
||||||
|
"aikido-python": "bin/aikido-python.js",
|
||||||
|
"aikido-python3": "bin/aikido-python3.js",
|
||||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||||
"aikido-yarn": "bin/aikido-yarn.js",
|
"aikido-yarn": "bin/aikido-yarn.js",
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,16 @@
|
||||||
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);
|
||||||
|
|
|
||||||
28
packages/safe-chain/bin/aikido-python.js
Executable file
28
packages/safe-chain/bin/aikido-python.js
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
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 { main } from "../src/main.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
// Strip nodejs and wrapper script from args
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY_PIP3 : PIP_INVOCATIONS.PY_PIP);
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER);
|
||||||
|
|
||||||
|
// Strip off the '-m pip' or '-m pip3' from the args
|
||||||
|
argv = argv.slice(2);
|
||||||
|
|
||||||
|
var exitCode = await main(argv);
|
||||||
|
process.exit(exitCode);
|
||||||
|
} else {
|
||||||
|
// Forward to real python binary for non-pip flows
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
spawn('python', argv, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
28
packages/safe-chain/bin/aikido-python3.js
Executable file
28
packages/safe-chain/bin/aikido-python3.js
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
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 { main } from "../src/main.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
// Strip nodejs and wrapper script from args
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (argv[0] === '-m' && (argv[1] === 'pip' || argv[1] === 'pip3')) {
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
setCurrentPipInvocation(argv[1] === 'pip3' ? PIP_INVOCATIONS.PY3_PIP3 : PIP_INVOCATIONS.PY3_PIP);
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER);
|
||||||
|
|
||||||
|
// Strip off the '-m pip' or '-m pip3' from the args
|
||||||
|
argv = argv.slice(2);
|
||||||
|
|
||||||
|
var exitCode = await main(argv);
|
||||||
|
process.exit(exitCode);
|
||||||
|
} else {
|
||||||
|
// Forward to real python3 binary for non-pip flows
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
spawn('python3', argv, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
"aikido-bunx": "bin/aikido-bunx.js",
|
"aikido-bunx": "bin/aikido-bunx.js",
|
||||||
"aikido-pip": "bin/aikido-pip.js",
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
"aikido-pip3": "bin/aikido-pip3.js",
|
"aikido-pip3": "bin/aikido-pip3.js",
|
||||||
|
"aikido-python": "bin/aikido-python.js",
|
||||||
|
"aikido-python3": "bin/aikido-python3.js",
|
||||||
"safe-chain": "bin/safe-chain.js"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -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,18 @@
|
||||||
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];
|
||||||
|
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: () => [],
|
||||||
|
|
|
||||||
30
packages/safe-chain/src/packagemanager/pip/pipSettings.js
Normal file
30
packages/safe-chain/src/packagemanager/pip/pipSettings.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const PIP_PACKAGE_MANAGER = "pip";
|
||||||
|
|
||||||
|
// All supported 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"] },
|
||||||
|
PY_PIP3: { command: "python", args: ["-m", "pip3"] },
|
||||||
|
PY3_PIP3: { command: "python3", args: ["-m", "pip3"] }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
||||||
|
currentInvocation = invocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {{ command: string, args: string[] }}
|
||||||
|
*/
|
||||||
|
export function getCurrentPipInvocation() {
|
||||||
|
return currentInvocation;
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,8 @@ export async function runPip(command, args) {
|
||||||
if (error.status) {
|
if (error.status) {
|
||||||
return { status: error.status };
|
return { status: error.status };
|
||||||
} else {
|
} else {
|
||||||
ui.writeError("Error executing command:", error.message);
|
ui.writeError(`Error executing command: ${error.message}`);
|
||||||
|
ui.writeError(`Is '${command}' installed and available on your system?`);
|
||||||
return { status: 1 };
|
return { status: 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@ export async function auditChanges(changes) {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
//Uncomment next line during manual testing
|
|
||||||
//console.log(" Safe-chain: auditing package:", change);
|
|
||||||
const malwarePackage = malwarePackages.find(
|
const malwarePackage = malwarePackages.find(
|
||||||
(pkg) => pkg.name === change.name && pkg.version === change.version
|
(pkg) => pkg.name === change.name && pkg.version === change.version
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ export const knownAikidoTools = [
|
||||||
{ tool: "bunx", aikidoCommand: "aikido-bunx" },
|
{ tool: "bunx", aikidoCommand: "aikido-bunx" },
|
||||||
{ tool: "pip", aikidoCommand: "aikido-pip" },
|
{ tool: "pip", aikidoCommand: "aikido-pip" },
|
||||||
{ tool: "pip3", aikidoCommand: "aikido-pip3" },
|
{ tool: "pip3", aikidoCommand: "aikido-pip3" },
|
||||||
|
{ tool: "python", aikidoCommand: "aikido-python" },
|
||||||
|
{ tool: "python3", aikidoCommand: "aikido-python3" },
|
||||||
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,4 @@ else
|
||||||
echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
|
echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,9 @@ function createUnixShims(shimsDir) {
|
||||||
|
|
||||||
const template = fs.readFileSync(templatePath, "utf-8");
|
const template = fs.readFileSync(templatePath, "utf-8");
|
||||||
|
|
||||||
// Create a shim for each tool except pip (CI support not yet implemented)
|
// Create a shim for each tool
|
||||||
let created = 0;
|
let created = 0;
|
||||||
for (const toolInfo of knownAikidoTools) {
|
for (const toolInfo of knownAikidoTools) {
|
||||||
if (toolInfo.tool === "pip") {
|
|
||||||
continue; // Skip pip shims in CI for now
|
|
||||||
}
|
|
||||||
|
|
||||||
const shimContent = template
|
const shimContent = template
|
||||||
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
||||||
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
||||||
|
|
@ -98,18 +94,14 @@ function createWindowsShims(shimsDir) {
|
||||||
|
|
||||||
const template = fs.readFileSync(templatePath, "utf-8");
|
const template = fs.readFileSync(templatePath, "utf-8");
|
||||||
|
|
||||||
// Create a shim for each tool except pip (CI support not yet implemented)
|
// Create a shim for each tool
|
||||||
let created = 0;
|
let created = 0;
|
||||||
for (const toolInfo of knownAikidoTools) {
|
for (const toolInfo of knownAikidoTools) {
|
||||||
if (toolInfo.tool === "pip") {
|
|
||||||
continue; // Skip pip shims in CI for now
|
|
||||||
}
|
|
||||||
|
|
||||||
const shimContent = template
|
const shimContent = template
|
||||||
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
||||||
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
||||||
|
|
||||||
const shimPath = path.join(shimsDir, `${toolInfo.tool}.cmd`);
|
const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`;
|
||||||
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,4 +147,4 @@ describe("Setup CI shell integration", () => {
|
||||||
assert.ok(!fs.existsSync(unixNpmShim), "Unix npm shim should not exist on Windows");
|
assert.ok(!fs.existsSync(unixNpmShim), "Unix npm shim should not exist on Windows");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -79,26 +79,10 @@ end
|
||||||
|
|
||||||
# `python -m pip`, `python -m pip3`.
|
# `python -m pip`, `python -m pip3`.
|
||||||
function python
|
function python
|
||||||
if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2]
|
wrapSafeChainCommand "python" "aikido-python" $argv
|
||||||
set mod $argv[2]
|
|
||||||
set args $argv[3..-1]
|
|
||||||
if test $mod = "pip3"
|
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" $args
|
|
||||||
else
|
|
||||||
wrapSafeChainCommand "pip" "aikido-pip" $args
|
|
||||||
end
|
|
||||||
else
|
|
||||||
command python $argv
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# `python3 -m pip`, `python3 -m pip3'.
|
# `python3 -m pip`, `python3 -m pip3'.
|
||||||
function python3
|
function python3
|
||||||
if test (count $argv) -ge 2; and test $argv[1] = "-m"; and string match -qr '^pip(3)?$' -- $argv[2]
|
wrapSafeChainCommand "python3" "aikido-python3" $argv
|
||||||
set args $argv[3..-1]
|
|
||||||
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
|
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" $args
|
|
||||||
else
|
|
||||||
command python3 $argv
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -71,26 +71,10 @@ function pip3() {
|
||||||
|
|
||||||
# `python -m pip`, `python -m pip3`.
|
# `python -m pip`, `python -m pip3`.
|
||||||
function python() {
|
function python() {
|
||||||
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
wrapSafeChainCommand "python" "aikido-python" "$@"
|
||||||
local mod="$2"
|
|
||||||
shift 2
|
|
||||||
if [[ "$mod" == "pip3" ]]; then
|
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
|
||||||
else
|
|
||||||
wrapSafeChainCommand "pip" "aikido-pip" "$@"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
command python "$@"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# `python3 -m pip`, `python3 -m pip3'.
|
# `python3 -m pip`, `python3 -m pip3'.
|
||||||
function python3() {
|
function python3() {
|
||||||
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
wrapSafeChainCommand "python3" "aikido-python3" "$@"
|
||||||
shift 2
|
|
||||||
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
|
|
||||||
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
|
|
||||||
else
|
|
||||||
command python3 "$@"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,27 +97,11 @@ function pip3 {
|
||||||
|
|
||||||
# `python -m pip`, `python -m pip3`.
|
# `python -m pip`, `python -m pip3`.
|
||||||
function python {
|
function python {
|
||||||
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
|
Invoke-WrappedCommand 'python' 'aikido-python' $args
|
||||||
if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') {
|
|
||||||
$pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() }
|
|
||||||
if ($Args[1] -eq 'pip3') { Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs }
|
|
||||||
else { Invoke-WrappedCommand 'pip' 'aikido-pip' $pipArgs }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Invoke-RealCommand 'python' $Args
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# `python3 -m pip`, `python3 -m pip3'.
|
# `python3 -m pip`, `python3 -m pip3'.
|
||||||
function python3 {
|
function python3 {
|
||||||
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
|
Invoke-WrappedCommand 'python3' 'aikido-python3' $args
|
||||||
if ($Args.Length -ge 2 -and $Args[0] -eq '-m' -and $Args[1] -match '^pip(3)?$') {
|
|
||||||
# python3 always uses pip3, regardless of whether user types `pip` or `pip3`
|
|
||||||
$pipArgs = if ($Args.Length -gt 2) { $Args | Select-Object -Skip 2 } else { @() }
|
|
||||||
Invoke-WrappedCommand 'pip3' 'aikido-pip3' $pipArgs
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Invoke-RealCommand 'python3' $Args
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,19 @@ 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/pip3 /usr/local/bin/pip3
|
ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python && \
|
||||||
|
ln -sf /usr/bin/pip3 /usr/local/bin/pip3 && \
|
||||||
|
cat <<'EOF' > /usr/lib/python3/dist-packages/pip3.py
|
||||||
|
"""
|
||||||
|
Shim module so 'python[3] -m pip3 …' resolves to pip's CLI entry point.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import pip._internal
|
||||||
|
pip._internal.main()
|
||||||
|
except Exception as exc:
|
||||||
|
print("pip3 module shim failed:", exc)
|
||||||
|
raise
|
||||||
|
EOF
|
||||||
|
|
||||||
# Copy and install Safe chain
|
# Copy and install Safe chain
|
||||||
COPY --from=builder /app/*.tgz /pkgs/
|
COPY --from=builder /app/*.tgz /pkgs/
|
||||||
|
|
|
||||||
171
test/e2e/pip-ci.e2e.spec.js
Normal file
171
test/e2e/pip-ci.e2e.spec.js
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||||
|
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
|
||||||
|
let container;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
DockerTestContainer.buildImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
container = new DockerTestContainer();
|
||||||
|
await container.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (container) {
|
||||||
|
await container.stop();
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("E2E: pip CI support", () => {
|
||||||
|
it("does not intercept python3 --version", async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand("python3 --version");
|
||||||
|
assert.ok(result.output.match(/Python \d+\.\d+\.\d+/), `Output was: ${result.output}`);
|
||||||
|
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 command");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not intercept python3 -c 'print(\"hello\")'", async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand("python3 -c 'print(\"hello\")'");
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
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"]) {
|
||||||
|
it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => {
|
||||||
|
// Setup safe-chain CI shims
|
||||||
|
const installationShell = await container.openShell(shell);
|
||||||
|
await installationShell.runCommand("safe-chain setup-ci");
|
||||||
|
|
||||||
|
// Add $HOME/.safe-chain/shims to PATH for subsequent shells
|
||||||
|
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);
|
||||||
|
// Use --break-system-packages to avoid Debian/Ubuntu external management restrictions
|
||||||
|
const result = await projectShell.runCommand(
|
||||||
|
"pip3 install --break-system-packages certifi"
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasExpectedOutput = result.output.includes(
|
||||||
|
"no malware found."
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
hasExpectedOutput,
|
||||||
|
hasExpectedOutput
|
||||||
|
? "Expected pip3 command to be wrapped by safe-chain"
|
||||||
|
: `Output did not contain \"no malware found.\": \n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`setup-ci routes python -m pip 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 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 python3 -m pip 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(
|
||||||
|
"python3 -m 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 pip 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(
|
||||||
|
"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(
|
||||||
|
result.output.includes("no malware found."),
|
||||||
|
`Output did not contain scan message. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -323,4 +323,12 @@ describe("E2E: pip coverage", () => {
|
||||||
`Should not have SSL/certificate errors for tunneled hosts. Output was:\n${result.output}`
|
`Should not have SSL/certificate errors for tunneled hosts. Output was:\n${result.output}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`pip3 install requests with --safe-chain-logging=verbose`, async () => {
|
||||||
|
const shell = await container.openShell("zsh");
|
||||||
|
const result = await shell.runCommand(
|
||||||
|
"pip3 install --break-system-packages requests --safe-chain-logging=verbose"
|
||||||
|
);
|
||||||
|
assert.ok(result.output.includes("no malware found."), `Output did not include expected text. Output was:\n${result.output}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue