Add rushx support too

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
James McMeeking 2026-05-01 17:04:28 +01:00
parent 5cf2ffe201
commit 98a1ba7d10
No known key found for this signature in database
GPG key ID: C69A11061EE15228
16 changed files with 101 additions and 27 deletions

View file

@ -0,0 +1,14 @@
#!/usr/bin/env node
import { main } from "../src/main.js";
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
setEcoSystem(ECOSYSTEM_JS);
const packageManagerName = "rushx";
initializePackageManager(packageManagerName);
(async () => {
var exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
})();

View file

@ -108,7 +108,7 @@ function writeHelp() {
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain setup",
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, rush, bun, bunx, pip and pip3.`,
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, pip and pip3.`,
);
ui.writeInformation(
`- ${chalk.cyan(

View file

@ -14,6 +14,7 @@
"aikido-pnpm": "bin/aikido-pnpm.js",
"aikido-pnpx": "bin/aikido-pnpx.js",
"aikido-rush": "bin/aikido-rush.js",
"aikido-rushx": "bin/aikido-rushx.js",
"aikido-bun": "bin/aikido-bun.js",
"aikido-bunx": "bin/aikido-bunx.js",
"aikido-uv": "bin/aikido-uv.js",
@ -38,7 +39,7 @@
"keywords": [],
"author": "Aikido Security",
"license": "AGPL-3.0-or-later",
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [rush](https://rushjs.io/), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, rush, bun, bunx, uv, uvx, or pip/pip3 from downloading or running the malware.",
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [rush](https://rushjs.io/), [rushx](https://rushjs.io/pages/commands/rushx/), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, uv, uvx, or pip/pip3 from downloading or running the malware.",
"dependencies": {
"certifi": "14.5.15",
"chalk": "5.4.1",

View file

@ -14,6 +14,7 @@ import { createUvPackageManager } from "./uv/createUvPackageManager.js";
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
import { createRushPackageManager } from "./rush/createRushPackageManager.js";
import { createRushxPackageManager } from "./rushx/createRushxPackageManager.js";
import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js";
/**
@ -70,6 +71,8 @@ export function initializePackageManager(packageManagerName, context) {
state.packageManagerName = createPipXPackageManager();
} else if (packageManagerName === "rush") {
state.packageManagerName = createRushPackageManager();
} else if (packageManagerName === "rushx") {
state.packageManagerName = createRushxPackageManager();
} else {
throw new Error("Unsupported package manager: " + packageManagerName);
}

View file

@ -6,7 +6,7 @@ import { resolvePackageVersion } from "../../api/npmApi.js";
*/
export function createRushPackageManager() {
return {
runCommand: runRushCommand,
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,

View file

@ -3,23 +3,24 @@ 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(args) {
export async function runRushCommand(executableName, args) {
try {
const env = normalizeProxyEnvironmentVariables(
mergeSafeChainProxyEnvironmentVariables(process.env),
);
const result = await safeSpawn("rush", args, {
const result = await safeSpawn(executableName, args, {
stdio: "inherit",
env,
});
return { status: result.status };
} catch (/** @type any */ error) {
return reportCommandExecutionFailure(error, "rush");
return reportCommandExecutionFailure(error, executableName);
}
}

View file

@ -64,7 +64,7 @@ describe("runRushCommand", () => {
});
it("spawns rush with merged proxy env", async () => {
const res = await runRushCommand(["install"]);
const res = await runRushCommand("rush", ["install"]);
assert.strictEqual(res.status, 0);
assert.strictEqual(safeSpawnMock.mock.calls.length, 1);
@ -88,7 +88,7 @@ describe("runRushCommand", () => {
it("returns spawn result status", async () => {
nextSpawnStatus = 7;
const res = await runRushCommand(["update"]);
const res = await runRushCommand("rush", ["update"]);
assert.strictEqual(res.status, 7);
});
@ -98,7 +98,7 @@ describe("runRushCommand", () => {
code: "ENOENT",
});
const res = await runRushCommand(["install"]);
const res = await runRushCommand("rush", ["install"]);
assert.strictEqual(res.status, 1);
});
@ -108,7 +108,7 @@ describe("runRushCommand", () => {
HTTPS_PROXY: "http://localhost:8080",
};
await runRushCommand(["install"]);
await runRushCommand("rush", ["install"]);
assert.deepStrictEqual(mergeResultEnv, {
HTTPS_PROXY: "http://localhost:8080",

View file

@ -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: () => [],
};
}

View file

@ -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(), []);
});

View file

@ -54,6 +54,12 @@ export const knownAikidoTools = [
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "rush",
},
{
tool: "rushx",
aikidoCommand: "aikido-rushx",
ecoSystem: ECOSYSTEM_JS,
internalPackageManagerName: "rushx",
},
{
tool: "bun",
aikidoCommand: "aikido-bun",

View file

@ -48,9 +48,8 @@ describe("Setup CI shell integration", () => {
knownAikidoTools: [
{ tool: "npm", aikidoCommand: "aikido-npm" },
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
{ tool: "rush", aikidoCommand: "aikido-rush" },
],
getPackageManagerList: () => "npm, yarn, rush",
getPackageManagerList: () => "npm, yarn",
},
});
@ -108,10 +107,6 @@ 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");
@ -138,9 +133,6 @@ 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");

View file

@ -19,6 +19,14 @@ function pnpx
wrapSafeChainCommand "pnpx" $argv
end
function rush
wrapSafeChainCommand "rush" $argv
end
function rushx
wrapSafeChainCommand "rushx" $argv
end
function bun
wrapSafeChainCommand "bun" $argv
end

View file

@ -28,6 +28,14 @@ function pnpx() {
wrapSafeChainCommand "pnpx" "$@"
}
function rush() {
wrapSafeChainCommand "rush" "$@"
}
function rushx() {
wrapSafeChainCommand "rushx" "$@"
}
function bun() {
wrapSafeChainCommand "bun" "$@"
}

View file

@ -22,6 +22,14 @@ function pnpx {
Invoke-WrappedCommand "pnpx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
}
function rush {
Invoke-WrappedCommand "rush" $args $MyInvocation.Line $MyInvocation.OffsetInLine
}
function rushx {
Invoke-WrappedCommand "rushx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
}
function bun {
Invoke-WrappedCommand "bun" $args $MyInvocation.Line $MyInvocation.OffsetInLine
}