From 5969ac8ccf8b62d75023cbaadb416d28affac545 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 6 May 2026 08:38:58 +0100 Subject: [PATCH] test: parallelize plugin package scan --- .../npm-install-security-scan.release.test.ts | 130 +++++++++++++----- 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/src/plugins/npm-install-security-scan.release.test.ts b/src/plugins/npm-install-security-scan.release.test.ts index 035b59795e0..9187b30605c 100644 --- a/src/plugins/npm-install-security-scan.release.test.ts +++ b/src/plugins/npm-install-security-scan.release.test.ts @@ -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 { + 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( + items: readonly T[], + concurrency: number, + fn: (item: T) => Promise, +): Promise { + const results = new Array(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(), );