mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Adapt the structure to parse the initial pip commands
This commit is contained in:
parent
982da4aa77
commit
1b82aeb6b0
7 changed files with 273 additions and 22 deletions
|
|
@ -1,6 +1,11 @@
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||||
import { safeSpawn } from "../../utils/safeSpawn.js";
|
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
||||||
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
import { runPip } from "./runPipCommand.js";
|
||||||
|
import {
|
||||||
|
getPipCommandForArgs,
|
||||||
|
pipInstallCommand,
|
||||||
|
pipUninstallCommand,
|
||||||
|
} from "./utils/pipCommands.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a package manager interface for Python's pip package installer
|
* Creates a package manager interface for Python's pip package installer
|
||||||
|
|
@ -8,28 +13,40 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg
|
||||||
* @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip"
|
* @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip"
|
||||||
*/
|
*/
|
||||||
export function createPipPackageManager(command = "pip") {
|
export function createPipPackageManager(command = "pip") {
|
||||||
return {
|
function isSupportedCommand(args) {
|
||||||
runCommand: (args) => runPipCommand(command, args),
|
const scanner = findDependencyScannerForCommand(
|
||||||
|
commandScannerMapping,
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return scanner.shouldScan(args);
|
||||||
|
}
|
||||||
|
|
||||||
// For pip, set proxy server
|
function getDependencyUpdatesForCommand(args) {
|
||||||
isSupportedCommand: () => false,
|
const scanner = findDependencyScannerForCommand(
|
||||||
getDependencyUpdatesForCommand: () => [],
|
commandScannerMapping,
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return scanner.scan(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runCommand: (args) => runPip(command, args),
|
||||||
|
isSupportedCommand,
|
||||||
|
getDependencyUpdatesForCommand,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPipCommand(command, args) {
|
const commandScannerMapping = {
|
||||||
try {
|
[pipInstallCommand]: commandArgumentScanner(),
|
||||||
const result = await safeSpawn(command, args, {
|
[pipUninstallCommand]: nullScanner(), // Uninstall doesn't need scanning
|
||||||
stdio: "inherit",
|
};
|
||||||
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
||||||
});
|
function findDependencyScannerForCommand(scanners, args) {
|
||||||
return { status: result.status };
|
const command = getPipCommandForArgs(args);
|
||||||
} catch (error) {
|
if (!command) {
|
||||||
if (error.status) {
|
return nullScanner();
|
||||||
return { status: error.status };
|
|
||||||
} else {
|
|
||||||
ui.writeError("Error executing command:", error.message);
|
|
||||||
return { status: 1 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scanner = scanners[command];
|
||||||
|
return scanner ? scanner : nullScanner();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
function shouldScan(args) {
|
||||||
|
// For now, pip scanning is not yet implemented
|
||||||
|
// This would need to detect 'install' commands and package arguments
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scan(args) {
|
||||||
|
// Future implementation would parse pip install arguments
|
||||||
|
// and return array of {name, version, type} objects
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldScan,
|
||||||
|
scan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Null scanner that returns no dependencies
|
||||||
|
* Used when a command is not supported for scanning
|
||||||
|
*/
|
||||||
|
export function nullScanner() {
|
||||||
|
return {
|
||||||
|
shouldScan: () => false,
|
||||||
|
scan: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Parses package specifications from pip install arguments
|
||||||
|
*
|
||||||
|
* Pip supports various package specification formats:
|
||||||
|
* - package_name
|
||||||
|
* - package_name==version
|
||||||
|
* - package_name>=version
|
||||||
|
* - package_name~=version
|
||||||
|
* - git+https://...
|
||||||
|
* - -r requirements.txt
|
||||||
|
* - . (local directory)
|
||||||
|
*
|
||||||
|
* @param {string[]} args - pip install command arguments
|
||||||
|
* @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications
|
||||||
|
*/
|
||||||
|
export function parsePackagesFromInstallArgs(args) {
|
||||||
|
const packages = [];
|
||||||
|
let skipNext = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
if (skipNext) {
|
||||||
|
skipNext = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the command itself (install, uninstall, etc.)
|
||||||
|
if (i === 0 && !arg.startsWith("-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip flags and their values
|
||||||
|
if (arg.startsWith("-")) {
|
||||||
|
// Some flags take a value, skip the next arg for those
|
||||||
|
if (arg === "-r" || arg === "--requirement" ||
|
||||||
|
arg === "-c" || arg === "--constraint" ||
|
||||||
|
arg === "-e" || arg === "--editable" ||
|
||||||
|
arg === "-t" || arg === "--target" ||
|
||||||
|
arg === "-i" || arg === "--index-url" ||
|
||||||
|
arg === "--extra-index-url") {
|
||||||
|
skipNext = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement full parsing logic
|
||||||
|
// For now, this is a placeholder that would need to handle:
|
||||||
|
// - Version specifiers (==, >=, <=, ~=, !=, <, >)
|
||||||
|
// - 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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { parsePackagesFromInstallArgs } from "./parsePackagesFromInstallArgs.js";
|
||||||
|
|
||||||
|
describe("parsePackagesFromInstallArgs", () => {
|
||||||
|
it("should parse simple package name", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "requests"]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "requests", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse package with version specifier", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "requests==2.28.0", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip flags", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "requests", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiple packages", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]);
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{ name: "requests", type: "add" },
|
||||||
|
{ name: "flask", type: "add" },
|
||||||
|
{ name: "django==4.0", type: "add" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for no packages", () => {
|
||||||
|
const result = parsePackagesFromInstallArgs(["install", "--help"]);
|
||||||
|
assert.deepEqual(result, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
66
packages/safe-chain/src/packagemanager/pip/runPipCommand.js
Normal file
66
packages/safe-chain/src/packagemanager/pip/runPipCommand.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
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, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
|
});
|
||||||
|
return { status: result.status };
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status) {
|
||||||
|
return { status: error.status };
|
||||||
|
} else {
|
||||||
|
ui.writeError("Error executing command:", error.message);
|
||||||
|
return { status: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
const result = await safeSpawn(
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
{
|
||||||
|
stdio: "pipe",
|
||||||
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: result.status,
|
||||||
|
output: result.status === 0 ? result.stdout : result.stderr,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status) {
|
||||||
|
const output =
|
||||||
|
error.stdout?.toString() ??
|
||||||
|
error.stderr?.toString() ??
|
||||||
|
error.message ??
|
||||||
|
"";
|
||||||
|
return { status: error.status, output };
|
||||||
|
} else {
|
||||||
|
ui.writeError("Error executing command:", error.message);
|
||||||
|
return { status: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Pip command constants
|
||||||
|
*/
|
||||||
|
export const pipInstallCommand = "install";
|
||||||
|
export const pipUninstallCommand = "uninstall";
|
||||||
|
export const pipListCommand = "list";
|
||||||
|
export const pipShowCommand = "show";
|
||||||
|
export const pipFreeze = "freeze";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first non-flag argument is typically the command
|
||||||
|
for (const arg of args) {
|
||||||
|
if (!arg.startsWith("-")) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue