diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index aa26aa44136d..6e2026a76c6a 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -332,7 +332,6 @@ jobs: fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "plugin-prerelease" ]]; then echo "- Plugin prerelease: \`Plugin Prerelease\` with \`target_ref=${TARGET_SHA}\`" - echo "- Plugin prerelease Node exclusions: \`${plugin_prerelease_node_exclusions}\`" else echo "- Plugin prerelease: skipped by rerun group" fi diff --git a/scripts/lib/plugin-npm-security-scan.mts b/scripts/lib/plugin-npm-security-scan.mts index a605365fb6b8..2eb81e5d04af 100644 --- a/scripts/lib/plugin-npm-security-scan.mts +++ b/scripts/lib/plugin-npm-security-scan.mts @@ -72,7 +72,7 @@ const COMMON_REVIEWED_CRITICAL_FINDING_COUNTS = new Map([ const REVIEWED_RELEASE_LAYOUTS = Object.freeze([ { - id: "beta3", + id: "frozen-legacy", findings: new Map([ ["@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/http.ts", 1], ["@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/processes.ts", 1], @@ -382,19 +382,12 @@ function expectedRequiredFindingsForPackage( ); } -export async function runPluginNpmSecurityScan(params: { - candidateDir: string; - toolingDir: string; -}): Promise { - const candidateDir = realpathSync(params.candidateDir); - const toolingDir = realpathSync(params.toolingDir); - const [candidateSha, toolingSha, packages] = await Promise.all([ - gitOutput(candidateDir, ["rev-parse", "HEAD"]), - gitOutput(toolingDir, ["rev-parse", "HEAD"]), - listPublishablePluginPackages(candidateDir), - ]); - - const packageResults = await Promise.all(packages.map(scanPublishablePluginPackage)); +export function buildPluginNpmSecurityScanReport(params: { + candidateSha: string; + packageResults: ScanPackageResult[]; + toolingSha: string; +}): PluginNpmSecurityScanReport { + const { candidateSha, packageResults, toolingSha } = params; const allReviewedFindings = packageResults.flatMap((result) => result.reviewedCriticalFindings); const layout = resolveReviewedSourceLayout(allReviewedFindings); const errors: string[] = []; @@ -402,11 +395,11 @@ export async function runPluginNpmSecurityScan(params: { if (!layout) { errors.push("Reviewed critical findings do not match exactly one supported release layout."); } - if (packages.length === 0) { + if (packageResults.length === 0) { errors.push("No publishable npm plugins were found in the candidate checkout."); } - const publishablePackageNames = new Set(packages.map((plugin) => plugin.packageName)); + const publishablePackageNames = new Set(packageResults.map((result) => result.packageName)); const requiredFindingCounts = new Map([ ...COMMON_REVIEWED_CRITICAL_FINDING_COUNTS, ...(layout?.findings ?? []), @@ -462,3 +455,18 @@ export async function runPluginNpmSecurityScan(params: { toolingSha, }; } + +export async function runPluginNpmSecurityScan(params: { + candidateDir: string; + toolingDir: string; +}): Promise { + const candidateDir = realpathSync(params.candidateDir); + const toolingDir = realpathSync(params.toolingDir); + const [candidateSha, toolingSha, packages] = await Promise.all([ + gitOutput(candidateDir, ["rev-parse", "HEAD"]), + gitOutput(toolingDir, ["rev-parse", "HEAD"]), + listPublishablePluginPackages(candidateDir), + ]); + const packageResults = await Promise.all(packages.map(scanPublishablePluginPackage)); + return buildPluginNpmSecurityScanReport({ candidateSha, packageResults, toolingSha }); +} diff --git a/test/scripts/plugin-npm-security-scan.test.ts b/test/scripts/plugin-npm-security-scan.test.ts index d4fbe9acdb62..0699b495145a 100644 --- a/test/scripts/plugin-npm-security-scan.test.ts +++ b/test/scripts/plugin-npm-security-scan.test.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { assertCompleteScannerSummary, + buildPluginNpmSecurityScanReport, collectNpmPackedFiles, resolveReviewedSourceLayout, runPluginNpmSecurityScan, @@ -13,12 +14,12 @@ import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); describe("scripts/lib/plugin-npm-security-scan.mts", () => { - it("accepts only the complete current and beta3 source layouts", () => { + it("accepts only the complete current and frozen-legacy source layouts", () => { const current = [ "@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/sandbox-child.ts", "@openclaw/codex:dangerous-exec:src/app-server/transport-process-containment.ts", ]; - const beta3 = [ + const frozenLegacy = [ "@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/http.ts", "@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/processes.ts", "@openclaw/codex:dangerous-exec:src/node-cli-sessions.ts", @@ -26,9 +27,9 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { ]; expect(resolveReviewedSourceLayout(current)?.id).toBe("current"); - expect(resolveReviewedSourceLayout(beta3)?.id).toBe("beta3"); - expect(resolveReviewedSourceLayout(beta3.slice(0, -1))).toBeUndefined(); - expect(resolveReviewedSourceLayout([...current, beta3[0]!])).toBeUndefined(); + expect(resolveReviewedSourceLayout(frozenLegacy)?.id).toBe("frozen-legacy"); + expect(resolveReviewedSourceLayout(frozenLegacy.slice(0, -1))).toBeUndefined(); + expect(resolveReviewedSourceLayout([...current, frozenLegacy[0]!])).toBeUndefined(); expect(resolveReviewedSourceLayout([...current, current[0]!])).toBeUndefined(); }); @@ -97,5 +98,19 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { }); expect(report.summary.packageCount).toBe(report.packages.length); expect(report.summary.packageCount).toBeGreaterThan(0); + + const packageResults = structuredClone(report.packages); + packageResults[0]!.unexpectedCriticalFindings.push( + `${packageResults[0]!.packageName}:dangerous-exec:src/candidate-owned-scanner.ts:1:exec`, + ); + const rejectedReport = buildPluginNpmSecurityScanReport({ + candidateSha: report.candidateSha, + packageResults, + toolingSha: report.toolingSha, + }); + expect(rejectedReport.status).toBe("fail"); + expect(rejectedReport.errors).toContainEqual( + expect.stringContaining("unexpected critical findings"), + ); }, 120_000); }); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 8894e8e39bdc..0773bb7b8994 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -326,6 +326,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(runSecurityScan?.run).toContain( "node --import tsx scripts/plugin-npm-security-scan.mts", ); + expect(runSecurityScan?.run).not.toContain( + ".release-candidate/scripts/plugin-npm-security-scan.mts", + ); expect(runSecurityScan?.run).toContain("--candidate-root .release-candidate"); expect(runSecurityScan?.run).toContain('--candidate-sha "$CANDIDATE_SHA"'); expect(uploadReport).toMatchObject({ @@ -341,6 +344,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(pluginSource).not.toContain("npm-install-security-scan.release.test.ts"); expect(pluginSource).not.toContain("node_test_exclude_patterns_json"); expect(releaseSource).not.toContain("plugin_prerelease_node_exclude_patterns_json"); + expect(releaseSource).not.toContain("Plugin prerelease Node exclusions"); expect(pluginDispatch?.run).not.toContain("node_test_exclude_patterns_json"); });