mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Merge remote-tracking branch 'aikido/main' into feat/pdm-support
This commit is contained in:
commit
8453012f7b
44 changed files with 1311 additions and 202 deletions
|
|
@ -14,6 +14,8 @@ import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
|||
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
||||
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
|
||||
import { createPdmPackageManager } from "./pdm/createPdmPackageManager.js";
|
||||
import { createRushPackageManager } from "./rush/createRushPackageManager.js";
|
||||
import { createRushxPackageManager } from "./rushx/createRushxPackageManager.js";
|
||||
import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js";
|
||||
|
||||
/**
|
||||
|
|
@ -70,6 +72,10 @@ export function initializePackageManager(packageManagerName, context) {
|
|||
state.packageManagerName = createPipXPackageManager();
|
||||
} else if (packageManagerName === "pdm") {
|
||||
state.packageManagerName = createPdmPackageManager();
|
||||
} else if (packageManagerName === "rush") {
|
||||
state.packageManagerName = createRushPackageManager();
|
||||
} else if (packageManagerName === "rushx") {
|
||||
state.packageManagerName = createRushxPackageManager();
|
||||
} else {
|
||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { runRushCommand } from "./runRushCommand.js";
|
||||
import { resolvePackageVersion } from "../../api/npmApi.js";
|
||||
import { parsePackagesFromRushAddArgs } from "./parsing/parsePackagesFromRushAddArgs.js";
|
||||
|
||||
/**
|
||||
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||
*/
|
||||
export function createRushPackageManager() {
|
||||
return {
|
||||
runCommand: (args) => runRushCommand("rush", args),
|
||||
// We pre-scan rush add commands and rely on MITM for install/update flows.
|
||||
isSupportedCommand: (args) => getRushCommand(args) === "add",
|
||||
getDependencyUpdatesForCommand: scanRushAddCommand,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {Promise<import("../currentPackageManager.js").GetDependencyUpdatesResult[]>}
|
||||
*/
|
||||
async function scanRushAddCommand(args) {
|
||||
if (getRushCommand(args) !== "add") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsedSpecs = parsePackagesFromRushAddArgs(args.slice(1));
|
||||
|
||||
const resolvedVersions = await Promise.all(
|
||||
parsedSpecs.map(async (parsed) => {
|
||||
const exactVersion = await resolvePackageVersion(parsed.name, parsed.version);
|
||||
return {
|
||||
parsed,
|
||||
exactVersion,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const changes = [];
|
||||
for (const resolved of resolvedVersions) {
|
||||
if (!resolved.exactVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
name: resolved.parsed.name,
|
||||
version: resolved.exactVersion,
|
||||
type: "add",
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function getRushCommand(args) {
|
||||
if (!args || args.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return args[0]?.toLowerCase();
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { test, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
|
||||
test("createRushPackageManager", async (t) => {
|
||||
mock.module("../../api/npmApi.js", {
|
||||
namedExports: {
|
||||
resolvePackageVersion: async (name, version) => {
|
||||
if (name === "safe-chain-test") {
|
||||
return "0.0.1-security";
|
||||
}
|
||||
|
||||
if (name === "@scope/tool") {
|
||||
return version || "2.0.0";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const { createRushPackageManager } = await import("./createRushPackageManager.js");
|
||||
|
||||
await t.test("should create package manager with required interface", () => {
|
||||
const pm = createRushPackageManager();
|
||||
|
||||
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 scan rush add commands", () => {
|
||||
const pm = createRushPackageManager();
|
||||
|
||||
assert.strictEqual(pm.isSupportedCommand(["add", "--package", "safe-chain-test"]), true);
|
||||
assert.strictEqual(pm.isSupportedCommand(["install"]), false);
|
||||
});
|
||||
|
||||
await t.test("should parse rush add package specs and resolve versions", async () => {
|
||||
const pm = createRushPackageManager();
|
||||
|
||||
const changes = await pm.getDependencyUpdatesForCommand([
|
||||
"add",
|
||||
"--package",
|
||||
"safe-chain-test",
|
||||
"--package=@scope/tool@1.2.3",
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(changes, [
|
||||
{ name: "safe-chain-test", version: "0.0.1-security", type: "add" },
|
||||
{ name: "@scope/tool", version: "1.2.3", type: "add" },
|
||||
]);
|
||||
});
|
||||
|
||||
await t.test("should return no changes for non-add commands", async () => {
|
||||
const pm = createRushPackageManager();
|
||||
|
||||
const changes = await pm.getDependencyUpdatesForCommand(["install"]);
|
||||
|
||||
assert.deepStrictEqual(changes, []);
|
||||
});
|
||||
} finally {
|
||||
mock.reset();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {{name: string, version: string | null}[]}
|
||||
*/
|
||||
export function parsePackagesFromRushAddArgs(args) {
|
||||
const packageSpecs = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--package" || arg === "-p") {
|
||||
const next = args[i + 1];
|
||||
if (next && !next.startsWith("-")) {
|
||||
packageSpecs.push(next);
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--package=")) {
|
||||
const value = arg.slice("--package=".length);
|
||||
if (value) {
|
||||
packageSpecs.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packageSpecs
|
||||
.map((spec) => parsePackageSpec(spec))
|
||||
.filter((spec) => spec !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} spec
|
||||
* @returns {{name: string, version: string | null} | null}
|
||||
*/
|
||||
function parsePackageSpec(spec) {
|
||||
const value = removeAlias(spec.trim());
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastAtIndex = value.lastIndexOf("@");
|
||||
if (lastAtIndex > 0) {
|
||||
return {
|
||||
name: value.slice(0, lastAtIndex),
|
||||
version: value.slice(lastAtIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: value,
|
||||
version: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} spec
|
||||
* @returns {string}
|
||||
*/
|
||||
function removeAlias(spec) {
|
||||
const aliasIndex = spec.indexOf("@npm:");
|
||||
if (aliasIndex !== -1) {
|
||||
return spec.slice(aliasIndex + 5);
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parsePackagesFromRushAddArgs } from "./parsePackagesFromRushAddArgs.js";
|
||||
|
||||
describe("parsePackagesFromRushAddArgs", () => {
|
||||
it("returns an empty array when no packages are provided", () => {
|
||||
const result = parsePackagesFromRushAddArgs([]);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("parses packages from --package arguments", () => {
|
||||
const result = parsePackagesFromRushAddArgs([
|
||||
"--package",
|
||||
"axios@1.9.0",
|
||||
"--package",
|
||||
"@scope/tool@2.0.0",
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "axios", version: "1.9.0" },
|
||||
{ name: "@scope/tool", version: "2.0.0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses packages from -p arguments", () => {
|
||||
const result = parsePackagesFromRushAddArgs(["-p", "axios"]);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: null }]);
|
||||
});
|
||||
|
||||
it("parses packages from --package=value arguments", () => {
|
||||
const result = parsePackagesFromRushAddArgs(["--package=axios@^1.9.0"]);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]);
|
||||
});
|
||||
|
||||
it("ignores positional packages because rush add requires --package", () => {
|
||||
const result = parsePackagesFromRushAddArgs(["axios", "--dev"]);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("parses aliases", () => {
|
||||
const result = parsePackagesFromRushAddArgs(["--package", "server@npm:axios@1.9.0"]);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||
|
||||
/**
|
||||
* @param {"rush" | "rushx"} executableName
|
||||
* @param {string[]} args
|
||||
* @returns {Promise<{status: number}>}
|
||||
*/
|
||||
export async function runRushCommand(executableName, args) {
|
||||
try {
|
||||
const result = await safeSpawn(executableName, args, {
|
||||
stdio: "inherit",
|
||||
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||
});
|
||||
|
||||
return { status: result.status };
|
||||
} catch (/** @type any */ error) {
|
||||
return reportCommandExecutionFailure(error, executableName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("runRushCommand", () => {
|
||||
let runRushCommand;
|
||||
let safeSpawnMock;
|
||||
let mergeCalls;
|
||||
let mergeResultEnv;
|
||||
let nextSpawnStatus;
|
||||
let nextSpawnError;
|
||||
|
||||
beforeEach(async () => {
|
||||
mergeCalls = [];
|
||||
mergeResultEnv = null;
|
||||
nextSpawnStatus = 0;
|
||||
nextSpawnError = null;
|
||||
safeSpawnMock = mock.fn(async () => {
|
||||
if (nextSpawnError) {
|
||||
const error = nextSpawnError;
|
||||
nextSpawnError = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { status: nextSpawnStatus };
|
||||
});
|
||||
|
||||
mock.module("../../utils/safeSpawn.js", {
|
||||
namedExports: {
|
||||
safeSpawn: safeSpawnMock,
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../../registryProxy/registryProxy.js", {
|
||||
namedExports: {
|
||||
mergeSafeChainProxyEnvironmentVariables: (env) => {
|
||||
mergeCalls.push(env);
|
||||
if (mergeResultEnv) {
|
||||
return mergeResultEnv;
|
||||
}
|
||||
|
||||
return {
|
||||
...env,
|
||||
HTTPS_PROXY: "http://localhost:8080",
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// commandErrors reports through ui on failures, so provide a no-op mock
|
||||
mock.module("../../environment/userInteraction.js", {
|
||||
namedExports: {
|
||||
ui: {
|
||||
writeError: () => {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mod = await import("./runRushCommand.js");
|
||||
runRushCommand = mod.runRushCommand;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
it("spawns rush with merged proxy env", async () => {
|
||||
const res = await runRushCommand("rush", ["install"]);
|
||||
|
||||
assert.strictEqual(res.status, 0);
|
||||
assert.strictEqual(safeSpawnMock.mock.calls.length, 1);
|
||||
|
||||
const [command, args, options] = safeSpawnMock.mock.calls[0].arguments;
|
||||
assert.strictEqual(command, "rush");
|
||||
assert.deepStrictEqual(args, ["install"]);
|
||||
assert.strictEqual(options.stdio, "inherit");
|
||||
assert.strictEqual(options.env.HTTPS_PROXY, "http://localhost:8080");
|
||||
assert.ok(mergeCalls.length >= 1, "proxy env merge should be called");
|
||||
});
|
||||
|
||||
it("returns spawn result status", async () => {
|
||||
nextSpawnStatus = 7;
|
||||
|
||||
const res = await runRushCommand("rush", ["update"]);
|
||||
|
||||
assert.strictEqual(res.status, 7);
|
||||
});
|
||||
|
||||
it("reports failures with rush target", async () => {
|
||||
nextSpawnError = Object.assign(new Error("spawn failed"), {
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
const res = await runRushCommand("rush", ["install"]);
|
||||
|
||||
assert.strictEqual(res.status, 1);
|
||||
});
|
||||
|
||||
it("does not mutate merged env object", async () => {
|
||||
mergeResultEnv = {
|
||||
HTTPS_PROXY: "http://localhost:8080",
|
||||
};
|
||||
|
||||
await runRushCommand("rush", ["install"]);
|
||||
|
||||
assert.deepStrictEqual(mergeResultEnv, {
|
||||
HTTPS_PROXY: "http://localhost:8080",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { runRushCommand } from "../rush/runRushCommand.js";
|
||||
|
||||
/**
|
||||
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||
*/
|
||||
export function createRushxPackageManager() {
|
||||
return {
|
||||
/**
|
||||
* @param {string[]} args
|
||||
*/
|
||||
runCommand: (args) => {
|
||||
return runRushCommand("rushx", args);
|
||||
},
|
||||
// For rushx, rely solely on MITM.
|
||||
isSupportedCommand: () => false,
|
||||
getDependencyUpdatesForCommand: () => [],
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { createRushxPackageManager } from "./createRushxPackageManager.js";
|
||||
|
||||
test("createRushxPackageManager returns valid package manager interface", () => {
|
||||
const pm = createRushxPackageManager();
|
||||
|
||||
assert.ok(pm);
|
||||
assert.strictEqual(typeof pm.runCommand, "function");
|
||||
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||
assert.strictEqual(pm.isSupportedCommand(), false);
|
||||
assert.deepStrictEqual(pm.getDependencyUpdatesForCommand(), []);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue