Merge pull request #152 from AikidoSec/pypi-feature-flag

Add feature flag in setup for python support.
This commit is contained in:
Sander Declerck 2025-11-24 10:00:23 +01:00 committed by GitHub
commit f34fb3576d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 425 additions and 100 deletions

View file

@ -32,6 +32,12 @@ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
```shell ```shell
safe-chain setup safe-chain setup
``` ```
To enable Python (pip/pip3) support (beta), use the `--include-python` flag:
```shell
safe-chain setup --include-python
```
3. **❗Restart your terminal** to start using the Aikido Safe Chain. 3. **❗Restart your terminal** to start using the Aikido Safe Chain.
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available. - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, and pip/pip3 are loaded correctly. If you do not restart your terminal, the aliases will not be available.
4. **Verify the installation** by running one of the following commands: 4. **Verify the installation** by running one of the following commands:
@ -120,6 +126,12 @@ To use Aikido Safe Chain in CI/CD environments, run the following command after
safe-chain setup-ci safe-chain setup-ci
``` ```
To enable Python (pip/pip3) support (beta) in CI/CD, use the `--include-python` flag:
```shell
safe-chain setup-ci --include-python
```
This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands. This automatically configures your CI environment to use Aikido Safe Chain for all package manager commands.
## Supported Platforms ## Supported Platforms

View file

@ -6,6 +6,7 @@ import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js"; import { setup } from "../src/shell-integration/setup.js";
import { teardown } from "../src/shell-integration/teardown.js"; import { teardown } from "../src/shell-integration/teardown.js";
import { setupCi } from "../src/shell-integration/setup-ci.js"; import { setupCi } from "../src/shell-integration/setup-ci.js";
import { initializeCliArguments } from "../src/config/cliArguments.js";
if (process.argv.length < 3) { if (process.argv.length < 3) {
ui.writeError("No command provided. Please provide a command to execute."); ui.writeError("No command provided. Please provide a command to execute.");
@ -14,6 +15,8 @@ if (process.argv.length < 3) {
process.exit(1); process.exit(1);
} }
initializeCliArguments(process.argv);
const command = process.argv[2]; const command = process.argv[2];
if (command === "help" || command === "--help" || command === "-h") { if (command === "help" || command === "--help" || command === "-h") {
@ -56,6 +59,11 @@ function writeHelp() {
"safe-chain setup" "safe-chain setup"
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.` )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`
); );
ui.writeInformation(
` ${chalk.yellow(
"--include-python"
)}: Experimental: include Python package managers (pip, pip3) in the setup.`
);
ui.writeInformation( ui.writeInformation(
`- ${chalk.cyan( `- ${chalk.cyan(
"safe-chain teardown" "safe-chain teardown"
@ -67,9 +75,14 @@ function writeHelp() {
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.` )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
); );
ui.writeInformation( ui.writeInformation(
`- ${chalk.cyan( ` ${chalk.yellow(
"safe-chain --version" "--include-python"
)} (or ${chalk.cyan("-v")}): Display the current version of safe-chain.` )}: Experimental: include Python package managers (pip, pip3) in the setup.`
);
ui.writeInformation(
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
"-v"
)}): Display the current version of safe-chain.`
); );
ui.emptyLine(); ui.emptyLine();
} }

View file

@ -1,8 +1,9 @@
/** /**
* @type {{loggingLevel: string | undefined}} * @type {{loggingLevel: string | undefined, includePython: boolean}}
*/ */
const state = { const state = {
loggingLevel: undefined, loggingLevel: undefined,
includePython: false,
}; };
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
@ -27,6 +28,7 @@ export function initializeCliArguments(args) {
} }
setLoggingLevel(safeChainArgs); setLoggingLevel(safeChainArgs);
setIncludePython(args);
return remainingArgs; return remainingArgs;
} }
@ -64,3 +66,31 @@ function setLoggingLevel(args) {
export function getLoggingLevel() { export function getLoggingLevel() {
return state.loggingLevel; return state.loggingLevel;
} }
/**
* @param {string[]} args
*/
function setIncludePython(args) {
// This flag doesn't have the --safe-chain- prefix because
// it is only used for the safe-chain command itself and
// not when wrapped around package manager commands.
state.includePython = hasFlagArg(args, "--include-python");
}
export function includePython() {
return state.includePython;
}
/**
* @param {string[]} args
* @param {string} flagName
* @returns {boolean}
*/
function hasFlagArg(args, flagName) {
for (const arg of args) {
if (arg.toLowerCase() === flagName.toLowerCase()) {
return true;
}
}
return false;
}

View file

@ -2,28 +2,30 @@ import { spawnSync } from "child_process";
import * as os from "os"; import * as os from "os";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
/** /**
* @typedef {Object} AikidoTool * @typedef {Object} AikidoTool
* @property {string} tool * @property {string} tool
* @property {string} aikidoCommand * @property {string} aikidoCommand
* @property {string} ecoSystem
*/ */
/** /**
* @type {AikidoTool[]} * @type {AikidoTool[]}
*/ */
export const knownAikidoTools = [ export const knownAikidoTools = [
{ tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npm", aikidoCommand: "aikido-npm", ecoSystem: ECOSYSTEM_JS },
{ tool: "npx", aikidoCommand: "aikido-npx" }, { tool: "npx", aikidoCommand: "aikido-npx", ecoSystem: ECOSYSTEM_JS },
{ tool: "yarn", aikidoCommand: "aikido-yarn" }, { tool: "yarn", aikidoCommand: "aikido-yarn", ecoSystem: ECOSYSTEM_JS },
{ tool: "pnpm", aikidoCommand: "aikido-pnpm" }, { tool: "pnpm", aikidoCommand: "aikido-pnpm", ecoSystem: ECOSYSTEM_JS },
{ tool: "pnpx", aikidoCommand: "aikido-pnpx" }, { tool: "pnpx", aikidoCommand: "aikido-pnpx", ecoSystem: ECOSYSTEM_JS },
{ tool: "bun", aikidoCommand: "aikido-bun" }, { tool: "bun", aikidoCommand: "aikido-bun", ecoSystem: ECOSYSTEM_JS },
{ tool: "bunx", aikidoCommand: "aikido-bunx" }, { tool: "bunx", aikidoCommand: "aikido-bunx", ecoSystem: ECOSYSTEM_JS },
{ tool: "pip", aikidoCommand: "aikido-pip" }, { tool: "pip", aikidoCommand: "aikido-pip", ecoSystem: ECOSYSTEM_PY },
{ tool: "pip3", aikidoCommand: "aikido-pip3" }, { tool: "pip3", aikidoCommand: "aikido-pip3", ecoSystem: ECOSYSTEM_PY },
{ tool: "python", aikidoCommand: "aikido-python" }, { tool: "python", aikidoCommand: "aikido-python", ecoSystem: ECOSYSTEM_PY },
{ tool: "python3", aikidoCommand: "aikido-python3" }, { tool: "python3", aikidoCommand: "aikido-python3", ecoSystem: ECOSYSTEM_PY },
// 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
]; ];

View file

@ -1,10 +1,12 @@
import chalk from "chalk"; import chalk from "chalk";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
import fs from "fs"; import fs from "fs";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
import { ECOSYSTEM_PY } from "../config/settings.js";
/** /**
* Loops over the detected shells and calls the setup function for each. * Loops over the detected shells and calls the setup function for each.
@ -53,7 +55,7 @@ function createUnixShims(shimsDir) {
// Create a shim for each tool // Create a shim for each tool
let created = 0; let created = 0;
for (const toolInfo of knownAikidoTools) { for (const toolInfo of getToolsToSetup()) {
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);
@ -66,9 +68,7 @@ function createUnixShims(shimsDir) {
created++; created++;
} }
ui.writeInformation( ui.writeInformation(`Created ${created} Unix shim(s) in ${shimsDir}`);
`Created ${created} Unix shim(s) in ${shimsDir}`
);
} }
/** /**
@ -96,19 +96,17 @@ function createWindowsShims(shimsDir) {
// Create a shim for each tool // Create a shim for each tool
let created = 0; let created = 0;
for (const toolInfo of knownAikidoTools) { for (const toolInfo of getToolsToSetup()) {
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 = `${shimsDir}/${toolInfo.tool}.cmd`; const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`;
fs.writeFileSync(shimPath, shimContent, "utf-8"); fs.writeFileSync(shimPath, shimContent, "utf-8");
created++; created++;
} }
ui.writeInformation( ui.writeInformation(`Created ${created} Windows shim(s) in ${shimsDir}`);
`Created ${created} Windows shim(s) in ${shimsDir}`
);
} }
/** /**
@ -145,3 +143,11 @@ function modifyPathForCi(shimsDir) {
ui.writeInformation("##vso[task.prependpath]" + shimsDir); ui.writeInformation("##vso[task.prependpath]" + shimsDir);
} }
} }
function getToolsToSetup() {
if (includePython()) {
return knownAikidoTools;
} else {
return knownAikidoTools.filter((tool) => tool.ecoSystem !== ECOSYSTEM_PY);
}
}

View file

@ -6,6 +6,7 @@ import fs from "fs";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { includePython } from "../config/cliArguments.js";
/** /**
* Loops over the detected shells and calls the setup function for each. * Loops over the detected shells and calls the setup function for each.
@ -104,7 +105,11 @@ function copyStartupFiles() {
// Use absolute path for source // Use absolute path for source
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const sourcePath = path.resolve(__dirname, "startup-scripts", file); const sourcePath = path.resolve(
__dirname,
includePython() ? "startup-scripts/include-python" : "startup-scripts",
file
);
fs.copyFileSync(sourcePath, targetPath); fs.copyFileSync(sourcePath, targetPath);
} }
} }

View file

@ -0,0 +1,88 @@
function printSafeChainWarning
set original_cmd $argv[1]
# Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
set_color -b yellow black
printf "Warning:"
set_color normal
printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
# Cyan text for the install command
printf "Install safe-chain by using "
set_color cyan
printf "npm install -g @aikidosec/safe-chain"
set_color normal
printf ".\n"
end
function wrapSafeChainCommand
set original_cmd $argv[1]
set aikido_cmd $argv[2]
set cmd_args $argv[3..-1]
if type -q $aikido_cmd
# If the aikido command is available, just run it with the provided arguments
$aikido_cmd $cmd_args
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning $original_cmd
command $original_cmd $cmd_args
end
end
function npx
wrapSafeChainCommand "npx" "aikido-npx" $argv
end
function yarn
wrapSafeChainCommand "yarn" "aikido-yarn" $argv
end
function pnpm
wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
end
function pnpx
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
end
function bun
wrapSafeChainCommand "bun" "aikido-bun" $argv
end
function bunx
wrapSafeChainCommand "bunx" "aikido-bunx" $argv
end
function npm
# If args is just -v or --version and nothing else, just run the `npm -v` command
# This is because nvm uses this to check the version of npm
set argc (count $argv)
if test $argc -eq 1
switch $argv[1]
case "-v" "--version"
command npm $argv
return
end
end
wrapSafeChainCommand "npm" "aikido-npm" $argv
end
function pip
wrapSafeChainCommand "pip" "aikido-pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
end
# `python -m pip`, `python -m pip3`.
function python
wrapSafeChainCommand "python" "aikido-python" $argv
end
# `python3 -m pip`, `python3 -m pip3'.
function python3
wrapSafeChainCommand "python3" "aikido-python3" $argv
end

View file

@ -0,0 +1,80 @@
function printSafeChainWarning() {
# \033[43;30m is used to set the background color to yellow and text color to black
# \033[0m is used to reset the text formatting
printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1"
# \033[36m is used to set the text color to cyan
printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n"
}
function wrapSafeChainCommand() {
local original_cmd="$1"
local aikido_cmd="$2"
# Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
# so that "$@" now contains only the arguments passed to the original command
shift 2
if command -v "$aikido_cmd" > /dev/null 2>&1; then
# If the aikido command is available, just run it with the provided arguments
"$aikido_cmd" "$@"
else
# If the aikido command is not available, print a warning and run the original command
printSafeChainWarning "$original_cmd"
command "$original_cmd" "$@"
fi
}
function npx() {
wrapSafeChainCommand "npx" "aikido-npx" "$@"
}
function yarn() {
wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
}
function pnpm() {
wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
}
function pnpx() {
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "aikido-bun" "$@"
}
function bunx() {
wrapSafeChainCommand "bunx" "aikido-bunx" "$@"
}
function npm() {
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
command npm "$@"
return
fi
wrapSafeChainCommand "npm" "aikido-npm" "$@"
}
function pip() {
wrapSafeChainCommand "pip" "aikido-pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "aikido-python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "aikido-python3" "$@"
}

View file

@ -0,0 +1,107 @@
function Write-SafeChainWarning {
param([string]$Command)
# PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:"
Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it."
# Cyan text for the install command
Write-Host "Install safe-chain by using " -NoNewline
Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline
Write-Host "."
}
function Test-CommandAvailable {
param([string]$Command)
try {
Get-Command $Command -ErrorAction Stop | Out-Null
return $true
}
catch {
return $false
}
}
function Invoke-RealCommand {
param(
[string]$Command,
[string[]]$Arguments
)
# Find the real executable to avoid calling our wrapped functions
$realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1
if ($realCommand) {
& $realCommand.Source @Arguments
}
}
function Invoke-WrappedCommand {
param(
[string]$OriginalCmd,
[string]$AikidoCmd,
[string[]]$Arguments
)
if (Test-CommandAvailable $AikidoCmd) {
& $AikidoCmd @Arguments
}
else {
Write-SafeChainWarning $OriginalCmd
Invoke-RealCommand $OriginalCmd $Arguments
}
}
function npx {
Invoke-WrappedCommand "npx" "aikido-npx" $args
}
function yarn {
Invoke-WrappedCommand "yarn" "aikido-yarn" $args
}
function pnpm {
Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
}
function pnpx {
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
}
function bun {
Invoke-WrappedCommand "bun" "aikido-bun" $args
}
function bunx {
Invoke-WrappedCommand "bunx" "aikido-bunx" $args
}
function npm {
# If args is just -v or --version and nothing else, just run the npm version command
# This is because nvm uses this to check the version of npm
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
Invoke-RealCommand "npm" $args
return
}
Invoke-WrappedCommand "npm" "aikido-npm" $args
}
function pip {
Invoke-WrappedCommand "pip" "aikido-pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
}
# `python -m pip`, `python -m pip3`.
function python {
Invoke-WrappedCommand 'python' 'aikido-python' $args
}
# `python3 -m pip`, `python3 -m pip3'.
function python3 {
Invoke-WrappedCommand 'python3' 'aikido-python3' $args
}

View file

@ -68,21 +68,3 @@ function npm
wrapSafeChainCommand "npm" "aikido-npm" $argv wrapSafeChainCommand "npm" "aikido-npm" $argv
end end
function pip
wrapSafeChainCommand "pip" "aikido-pip" $argv
end
function pip3
wrapSafeChainCommand "pip3" "aikido-pip3" $argv
end
# `python -m pip`, `python -m pip3`.
function python
wrapSafeChainCommand "python" "aikido-python" $argv
end
# `python3 -m pip`, `python3 -m pip3'.
function python3
wrapSafeChainCommand "python3" "aikido-python3" $argv
end

View file

@ -60,21 +60,3 @@ function npm() {
wrapSafeChainCommand "npm" "aikido-npm" "$@" wrapSafeChainCommand "npm" "aikido-npm" "$@"
} }
function pip() {
wrapSafeChainCommand "pip" "aikido-pip" "$@"
}
function pip3() {
wrapSafeChainCommand "pip3" "aikido-pip3" "$@"
}
# `python -m pip`, `python -m pip3`.
function python() {
wrapSafeChainCommand "python" "aikido-python" "$@"
}
# `python3 -m pip`, `python3 -m pip3'.
function python3() {
wrapSafeChainCommand "python3" "aikido-python3" "$@"
}

View file

@ -86,22 +86,3 @@ function npm {
Invoke-WrappedCommand "npm" "aikido-npm" $args Invoke-WrappedCommand "npm" "aikido-npm" $args
} }
function pip {
Invoke-WrappedCommand "pip" "aikido-pip" $args
}
function pip3 {
Invoke-WrappedCommand "pip3" "aikido-pip3" $args
}
# `python -m pip`, `python -m pip3`.
function python {
Invoke-WrappedCommand 'python' 'aikido-python' $args
}
# `python3 -m pip`, `python3 -m pip3'.
function python3 {
Invoke-WrappedCommand 'python3' 'aikido-python3' $args
}

View file

@ -25,31 +25,55 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it("does not intercept python3 --version", async () => { it("does not intercept python3 --version", async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
const result = await shell.runCommand("python3 --version"); const result = await shell.runCommand("python3 --version");
assert.ok(result.output.match(/Python \d+\.\d+\.\d+/), `Output was: ${result.output}`); assert.ok(
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 command"); 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 () => { it("does not intercept python3 -c 'print(\"hello\")'", async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
const result = await shell.runCommand("python3 -c 'print(\"hello\")'"); const result = await shell.runCommand("python3 -c 'print(\"hello\")'");
assert.ok(result.output.includes("hello"), `Output was: ${result.output}`); assert.ok(
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 -c command"); 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 () => { it("does not intercept python3 test.py", async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py");
const result = await shell.runCommand("python3 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(
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python3 script execution"); 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 () => { it("does not intercept python test.py", async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py"); await shell.runCommand("echo 'print(\"Hello from test.py!\")' > test.py");
const result = await shell.runCommand("python 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(
assert.ok(!result.output.includes("Safe-chain"), "Safe Chain should not intercept generic python script execution"); 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"
);
}); });
}); });
@ -57,7 +81,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => { it(`safe-chain setup-ci wraps pip3 command with PATH shim after installation for ${shell}`, async () => {
// Setup safe-chain CI shims // Setup safe-chain CI shims
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 --include-python"
);
// Add $HOME/.safe-chain/shims to PATH for subsequent shells // Add $HOME/.safe-chain/shims to PATH for subsequent shells
await installationShell.runCommand( await installationShell.runCommand(
@ -73,9 +99,7 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
"pip3 install --break-system-packages certifi" "pip3 install --break-system-packages certifi"
); );
const hasExpectedOutput = result.output.includes( const hasExpectedOutput = result.output.includes("no malware found.");
"no malware found."
);
assert.ok( assert.ok(
hasExpectedOutput, hasExpectedOutput,
hasExpectedOutput hasExpectedOutput
@ -86,7 +110,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes python -m pip through safe-chain for ${shell}`, async () => { it(`setup-ci routes python -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 --include-python"
);
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
); );
@ -107,7 +133,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
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 --include-python"
);
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
); );
@ -128,7 +156,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes pip 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 --include-python"
);
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
); );
@ -149,7 +179,9 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => {
it(`setup-ci routes pip3 through safe-chain for ${shell}`, async () => { it(`setup-ci routes pip3 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 --include-python"
);
await installationShell.runCommand( await installationShell.runCommand(
"echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc" "echo 'export PATH=\"$HOME/.safe-chain/shims:$PATH\"' >> ~/.zshrc"
); );

View file

@ -15,7 +15,7 @@ describe("E2E: pip coverage", () => {
await container.start(); await container.start();
const installationShell = await container.openShell("zsh"); const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup"); await installationShell.runCommand("safe-chain setup --include-python");
}); });
afterEach(async () => { afterEach(async () => {
@ -96,7 +96,9 @@ describe("E2E: pip coverage", () => {
it(`python3 -m pip install routes through safe-chain`, async () => { it(`python3 -m pip install routes through safe-chain`, async () => {
const shell = await container.openShell("zsh"); const shell = await container.openShell("zsh");
const result = await shell.runCommand("python3 -m pip install --break-system-packages requests"); const result = await shell.runCommand(
"python3 -m pip install --break-system-packages requests"
);
assert.ok( assert.ok(
result.output.includes("no malware found."), result.output.includes("no malware found."),
@ -329,6 +331,9 @@ describe("E2E: pip coverage", () => {
const result = await shell.runCommand( const result = await shell.runCommand(
"pip3 install --break-system-packages requests --safe-chain-logging=verbose" "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}`); assert.ok(
result.output.includes("no malware found."),
`Output did not include expected text. Output was:\n${result.output}`
);
}); });
}); });