mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Add rush command wrapper and tests
This commit is contained in:
parent
308ccb3d2b
commit
5690e55d99
12 changed files with 403 additions and 7 deletions
|
|
@ -13,6 +13,7 @@ import { createPipPackageManager } from "./pip/createPackageManager.js";
|
|||
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
||||
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
||||
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
|
||||
import { createRushPackageManager } from "./rush/createRushPackageManager.js";
|
||||
|
||||
/**
|
||||
* @type {{packageManagerName: PackageManager | null}}
|
||||
|
|
@ -64,6 +65,8 @@ export function initializePackageManager(packageManagerName, context) {
|
|||
state.packageManagerName = createPoetryPackageManager();
|
||||
} else if (packageManagerName === "pipx") {
|
||||
state.packageManagerName = createPipXPackageManager();
|
||||
} else if (packageManagerName === "rush") {
|
||||
state.packageManagerName = createRushPackageManager();
|
||||
} else {
|
||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import { runRushCommand } from "./runRushCommand.js";
|
||||
import { resolvePackageVersion } from "../../api/npmApi.js";
|
||||
|
||||
/**
|
||||
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||
*/
|
||||
export function createRushPackageManager() {
|
||||
return {
|
||||
runCommand: runRushCommand,
|
||||
// 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 packageSpecs = extractRushAddPackageSpecs(args);
|
||||
const changes = [];
|
||||
|
||||
for (const spec of packageSpecs) {
|
||||
const parsed = parsePackageSpec(spec);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const exactVersion = await resolvePackageVersion(parsed.name, parsed.version);
|
||||
if (!exactVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
name: parsed.name,
|
||||
version: 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractRushAddPackageSpecs(args) {
|
||||
const packageSpecs = [];
|
||||
|
||||
for (let i = 1; 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);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!arg.startsWith("-")) {
|
||||
packageSpecs.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return packageSpecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,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,63 @@
|
|||
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {Promise<{status: number}>}
|
||||
*/
|
||||
export async function runRushCommand(args) {
|
||||
try {
|
||||
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||
normalizeProxyEnvironmentVariables(env);
|
||||
|
||||
const result = await safeSpawn("rush", args, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
|
||||
return { status: result.status };
|
||||
} catch (/** @type any */ error) {
|
||||
return reportCommandExecutionFailure(error, "rush");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure proxy settings are visible to package manager variants that rely on
|
||||
* lowercase or npm/yarn-specific environment variables.
|
||||
*
|
||||
* @param {Record<string, string>} env
|
||||
*/
|
||||
function normalizeProxyEnvironmentVariables(env) {
|
||||
if (env.HTTPS_PROXY && !env.HTTP_PROXY) {
|
||||
env.HTTP_PROXY = env.HTTPS_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTP_PROXY && !env.http_proxy) {
|
||||
env.http_proxy = env.HTTP_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTPS_PROXY && !env.https_proxy) {
|
||||
env.https_proxy = env.HTTPS_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTP_PROXY && !env.npm_config_proxy) {
|
||||
env.npm_config_proxy = env.HTTP_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTPS_PROXY && !env.npm_config_https_proxy) {
|
||||
env.npm_config_https_proxy = env.HTTPS_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTP_PROXY && !env.NPM_CONFIG_PROXY) {
|
||||
env.NPM_CONFIG_PROXY = env.HTTP_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTPS_PROXY && !env.NPM_CONFIG_HTTPS_PROXY) {
|
||||
env.NPM_CONFIG_HTTPS_PROXY = env.HTTPS_PROXY;
|
||||
}
|
||||
|
||||
if (env.HTTPS_PROXY && !env.YARN_HTTPS_PROXY) {
|
||||
env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("runRushCommand", () => {
|
||||
let runRushCommand;
|
||||
let safeSpawnMock;
|
||||
let mergeCalls;
|
||||
let nextSpawnStatus;
|
||||
let nextSpawnError;
|
||||
|
||||
beforeEach(async () => {
|
||||
mergeCalls = [];
|
||||
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);
|
||||
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(["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.strictEqual(options.env.HTTP_PROXY, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.https_proxy, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.http_proxy, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.npm_config_https_proxy, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.npm_config_proxy, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.NPM_CONFIG_HTTPS_PROXY, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.NPM_CONFIG_PROXY, "http://localhost:8080");
|
||||
assert.strictEqual(options.env.YARN_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(["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(["install"]);
|
||||
|
||||
assert.strictEqual(res.status, 1);
|
||||
});
|
||||
});
|
||||
|
|
@ -48,6 +48,12 @@ export const knownAikidoTools = [
|
|||
ecoSystem: ECOSYSTEM_JS,
|
||||
internalPackageManagerName: "pnpx",
|
||||
},
|
||||
{
|
||||
tool: "rush",
|
||||
aikidoCommand: "aikido-rush",
|
||||
ecoSystem: ECOSYSTEM_JS,
|
||||
internalPackageManagerName: "rush",
|
||||
},
|
||||
{
|
||||
tool: "bun",
|
||||
aikidoCommand: "aikido-bun",
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ describe("Setup CI shell integration", () => {
|
|||
knownAikidoTools: [
|
||||
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
||||
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
||||
{ tool: "rush", aikidoCommand: "aikido-rush" },
|
||||
],
|
||||
getPackageManagerList: () => "npm, yarn",
|
||||
getPackageManagerList: () => "npm, yarn, rush",
|
||||
getShimsDir: () => mockShimsDir,
|
||||
},
|
||||
});
|
||||
|
|
@ -115,6 +116,10 @@ describe("Setup CI shell integration", () => {
|
|||
const yarnShimPath = path.join(mockShimsDir, "yarn");
|
||||
assert.ok(fs.existsSync(yarnShimPath), "yarn shim should exist");
|
||||
|
||||
// Check if rush shim was created
|
||||
const rushShimPath = path.join(mockShimsDir, "rush");
|
||||
assert.ok(fs.existsSync(rushShimPath), "rush shim should exist");
|
||||
|
||||
// Check content of npm shim
|
||||
const npmShimContent = fs.readFileSync(npmShimPath, "utf-8");
|
||||
assert.ok(npmShimContent.includes("aikido-npm"), "npm shim should contain aikido-npm");
|
||||
|
|
@ -137,6 +142,9 @@ describe("Setup CI shell integration", () => {
|
|||
const yarnShimPath = path.join(mockShimsDir, "yarn.cmd");
|
||||
assert.ok(fs.existsSync(yarnShimPath), "yarn.cmd shim should exist");
|
||||
|
||||
const rushShimPath = path.join(mockShimsDir, "rush.cmd");
|
||||
assert.ok(fs.existsSync(rushShimPath), "rush.cmd shim should exist");
|
||||
|
||||
// Check content of npm.cmd shim
|
||||
const npmShimContent = fs.readFileSync(npmShimPath, "utf-8");
|
||||
assert.ok(npmShimContent.includes("aikido-npm"), "npm.cmd should contain aikido-npm");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue