Merge branch 'main' into package-min-age

This commit is contained in:
Sander Declerck 2025-11-24 14:15:55 +01:00
commit a04bea26da
No known key found for this signature in database
20 changed files with 770 additions and 112 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.
@ -132,6 +138,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

18
package-lock.json generated
View file

@ -411,6 +411,13 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@types/ini": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz",
"integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/make-fetch-happen": { "node_modules/@types/make-fetch-happen": {
"version": "10.0.4", "version": "10.0.4",
"resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.4.tgz",
@ -1090,6 +1097,15 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/ini": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@ -2083,6 +2099,7 @@
"certifi": "^14.5.15", "certifi": "^14.5.15",
"chalk": "5.4.1", "chalk": "5.4.1",
"https-proxy-agent": "7.0.6", "https-proxy-agent": "7.0.6",
"ini": "^6.0.0",
"make-fetch-happen": "14.0.3", "make-fetch-happen": "14.0.3",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"npm-registry-fetch": "18.0.2", "npm-registry-fetch": "18.0.2",
@ -2104,6 +2121,7 @@
"safe-chain": "bin/safe-chain.js" "safe-chain": "bin/safe-chain.js"
}, },
"devDependencies": { "devDependencies": {
"@types/ini": "^4.1.1",
"@types/make-fetch-happen": "^10.0.4", "@types/make-fetch-happen": "^10.0.4",
"@types/node": "^18.19.130", "@types/node": "^18.19.130",
"@types/node-forge": "^1.3.14", "@types/node-forge": "^1.3.14",

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

@ -38,6 +38,7 @@
"certifi": "^14.5.15", "certifi": "^14.5.15",
"chalk": "5.4.1", "chalk": "5.4.1",
"https-proxy-agent": "7.0.6", "https-proxy-agent": "7.0.6",
"ini": "^6.0.0",
"make-fetch-happen": "14.0.3", "make-fetch-happen": "14.0.3",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"npm-registry-fetch": "18.0.2", "npm-registry-fetch": "18.0.2",
@ -45,6 +46,7 @@
"semver": "7.7.2" "semver": "7.7.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ini": "^4.1.1",
"@types/make-fetch-happen": "^10.0.4", "@types/make-fetch-happen": "^10.0.4",
"@types/node": "^18.19.130", "@types/node": "^18.19.130",
"@types/npm-registry-fetch": "^8.0.9", "@types/npm-registry-fetch": "^8.0.9",

View file

@ -1,9 +1,10 @@
/** /**
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined}} * @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, includePython: boolean}}
*/ */
const state = { const state = {
loggingLevel: undefined, loggingLevel: undefined,
skipMinimumPackageAge: undefined, skipMinimumPackageAge: undefined,
includePython: false,
}; };
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
@ -30,6 +31,7 @@ export function initializeCliArguments(args) {
setLoggingLevel(safeChainArgs); setLoggingLevel(safeChainArgs);
setSkipMinimumPackageAge(safeChainArgs); setSkipMinimumPackageAge(safeChainArgs);
setIncludePython(args);
return remainingArgs; return remainingArgs;
} }
@ -97,3 +99,31 @@ function setSkipMinimumPackageAge(args) {
export function getSkipMinimumPackageAge() { export function getSkipMinimumPackageAge() {
return state.skipMinimumPackageAge; return state.skipMinimumPackageAge;
} }
/**
* @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,12 +2,53 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import fs from "node:fs/promises";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import ini from "ini";
/** /**
* @param {string} command * Sets fallback CA bundle environment variables used by Python libraries.
* @param {string[]} args * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
* network libraries respect the combined CA bundle, even if they don't read pip's config.
* *
* @returns {Promise<{status: number}>} * @param {NodeJS.ProcessEnv} env - Environment object to modify
* @param {string} combinedCaPath - Path to the combined CA bundle
*/
function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
// REQUESTS_CA_BUNDLE: Used by the popular 'requests' library
if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
}
env.REQUESTS_CA_BUNDLE = combinedCaPath;
// SSL_CERT_FILE: Used by some Python SSL libraries and urllib
if (env.SSL_CERT_FILE) {
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
}
env.SSL_CERT_FILE = combinedCaPath;
// PIP_CERT: Pip's own environment variable for certificate verification
if (env.PIP_CERT) {
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
}
env.PIP_CERT = combinedCaPath;
}
/**
* Runs a pip command with safe-chain's certificate bundle and proxy configuration.
*
* Creates a temporary pip config file to configure:
* - Cert bundle for HTTPS verification
* - Proxy settings
*
* If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
* their settings with safe-chain's, leaving the original file unchanged.
*
* @param {string} command - The pip command to execute (e.g., 'pip3')
* @param {string[]} args - Command line arguments to pass to pip
* @returns {Promise<{status: number}>} Exit status of the pip command
*/ */
export async function runPip(command, args) { export async function runPip(command, args) {
try { try {
@ -17,13 +58,85 @@ export async function runPip(command, args) {
// so that any network request made by pip, including those outside explicit CLI args, // so that any network request made by pip, including those outside explicit CLI args,
// validates correctly under both MITM'd and tunneled HTTPS. // validates correctly under both MITM'd and tunneled HTTPS.
const combinedCaPath = getCombinedCaBundlePath(); const combinedCaPath = getCombinedCaBundlePath();
env.REQUESTS_CA_BUNDLE = combinedCaPath;
env.SSL_CERT_FILE = combinedCaPath; // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
// will tell pip to use the provided CA bundle for HTTPS verification.
// Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active),
// otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables
const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
const tmpDir = os.tmpdir();
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
let cleanupConfigPath = null; // Track temp file for cleanup
// Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
if (!env.PIP_CONFIG_FILE) {
/** @type {{ global: { cert: string, proxy?: string } }} */
const configObj = { global: { cert: combinedCaPath } };
if (proxy) {
configObj.global.proxy = proxy;
}
const pipConfig = ini.stringify(configObj);
await fs.writeFile(pipConfigPath, pipConfig);
env.PIP_CONFIG_FILE = pipConfigPath;
cleanupConfigPath = pipConfigPath;
} else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
const userConfig = env.PIP_CONFIG_FILE;
// Read the existing config without modifying it
let content = await fs.readFile(userConfig, "utf-8");
const parsed = ini.parse(content);
// Ensure [global] section exists
parsed.global = parsed.global || {};
// Cert
if (typeof parsed.global.cert !== "undefined") {
ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
}
parsed.global.cert = combinedCaPath;
// Proxy
if (typeof parsed.global.proxy !== "undefined") {
ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
}
if (proxy) {
parsed.global.proxy = proxy;
}
const updated = ini.stringify(parsed);
// Save to a new temp file to avoid overwriting user's original config
await fs.writeFile(pipConfigPath, updated, "utf-8");
env.PIP_CONFIG_FILE = pipConfigPath;
cleanupConfigPath = pipConfigPath;
} else {
// The user provided PIP_CONFIG_FILE does not exist on disk
// PIP will handle this as an error and inform the user
}
// Set fallback CA bundle environment variables for Python libraries that don't read pip config
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
const result = await safeSpawn(command, args, { const result = await safeSpawn(command, args, {
stdio: "inherit", stdio: "inherit",
env, env,
}); });
// Cleanup temporary config file if we created one
if (cleanupConfigPath) {
try {
await fs.unlink(cleanupConfigPath);
} catch {
// Ignore cleanup errors - the file may have already been deleted or is inaccessible
// Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform
}
}
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { if (error.status) {

View file

@ -1,29 +1,48 @@
import { describe, it, beforeEach, afterEach, mock } from "node:test"; import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import ini from "ini";
describe("runPipCommand environment variable handling", () => { describe("runPipCommand environment variable handling", () => {
let runPip; let runPip;
let capturedArgs = null; let capturedArgs = null;
let customEnv = null;
let capturedConfigContent = null; // Capture config file content before cleanup
beforeEach(async () => { beforeEach(async () => {
capturedArgs = null; capturedArgs = null;
capturedConfigContent = null;
// Mock safeSpawn to capture args // Mock safeSpawn to capture args and config file content before cleanup
mock.module("../../utils/safeSpawn.js", { mock.module("../../utils/safeSpawn.js", {
namedExports: { namedExports: {
safeSpawn: async (command, args, options) => { safeSpawn: async (command, args, options) => {
capturedArgs = { command, args, options }; capturedArgs = { command, args, options };
// Capture the config file content before the function cleans it up
if (options.env.PIP_CONFIG_FILE) {
try {
capturedConfigContent = await fs.readFile(options.env.PIP_CONFIG_FILE, "utf-8");
} catch {
// Ignore if file doesn't exist or can't be read
}
}
return { status: 0 }; return { status: 0 };
}, },
}, },
}); });
// Mock proxy env merge // Mock proxy env merge, allow custom env override
mock.module("../../registryProxy/registryProxy.js", { mock.module("../../registryProxy/registryProxy.js", {
namedExports: { namedExports: {
mergeSafeChainProxyEnvironmentVariables: (env) => ({ mergeSafeChainProxyEnvironmentVariables: (env) => ({
...env, ...env,
...customEnv,
// Force deterministic proxy for tests regardless of ambient env
GLOBAL_AGENT_HTTP_PROXY: "http://localhost:8080",
HTTPS_PROXY: "http://localhost:8080", HTTPS_PROXY: "http://localhost:8080",
HTTP_PROXY: "",
}), }),
}, },
}); });
@ -43,6 +62,23 @@ describe("runPipCommand environment variable handling", () => {
mock.reset(); mock.reset();
}); });
it("should set PIP_CERT env var and create config file", async () => {
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedArgs, "safeSpawn should have been called");
// Check PIP_CERT env var
assert.strictEqual(
capturedArgs.options.env.PIP_CERT,
"/tmp/test-combined-ca.pem",
"PIP_CERT should be set to combined bundle path"
);
// Check PIP_CONFIG_FILE env var exists and is a non-empty string
const configPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.ok(configPath, "PIP_CONFIG_FILE should be set");
assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string");
assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path");
});
it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => { it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => {
const res = await runPip("pip3", ["install", "requests"]); const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0); assert.strictEqual(res.status, 0);
@ -60,9 +96,6 @@ describe("runPipCommand environment variable handling", () => {
"/tmp/test-combined-ca.pem", "/tmp/test-combined-ca.pem",
"SSL_CERT_FILE should be set to combined bundle path" "SSL_CERT_FILE should be set to combined bundle path"
); );
// Args should be unchanged (no arg injection)
assert.deepStrictEqual(capturedArgs.args, ["install", "requests"]);
}); });
it("should set CA environment variables even for external/test PyPI mirror (covers non-CLI traffic)", async () => { it("should set CA environment variables even for external/test PyPI mirror (covers non-CLI traffic)", async () => {
@ -110,4 +143,161 @@ describe("runPipCommand environment variable handling", () => {
"HTTPS_PROXY should be set by proxy merge" "HTTPS_PROXY should be set by proxy merge"
); );
}); });
it("should create a new temp config when existing config exists (original file untouched)", async () => {
const tmpDir = os.tmpdir();
const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
const initial = "[global]\nindex-url = https://example.com/simple\n";
await fs.writeFile(userCfgPath, initial, "utf-8");
customEnv = { PIP_CONFIG_FILE: userCfgPath };
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual(newCfgPath, userCfgPath, "should point to a new temp config file");
// Original file unchanged
const originalContent = await fs.readFile(userCfgPath, "utf-8");
const originalParsed = ini.parse(originalContent);
assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert");
// New file has merged settings (read from captured content before cleanup)
assert.ok(capturedConfigContent, "config content should have been captured");
const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert");
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env");
assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved");
customEnv = null;
});
it("should create new config with proxy set from env (ini-validated)", async () => {
// No PIP_CONFIG_FILE in env => creation path
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);
assert.ok(capturedConfigContent, "config content should have been captured");
const parsed = ini.parse(capturedConfigContent);
assert.ok(parsed.global, "[global] should exist after creation");
assert.strictEqual(
parsed.global.proxy,
"http://localhost:8080",
"proxy should be set from merged env"
);
assert.strictEqual(
parsed.global.cert,
"/tmp/test-combined-ca.pem",
"cert should be set during creation"
);
});
it("should create new temp config adding cert but preserving existing proxy (original file unchanged)", async () => {
const tmpDir = os.tmpdir();
const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
const initial = "[global]\nproxy = http://original:9999\n";
await fs.writeFile(userCfgPath, initial, "utf-8");
customEnv = { PIP_CONFIG_FILE: userCfgPath };
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual(newCfgPath, userCfgPath, "should use a new temp config file");
// Original file unchanged
const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8"));
assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert");
assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains");
// New file: cert and proxy always overwritten (read from captured content)
assert.ok(capturedConfigContent, "config content should have been captured");
const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
customEnv = null;
});
it("should create new temp config preserving existing cert and proxy while leaving original file unchanged", async () => {
const tmpDir = os.tmpdir();
const cfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
const initialIni = [
"[global]",
"cert = /path/to/existing.pem",
"proxy = http://original:9999",
""
].join("\n");
await fs.writeFile(cfgPath, initialIni, "utf-8");
customEnv = { PIP_CONFIG_FILE: cfgPath };
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0, "execution should succeed");
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual(newCfgPath, cfgPath, "should use a newly generated temp config file");
// Original file stays untouched
const originalContent = await fs.readFile(cfgPath, "utf-8");
const originalParsed = ini.parse(originalContent);
assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved");
assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved");
// New temp config: cert and proxy always overwritten (read from captured content)
assert.ok(capturedConfigContent, "config content should have been captured");
const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
customEnv = null;
});
it("should create new temp config preserving existing cert and adding missing proxy", async () => {
const tmpDir = os.tmpdir();
const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
const initial = "[global]\ncert = /path/to/existing.pem\n";
await fs.writeFile(userCfgPath, initial, "utf-8");
customEnv = { PIP_CONFIG_FILE: userCfgPath };
const res = await runPip("pip3", ["install", "requests"]);
assert.strictEqual(res.status, 0);
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
assert.notStrictEqual(newCfgPath, userCfgPath, "should produce a new temp config file");
// Original remains unchanged
const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8"));
assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged");
assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing");
// New file: cert and proxy always overwritten (read from captured content)
assert.ok(capturedConfigContent, "config content should have been captured");
const newParsed = ini.parse(capturedConfigContent);
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
customEnv = null;
});
it("should log warnings when cert and proxy are already set in user config file", async () => {
const tmpDir = os.tmpdir();
const cfgPath = path.join(tmpDir, `safe-chain-test-pip-warn-${Date.now()}.ini`);
const initialIni = [
"[global]",
"cert = /user/cert.pem",
"proxy = http://user-proxy:9999",
""
].join("\n");
await fs.writeFile(cfgPath, initialIni, "utf-8");
customEnv = { PIP_CONFIG_FILE: cfgPath };
// Capture stdout/stderr
let output = "";
const originalWrite = process.stdout.write;
const originalError = process.stderr.write;
process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); };
process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); };
await runPip("pip3", ["install", "requests"]);
process.stdout.write = originalWrite;
process.stderr.write = originalError;
assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output");
assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output");
customEnv = null;
});
}); });

View file

@ -48,6 +48,16 @@ export function generateCertForHost(hostname) {
digitalSignature: true, digitalSignature: true,
keyEncipherment: true, keyEncipherment: true,
}, },
{
/*
extKeyUsage serverAuth is required for TLS server authentication.
This is especially important for Python venv environments, which use their own
certificate validation logic and will reject certificates lacking the serverAuth EKU.
Adding serverAuth does not impact other usages
*/
name: "extKeyUsage",
serverAuth: true,
},
]); ]);
cert.sign(ca.privateKey, forge.md.sha256.create()); cert.sign(ca.privateKey, forge.md.sha256.create());

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,7 +96,7 @@ 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);
@ -106,9 +106,7 @@ function createWindowsShims(shimsDir) {
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}`
);
}); });
}); });