Implement a proxy blocking tarball requests for packages containing malware.

This commit is contained in:
Sander Declerck 2025-09-30 13:52:21 +02:00
parent 04cb001006
commit e2afcb16e3
No known key found for this signature in database
16 changed files with 633 additions and 33 deletions

View file

@ -8,7 +8,8 @@ export function dryRunScanner(scannerOptions) {
shouldScan: (args) => shouldScanDependencies(scannerOptions, args),
};
}
function scanDependencies(scannerOptions, args) {
async function scanDependencies(scannerOptions, args) {
let dryRunArgs = args;
if (scannerOptions?.dryRunCommand) {
@ -31,8 +32,8 @@ function shouldScanDependencies(scannerOptions, args) {
return true;
}
function checkChangesWithDryRun(args) {
const dryRunOutput = dryRunNpmCommandAndOutput(args);
async function checkChangesWithDryRun(args) {
const dryRunOutput = await dryRunNpmCommandAndOutput(args);
// Dry-run can return a non-zero status code in some cases
// e.g., when running "npm audit fix --dry-run", it returns exit code 1

View file

@ -36,7 +36,7 @@ describe("dryRunScanner", async () => {
}));
const scanner = dryRunScanner();
const result = scanner.scan(["audit", "fix"]);
const result = await scanner.scan(["audit", "fix"]);
// Should not throw an error for audit fix commands
assert.ok(Array.isArray(result));
@ -53,8 +53,8 @@ describe("dryRunScanner", async () => {
const scanner = dryRunScanner();
assert.throws(() => {
scanner.scan(["install", "lodash"]);
await assert.rejects(async () => {
await scanner.scan(["install", "lodash"]);
}, /Dry-run command failed with exit code 1/);
});
@ -67,7 +67,7 @@ describe("dryRunScanner", async () => {
}));
const scanner = dryRunScanner();
const result = scanner.scan(["install", "lodash"]);
const result = await scanner.scan(["install", "lodash"]);
assert.ok(Array.isArray(result));
assert.equal(mockWriteError.mock.callCount(), 0);
@ -83,8 +83,8 @@ describe("dryRunScanner", async () => {
const scanner = dryRunScanner();
assert.throws(() => {
scanner.scan(["audit", "fix"]);
await assert.rejects(async () => {
await scanner.scan(["audit", "fix"]);
}, /Dry-run command failed with exit code 1/);
});
});
@ -99,7 +99,7 @@ describe("dryRunScanner", async () => {
}));
const scanner = dryRunScanner({ dryRunCommand: "install" });
scanner.scan(["install-test", "lodash"]);
await scanner.scan(["install-test", "lodash"]);
// Should call with "install" instead of "install-test"
assert.equal(mockDryRunNpmCommandAndOutput.mock.callCount(), 1);

View file

@ -1,10 +1,14 @@
import { execSync } from "child_process";
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
export function runNpm(args) {
export async function runNpm(args) {
try {
const npmCommand = `npm ${args.join(" ")}`;
execSync(npmCommand, { stdio: "inherit" });
const result = await safeSpawn("npm", args, {
stdio: "inherit",
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
return { status: result.status };
} catch (error) {
if (error.status) {
return { status: error.status };
@ -13,17 +17,29 @@ export function runNpm(args) {
return { status: 1 };
}
}
return { status: 0 };
}
export function dryRunNpmCommandAndOutput(args) {
export async function dryRunNpmCommandAndOutput(args) {
try {
const npmCommand = `npm ${args.join(" ")} --ignore-scripts --dry-run`;
const output = execSync(npmCommand, { stdio: "pipe" });
return { status: 0, output: output.toString() };
const result = await safeSpawn(
"npm",
[...args, "--ignore-scripts", "--dry-run"],
{
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 ? error.stdout.toString() : "";
const output =
error.stdout?.toString() ??
error.stderr?.toString() ??
error.message ??
"";
return { status: error.status, output };
} else {
ui.writeError("Error executing command:", error.message);

View file

@ -1,13 +1,20 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawnSync } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
export function runPnpmCommand(args, toolName = "pnpm") {
export async function runPnpmCommand(args, toolName = "pnpm") {
try {
let result;
if (toolName === "pnpm") {
result = safeSpawnSync("pnpm", args, { stdio: "inherit" });
result = await safeSpawn("pnpm", args, {
stdio: "inherit",
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
} else if (toolName === "pnpx") {
result = safeSpawnSync("pnpx", args, { stdio: "inherit" });
result = await safeSpawn("pnpx", args, {
stdio: "inherit",
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
} else {
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
}

View file

@ -1,10 +1,17 @@
import { execSync } from "child_process";
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
export function runYarnCommand(args) {
export async function runYarnCommand(args) {
try {
const npxCommand = `yarn ${args.join(" ")}`;
execSync(npxCommand, { stdio: "inherit" });
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
await fixYarnProxyEnvironmentVariables(env);
const result = await safeSpawn("yarn", args, {
stdio: "inherit",
env,
});
return { status: result.status };
} catch (error) {
if (error.status) {
return { status: error.status };
@ -13,5 +20,34 @@ export function runYarnCommand(args) {
return { status: 1 };
}
}
return { status: 0 };
}
async function fixYarnProxyEnvironmentVariables(env) {
// Yarn ignores standard proxy environment variables HTTPS_PROXY and NODE_EXTRA_CA_CERTS
// Yarn v2/v3 and v4+ use different environment variables for proxy and CA certs
// When setting all variables, yarn returns an error about conflicting variables
// - v2/v3: "Usage Error: Unrecognized or legacy configuration settings found: httpsCaFilePath"
// - v4+: "Usage Error: Unrecognized or legacy configuration settings found: caFilePath"
const version = await yarnVersion();
const majorVersion = parseInt(version.split(".")[0]);
if (majorVersion >= 4) {
env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
env.YARN_HTTPS_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS;
} else if (majorVersion === 2 || majorVersion === 3) {
env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
env.YARN_CA_FILE_PATH = env.NODE_EXTRA_CA_CERTS;
}
}
async function yarnVersion() {
const result = await safeSpawn("yarn", ["--version"], {
stdio: "pipe",
});
if (result.status !== 0) {
throw new Error("Failed to get yarn version");
}
return result.stdout.trim();
}