mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Fix some border cases
This commit is contained in:
parent
1b82aeb6b0
commit
1fdb15a392
9 changed files with 685 additions and 46 deletions
|
|
@ -9,7 +9,7 @@ import {
|
||||||
createPnpxPackageManager,
|
createPnpxPackageManager,
|
||||||
} from "./pnpm/createPackageManager.js";
|
} from "./pnpm/createPackageManager.js";
|
||||||
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
||||||
import { createPipPackageManager } from "./pip/createPipPackageManager.js";
|
import { createPipPackageManager } from "./pip/createPackageManager.js";
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
packageManagerName: null,
|
packageManagerName: null,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import { runPip } from "./runPipCommand.js";
|
||||||
import {
|
import {
|
||||||
getPipCommandForArgs,
|
getPipCommandForArgs,
|
||||||
pipInstallCommand,
|
pipInstallCommand,
|
||||||
pipUninstallCommand,
|
pipDownloadCommand,
|
||||||
|
pipWheelCommand,
|
||||||
} from "./utils/pipCommands.js";
|
} from "./utils/pipCommands.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,7 +39,9 @@ export function createPipPackageManager(command = "pip") {
|
||||||
|
|
||||||
const commandScannerMapping = {
|
const commandScannerMapping = {
|
||||||
[pipInstallCommand]: commandArgumentScanner(),
|
[pipInstallCommand]: commandArgumentScanner(),
|
||||||
[pipUninstallCommand]: nullScanner(), // Uninstall doesn't need scanning
|
[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
|
||||||
};
|
};
|
||||||
|
|
||||||
function findDependencyScannerForCommand(scanners, args) {
|
function findDependencyScannerForCommand(scanners, args) {
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
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", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should create package manager with custom pip3 command", () => {
|
||||||
|
const pm = createPipPackageManager("pip3");
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should recognize install command as supported", () => {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should recognize download command as supported", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
|
const result = pm.isSupportedCommand(["download", "requests"]);
|
||||||
|
assert.strictEqual(typeof result, "boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should recognize wheel command as supported", () => {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
|
||||||
|
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.strictEqual(result.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should handle empty args array", () => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
|
||||||
|
import { hasDryRunArg } from "../utils/pipCommands.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scanner for pip command arguments to detect package installations
|
* Scanner for pip command arguments to detect package installations
|
||||||
*
|
*
|
||||||
|
|
@ -9,15 +12,11 @@ export function commandArgumentScanner(options = {}) {
|
||||||
const { ignoreDryRun = false } = options;
|
const { ignoreDryRun = false } = options;
|
||||||
|
|
||||||
function shouldScan(args) {
|
function shouldScan(args) {
|
||||||
// For now, pip scanning is not yet implemented
|
return shouldScanDependencies(args, ignoreDryRun);
|
||||||
// This would need to detect 'install' commands and package arguments
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scan(args) {
|
function scan(args) {
|
||||||
// Future implementation would parse pip install arguments
|
return scanDependencies(args);
|
||||||
// and return array of {name, version, type} objects
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -25,3 +24,28 @@ export function commandArgumentScanner(options = {}) {
|
||||||
scan,
|
scan,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldScanDependencies(args, ignoreDryRun) {
|
||||||
|
return ignoreDryRun || !hasDryRunArg(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
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", () => {
|
||||||
|
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) => {
|
||||||
|
await t.test("should return true for normal install command", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.shouldScan(["install", "requests"]);
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return false for install with --dry-run", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.shouldScan(["install", "--dry-run", "requests"]);
|
||||||
|
assert.strictEqual(result, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return true for install with --dry-run when ignoreDryRun is true", () => {
|
||||||
|
const scanner = commandArgumentScanner({ ignoreDryRun: true });
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
await t.test("should scan simple package installation", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.scan(["install", "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 scan package with exact version", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.scan(["install", "requests==2.28.0"]);
|
||||||
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.deepEqual(result[0], {
|
||||||
|
name: "requests",
|
||||||
|
version: "2.28.0",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should scan multiple packages", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.scan(["install", "requests==2.28.0", "flask"]);
|
||||||
|
assert.strictEqual(result.length, 2);
|
||||||
|
assert.deepEqual(result[0], {
|
||||||
|
name: "requests",
|
||||||
|
version: "2.28.0",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
assert.deepEqual(result[1], {
|
||||||
|
name: "flask",
|
||||||
|
version: "latest",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should skip packages with range specifiers", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.scan(["install", "requests>=2.0.0", "flask==2.0.0"]);
|
||||||
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.deepEqual(result[0], {
|
||||||
|
name: "flask",
|
||||||
|
version: "2.0.0",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should skip flags with parameters", () => {
|
||||||
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
const result = scanner.scan([
|
||||||
|
"install",
|
||||||
|
"-r",
|
||||||
|
"requirements.txt",
|
||||||
|
"requests==2.28.0",
|
||||||
|
]);
|
||||||
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.deepEqual(result[0], {
|
||||||
|
name: "requests",
|
||||||
|
version: "2.28.0",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
const result = scanner.scan(["install", "requests===2.28.0"]);
|
||||||
|
assert.strictEqual(result.length, 1);
|
||||||
|
assert.deepEqual(result[0], {
|
||||||
|
name: "requests",
|
||||||
|
version: "2.28.0",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkChangesFromArgs", async (t) => {
|
||||||
|
await t.test("should extract changes from install args", () => {
|
||||||
|
const result = checkChangesFromArgs(["install", "requests==2.28.0", "flask"]);
|
||||||
|
|
||||||
|
assert.strictEqual(result.length, 2);
|
||||||
|
assert.deepEqual(result[0], {
|
||||||
|
name: "requests",
|
||||||
|
version: "2.28.0",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
assert.deepEqual(result[1], {
|
||||||
|
name: "flask",
|
||||||
|
version: "latest",
|
||||||
|
type: "add",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
/**
|
/**
|
||||||
* Parses package specifications from pip install arguments
|
* Parses package specifications from pip install arguments
|
||||||
*
|
*
|
||||||
* Pip supports various package specification formats:
|
* Only returns packages with exact version specifiers (== or ===) to ensure
|
||||||
* - package_name
|
* we can check specific versions against the malware database.
|
||||||
* - package_name==version
|
*
|
||||||
* - package_name>=version
|
* Supported formats that will be returned:
|
||||||
* - package_name~=version
|
* - package_name (no version)
|
||||||
* - git+https://...
|
* - package_name==version (exact version)
|
||||||
* - -r requirements.txt
|
* - package_name===version (exact version, PEP 440)
|
||||||
* - . (local directory)
|
*
|
||||||
|
* 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)
|
||||||
|
* - git+https://... (VCS URLs - returned without version)
|
||||||
|
* - -r requirements.txt (handled by flag skipping)
|
||||||
*
|
*
|
||||||
* @param {string[]} args - pip install command arguments
|
* @param {string[]} args - pip install command arguments
|
||||||
* @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications
|
* @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications with exact versions only
|
||||||
*/
|
*/
|
||||||
export function parsePackagesFromInstallArgs(args) {
|
export function parsePackagesFromInstallArgs(args) {
|
||||||
const packages = [];
|
const packages = [];
|
||||||
|
|
@ -32,31 +41,126 @@ export function parsePackagesFromInstallArgs(args) {
|
||||||
|
|
||||||
// Skip flags and their values
|
// Skip flags and their values
|
||||||
if (arg.startsWith("-")) {
|
if (arg.startsWith("-")) {
|
||||||
// Some flags take a value, skip the next arg for those
|
// Flags that take a value - skip the next arg for those
|
||||||
if (arg === "-r" || arg === "--requirement" ||
|
if (isPipOptionWithParameter(arg)) {
|
||||||
arg === "-c" || arg === "--constraint" ||
|
|
||||||
arg === "-e" || arg === "--editable" ||
|
|
||||||
arg === "-t" || arg === "--target" ||
|
|
||||||
arg === "-i" || arg === "--index-url" ||
|
|
||||||
arg === "--extra-index-url") {
|
|
||||||
skipNext = true;
|
skipNext = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement full parsing logic
|
const parsed = parsePipSpec(arg);
|
||||||
// For now, this is a placeholder that would need to handle:
|
if (parsed) {
|
||||||
// - Version specifiers (==, >=, <=, ~=, !=, <, >)
|
packages.push({ ...parsed, type: "add" });
|
||||||
// - VCS urls (git+, hg+, svn+, bzr+)
|
}
|
||||||
// - Local file paths
|
|
||||||
// - Requirements files (-r, --requirement)
|
|
||||||
// - Extras (package[extra1,extra2])
|
|
||||||
|
|
||||||
packages.push({
|
|
||||||
name: arg,
|
|
||||||
type: "add",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a pip flag takes a parameter
|
||||||
|
function isPipOptionWithParameter(arg) {
|
||||||
|
const optionsWithParameters = [
|
||||||
|
// Install options
|
||||||
|
"-r",
|
||||||
|
"--requirement",
|
||||||
|
"-c",
|
||||||
|
"--constraint",
|
||||||
|
"-e",
|
||||||
|
"--editable",
|
||||||
|
"-t",
|
||||||
|
"--target",
|
||||||
|
"--platform",
|
||||||
|
"--python-version",
|
||||||
|
"--implementation",
|
||||||
|
"--abi",
|
||||||
|
"--root",
|
||||||
|
"--prefix",
|
||||||
|
"--src",
|
||||||
|
"--upgrade-strategy",
|
||||||
|
"--progress-bar",
|
||||||
|
"--root-user-action",
|
||||||
|
"--report",
|
||||||
|
"--group",
|
||||||
|
// Package index options
|
||||||
|
"-i",
|
||||||
|
"--index-url",
|
||||||
|
"--extra-index-url",
|
||||||
|
"-f",
|
||||||
|
"--find-links",
|
||||||
|
// General options
|
||||||
|
"--python",
|
||||||
|
"--log",
|
||||||
|
"--keyring-provider",
|
||||||
|
"--proxy",
|
||||||
|
"--retries",
|
||||||
|
"--timeout",
|
||||||
|
"--exists-action",
|
||||||
|
"--trusted-host",
|
||||||
|
"--cert",
|
||||||
|
"--client-cert",
|
||||||
|
"--cache-dir",
|
||||||
|
"--use-feature",
|
||||||
|
"--use-deprecated",
|
||||||
|
"--resume-retries",
|
||||||
|
];
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (
|
||||||
|
lower.startsWith("git+") ||
|
||||||
|
lower.startsWith("hg+") ||
|
||||||
|
lower.startsWith("svn+") ||
|
||||||
|
lower.startsWith("bzr+") ||
|
||||||
|
lower.startsWith("http:") ||
|
||||||
|
lower.startsWith("https:") ||
|
||||||
|
lower.startsWith("file:") ||
|
||||||
|
spec.startsWith("./") ||
|
||||||
|
spec.startsWith("../") ||
|
||||||
|
spec.startsWith("/")
|
||||||
|
) {
|
||||||
|
return { name: spec, version: "latest" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip extras: package[extra1,extra2]
|
||||||
|
const extrasStart = spec.indexOf("[");
|
||||||
|
const extrasEnd = extrasStart >= 0 ? spec.indexOf("]", extrasStart) : -1;
|
||||||
|
let base = spec;
|
||||||
|
if (extrasStart >= 0 && extrasEnd > extrasStart) {
|
||||||
|
base = spec.slice(0, extrasStart) + spec.slice(extrasEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on first occurrence of a comparator or comma spec
|
||||||
|
// Support multi-constraint lists like ">=1,<2" by detecting the first comparator
|
||||||
|
const comparatorRegex = /(===|==|!=|~=|>=|<=|<|>)/;
|
||||||
|
const m = base.match(comparatorRegex);
|
||||||
|
if (!m) {
|
||||||
|
// No comparator => just a name, use "latest" as version
|
||||||
|
return { name: base, version: "latest" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = m.index;
|
||||||
|
const name = base.slice(0, idx);
|
||||||
|
const versionPart = base.slice(idx); // e.g. '==2.28.0' or '>=1,<2'
|
||||||
|
|
||||||
|
// Normalize whitespace inside versionPart
|
||||||
|
const versionWithOperator = versionPart.replace(/\s+/g, "");
|
||||||
|
|
||||||
|
// Only return packages with exact version specifiers (== or ===)
|
||||||
|
// Skip range specifiers (<, >, <=, >=, ~=, !=) since they don't provide a specific version
|
||||||
|
if (!versionWithOperator.startsWith("==")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the == or === operator to get just the version number
|
||||||
|
const version = versionWithOperator.replace(/^===?/, "");
|
||||||
|
|
||||||
|
return { name, version };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,30 +6,82 @@ describe("parsePackagesFromInstallArgs", () => {
|
||||||
it("should parse simple package name", () => {
|
it("should parse simple package name", () => {
|
||||||
const result = parsePackagesFromInstallArgs(["install", "requests"]);
|
const result = parsePackagesFromInstallArgs(["install", "requests"]);
|
||||||
assert.deepEqual(result, [
|
assert.deepEqual(result, [
|
||||||
{ name: "requests", type: "add" },
|
{ name: "requests", version: "latest", type: "add" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse package with version specifier", () => {
|
it("should parse package with version specifier", () => {
|
||||||
const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]);
|
const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]);
|
||||||
assert.deepEqual(result, [
|
assert.deepEqual(result, [
|
||||||
{ name: "requests==2.28.0", type: "add" },
|
{ name: "requests", version: "2.28.0", type: "add" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should skip flags", () => {
|
it("should skip flags", () => {
|
||||||
const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]);
|
const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]);
|
||||||
assert.deepEqual(result, [
|
assert.deepEqual(result, [
|
||||||
{ name: "requests", type: "add" },
|
{ name: "requests", version: "latest", type: "add" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse multiple packages", () => {
|
it("should parse multiple packages", () => {
|
||||||
const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]);
|
const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]);
|
||||||
assert.deepEqual(result, [
|
assert.deepEqual(result, [
|
||||||
{ name: "requests", type: "add" },
|
{ name: "requests", version: "latest", type: "add" },
|
||||||
{ name: "flask", type: "add" },
|
{ name: "flask", version: "latest", type: "add" },
|
||||||
{ name: "django==4.0", type: "add" },
|
{ name: "django", version: "4.0", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse extras and strip them from name", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "django[postgres]==4.2.1"]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "django", version: "4.2.1", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiple constraints", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "requests>=2,<3"]);
|
||||||
|
// Range specifiers should be skipped since they don't provide exact versions
|
||||||
|
assert.deepEqual(result, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip packages with range specifiers", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs([
|
||||||
|
"install",
|
||||||
|
"requests>=2.0.0",
|
||||||
|
"flask>1.0",
|
||||||
|
"django<=4.0",
|
||||||
|
"numpy~=1.20",
|
||||||
|
"scipy!=1.5.0",
|
||||||
|
"pandas==1.3.0",
|
||||||
|
]);
|
||||||
|
// Only pandas with exact version (==) should be returned
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "pandas", version: "1.3.0", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support === exact version specifier", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "requests===2.28.0"]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "requests", version: "2.28.0", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should treat VCS/URL/path specs as names (no version)", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs([
|
||||||
|
"install",
|
||||||
|
"git+https://github.com/pallets/flask.git",
|
||||||
|
"https://files.pythonhosted.org/packages/foo/bar.whl",
|
||||||
|
"file:/tmp/pkg.whl",
|
||||||
|
"./localpkg",
|
||||||
|
]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "git+https://github.com/pallets/flask.git", version: "latest", type: "add" },
|
||||||
|
{ name: "https://files.pythonhosted.org/packages/foo/bar.whl", version: "latest", type: "add" },
|
||||||
|
{ name: "file:/tmp/pkg.whl", version: "latest", type: "add" },
|
||||||
|
{ name: "./localpkg", version: "latest", type: "add" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -37,4 +89,28 @@ describe("parsePackagesFromInstallArgs", () => {
|
||||||
const result = parsePackagesFromInstallArgs(["install", "--help"]);
|
const result = parsePackagesFromInstallArgs(["install", "--help"]);
|
||||||
assert.deepEqual(result, []);
|
assert.deepEqual(result, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should skip all flags with parameters", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs([
|
||||||
|
"install",
|
||||||
|
"--target",
|
||||||
|
"/tmp/target",
|
||||||
|
"--platform",
|
||||||
|
"linux",
|
||||||
|
"--python-version",
|
||||||
|
"3.9",
|
||||||
|
"--index-url",
|
||||||
|
"https://pypi.org/simple",
|
||||||
|
"--trusted-host",
|
||||||
|
"pypi.org",
|
||||||
|
"requests==2.28.0",
|
||||||
|
"--cache-dir",
|
||||||
|
"/tmp/cache",
|
||||||
|
"flask",
|
||||||
|
]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "requests", version: "2.28.0", type: "add" },
|
||||||
|
{ name: "flask", version: "latest", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* Pip command constants
|
* 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 pipInstallCommand = "install";
|
||||||
|
export const pipDownloadCommand = "download";
|
||||||
|
export const pipWheelCommand = "wheel";
|
||||||
export const pipUninstallCommand = "uninstall";
|
export const pipUninstallCommand = "uninstall";
|
||||||
export const pipListCommand = "list";
|
|
||||||
export const pipShowCommand = "show";
|
|
||||||
export const pipFreeze = "freeze";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the pip command from the arguments array
|
* Gets the pip command from the arguments array
|
||||||
|
|
@ -27,3 +29,13 @@ export function getPipCommandForArgs(args) {
|
||||||
|
|
||||||
return null;
|
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");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import {
|
||||||
|
getPipCommandForArgs,
|
||||||
|
hasDryRunArg,
|
||||||
|
pipInstallCommand,
|
||||||
|
pipDownloadCommand,
|
||||||
|
pipWheelCommand,
|
||||||
|
pipUninstallCommand,
|
||||||
|
} from "./pipCommands.js";
|
||||||
|
|
||||||
|
test("getPipCommandForArgs", async (t) => {
|
||||||
|
await t.test("should return null for empty args", () => {
|
||||||
|
assert.strictEqual(getPipCommandForArgs([]), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return null for null args", () => {
|
||||||
|
assert.strictEqual(getPipCommandForArgs(null), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return the first non-flag argument", () => {
|
||||||
|
assert.strictEqual(getPipCommandForArgs(["install"]), "install");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should skip flags and return command", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getPipCommandForArgs(["-v", "--verbose", "install"]),
|
||||||
|
"install"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return install command", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getPipCommandForArgs(["install", "requests"]),
|
||||||
|
"install"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return uninstall command", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getPipCommandForArgs(["uninstall", "requests"]),
|
||||||
|
"uninstall"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return null if only flags", () => {
|
||||||
|
assert.strictEqual(getPipCommandForArgs(["--version", "-v"]), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hasDryRunArg", async (t) => {
|
||||||
|
await t.test("should return false for empty args", () => {
|
||||||
|
assert.strictEqual(hasDryRunArg([]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return true if --dry-run is present", () => {
|
||||||
|
assert.strictEqual(hasDryRunArg(["install", "--dry-run", "requests"]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return false if --dry-run is not present", () => {
|
||||||
|
assert.strictEqual(hasDryRunArg(["install", "requests"]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return true for --dry-run with other flags", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
hasDryRunArg(["install", "-v", "--dry-run", "--upgrade", "requests"]),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("command constants", async (t) => {
|
||||||
|
await t.test("should have correct install command", () => {
|
||||||
|
assert.strictEqual(pipInstallCommand, "install");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should have correct download command", () => {
|
||||||
|
assert.strictEqual(pipDownloadCommand, "download");
|
||||||
|
});
|
||||||
|
|
||||||
|
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