test: parallelize plugin package scan

This commit is contained in:
Peter Steinberger
2026-05-06 08:38:58 +01:00
parent c5fcfa1b56
commit 5969ac8ccf

View File

@@ -1,7 +1,8 @@
import { execFileSync } from "node:child_process";
import { execFile } from "node:child_process";
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve, sep } from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { isScannable, scanDirectoryWithSummary } from "../security/skill-scanner.js";
@@ -18,6 +19,9 @@ type PublishablePluginPackage = {
packageName: string;
};
const execFileAsync = promisify(execFile);
const PACKAGE_SCAN_CONCURRENCY = 6;
const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS = new Set([
"@openclaw/acpx:dangerous-exec:src/codex-auth-bridge.ts",
"@openclaw/acpx:dangerous-exec:src/runtime-internals/mcp-proxy.mjs",
@@ -61,14 +65,17 @@ function parseNpmPackFiles(raw: string, packageName: string): string[] {
.toSorted();
}
function collectNpmPackedFiles(packageDir: string, packageName: string): string[] {
const raw = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
cwd: packageDir,
encoding: "utf8",
maxBuffer: 128 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
});
return parseNpmPackFiles(raw, packageName);
async function collectNpmPackedFiles(packageDir: string, packageName: string): Promise<string[]> {
const { stdout } = await execFileAsync(
"npm",
["pack", "--dry-run", "--json", "--ignore-scripts"],
{
cwd: packageDir,
encoding: "utf8",
maxBuffer: 128 * 1024 * 1024,
},
);
return parseNpmPackFiles(stdout, packageName);
}
function isScannerWalkedPackedPath(packedPath: string): boolean {
@@ -141,6 +148,72 @@ function collectPublishablePluginPackages(): PublishablePluginPackage[] {
.toSorted((left, right) => left.packageName.localeCompare(right.packageName));
}
async function mapWithConcurrency<T, U>(
items: readonly T[],
concurrency: number,
fn: (item: T) => Promise<U>,
): Promise<U[]> {
const results = new Array<U>(items.length);
let nextIndex = 0;
const workerCount = Math.min(concurrency, items.length);
await Promise.all(
Array.from({ length: workerCount }, async () => {
while (nextIndex < items.length) {
const index = nextIndex;
nextIndex += 1;
results[index] = await fn(items[index]!);
}
}),
);
return results;
}
async function scanPublishablePluginPackage(plugin: PublishablePluginPackage): Promise<{
reviewedCriticalFindings: string[];
expectedReviewedCriticalFindings: string[];
unexpectedCriticalFindings: string[];
}> {
const reviewedCriticalFindings: string[] = [];
const expectedReviewedCriticalFindings: string[] = [];
const unexpectedCriticalFindings: string[] = [];
const packedFiles = await collectNpmPackedFiles(plugin.packageDir, plugin.packageName);
for (const packedFile of packedFiles) {
const key = `${plugin.packageName}:dangerous-exec:${normalizePackedFindingPath(packedFile)}`;
if (OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS.has(key)) {
expectedReviewedCriticalFindings.push(key);
}
}
const stageDir = stageScannerRelevantPackedFiles(plugin.packageDir, packedFiles);
const summary = await scanDirectoryWithSummary(stageDir, {
excludeTestFiles: true,
maxFiles: 10_000,
});
for (const finding of summary.findings) {
if (finding.severity !== "critical") {
continue;
}
const packedPath = normalizePackedFindingPath(
relative(stageDir, finding.file).split(sep).join("/"),
);
const key = `${plugin.packageName}:${finding.ruleId}:${packedPath}`;
if (
REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS.has(key) ||
OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS.has(key)
) {
reviewedCriticalFindings.push(key);
continue;
}
unexpectedCriticalFindings.push([key, `${finding.line}`, finding.evidence].join(":"));
}
return {
reviewedCriticalFindings,
expectedReviewedCriticalFindings,
unexpectedCriticalFindings,
};
}
describe("publishable plugin npm package install security scan", () => {
it("keeps npm-published plugin files clear of unexpected critical hits", async () => {
const unexpectedCriticalFindings: string[] = [];
@@ -149,37 +222,22 @@ describe("publishable plugin npm package install security scan", () => {
REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS,
);
for (const plugin of collectPublishablePluginPackages()) {
const packedFiles = collectNpmPackedFiles(plugin.packageDir, plugin.packageName);
for (const packedFile of packedFiles) {
const key = `${plugin.packageName}:dangerous-exec:${normalizePackedFindingPath(packedFile)}`;
if (OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS.has(key)) {
expectedReviewedCriticalFindings.add(key);
}
const packageResults = await mapWithConcurrency(
collectPublishablePluginPackages(),
PACKAGE_SCAN_CONCURRENCY,
scanPublishablePluginPackage,
);
for (const result of packageResults) {
for (const key of result.expectedReviewedCriticalFindings) {
expectedReviewedCriticalFindings.add(key);
}
const stageDir = stageScannerRelevantPackedFiles(plugin.packageDir, packedFiles);
const summary = await scanDirectoryWithSummary(stageDir, {
excludeTestFiles: true,
maxFiles: 10_000,
});
for (const finding of summary.findings) {
if (finding.severity !== "critical") {
continue;
}
const packedPath = normalizePackedFindingPath(
relative(stageDir, finding.file).split(sep).join("/"),
);
const key = `${plugin.packageName}:${finding.ruleId}:${packedPath}`;
if (expectedReviewedCriticalFindings.has(key)) {
reviewedCriticalFindings.add(key);
continue;
}
unexpectedCriticalFindings.push([key, `${finding.line}`, finding.evidence].join(":"));
for (const key of result.reviewedCriticalFindings) {
reviewedCriticalFindings.add(key);
}
unexpectedCriticalFindings.push(...result.unexpectedCriticalFindings);
}
expect(unexpectedCriticalFindings).toEqual([]);
expect(unexpectedCriticalFindings.toSorted()).toEqual([]);
expect([...reviewedCriticalFindings].toSorted()).toEqual(
[...expectedReviewedCriticalFindings].toSorted(),
);