Implement e2e tests

This commit is contained in:
Reinier Criel 2025-10-23 11:41:13 -07:00
parent f817bf887a
commit 059cba06bc
17 changed files with 163 additions and 293 deletions

34
package-lock.json generated
View file

@ -154,8 +154,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@oven/bun-darwin-x64": {
"version": "1.2.21",
@ -168,8 +167,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@oven/bun-darwin-x64-baseline": {
"version": "1.2.21",
@ -182,8 +180,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@oven/bun-linux-aarch64": {
"version": "1.2.21",
@ -196,8 +193,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@oven/bun-linux-aarch64-musl": {
"version": "1.2.21",
@ -210,8 +206,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@oven/bun-linux-x64": {
"version": "1.2.21",
@ -224,8 +219,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@oven/bun-linux-x64-baseline": {
"version": "1.2.21",
@ -238,8 +232,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@oven/bun-linux-x64-musl": {
"version": "1.2.21",
@ -252,8 +245,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@oven/bun-linux-x64-musl-baseline": {
"version": "1.2.21",
@ -266,8 +258,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@oven/bun-windows-x64": {
"version": "1.2.21",
@ -280,8 +271,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@oven/bun-windows-x64-baseline": {
"version": "1.2.21",
@ -294,8 +284,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@oxlint/darwin-arm64": {
"version": "1.22.0",
@ -1735,6 +1724,7 @@
"aikido-bunx": "bin/aikido-bunx.js",
"aikido-npm": "bin/aikido-npm.js",
"aikido-npx": "bin/aikido-npx.js",
"aikido-pip": "bin/aikido-pip.js",
"aikido-pnpm": "bin/aikido-pnpm.js",
"aikido-pnpx": "bin/aikido-pnpx.js",
"aikido-yarn": "bin/aikido-yarn.js",

View file

@ -8,20 +8,19 @@ import { setEcoSystem } from "../src/config/settings.js";
let packageManagerName = "pip";
let targetVersionMajor;
// Copy argv so we can mutate while parsing
// Copy argv so we can modify it
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const a = argv[i];
// --target-version-major
if (a === "--target-version-major" && i + 1 < argv.length) {
console.log("Setting targetVersionMajor from CLI arg:", argv[i + 1]);
targetVersionMajor = argv[i + 1];
argv.splice(i, 2);
i -= 1;
continue;
}
// --target-version-major tells us which pip version is being used (2 or 3)
if (a === "--target-version-major" && i + 1 < argv.length) {
targetVersionMajor = argv[i + 1];
argv.splice(i, 2);
i -= 1;
continue;
}
}
// If the user explicitly called python3, prefer pip3
@ -29,12 +28,10 @@ if (targetVersionMajor && String(targetVersionMajor).trim() === "3") {
packageManagerName = "pip3";
}
console.log("** aikido-pip ** Final arguments (after processing):", argv);
// Set eco system
setEcoSystem("py");
initializePackageManager(packageManagerName);
var exitCode = await main(argv);
const exitCode = await main(argv);
process.exit(exitCode);

View file

@ -3,16 +3,22 @@ import { getEcoSystem } from "../config/settings.js";
const malwareDatabaseUrls = {
js: "https://malware-list.aikido.dev/malware_predictions.json",
python: "https://malware-list.aikido.dev/malware_predictions_python.json",
py: "https://malware-list.aikido.dev/malware_predictions_python.json",
};
export async function fetchMalwareDatabase() {
const ecosystem = getEcoSystem() || "js";
if (ecosystem === "py") {
console.log("**aikido.js** Using 'python' ecosystem for malware database fetch");
}
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
const response = await fetch(malwareDatabaseUrl);
// Python malware database doesn't exist yet, return empty database
if (!response.ok && ecosystem === "py" && response.status === 403) {
return {
malwareDatabase: [],
version: undefined,
};
}
if (!response.ok) {
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`);
}
@ -30,14 +36,17 @@ export async function fetchMalwareDatabase() {
export async function fetchMalwareDatabaseVersion() {
const ecosystem = getEcoSystem() || "js";
if (ecosystem === "py") {
console.log("**aikido.js** Using 'python' ecosystem for malware database fetch");
}
const malwareDatabaseUrl = malwareDatabaseUrls[ecosystem];
const response = await fetch(malwareDatabaseUrl, {
method: "HEAD",
});
// Python malware database doesn't exist yet, return undefined
if (!response.ok && ecosystem === "py" && response.status === 403) {
return undefined;
}
if (!response.ok) {
throw new Error(
`Error fetching ${ecosystem} malware database version: ${response.statusText}`

View file

@ -12,7 +12,6 @@ export async function main(args) {
await proxy.startServer();
try {
console.log(chalk.blueBright.bold("main.js: Scanning for malicious packages..."));
// This parses all the --safe-chain arguments and removes them from the args array
args = initializeCliArguments(args);

View file

@ -15,8 +15,6 @@ const state = {
packageManagerName: null,
};
const PIP_COMMANDS = new Set(["pip", "pip3"]);
export function initializePackageManager(packageManagerName) {
if (packageManagerName === "npm") {
state.packageManagerName = createNpmPackageManager();
@ -32,7 +30,7 @@ export function initializePackageManager(packageManagerName) {
state.packageManagerName = createBunPackageManager();
} else if (packageManagerName === "bunx") {
state.packageManagerName = createBunxPackageManager();
} else if (PIP_COMMANDS.has(packageManagerName)) {
} else if (packageManagerName === "pip" || packageManagerName === "pip3") {
state.packageManagerName = createPipPackageManager(packageManagerName);
} else {
throw new Error("Unsupported package manager: " + packageManagerName);

View file

@ -1,5 +1,4 @@
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
import { nullScanner } from "./dependencyScanner/nullScanner.js";
import { runPip } from "./runPipCommand.js";
import {
getPipCommandForArgs,
@ -9,8 +8,7 @@ import {
} from "./utils/pipCommands.js";
/**
* Creates a package manager interface for Python's pip package installer
*
* Creates a package manager
* @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip"
*/
export function createPipPackageManager(command = "pip") {
@ -41,15 +39,20 @@ const commandScannerMapping = {
[pipInstallCommand]: commandArgumentScanner(),
[pipDownloadCommand]: commandArgumentScanner(), // download also fetches packages from PyPI
[pipWheelCommand]: commandArgumentScanner(), // wheel downloads and builds packages
// Other commands (uninstall, list, etc.) will use nullScanner() by default
// Other commands return null scanner by default
};
const NULL_SCANNER = {
shouldScan: () => false,
scan: () => [],
};
function findDependencyScannerForCommand(scanners, args) {
const command = getPipCommandForArgs(args);
if (!command) {
return nullScanner();
return NULL_SCANNER;
}
const scanner = scanners[command];
return scanner ? scanner : nullScanner();
return scanner || NULL_SCANNER;
}

View file

@ -3,7 +3,7 @@ import assert from "node:assert";
import { createPipPackageManager } from "./createPackageManager.js";
test("createPipPackageManager", async (t) => {
await t.test("should create package manager with default pip command", () => {
await t.test("should create package manager with required interface", () => {
const pm = createPipPackageManager();
assert.ok(pm);
@ -12,106 +12,49 @@ test("createPipPackageManager", async (t) => {
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
});
await t.test("should create package manager with custom pip3 command", () => {
await t.test("should accept pip3 as command parameter", () => {
const pm = createPipPackageManager("pip3");
assert.ok(pm);
assert.strictEqual(typeof pm.runCommand, "function");
});
await t.test("should recognize install command as supported", () => {
await t.test("should support install, download, and wheel commands", () => {
const pm = createPipPackageManager();
// Note: Currently returns false because commandArgumentScanner is not yet implemented
// When implemented, this should return true
const result = pm.isSupportedCommand(["install", "requests"]);
assert.strictEqual(typeof result, "boolean");
assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), true);
assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), true);
assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), true);
});
await t.test("should recognize download command as supported", () => {
await t.test("should not support uninstall and info commands", () => {
const pm = createPipPackageManager();
const result = pm.isSupportedCommand(["download", "requests"]);
assert.strictEqual(typeof result, "boolean");
assert.strictEqual(pm.isSupportedCommand(["uninstall", "requests"]), false);
assert.strictEqual(pm.isSupportedCommand(["list"]), false);
assert.strictEqual(pm.isSupportedCommand(["show", "requests"]), false);
});
await t.test("should recognize wheel command as supported", () => {
await t.test("should extract packages from install command", () => {
const pm = createPipPackageManager();
const result = pm.isSupportedCommand(["wheel", "requests"]);
assert.strictEqual(typeof result, "boolean");
});
await t.test("should not support uninstall command", () => {
const pm = createPipPackageManager();
const result = pm.isSupportedCommand(["uninstall", "requests"]);
assert.strictEqual(result, false);
});
await t.test("should not support list command", () => {
const pm = createPipPackageManager();
const result = pm.isSupportedCommand(["list"]);
assert.strictEqual(result, false);
});
await t.test("should not support show command", () => {
const pm = createPipPackageManager();
const result = pm.isSupportedCommand(["show", "requests"]);
assert.strictEqual(result, false);
});
await t.test("should return empty array for getDependencyUpdatesForCommand on install", () => {
const pm = createPipPackageManager();
// Note: Currently returns [] because commandArgumentScanner is not yet implemented
const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]);
assert.ok(Array.isArray(result));
});
await t.test("should return empty array for getDependencyUpdatesForCommand on download", () => {
const pm = createPipPackageManager();
const result = pm.getDependencyUpdatesForCommand(["download", "flask"]);
assert.ok(Array.isArray(result));
});
await t.test("should return empty array for getDependencyUpdatesForCommand on wheel", () => {
const pm = createPipPackageManager();
const result = pm.getDependencyUpdatesForCommand(["wheel", "django"]);
assert.ok(Array.isArray(result));
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, "requests");
assert.strictEqual(result[0].version, "2.28.0");
});
await t.test("should return empty array for unsupported commands", () => {
const pm = createPipPackageManager();
const result = pm.getDependencyUpdatesForCommand(["uninstall", "requests"]);
assert.strictEqual(Array.isArray(result), true);
assert.ok(Array.isArray(result));
assert.strictEqual(result.length, 0);
});
await t.test("should handle empty args array", () => {
await t.test("should handle empty args gracefully", () => {
const pm = createPipPackageManager();
const supported = pm.isSupportedCommand([]);
assert.strictEqual(supported, false);
const deps = pm.getDependencyUpdatesForCommand([]);
assert.ok(Array.isArray(deps));
assert.strictEqual(deps.length, 0);
});
await t.test("should handle args with only flags", () => {
const pm = createPipPackageManager();
const supported = pm.isSupportedCommand(["--version"]);
assert.strictEqual(supported, false);
const deps = pm.getDependencyUpdatesForCommand(["-h", "--help"]);
assert.ok(Array.isArray(deps));
assert.strictEqual(deps.length, 0);
assert.strictEqual(pm.isSupportedCommand([]), false);
assert.deepStrictEqual(pm.getDependencyUpdatesForCommand([]), []);
});
});

View file

@ -1,13 +1,6 @@
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
import { hasDryRunArg } from "../utils/pipCommands.js";
/**
* Scanner for pip command arguments to detect package installations
*
* @param {Object} options - Scanner options
* @param {boolean} [options.ignoreDryRun=false] - Whether to ignore dry-run flag
* @returns {Object} Scanner interface
*/
export function commandArgumentScanner(options = {}) {
const { ignoreDryRun = false } = options;
@ -33,15 +26,6 @@ function scanDependencies(args) {
return checkChangesFromArgs(args);
}
/**
* Extracts package changes from pip command arguments
*
* Unlike npm, pip's parser already returns exact versions (== or ===)
* or "latest" for unversioned packages, so no version resolution is needed.
*
* @param {string[]} args - Command line arguments
* @returns {Array<{name: string, version: string, type: string}>} Package changes
*/
export function checkChangesFromArgs(args) {
const packageUpdates = parsePackagesFromInstallArgs(args);

View file

@ -2,22 +2,14 @@ import { test } from "node:test";
import assert from "node:assert";
import { commandArgumentScanner, checkChangesFromArgs } from "./commandArgumentScanner.js";
test("commandArgumentScanner", async (t) => {
await t.test("should create scanner with default options", () => {
test("commandArgumentScanner factory", async (t) => {
await t.test("should create scanner with required interface", () => {
const scanner = commandArgumentScanner();
assert.ok(scanner);
assert.strictEqual(typeof scanner.shouldScan, "function");
assert.strictEqual(typeof scanner.scan, "function");
});
await t.test("should create scanner with ignoreDryRun option", () => {
const scanner = commandArgumentScanner({ ignoreDryRun: true });
assert.ok(scanner);
assert.strictEqual(typeof scanner.shouldScan, "function");
assert.strictEqual(typeof scanner.scan, "function");
});
});
test("shouldScan", async (t) => {
@ -41,20 +33,6 @@ test("shouldScan", async (t) => {
const result = scanner.shouldScan(["install", "--dry-run", "requests"]);
assert.strictEqual(result, true);
});
await t.test("should return true for download command", () => {
const scanner = commandArgumentScanner();
const result = scanner.shouldScan(["download", "flask"]);
assert.strictEqual(result, true);
});
await t.test("should return true for wheel command", () => {
const scanner = commandArgumentScanner();
const result = scanner.shouldScan(["wheel", "django"]);
assert.strictEqual(result, true);
});
});
test("scan", async (t) => {
@ -129,46 +107,6 @@ test("scan", async (t) => {
});
});
await t.test("should work with download command", () => {
const scanner = commandArgumentScanner();
const result = scanner.scan(["download", "django==4.2.0"]);
assert.strictEqual(result.length, 1);
assert.deepEqual(result[0], {
name: "django",
version: "4.2.0",
type: "add",
});
});
await t.test("should work with wheel command", () => {
const scanner = commandArgumentScanner();
const result = scanner.scan(["wheel", "numpy==1.24.0"]);
assert.strictEqual(result.length, 1);
assert.deepEqual(result[0], {
name: "numpy",
version: "1.24.0",
type: "add",
});
});
await t.test("should parse packages even for unsupported commands", () => {
const scanner = commandArgumentScanner();
// Note: The parser treats the first non-flag arg as the command and skips it
// So "uninstall" is treated as the command, and "requests" is parsed as a package
// The scanner itself doesn't filter by command type - that's done at a higher level
const result = scanner.scan(["uninstall", "requests"]);
assert.ok(Array.isArray(result));
assert.strictEqual(result.length, 1);
assert.deepEqual(result[0], {
name: "requests",
version: "latest",
type: "add",
});
});
await t.test("should handle === exact version specifier", () => {
const scanner = commandArgumentScanner();
@ -182,8 +120,8 @@ test("scan", async (t) => {
});
});
test("checkChangesFromArgs", async (t) => {
await t.test("should extract changes from install args", () => {
test("checkChangesFromArgs helper", async (t) => {
await t.test("should extract packages from args", () => {
const result = checkChangesFromArgs(["install", "requests==2.28.0", "flask"]);
assert.strictEqual(result.length, 2);
@ -199,17 +137,8 @@ test("checkChangesFromArgs", async (t) => {
});
});
await t.test("should return empty array for commands with no packages", () => {
const result = checkChangesFromArgs(["install", "-r", "requirements.txt"]);
assert.ok(Array.isArray(result));
assert.strictEqual(result.length, 0);
});
await t.test("should handle empty args", () => {
const result = checkChangesFromArgs([]);
assert.ok(Array.isArray(result));
assert.strictEqual(result.length, 0);
assert.deepStrictEqual(result, []);
});
});

View file

@ -1,10 +0,0 @@
/**
* Null scanner that returns no dependencies
* Used when a command is not supported for scanning
*/
export function nullScanner() {
return {
shouldScan: () => false,
scan: () => [],
};
}

View file

@ -1,26 +1,18 @@
/**
* Parses package specifications from pip install arguments
*
* Only returns packages with exact version specifiers (== or ===) to ensure
* we can check specific versions against the malware database.
*
* Supported formats that will be returned:
* - package_name (no version)
* - package_name==version (exact version)
* - package_name===version (exact version, PEP 440)
*
* Skipped formats (won't be returned):
* - package_name>=version (range specifier)
* - package_name<=version (range specifier)
* - package_name>version (range specifier)
* - package_name<version (range specifier)
* - package_name~=version (compatible release)
* - package_name!=version (exclusion)
* "Ranges". Because they don't specify an exact version, the following formats are skipped and we will rely solely on the mitm scanner:
* - package_name>=version
* - package_name<=version
* - package_name>version
* - package_name<version
* - package_name~=version
* - package_name!=version
* - git+https://... (VCS URLs - returned without version)
* - -r requirements.txt (handled by flag skipping)
*
* @param {string[]} args - pip install command arguments
* @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications with exact versions only
*/
export function parsePackagesFromInstallArgs(args) {
const packages = [];
@ -34,14 +26,13 @@ export function parsePackagesFromInstallArgs(args) {
continue;
}
// Skip the command itself (install, uninstall, etc.)
// Skip the command itself (install, etc.)
if (i === 0 && !arg.startsWith("-")) {
continue;
}
// Skip flags and their values
if (arg.startsWith("-")) {
// Flags that take a value - skip the next arg for those
if (isPipOptionWithParameter(arg)) {
skipNext = true;
}
@ -57,8 +48,10 @@ export function parsePackagesFromInstallArgs(args) {
return packages;
}
// Check if a pip flag takes a parameter
function isPipOptionWithParameter(arg) {
// Check if a pip flag takes a parameter
// TODO it would be better to query pip itself for this info
const optionsWithParameters = [
// Install options
"-r",
@ -107,10 +100,7 @@ function isPipOptionWithParameter(arg) {
return optionsWithParameters.includes(arg);
}
// Parse a single pip requirement spec
// Always returns { name, version } where version defaults to "latest" if not specified
function parsePipSpec(spec) {
// Ignore obvious URLs and paths
// These cannot be scanned from the malware database
const lower = spec.toLowerCase();

View file

@ -2,13 +2,7 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
/**
* Runs a pip command with the specified arguments
*
* @param {string} command - The pip command to use (e.g., "pip", "pip3")
* @param {string[]} args - Command arguments
* @returns {Promise<{status: number}>} Result object with status code
*/
export async function runPip(command, args) {
try {
const result = await safeSpawn(command, args, {
@ -26,18 +20,10 @@ export async function runPip(command, args) {
}
}
/**
* Runs a pip command in dry-run mode and captures output
* Note: pip doesn't have a native --dry-run flag, so this may need adjustment
*
* @param {string} command - The pip command to use
* @param {string[]} args - Command arguments
* @returns {Promise<{status: number, output: string}>} Result with status and output
*/
export async function dryRunPipCommandAndOutput(command, args) {
try {
// Note: pip doesn't have a --dry-run flag like npm
// This would need to be implemented differently if dry-run functionality is needed
// Note: pip supports --dry-run for the "install" command only; "download" and "wheel" do not.
// We don't mutate args here — callers should include --dry-run when appropriate.
const result = await safeSpawn(
command,
args,

View file

@ -1,20 +1,7 @@
/**
* Pip command constants
*
* Note: Unlike npm, pip does not support command aliases or abbreviations.
* Commands must be spelled out fully (e.g., "install", not "i" or "add").
*/
export const pipInstallCommand = "install";
export const pipDownloadCommand = "download";
export const pipWheelCommand = "wheel";
export const pipUninstallCommand = "uninstall";
/**
* Gets the pip command from the arguments array
*
* @param {string[]} args - Command line arguments
* @returns {string|null} The pip command or null if not found
*/
export function getPipCommandForArgs(args) {
if (!args || args.length === 0) {
return null;
@ -30,12 +17,6 @@ export function getPipCommandForArgs(args) {
return null;
}
/**
* Checks if the arguments contain the --dry-run flag
*
* @param {string[]} args - Command line arguments
* @returns {boolean} True if --dry-run is present
*/
export function hasDryRunArg(args) {
return args.some((arg) => arg === "--dry-run");
}

View file

@ -6,7 +6,6 @@ import {
pipInstallCommand,
pipDownloadCommand,
pipWheelCommand,
pipUninstallCommand,
} from "./pipCommands.js";
test("getPipCommandForArgs", async (t) => {
@ -81,8 +80,4 @@ test("command constants", async (t) => {
await t.test("should have correct wheel command", () => {
assert.strictEqual(pipWheelCommand, "wheel");
});
await t.test("should have correct uninstall command", () => {
assert.strictEqual(pipUninstallCommand, "uninstall");
});
});

View file

@ -7,7 +7,6 @@ export async function auditChanges(changes) {
const allowedChanges = [];
const disallowedChanges = [];
console.log("**audit/index.js** Auditing changes:", changes);
var malwarePackages = await getPackagesWithMalware(
changes.filter(
(change) => change.type === "add" || change.type === "change"

View file

@ -25,6 +25,7 @@ ARG NODE_VERSION=latest
ARG NPM_VERSION=latest
ARG YARN_VERSION=latest
ARG PNPM_VERSION=latest
ARG PYTHON_VERSION=3
SHELL ["/bin/bash", "-c"]
ENV BASH_ENV=~/.bashrc
@ -49,6 +50,11 @@ RUN volta install pnpm@${PNPM_VERSION}
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash
# Install Python and pip (pip3)
RUN apt-get update && apt-get install -y python${PYTHON_VERSION} python3-pip && \
ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 && \
ln -sf /usr/bin/pip3 /usr/local/bin/pip3
# Copy and install Safe chain
COPY --from=builder /app/*.tgz /pkgs/
RUN npm install -g /pkgs/*.tgz

71
test/e2e/pip.e2e.spec.js Normal file
View file

@ -0,0 +1,71 @@
import { describe, it, before, beforeEach, afterEach } from "node:test";
import { DockerTestContainer } from "./DockerTestContainer.js";
import assert from "node:assert";
// Note: These tests require Docker. If Docker isn't available locally,
// they will be skipped by the runner or fail to build the image.
describe("E2E: pip coverage", () => {
let container;
before(async () => {
DockerTestContainer.buildImage();
});
beforeEach(async () => {
// Run a new Docker container for each test
container = new DockerTestContainer();
await container.start();
const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup");
});
afterEach(async () => {
// Stop and clean up the container after each test
if (container) {
await container.stop();
container = null;
}
});
it(`safe-chain successfully installs safe packages with pip3`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 install requests");
assert.ok(
result.output.includes("no malicious packages found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`pip3 download works with safe-chain proxy`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 download requests");
assert.ok(
result.output.includes("no malicious packages found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`pip3 wheel works with safe-chain proxy`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 wheel requests");
assert.ok(
result.output.includes("no malicious packages found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`pip3 install --dry-run is respected by scanner`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("pip3 install --dry-run requests");
// Scanner intentionally skips when --dry-run is present for install
assert.ok(
result.output.includes("no malicious packages found."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
});