mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Skeleton for CI support
This commit is contained in:
parent
18f30ac66e
commit
6241c56fda
5 changed files with 316 additions and 11 deletions
19
README.md
19
README.md
|
|
@ -165,3 +165,22 @@ This automatically configures your CI environment to use Aikido Safe Chain for a
|
||||||
```
|
```
|
||||||
|
|
||||||
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
|
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
|
||||||
|
|
||||||
|
### Python (pip/pip3) example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Setup safe-chain
|
||||||
|
run: |
|
||||||
|
npm i -g @aikidosec/safe-chain
|
||||||
|
safe-chain setup-ci
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
pip3 install --upgrade pip
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Generated wrapper for python/python3 by safe-chain
|
||||||
|
# Intercepts `python[3] -m pip[...]` in CI environments
|
||||||
|
|
||||||
|
# Function to remove shim from PATH (POSIX-compliant)
|
||||||
|
remove_shim_from_path() {
|
||||||
|
echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine which python variant we were invoked as based on the script name
|
||||||
|
invoked=$(basename "$0")
|
||||||
|
|
||||||
|
# If invoked as `python -m pip[...]` or `python3 -m pip[...]`, route to aikido
|
||||||
|
if [ "$1" = "-m" ] && [ -n "$2" ] && echo "$2" | grep -Eq '^pip(3)?$'; then
|
||||||
|
mod="$2"
|
||||||
|
shift 2
|
||||||
|
if [ "$invoked" = "python3" ] || [ "$mod" = "pip3" ]; then
|
||||||
|
PATH=$(remove_shim_from_path) exec aikido-pip3 "$@"
|
||||||
|
else
|
||||||
|
PATH=$(remove_shim_from_path) exec aikido-pip "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Otherwise, find and exec the real python/python3 matching the invoked name
|
||||||
|
original_cmd=$(PATH=$(remove_shim_from_path) command -v "$invoked")
|
||||||
|
if [ -n "$original_cmd" ]; then
|
||||||
|
exec "$original_cmd" "$@"
|
||||||
|
else
|
||||||
|
echo "Error: Could not find original $invoked" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
@echo off
|
||||||
|
REM Generated wrapper for python/python3 by safe-chain
|
||||||
|
REM Intercepts `python[3] -m pip[...]` in CI environments
|
||||||
|
|
||||||
|
REM Remove shim directory from PATH to prevent infinite loops
|
||||||
|
set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
|
||||||
|
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
||||||
|
|
||||||
|
REM Determine invoked name (python or python3) from the script name
|
||||||
|
set "INVOKED=%~n0"
|
||||||
|
|
||||||
|
REM Check for -m pip or -m pip3
|
||||||
|
if "%1"=="-m" (
|
||||||
|
if /I "%2"=="pip3" (
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
set "PATH=%CLEAN_PATH%" & aikido-pip3 %*
|
||||||
|
goto :eof
|
||||||
|
)
|
||||||
|
if /I "%2"=="pip" (
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
if /I "%INVOKED%"=="python3" (
|
||||||
|
set "PATH=%CLEAN_PATH%" & aikido-pip3 %*
|
||||||
|
) else (
|
||||||
|
set "PATH=%CLEAN_PATH%" & aikido-pip %*
|
||||||
|
)
|
||||||
|
goto :eof
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Fallback to real python/python3 matching the invoked name
|
||||||
|
for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where %INVOKED% 2^>nul') do (
|
||||||
|
"%%i" %*
|
||||||
|
goto :eof
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Error: Could not find original %INVOKED% 1>&2
|
||||||
|
exit /b 1
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -70,11 +66,54 @@ function createUnixShims(shimsDir) {
|
||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also create python and python3 shims to support `python[3] -m pip[3]` in CI
|
||||||
|
createUnixPythonShims(shimsDir);
|
||||||
|
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`Created ${created} Unix shim(s) in ${shimsDir}`
|
`Created ${created} Unix shim(s) in ${shimsDir}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} shimsDir
|
||||||
|
*/
|
||||||
|
function createUnixPythonShims(shimsDir) {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const entries = [
|
||||||
|
{
|
||||||
|
name: "python",
|
||||||
|
template: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"path-wrappers",
|
||||||
|
"templates",
|
||||||
|
"unix-python-wrapper.template.sh"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "python3",
|
||||||
|
template: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"path-wrappers",
|
||||||
|
"templates",
|
||||||
|
"unix-python-wrapper.template.sh"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!fs.existsSync(entry.template)) {
|
||||||
|
ui.writeError(`Template file not found: ${entry.template}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const shimContent = fs.readFileSync(entry.template, "utf-8");
|
||||||
|
const shimPath = `${shimsDir}/${entry.name}`;
|
||||||
|
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
||||||
|
fs.chmodSync(shimPath, 0o755);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} shimsDir
|
* @param {string} shimsDir
|
||||||
*
|
*
|
||||||
|
|
@ -98,27 +137,65 @@ 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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also create python and python3 shims for Windows to support `python[3] -m pip[3]` in CI
|
||||||
|
createWindowsPythonShims(shimsDir);
|
||||||
|
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`Created ${created} Windows shim(s) in ${shimsDir}`
|
`Created ${created} Windows shim(s) in ${shimsDir}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} shimsDir
|
||||||
|
*/
|
||||||
|
function createWindowsPythonShims(shimsDir) {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const entries = [
|
||||||
|
{
|
||||||
|
name: "python.cmd",
|
||||||
|
template: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"path-wrappers",
|
||||||
|
"templates",
|
||||||
|
"windows-python-wrapper.template.cmd"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "python3.cmd",
|
||||||
|
template: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"path-wrappers",
|
||||||
|
"templates",
|
||||||
|
"windows-python-wrapper.template.cmd"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!fs.existsSync(entry.template)) {
|
||||||
|
ui.writeError(`Windows template file not found: ${entry.template}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const shimContent = fs.readFileSync(entry.template, "utf-8");
|
||||||
|
const shimPath = `${shimsDir}/${entry.name}`;
|
||||||
|
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} shimsDir
|
* @param {string} shimsDir
|
||||||
*
|
*
|
||||||
|
|
|
||||||
139
test/e2e/pip-ci.e2e.spec.js
Normal file
139
test/e2e/pip-ci.e2e.spec.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
|
"Scanning for malicious packages..."
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
hasExpectedOutput,
|
||||||
|
hasExpectedOutput
|
||||||
|
? "Expected pip3 command to be wrapped by safe-chain"
|
||||||
|
: `Output did not contain \"Scanning for malicious packages...\": \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("Scanning for malicious packages..."),
|
||||||
|
`Output did not contain scan message. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("Scanning for malicious packages..."),
|
||||||
|
`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("Scanning for malicious packages..."),
|
||||||
|
`Output did not contain scan message. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`setup-ci routes python3 -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(
|
||||||
|
"python3 -m pip3 install --break-system-packages certifi"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.output.includes("Scanning for malicious packages..."),
|
||||||
|
`Output did not contain scan message. Output was:\n${result.output}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue