mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Implement e2e tests
This commit is contained in:
parent
f817bf887a
commit
059cba06bc
17 changed files with 163 additions and 293 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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([]), []);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, []);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: () => [],
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue