diff --git a/package-lock.json b/package-lock.json index 88e9fb5..0d64f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/safe-chain/bin/aikido-pip.js b/packages/safe-chain/bin/aikido-pip.js index 7834bc5..29c68bc 100755 --- a/packages/safe-chain/bin/aikido-pip.js +++ b/packages/safe-chain/bin/aikido-pip.js @@ -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); diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 2cabbe0..9e5f6bd 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -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}` diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index 4eaf8d2..e106e83 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -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); diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index ff2481d..665ac92 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -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); diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js index 2f0fc0d..2eaab01 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.js @@ -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; } diff --git a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js index d525e2c..2d38b0d 100644 --- a/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/createPackageManager.spec.js @@ -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([]), []); }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js index f0e47f1..dbe92d6 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.js @@ -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,18 +26,9 @@ 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); - + // Parser already provides exact versions or "latest", no need to resolve // Just return the packages with type "add" return packageUpdates; diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js index 6e69386..9570756 100644 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/commandArgumentScanner.spec.js @@ -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, []); }); }); diff --git a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js deleted file mode 100644 index ec3ba12..0000000 --- a/packages/safe-chain/src/packagemanager/pip/dependencyScanner/nullScanner.js +++ /dev/null @@ -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: () => [], - }; -} diff --git a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js index 3dc46ed..71c99c0 100644 --- a/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/pip/parsing/parsePackagesFromInstallArgs.js @@ -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 + * - package_name<=version + * - package_name>version + * - package_name} 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(); diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 859326a..878d856 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -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, diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js index e88b262..2818c87 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.js @@ -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"); } diff --git a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js index bfe8339..346ad8f 100644 --- a/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/utils/pipCommands.spec.js @@ -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"); - }); }); diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index be46fdb..215bfa0 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -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" diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 484f5fe..e590d19 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -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 diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js new file mode 100644 index 0000000..50619ff --- /dev/null +++ b/test/e2e/pip.e2e.spec.js @@ -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}` + ); + }); +});