mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10: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
packages/safe-chain/bin/aikido-rush.js
Executable file
14
packages/safe-chain/bin/aikido-rush.js
Executable 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 = "rush";
|
||||
initializePackageManager(packageManagerName);
|
||||
|
||||
(async () => {
|
||||
var exitCode = await main(process.argv.slice(2));
|
||||
process.exit(exitCode);
|
||||
})();
|
||||
14
packages/safe-chain/bin/aikido-rushx.js
Executable file
14
packages/safe-chain/bin/aikido-rushx.js
Executable 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);
|
||||
})();
|
||||
|
|
@ -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, 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(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
"aikido-yarn": "bin/aikido-yarn.js",
|
||||
"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 +40,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), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), [pip](https://pip.pypa.io/), and [pdm](https://pdm-project.org/) 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, bun, bunx, uv, pip/pip3, or pdm 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), [pip](https://pip.pypa.io/), and [pdm](https://pdm-project.org/) 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, pip/pip3, or pdm from downloading or running the malware.",
|
||||
"dependencies": {
|
||||
"certifi": "14.5.15",
|
||||
"chalk": "5.4.1",
|
||||
|
|
|
|||
|
|
@ -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(), []);
|
||||
});
|
||||
|
|
@ -42,7 +42,7 @@ function getSafeChainProxyEnvironmentVariables() {
|
|||
return {};
|
||||
}
|
||||
|
||||
const proxyUrl = `http://localhost:${state.port}`;
|
||||
const proxyUrl = `http://127.0.0.1:${state.port}`;
|
||||
const caCertPath = getCombinedCaBundlePath();
|
||||
|
||||
return {
|
||||
|
|
@ -95,8 +95,11 @@ function createProxyServer() {
|
|||
*/
|
||||
function startServer(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Passing port 0 makes the OS assign an available port
|
||||
server.listen(0, () => {
|
||||
// Bind to loopback only. Without an explicit host, Node listens on every
|
||||
// interface, turning the proxy into an unauthenticated forward proxy that
|
||||
// anyone reachable on the network can use to hit the victim's localhost,
|
||||
// intranet, or cloud metadata endpoints. Port 0 lets the OS pick a port.
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (address && typeof address === "object") {
|
||||
state.port = address.port;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { before, after, describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import {
|
||||
createSafeChainProxy,
|
||||
mergeSafeChainProxyEnvironmentVariables,
|
||||
} from "./registryProxy.js";
|
||||
|
||||
describe("registryProxy loopback binding", () => {
|
||||
let proxy, proxyPort;
|
||||
|
||||
before(async () => {
|
||||
proxy = createSafeChainProxy();
|
||||
await proxy.startServer();
|
||||
const envVars = mergeSafeChainProxyEnvironmentVariables([]);
|
||||
proxyPort = parseInt(new URL(envVars.HTTPS_PROXY).port, 10);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await proxy.stopServer();
|
||||
});
|
||||
|
||||
it("advertises a loopback HTTPS_PROXY URL", () => {
|
||||
const envVars = mergeSafeChainProxyEnvironmentVariables([]);
|
||||
const hostname = new URL(envVars.HTTPS_PROXY).hostname;
|
||||
assert.ok(
|
||||
hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost",
|
||||
`expected loopback hostname, got ${hostname}`
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses connections on non-loopback interfaces", async () => {
|
||||
const externalAddrs = Object.values(os.networkInterfaces())
|
||||
.flat()
|
||||
.filter((iface) => iface && iface.family === "IPv4" && !iface.internal)
|
||||
.map((iface) => iface.address);
|
||||
|
||||
if (externalAddrs.length === 0) {
|
||||
// No non-loopback interface available (e.g. locked-down CI) - skip.
|
||||
return;
|
||||
}
|
||||
|
||||
for (const addr of externalAddrs) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const sock = net.createConnection({ host: addr, port: proxyPort });
|
||||
const timer = setTimeout(() => {
|
||||
sock.destroy();
|
||||
resolve(); // Filtered / dropped is also fine - we just don't want success.
|
||||
}, 500);
|
||||
sock.once("connect", () => {
|
||||
clearTimeout(timer);
|
||||
sock.destroy();
|
||||
reject(
|
||||
new Error(
|
||||
`proxy accepted a connection on non-loopback ${addr}:${proxyPort}`
|
||||
)
|
||||
);
|
||||
});
|
||||
sock.once("error", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -15,8 +15,12 @@ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
|
|||
* @property {function(string, string): boolean} isMalware
|
||||
*/
|
||||
|
||||
/** @type {MalwareDatabase | null} */
|
||||
let cachedMalwareDatabase = null;
|
||||
// Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
|
||||
// value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
|
||||
// control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
|
||||
// concurrent callers see it immediately and share a single fetch.
|
||||
/** @type {Promise<MalwareDatabase> | null} */
|
||||
let cachedMalwareDatabasePromise = null;
|
||||
|
||||
/**
|
||||
* Normalize package name for comparison.
|
||||
|
|
@ -34,45 +38,44 @@ function normalizePackageName(name) {
|
|||
return name;
|
||||
}
|
||||
|
||||
export async function openMalwareDatabase() {
|
||||
if (cachedMalwareDatabase) {
|
||||
return cachedMalwareDatabase;
|
||||
}
|
||||
export function openMalwareDatabase() {
|
||||
if (!cachedMalwareDatabasePromise) {
|
||||
cachedMalwareDatabasePromise = getMalwareDatabase().then((malwareDatabase) => {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPackageStatus(name, version) {
|
||||
const normalizedName = normalizePackageName(name);
|
||||
const packageData = malwareDatabase.find(
|
||||
(pkg) => {
|
||||
const normalizedPkgName = normalizePackageName(pkg.package_name);
|
||||
return normalizedPkgName === normalizedName &&
|
||||
(pkg.version === version || pkg.version === "*");
|
||||
}
|
||||
);
|
||||
|
||||
const malwareDatabase = await getMalwareDatabase();
|
||||
if (!packageData) {
|
||||
return MALWARE_STATUS_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPackageStatus(name, version) {
|
||||
const normalizedName = normalizePackageName(name);
|
||||
const packageData = malwareDatabase.find(
|
||||
(pkg) => {
|
||||
const normalizedPkgName = normalizePackageName(pkg.package_name);
|
||||
return normalizedPkgName === normalizedName &&
|
||||
(pkg.version === version || pkg.version === "*");
|
||||
return packageData.reason;
|
||||
}
|
||||
);
|
||||
|
||||
if (!packageData) {
|
||||
return MALWARE_STATUS_OK;
|
||||
}
|
||||
|
||||
return packageData.reason;
|
||||
return {
|
||||
getPackageStatus,
|
||||
isMalware: (/** @type {string} */ name, /** @type {string} */ version) => {
|
||||
const status = getPackageStatus(name, version);
|
||||
return isMalwareStatus(status);
|
||||
},
|
||||
};
|
||||
}).catch((error) => {
|
||||
cachedMalwareDatabasePromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// This implicitly caches the malware database
|
||||
// that's closed over by the getPackageStatus function
|
||||
cachedMalwareDatabase = {
|
||||
getPackageStatus,
|
||||
isMalware: (name, version) => {
|
||||
const status = getPackageStatus(name, version);
|
||||
return isMalwareStatus(status);
|
||||
},
|
||||
};
|
||||
return cachedMalwareDatabase;
|
||||
return cachedMalwareDatabasePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,30 +16,27 @@ import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.
|
|||
*/
|
||||
|
||||
// Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
|
||||
/** @type {NewPackagesDatabase | null} */
|
||||
let cachedNewPackagesDatabase = null;
|
||||
// Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
|
||||
// value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
|
||||
// control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
|
||||
// concurrent callers see it immediately and share a single fetch.
|
||||
/** @type {Promise<NewPackagesDatabase> | null} */
|
||||
let cachedNewPackagesDatabasePromise = null;
|
||||
|
||||
/**
|
||||
* @returns {Promise<NewPackagesDatabase>}
|
||||
*/
|
||||
export async function openNewPackagesDatabase() {
|
||||
if (cachedNewPackagesDatabase) {
|
||||
return cachedNewPackagesDatabase;
|
||||
export function openNewPackagesDatabase() {
|
||||
if (!cachedNewPackagesDatabasePromise) {
|
||||
cachedNewPackagesDatabasePromise = getNewPackagesList()
|
||||
.then((newPackagesList) => buildNewPackagesDatabase(newPackagesList))
|
||||
.catch((/** @type {any} */ error) => {
|
||||
warnOnceAboutUnavailableDatabase(error);
|
||||
cachedNewPackagesDatabasePromise = null;
|
||||
return { isNewlyReleasedPackage: () => false };
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import("../api/aikido.js").NewPackageEntry[]} */
|
||||
let newPackagesList;
|
||||
|
||||
try {
|
||||
newPackagesList = await getNewPackagesList();
|
||||
} catch (/** @type {any} */ error) {
|
||||
warnOnceAboutUnavailableDatabase(error);
|
||||
cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false };
|
||||
return cachedNewPackagesDatabase;
|
||||
}
|
||||
|
||||
cachedNewPackagesDatabase = buildNewPackagesDatabase(newPackagesList);
|
||||
return cachedNewPackagesDatabase;
|
||||
return cachedNewPackagesDatabasePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -48,6 +48,18 @@ export const knownAikidoTools = [
|
|||
ecoSystem: ECOSYSTEM_JS,
|
||||
internalPackageManagerName: "pnpx",
|
||||
},
|
||||
{
|
||||
tool: "rush",
|
||||
aikidoCommand: "aikido-rush",
|
||||
ecoSystem: ECOSYSTEM_JS,
|
||||
internalPackageManagerName: "rush",
|
||||
},
|
||||
{
|
||||
tool: "rushx",
|
||||
aikidoCommand: "aikido-rushx",
|
||||
ecoSystem: ECOSYSTEM_JS,
|
||||
internalPackageManagerName: "rushx",
|
||||
},
|
||||
{
|
||||
tool: "bun",
|
||||
aikidoCommand: "aikido-bun",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ function pnpx() {
|
|||
wrapSafeChainCommand "pnpx" "$@"
|
||||
}
|
||||
|
||||
function rush() {
|
||||
wrapSafeChainCommand "rush" "$@"
|
||||
}
|
||||
|
||||
function rushx() {
|
||||
wrapSafeChainCommand "rushx" "$@"
|
||||
}
|
||||
|
||||
function bun() {
|
||||
wrapSafeChainCommand "bun" "$@"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue