fix(release): harden trusted plugin scan

This commit is contained in:
Vincent Koc
2026-08-20 17:09:09 -07:00
parent 412ee3b783
commit e88d3410eb
4 changed files with 48 additions and 22 deletions
@@ -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
+24 -16
View File
@@ -72,7 +72,7 @@ const COMMON_REVIEWED_CRITICAL_FINDING_COUNTS = new Map<string, number>([
const REVIEWED_RELEASE_LAYOUTS = Object.freeze([
{
id: "beta3",
id: "frozen-legacy",
findings: new Map<string, number>([
["@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<PluginNpmSecurityScanReport> {
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<string, number>([
...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<PluginNpmSecurityScanReport> {
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 });
}
+20 -5
View File
@@ -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);
});
@@ -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");
});