From 60bb9477913b76d10dee4a2accb43047170c619e Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 20 Jul 2026 22:08:21 -0700 Subject: [PATCH 1/9] test(images): add security revision container verifier Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 575 ++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 test/openclaw-security-revision-container-e2e.test.ts diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts new file mode 100644 index 00000000000..16017e2684c --- /dev/null +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -0,0 +1,575 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { randomUUID } from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { describe } from "vitest"; + +import { + packReviewedNpmArchive, + removeReviewedNpmArchive, +} from "../scripts/lib/reviewed-npm-archive.mts"; +import { shellQuote } from "./e2e/fixtures/clients/command.ts"; +import { type DockerCommandResult, DockerProbe, resultText } from "./e2e/fixtures/docker-probe.ts"; +import { expect, test } from "./e2e/fixtures/e2e-test.ts"; + +const TARGET_ID = "openclaw-security-revision-container-e2e"; +const RUN_ENV = "NEMOCLAW_RUN_OPENCLAW_SECURITY_REVISION_CONTAINER_E2E"; +const IMAGE_ENV = "NEMOCLAW_OPENCLAW_SECURITY_REVISION_IMAGE"; +const REVIEWED_PLUGIN_SPEC = "@openclaw/slack@2026.6.10"; +const REVIEWED_PLUGIN_INTEGRITY = + "sha512-OOsMLjPcbWhQRM5XDwfdrACjJmKqavFtpuIlhHAXWrLrd/p7SyIVE9AoKS0yxOx6bqGDIMJ9+knzdViHMLgBdA=="; +const REVIEWED_PLUGIN_TARBALL = "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.6.10.tgz"; +const EVIDENCE_PREFIX = "NEMOCLAW_SECURITY_REVISION_EVIDENCE="; +const REPLACEMENT_ROOT = "/usr/local/share/nemoclaw/openclaw-plugin-axios-1.18.0"; +const CONTAINER_HOME = "/sandbox"; +const RUN_TIMEOUT_MS = 3 * 60_000; + +type InstallCase = Readonly<{ + args: readonly string[]; + env?: Readonly>; + expectedStateRoot: string; + id: string; +}>; + +type ProbeEvidence = Readonly<{ + agentBaseVersion: string | null; + axiosVersions: readonly string[]; + caseId: string; + commandExitCode: number; + expectedStateRoot: string; + httpsProxyAgentVersion: string | null; + installedAxiosVersion: string | null; + manifestAxiosVersion: string | null; + openClawVersion: string; + originalInstallSucceeded: boolean | null; + pluginSpecs: readonly string[]; + shrinkwrapAgentBaseVersion: string | null; + shrinkwrapAxiosVersion: string | null; +}>; + +const INSTALL_CASES: readonly InstallCase[] = [ + { + id: "profile-prefix", + args: ["--profile", "security-prefix", "plugins", "install"], + expectedStateRoot: `${CONTAINER_HOME}/.openclaw-security-prefix`, + }, + { + id: "profile-suffix", + args: ["plugins", "install"], + expectedStateRoot: `${CONTAINER_HOME}/.openclaw-security-suffix`, + }, + { + id: "dev-prefix", + args: ["--dev", "plugins", "install"], + expectedStateRoot: `${CONTAINER_HOME}/.openclaw-dev`, + }, + { + id: "dev-suffix", + args: ["plugins", "install"], + expectedStateRoot: `${CONTAINER_HOME}/.openclaw-dev`, + }, + { + id: "custom-state", + args: ["plugins", "install"], + env: { OPENCLAW_STATE_DIR: `${CONTAINER_HOME}/custom-state` }, + expectedStateRoot: `${CONTAINER_HOME}/custom-state`, + }, +]; + +function safeDockerName(value: string): string { + return value + .toLowerCase() + .replace(/[^a-z0-9_.-]+/gu, "-") + .replace(/^-+|-+$/gu, ""); +} + +function requireSafeImageReference(value: string): string { + const image = value.trim(); + if (!/^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/u.test(image)) { + throw new Error(`${IMAGE_ENV} must be a canonical Docker image reference`); + } + return image; +} + +function resolveConfiguredImage(env: NodeJS.ProcessEnv): string | undefined { + const selected = env.E2E_TARGET_ID === TARGET_ID; + const explicit = env[RUN_ENV]; + if (explicit !== undefined && explicit !== "0" && explicit !== "1") { + throw new Error(`${RUN_ENV} must be 0 or 1`); + } + const enabled = selected || explicit === "1"; + const image = env[IMAGE_ENV]?.trim(); + if (!enabled) { + if (image) throw new Error(`${IMAGE_ENV} requires ${RUN_ENV}=1`); + return undefined; + } + if (!image) throw new Error(`${IMAGE_ENV} is required when the container E2E is enabled`); + return requireSafeImageReference(image); +} + +function installArgs(testCase: InstallCase, archivePath: string): string[] { + const args = [...testCase.args]; + const installIndex = args.indexOf("install"); + if (installIndex < 0) throw new Error(`install case ${testCase.id} has no install command`); + args.splice(installIndex + 1, 0, archivePath); + if (testCase.id === "profile-suffix") args.push("--profile", "security-suffix"); + if (testCase.id === "dev-suffix") args.push("--dev"); + return args; +} + +const PROBE_SOURCE = String.raw` +const fs = require("node:fs"); +const path = require("node:path"); + +const stateRoot = process.env.NEMOCLAW_E2E_STATE_ROOT; +if (!stateRoot) throw new Error("NEMOCLAW_E2E_STATE_ROOT is required"); +const packages = []; +let visited = 0; + +function walk(directory) { + if (!fs.existsSync(directory)) return; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (++visited > 50000) throw new Error("state tree exceeded verifier entry bound"); + const child = path.join(directory, entry.name); + if (entry.isSymbolicLink()) continue; + if (entry.isDirectory()) { + walk(child); + continue; + } + if (!entry.isFile() || entry.name !== "package.json") continue; + const manifest = JSON.parse(fs.readFileSync(child, "utf8")); + packages.push({ manifest, root: path.dirname(child) }); + } +} + +function readJson(file) { + if (!fs.existsSync(file)) return undefined; + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +walk(stateRoot); +const plugins = packages.filter(({ manifest }) => manifest.name === "@openclaw/slack"); +const livePlugin = plugins.length === 1 ? plugins[0] : undefined; +const pluginRoot = livePlugin?.root; +const pluginManifest = livePlugin?.manifest; +const shrinkwrap = pluginRoot ? readJson(path.join(pluginRoot, "npm-shrinkwrap.json")) : undefined; +const axiosRoot = pluginRoot ? path.join(pluginRoot, "node_modules", "axios") : undefined; +const installedAxios = axiosRoot ? readJson(path.join(axiosRoot, "package.json")) : undefined; +const proxyRoot = axiosRoot ? path.join(axiosRoot, "node_modules", "https-proxy-agent") : undefined; +const proxyManifest = proxyRoot ? readJson(path.join(proxyRoot, "package.json")) : undefined; +const agentBaseRoot = proxyRoot ? path.join(proxyRoot, "node_modules", "agent-base") : undefined; +const agentBaseManifest = agentBaseRoot ? readJson(path.join(agentBaseRoot, "package.json")) : undefined; +const evidence = { + agentBaseVersion: agentBaseManifest?.version ?? null, + axiosVersions: packages + .filter(({ manifest }) => manifest.name === "axios") + .map(({ manifest }) => String(manifest.version)) + .sort(), + caseId: process.env.NEMOCLAW_E2E_CASE_ID, + commandExitCode: Number(process.env.NEMOCLAW_E2E_COMMAND_EXIT), + expectedStateRoot: stateRoot, + httpsProxyAgentVersion: proxyManifest?.version ?? null, + installedAxiosVersion: installedAxios?.version ?? null, + manifestAxiosVersion: pluginManifest?.dependencies?.axios ?? null, + openClawVersion: process.env.NEMOCLAW_E2E_OPENCLAW_VERSION ?? "", + originalInstallSucceeded: process.env.NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER + ? fs.existsSync(process.env.NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER) + : null, + pluginSpecs: plugins.map(({ manifest }) => String(manifest.name) + "@" + String(manifest.version)).sort(), + shrinkwrapAgentBaseVersion: + shrinkwrap?.packages?.["node_modules/axios/node_modules/https-proxy-agent/node_modules/agent-base"]?.version ?? null, + shrinkwrapAxiosVersion: shrinkwrap?.packages?.["node_modules/axios"]?.version ?? null, +}; +process.stdout.write(${JSON.stringify(EVIDENCE_PREFIX)} + JSON.stringify(evidence) + "\n"); +`; + +const ORIGINAL_SUCCESS_MARKER_SOURCE = String.raw` +const fs = require("node:fs"); +const entrypoint = process.argv[1]; +const marker = process.env.NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER; +if (entrypoint && marker) { + let resolved = entrypoint; + try { + resolved = fs.realpathSync(entrypoint); + } catch {} + if (resolved === "/usr/local/lib/node_modules/openclaw/openclaw.mjs") { + process.once("exit", (code) => { + if (code === 0) fs.writeFileSync(marker, "ok\n", { mode: 0o600 }); + }); + } +} +`; + +function probeScript(testCase: InstallCase, archivePath: string): string { + const env = Object.entries(testCase.env ?? {}) + .map(([name, value]) => `export ${name}=${shellQuote(value)}`) + .join("\n"); + const command = ["/usr/local/bin/openclaw", ...installArgs(testCase, archivePath)] + .map(shellQuote) + .join(" "); + return `set -uo pipefail +umask 077 +export HOME=${shellQuote(CONTAINER_HOME)} +export npm_config_cache=${shellQuote(`${CONTAINER_HOME}/.npm-cache`)} +export npm_config_fetch_retries=1 +export npm_config_fetch_retry_maxtimeout=15000 +export npm_config_fetch_timeout=15000 +export npm_config_ignore_scripts=true +${env} +set +e +${command} +status=$? +set -e +export NEMOCLAW_E2E_CASE_ID=${shellQuote(testCase.id)} +export NEMOCLAW_E2E_COMMAND_EXIT="$status" +export NEMOCLAW_E2E_STATE_ROOT=${shellQuote(testCase.expectedStateRoot)} +export NEMOCLAW_E2E_OPENCLAW_VERSION="$(/usr/local/bin/openclaw --version 2>/dev/null || true)" +node -e ${shellQuote(PROBE_SOURCE)}`; +} + +function secureDockerRunArgs(options: { + container: string; + fixtureVolume: string; + image: string; + script: string; + volume: string; + hideReplacement?: boolean; +}): string[] { + const args = [ + "run", + "--rm", + "--name", + options.container, + "--user", + "sandbox:sandbox", + "--read-only", + "--network", + "bridge", + "--cap-drop", + "ALL", + "--security-opt", + "no-new-privileges", + "--pids-limit", + "256", + "--memory", + "2g", + "--memory-swap", + "2g", + "--cpus", + "2", + "--ulimit", + "nofile=1024:1024", + "--mount", + `type=volume,source=${options.volume},target=${CONTAINER_HOME}`, + "--mount", + `type=volume,source=${options.fixtureVolume},target=/fixture,readonly`, + "--tmpfs", + "/tmp:rw,nosuid,nodev,size=256m,mode=1777", + ]; + if (options.hideReplacement) { + args.push( + "--mount", + `type=tmpfs,target=${REPLACEMENT_ROOT},tmpfs-size=1048576,tmpfs-mode=0555`, + ); + } + args.push("--entrypoint", "bash", options.image, "-lc", options.script); + return args; +} + +function parseEvidence(result: DockerCommandResult): ProbeEvidence { + if (result.exitCode !== 0) throw new Error(resultText(result)); + const line = result.stdout + .split(/\r?\n/gu) + .reverse() + .find((candidate) => candidate.startsWith(EVIDENCE_PREFIX)); + if (!line) + throw new Error(`container did not emit security revision evidence\n${resultText(result)}`); + return JSON.parse(line.slice(EVIDENCE_PREFIX.length)) as ProbeEvidence; +} + +function requireSuccessfulRemediation(testCase: InstallCase, evidence: ProbeEvidence): void { + expect(evidence.caseId).toBe(testCase.id); + expect(evidence.commandExitCode).toBe(0); + expect(evidence.expectedStateRoot).toBe(testCase.expectedStateRoot); + expect(evidence.openClawVersion).toContain("2026.6.10"); + expect(evidence.pluginSpecs).toEqual([REVIEWED_PLUGIN_SPEC]); + expect(evidence.installedAxiosVersion).toBe("1.18.0"); + expect(evidence.manifestAxiosVersion).toBe("1.18.0"); + expect(evidence.shrinkwrapAxiosVersion).toBe("1.18.0"); + expect(evidence.httpsProxyAgentVersion).toBe("5.0.1"); + expect(evidence.agentBaseVersion).toBe("6.0.2"); + expect(evidence.shrinkwrapAgentBaseVersion).toBe("6.0.2"); + expect(evidence.axiosVersions).toContain("1.18.0"); + expect(evidence.axiosVersions).not.toContain("1.16.0"); +} + +function requireFailedInstallRemoved(evidence: ProbeEvidence): void { + expect(evidence.commandExitCode).not.toBe(0); + expect(evidence.originalInstallSucceeded).toBe(true); + expect(evidence.pluginSpecs).toEqual([]); + expect(evidence.axiosVersions).not.toContain("1.16.0"); +} + +const configuredImage = resolveConfiguredImage(process.env); +const realContainerTest = configuredImage ? test : test.skip; + +describe("OpenClaw security revision container E2E contract (#7272)", () => { + test("keeps real Docker execution explicitly opt-in until the revision image lands", () => { + expect(resolveConfiguredImage({})).toBeUndefined(); + expect(() => resolveConfiguredImage({ [RUN_ENV]: "1" })).toThrow(IMAGE_ENV); + expect(() => resolveConfiguredImage({ [IMAGE_ENV]: "candidate:local" })).toThrow(RUN_ENV); + }); + + test("covers every supported OpenClaw state selector without shell-derived inputs", () => { + expect(INSTALL_CASES.map(({ id }) => id)).toEqual([ + "profile-prefix", + "profile-suffix", + "dev-prefix", + "dev-suffix", + "custom-state", + ]); + for (const testCase of INSTALL_CASES) { + expect(installArgs(testCase, "/fixture/plugin.tgz")).toContain("/fixture/plugin.tgz"); + expect(testCase.expectedStateRoot.startsWith(CONTAINER_HOME)).toBe(true); + } + }); + + test("builds an isolated least-privilege Docker boundary without host networking", () => { + const args = secureDockerRunArgs({ + container: "security-e2e", + fixtureVolume: "security-e2e-fixture", + image: "candidate:local", + script: "true", + volume: "security-e2e-state", + }); + expect(args).toContain("bridge"); + expect(args).not.toContain("host"); + expect(args).toContain("--read-only"); + expect(args).toContain("ALL"); + expect(args).toContain("no-new-privileges"); + expect(args.join(" ")).not.toContain("docker.sock"); + }); + + test("rejects vulnerable or inconsistent remediation evidence", () => { + const testCase = INSTALL_CASES[0]; + const good: ProbeEvidence = { + agentBaseVersion: "6.0.2", + axiosVersions: ["1.18.0"], + caseId: testCase.id, + commandExitCode: 0, + expectedStateRoot: testCase.expectedStateRoot, + httpsProxyAgentVersion: "5.0.1", + installedAxiosVersion: "1.18.0", + manifestAxiosVersion: "1.18.0", + openClawVersion: "OpenClaw 2026.6.10", + originalInstallSucceeded: null, + pluginSpecs: [REVIEWED_PLUGIN_SPEC], + shrinkwrapAgentBaseVersion: "6.0.2", + shrinkwrapAxiosVersion: "1.18.0", + }; + expect(() => requireSuccessfulRemediation(testCase, good)).not.toThrow(); + expect(() => + requireSuccessfulRemediation(testCase, { + ...good, + axiosVersions: ["1.16.0"], + }), + ).toThrow(); + expect(() => + requireSuccessfulRemediation(testCase, { + ...good, + shrinkwrapAxiosVersion: "1.16.0", + }), + ).toThrow(); + }); +}); + +realContainerTest( + "the installed wrapper remediates exact local plugins across state selectors and fails closed (#7272)", + async ({ artifacts, cleanup, docker, secrets }) => { + const image = configuredImage as string; + const probe = new DockerProbe(artifacts, (text, extraValues) => + secrets.redact(text, extraValues), + ); + const resourcePrefix = safeDockerName(`nemoclaw-security-e2e-${process.pid}-${randomUUID()}`); + const containers: string[] = []; + const volumes: string[] = []; + const npmHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-security-e2e-npm-")); + cleanup.add("remove reviewed plugin archive", () => + fs.rmSync(npmHome, { recursive: true, force: true }), + ); + cleanup.add("remove security revision containers and volumes", async () => { + for (const container of containers) { + const result = await probe.run(["rm", "-f", container], { + artifactName: `cleanup-${container}`, + timeoutMs: 30_000, + }); + if (result.exitCode !== 0 && !result.stderr.includes("No such container")) { + throw new Error(resultText(result)); + } + } + for (const volume of volumes) { + await probe.expect(["volume", "rm", "-f", volume], { + artifactName: `cleanup-${volume}`, + timeoutMs: 30_000, + }); + } + }); + + await artifacts.target.declare({ + id: TARGET_ID, + boundary: "historical-openclaw-image-wrapper", + image, + contracts: [ + "effective OpenClaw state selectors receive reviewed plugin remediation", + "post-install remediation failure removes the fresh vulnerable plugin", + ], + }); + await docker.requireDocker(); + await probe.expect(["image", "inspect", image], { + artifactName: "inspect-security-revision-image", + timeoutMs: 30_000, + }); + + const reviewedArchive = packReviewedNpmArchive({ + env: { + HOME: npmHome, + PATH: process.env.PATH, + npm_config_audit: "false", + npm_config_cache: path.join(npmHome, "cache"), + npm_config_fund: "false", + npm_config_ignore_scripts: "true", + npm_config_userconfig: "/dev/null", + }, + expectedIntegrity: REVIEWED_PLUGIN_INTEGRITY, + label: "OpenClaw security revision E2E fixture", + packageSpec: REVIEWED_PLUGIN_SPEC, + tarballUrl: REVIEWED_PLUGIN_TARBALL, + tempDirectory: npmHome, + }); + cleanup.add("remove packed reviewed plugin", () => removeReviewedNpmArchive(reviewedArchive)); + const fixtureVolume = `${resourcePrefix}-fixture`; + const fixtureLoader = `${resourcePrefix}-fixture-loader`; + const archiveInContainer = "/fixture/reviewed-plugin.tgz"; + const markerSource = path.join(npmHome, "original-success-marker.cjs"); + fs.writeFileSync(markerSource, ORIGINAL_SUCCESS_MARKER_SOURCE, { mode: 0o444 }); + fs.chmodSync(reviewedArchive.archivePath, 0o444); + volumes.push(fixtureVolume); + containers.push(fixtureLoader); + await probe.expect(["volume", "create", fixtureVolume], { + artifactName: "create-fixture-volume", + }); + await probe.expect( + [ + "run", + "-d", + "--name", + fixtureLoader, + "--network", + "none", + "--cap-drop", + "ALL", + "--security-opt", + "no-new-privileges", + "--pids-limit", + "64", + "--memory", + "128m", + "--mount", + `type=volume,source=${fixtureVolume},target=/fixture`, + "--entrypoint", + "bash", + image, + "-lc", + "sleep 300", + ], + { artifactName: "start-fixture-loader", timeoutMs: 30_000 }, + ); + await probe.expect( + ["cp", reviewedArchive.archivePath, `${fixtureLoader}:${archiveInContainer}`], + { + artifactName: "copy-reviewed-plugin-fixture", + timeoutMs: 30_000, + }, + ); + await probe.expect( + ["cp", markerSource, `${fixtureLoader}:/fixture/original-success-marker.cjs`], + { + artifactName: "copy-original-success-marker", + timeoutMs: 30_000, + }, + ); + await probe.expect(["rm", "-f", fixtureLoader], { + artifactName: "stop-fixture-loader", + timeoutMs: 30_000, + }); + containers.splice(containers.indexOf(fixtureLoader), 1); + + for (const testCase of INSTALL_CASES) { + const volume = `${resourcePrefix}-${testCase.id}`; + const container = `${resourcePrefix}-${testCase.id}`; + volumes.push(volume); + containers.push(container); + await probe.expect(["volume", "create", volume], { + artifactName: `create-${testCase.id}-state-volume`, + }); + const result = await probe.run( + secureDockerRunArgs({ + container, + fixtureVolume, + image, + script: probeScript(testCase, archiveInContainer), + volume, + }), + { artifactName: `install-${testCase.id}`, timeoutMs: RUN_TIMEOUT_MS }, + ); + const evidence = parseEvidence(result); + requireSuccessfulRemediation(testCase, evidence); + await artifacts.writeJson(`evidence/${testCase.id}.json`, evidence); + } + + const failureCase: InstallCase = { + id: "post-install-remediation-failure", + args: ["plugins", "install"], + env: { + NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER: `${CONTAINER_HOME}/.original-install-succeeded`, + NODE_OPTIONS: "--require=/fixture/original-success-marker.cjs", + }, + expectedStateRoot: `${CONTAINER_HOME}/.openclaw`, + }; + const failureVolume = `${resourcePrefix}-failure`; + const failureContainer = `${resourcePrefix}-failure`; + volumes.push(failureVolume); + containers.push(failureContainer); + await probe.expect(["volume", "create", failureVolume], { + artifactName: "create-failure-state-volume", + }); + const failureResult = await probe.run( + secureDockerRunArgs({ + container: failureContainer, + fixtureVolume, + hideReplacement: true, + image, + script: probeScript(failureCase, archiveInContainer), + volume: failureVolume, + }), + { artifactName: "post-install-remediation-failure", timeoutMs: RUN_TIMEOUT_MS }, + ); + const failureEvidence = parseEvidence(failureResult); + requireFailedInstallRemoved(failureEvidence); + await artifacts.writeJson("evidence/post-install-remediation-failure.json", failureEvidence); + + await artifacts.target.complete({ + id: TARGET_ID, + image, + assertions: { + failClosedCleanup: true, + stateSelectors: INSTALL_CASES.map(({ id }) => id), + }, + }); + }, + 20 * 60_000, +); From d5fbaa304c736a460499fcabdaeb92670ab57e69 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 20 Jul 2026 22:12:07 -0700 Subject: [PATCH 2/9] test(images): keep container verifier setup linear Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index 16017e2684c..b51d5ec76aa 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -27,6 +27,10 @@ const EVIDENCE_PREFIX = "NEMOCLAW_SECURITY_REVISION_EVIDENCE="; const REPLACEMENT_ROOT = "/usr/local/share/nemoclaw/openclaw-plugin-axios-1.18.0"; const CONTAINER_HOME = "/sandbox"; const RUN_TIMEOUT_MS = 3 * 60_000; +const INSTALL_SUFFIX_ARGS: Readonly> = { + "dev-suffix": ["--dev"], + "profile-suffix": ["--profile", "security-suffix"], +}; type InstallCase = Readonly<{ args: readonly string[]; @@ -87,37 +91,52 @@ function safeDockerName(value: string): string { .replace(/^-+|-+$/gu, ""); } +function requireCondition(condition: boolean, message: string): void { + switch (condition) { + case true: + return; + default: + throw new Error(message); + } +} + function requireSafeImageReference(value: string): string { const image = value.trim(); - if (!/^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/u.test(image)) { - throw new Error(`${IMAGE_ENV} must be a canonical Docker image reference`); - } + requireCondition( + /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/u.test(image), + `${IMAGE_ENV} must be a canonical Docker image reference`, + ); return image; } function resolveConfiguredImage(env: NodeJS.ProcessEnv): string | undefined { const selected = env.E2E_TARGET_ID === TARGET_ID; const explicit = env[RUN_ENV]; - if (explicit !== undefined && explicit !== "0" && explicit !== "1") { - throw new Error(`${RUN_ENV} must be 0 or 1`); - } + requireCondition( + explicit === undefined || explicit === "0" || explicit === "1", + `${RUN_ENV} must be 0 or 1`, + ); const enabled = selected || explicit === "1"; const image = env[IMAGE_ENV]?.trim(); - if (!enabled) { - if (image) throw new Error(`${IMAGE_ENV} requires ${RUN_ENV}=1`); - return undefined; + switch (enabled) { + case false: + requireCondition(!image, `${IMAGE_ENV} requires ${RUN_ENV}=1`); + return undefined; + default: + requireCondition( + Boolean(image), + `${IMAGE_ENV} is required when the container E2E is enabled`, + ); + return requireSafeImageReference(image as string); } - if (!image) throw new Error(`${IMAGE_ENV} is required when the container E2E is enabled`); - return requireSafeImageReference(image); } function installArgs(testCase: InstallCase, archivePath: string): string[] { const args = [...testCase.args]; const installIndex = args.indexOf("install"); - if (installIndex < 0) throw new Error(`install case ${testCase.id} has no install command`); + requireCondition(installIndex >= 0, `install case ${testCase.id} has no install command`); args.splice(installIndex + 1, 0, archivePath); - if (testCase.id === "profile-suffix") args.push("--profile", "security-suffix"); - if (testCase.id === "dev-suffix") args.push("--dev"); + args.push(...(INSTALL_SUFFIX_ARGS[testCase.id] ?? [])); return args; } @@ -270,25 +289,26 @@ function secureDockerRunArgs(options: { "--tmpfs", "/tmp:rw,nosuid,nodev,size=256m,mode=1777", ]; - if (options.hideReplacement) { - args.push( - "--mount", - `type=tmpfs,target=${REPLACEMENT_ROOT},tmpfs-size=1048576,tmpfs-mode=0555`, - ); - } + args.push( + ...(options.hideReplacement + ? ["--mount", `type=tmpfs,target=${REPLACEMENT_ROOT},tmpfs-size=1048576,tmpfs-mode=0555`] + : []), + ); args.push("--entrypoint", "bash", options.image, "-lc", options.script); return args; } function parseEvidence(result: DockerCommandResult): ProbeEvidence { - if (result.exitCode !== 0) throw new Error(resultText(result)); + requireCondition(result.exitCode === 0, resultText(result)); const line = result.stdout .split(/\r?\n/gu) .reverse() .find((candidate) => candidate.startsWith(EVIDENCE_PREFIX)); - if (!line) - throw new Error(`container did not emit security revision evidence\n${resultText(result)}`); - return JSON.parse(line.slice(EVIDENCE_PREFIX.length)) as ProbeEvidence; + requireCondition( + Boolean(line), + `container did not emit security revision evidence\n${resultText(result)}`, + ); + return JSON.parse((line as string).slice(EVIDENCE_PREFIX.length)) as ProbeEvidence; } function requireSuccessfulRemediation(testCase: InstallCase, evidence: ProbeEvidence): void { @@ -407,9 +427,10 @@ realContainerTest( artifactName: `cleanup-${container}`, timeoutMs: 30_000, }); - if (result.exitCode !== 0 && !result.stderr.includes("No such container")) { - throw new Error(resultText(result)); - } + requireCondition( + result.exitCode === 0 || result.stderr.includes("No such container"), + resultText(result), + ); } for (const volume of volumes) { await probe.expect(["volume", "rm", "-f", volume], { From 0862be3cd0f4182bb39866dfacb42a62226a1622 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 20 Jul 2026 23:34:55 -0700 Subject: [PATCH 3/9] test(images): validate current OpenClaw remediation Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 789 ++++++++---------- 1 file changed, 346 insertions(+), 443 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index b51d5ec76aa..d1ed0e82cfb 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -2,102 +2,77 @@ // SPDX-License-Identifier: Apache-2.0 import { randomUUID } from "node:crypto"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import { describe } from "vitest"; -import { - packReviewedNpmArchive, - removeReviewedNpmArchive, -} from "../scripts/lib/reviewed-npm-archive.mts"; -import { shellQuote } from "./e2e/fixtures/clients/command.ts"; import { type DockerCommandResult, DockerProbe, resultText } from "./e2e/fixtures/docker-probe.ts"; import { expect, test } from "./e2e/fixtures/e2e-test.ts"; const TARGET_ID = "openclaw-security-revision-container-e2e"; const RUN_ENV = "NEMOCLAW_RUN_OPENCLAW_SECURITY_REVISION_CONTAINER_E2E"; const IMAGE_ENV = "NEMOCLAW_OPENCLAW_SECURITY_REVISION_IMAGE"; -const REVIEWED_PLUGIN_SPEC = "@openclaw/slack@2026.6.10"; -const REVIEWED_PLUGIN_INTEGRITY = - "sha512-OOsMLjPcbWhQRM5XDwfdrACjJmKqavFtpuIlhHAXWrLrd/p7SyIVE9AoKS0yxOx6bqGDIMJ9+knzdViHMLgBdA=="; -const REVIEWED_PLUGIN_TARBALL = "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.6.10.tgz"; const EVIDENCE_PREFIX = "NEMOCLAW_SECURITY_REVISION_EVIDENCE="; -const REPLACEMENT_ROOT = "/usr/local/share/nemoclaw/openclaw-plugin-axios-1.18.0"; -const CONTAINER_HOME = "/sandbox"; -const RUN_TIMEOUT_MS = 3 * 60_000; -const INSTALL_SUFFIX_ARGS: Readonly> = { - "dev-suffix": ["--dev"], - "profile-suffix": ["--profile", "security-suffix"], -}; - -type InstallCase = Readonly<{ - args: readonly string[]; - env?: Readonly>; - expectedStateRoot: string; - id: string; +const OPENCLAW_ROOT = "/usr/local/lib/node_modules/openclaw"; +const OPENCLAW_ENTRYPOINT = "/usr/local/bin/openclaw"; +const TAR_VERSION = "7.5.19"; +const TAR_INTEGRITY = + "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw=="; +const TAR_TARBALL = "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz"; +const BRACE_EXPANSION_VERSION = "5.0.7"; +const BRACE_EXPANSION_INTEGRITY = + "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="; +const BRACE_EXPANSION_TARBALL = + "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz"; +const FS_SAFE_INTEGRITY = + "sha512-uIBE441CIt1kIURoP9qRGKZ8LkGyfD9ZzeESjwAd29ZPWtghws/5GR3Pjb67jKdcJHP1I6roNXcvnhzAU7lHlA=="; +const FS_SAFE_TARBALL = "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.3.0.tgz"; +const RUN_TIMEOUT_MS = 5 * 60_000; + +type LockedPackage = Readonly<{ + hasOptionalDependencies: boolean; + integrity: string | null; + optionalDependencies: Readonly> | null; + resolved: string | null; + version: string | null; }>; type ProbeEvidence = Readonly<{ - agentBaseVersion: string | null; - axiosVersions: readonly string[]; - caseId: string; - commandExitCode: number; - expectedStateRoot: string; - httpsProxyAgentVersion: string | null; - installedAxiosVersion: string | null; - manifestAxiosVersion: string | null; - openClawVersion: string; - originalInstallSucceeded: boolean | null; - pluginSpecs: readonly string[]; - shrinkwrapAgentBaseVersion: string | null; - shrinkwrapAxiosVersion: string | null; + command: Readonly<{ + exitCode: number; + output: string; + target: string | null; + }>; + npmLs: Readonly<{ + exitCode: number; + output: string; + }>; + fsSafeHasOptionalDependencies: boolean; + fsSafeOptionalDependencies: Readonly> | null; + openClaw: Readonly<{ + bundledDependencies: readonly string[] | null; + dependencies: Readonly>; + name: string | null; + version: string | null; + }>; + packageVersions: Readonly<{ + braceExpansion: readonly string[]; + fsSafe: readonly string[]; + jszip: readonly string[]; + tar: readonly string[]; + }>; + shrinkwrap: Readonly<{ + braceExpansion: LockedPackage; + fsSafe: LockedPackage; + hasNestedFsSafeJszip: boolean; + hasNestedFsSafeTar: boolean; + lockfileVersion: number | null; + rootDependencies: Readonly>; + tar: LockedPackage; + }>; }>; -const INSTALL_CASES: readonly InstallCase[] = [ - { - id: "profile-prefix", - args: ["--profile", "security-prefix", "plugins", "install"], - expectedStateRoot: `${CONTAINER_HOME}/.openclaw-security-prefix`, - }, - { - id: "profile-suffix", - args: ["plugins", "install"], - expectedStateRoot: `${CONTAINER_HOME}/.openclaw-security-suffix`, - }, - { - id: "dev-prefix", - args: ["--dev", "plugins", "install"], - expectedStateRoot: `${CONTAINER_HOME}/.openclaw-dev`, - }, - { - id: "dev-suffix", - args: ["plugins", "install"], - expectedStateRoot: `${CONTAINER_HOME}/.openclaw-dev`, - }, - { - id: "custom-state", - args: ["plugins", "install"], - env: { OPENCLAW_STATE_DIR: `${CONTAINER_HOME}/custom-state` }, - expectedStateRoot: `${CONTAINER_HOME}/custom-state`, - }, -]; - -function safeDockerName(value: string): string { - return value - .toLowerCase() - .replace(/[^a-z0-9_.-]+/gu, "-") - .replace(/^-+|-+$/gu, ""); -} - function requireCondition(condition: boolean, message: string): void { - switch (condition) { - case true: - return; - default: - throw new Error(message); - } + if (!condition) throw new Error(message); } function requireSafeImageReference(value: string): string { @@ -118,184 +93,185 @@ function resolveConfiguredImage(env: NodeJS.ProcessEnv): string | undefined { ); const enabled = selected || explicit === "1"; const image = env[IMAGE_ENV]?.trim(); - switch (enabled) { - case false: - requireCondition(!image, `${IMAGE_ENV} requires ${RUN_ENV}=1`); - return undefined; - default: - requireCondition( - Boolean(image), - `${IMAGE_ENV} is required when the container E2E is enabled`, - ); - return requireSafeImageReference(image as string); + if (!enabled) { + requireCondition(!image, `${IMAGE_ENV} requires ${RUN_ENV}=1`); + return undefined; } -} - -function installArgs(testCase: InstallCase, archivePath: string): string[] { - const args = [...testCase.args]; - const installIndex = args.indexOf("install"); - requireCondition(installIndex >= 0, `install case ${testCase.id} has no install command`); - args.splice(installIndex + 1, 0, archivePath); - args.push(...(INSTALL_SUFFIX_ARGS[testCase.id] ?? [])); - return args; + requireCondition(Boolean(image), `${IMAGE_ENV} is required when the container E2E is enabled`); + return requireSafeImageReference(image as string); } const PROBE_SOURCE = String.raw` const fs = require("node:fs"); const path = require("node:path"); +const { spawnSync } = require("node:child_process"); -const stateRoot = process.env.NEMOCLAW_E2E_STATE_ROOT; -if (!stateRoot) throw new Error("NEMOCLAW_E2E_STATE_ROOT is required"); -const packages = []; +const root = ${JSON.stringify(OPENCLAW_ROOT)}; +const entrypoint = ${JSON.stringify(OPENCLAW_ENTRYPOINT)}; let visited = 0; -function walk(directory) { - if (!fs.existsSync(directory)) return; - for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { - if (++visited > 50000) throw new Error("state tree exceeded verifier entry bound"); - const child = path.join(directory, entry.name); - if (entry.isSymbolicLink()) continue; - if (entry.isDirectory()) { - walk(child); +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function readLockedPackage(value) { + return { + hasOptionalDependencies: Object.prototype.hasOwnProperty.call( + value ?? {}, + "optionalDependencies", + ), + integrity: value?.integrity ?? null, + optionalDependencies: value?.optionalDependencies ?? null, + resolved: value?.resolved ?? null, + version: value?.version ?? null, + }; +} + +function packageDirectories(nodeModules) { + if (!fs.existsSync(nodeModules)) return []; + const directories = []; + for (const entry of fs.readdirSync(nodeModules, { withFileTypes: true })) { + if (++visited > 50000) throw new Error("installed package graph exceeded verifier bound"); + if (!entry.isDirectory() || entry.isSymbolicLink()) continue; + const candidate = path.join(nodeModules, entry.name); + if (!entry.name.startsWith("@")) { + directories.push(candidate); continue; } - if (!entry.isFile() || entry.name !== "package.json") continue; - const manifest = JSON.parse(fs.readFileSync(child, "utf8")); - packages.push({ manifest, root: path.dirname(child) }); + for (const scoped of fs.readdirSync(candidate, { withFileTypes: true })) { + if (++visited > 50000) throw new Error("installed package graph exceeded verifier bound"); + if (scoped.isDirectory() && !scoped.isSymbolicLink()) { + directories.push(path.join(candidate, scoped.name)); + } + } } + return directories; } -function readJson(file) { - if (!fs.existsSync(file)) return undefined; - return JSON.parse(fs.readFileSync(file, "utf8")); +const versions = new Map(); +function collectPackages(packageRoot) { + const manifestPath = path.join(packageRoot, "package.json"); + if (!fs.existsSync(manifestPath)) return; + const manifest = readJson(manifestPath); + if (typeof manifest.name === "string" && typeof manifest.version === "string") { + const found = versions.get(manifest.name) ?? new Set(); + found.add(manifest.version); + versions.set(manifest.name, found); + } + for (const child of packageDirectories(path.join(packageRoot, "node_modules"))) { + collectPackages(child); + } +} + +function versionsFor(name) { + return [...(versions.get(name) ?? [])].sort(); } -walk(stateRoot); -const plugins = packages.filter(({ manifest }) => manifest.name === "@openclaw/slack"); -const livePlugin = plugins.length === 1 ? plugins[0] : undefined; -const pluginRoot = livePlugin?.root; -const pluginManifest = livePlugin?.manifest; -const shrinkwrap = pluginRoot ? readJson(path.join(pluginRoot, "npm-shrinkwrap.json")) : undefined; -const axiosRoot = pluginRoot ? path.join(pluginRoot, "node_modules", "axios") : undefined; -const installedAxios = axiosRoot ? readJson(path.join(axiosRoot, "package.json")) : undefined; -const proxyRoot = axiosRoot ? path.join(axiosRoot, "node_modules", "https-proxy-agent") : undefined; -const proxyManifest = proxyRoot ? readJson(path.join(proxyRoot, "package.json")) : undefined; -const agentBaseRoot = proxyRoot ? path.join(proxyRoot, "node_modules", "agent-base") : undefined; -const agentBaseManifest = agentBaseRoot ? readJson(path.join(agentBaseRoot, "package.json")) : undefined; +const packageJson = readJson(path.join(root, "package.json")); +const shrinkwrap = readJson(path.join(root, "npm-shrinkwrap.json")); +const packages = shrinkwrap.packages ?? {}; +const fsSafePackage = readJson(path.join(root, "node_modules", "@openclaw", "fs-safe", "package.json")); +collectPackages(root); + +const command = spawnSync(entrypoint, ["--version"], { + encoding: "utf8", + env: { ...process.env, HOME: "/tmp/openclaw-security-revision-home" }, + stdio: ["ignore", "pipe", "pipe"], +}); +const npmLs = spawnSync( + "npm", + ["ls", "--global", "--all", "openclaw", "@openclaw/fs-safe", "tar", "jszip"], + { + encoding: "utf8", + env: { + ...process.env, + HOME: "/tmp/openclaw-security-revision-home", + npm_config_cache: "/tmp/npm-cache", + }, + maxBuffer: 4 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }, +); + +let commandTarget = null; +try { + commandTarget = fs.realpathSync(entrypoint); +} catch {} + const evidence = { - agentBaseVersion: agentBaseManifest?.version ?? null, - axiosVersions: packages - .filter(({ manifest }) => manifest.name === "axios") - .map(({ manifest }) => String(manifest.version)) - .sort(), - caseId: process.env.NEMOCLAW_E2E_CASE_ID, - commandExitCode: Number(process.env.NEMOCLAW_E2E_COMMAND_EXIT), - expectedStateRoot: stateRoot, - httpsProxyAgentVersion: proxyManifest?.version ?? null, - installedAxiosVersion: installedAxios?.version ?? null, - manifestAxiosVersion: pluginManifest?.dependencies?.axios ?? null, - openClawVersion: process.env.NEMOCLAW_E2E_OPENCLAW_VERSION ?? "", - originalInstallSucceeded: process.env.NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER - ? fs.existsSync(process.env.NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER) - : null, - pluginSpecs: plugins.map(({ manifest }) => String(manifest.name) + "@" + String(manifest.version)).sort(), - shrinkwrapAgentBaseVersion: - shrinkwrap?.packages?.["node_modules/axios/node_modules/https-proxy-agent/node_modules/agent-base"]?.version ?? null, - shrinkwrapAxiosVersion: shrinkwrap?.packages?.["node_modules/axios"]?.version ?? null, + command: { + exitCode: command.status ?? -1, + output: String(command.stdout ?? "").trim(), + target: commandTarget, + }, + fsSafeHasOptionalDependencies: Object.prototype.hasOwnProperty.call( + fsSafePackage, + "optionalDependencies", + ), + fsSafeOptionalDependencies: fsSafePackage.optionalDependencies ?? null, + npmLs: { + exitCode: npmLs.status ?? -1, + output: String(npmLs.stderr || npmLs.stdout || "").trim().slice(-4000), + }, + openClaw: { + bundledDependencies: packageJson.bundledDependencies ?? null, + dependencies: packageJson.dependencies ?? {}, + name: packageJson.name ?? null, + version: packageJson.version ?? null, + }, + packageVersions: { + braceExpansion: versionsFor("brace-expansion"), + fsSafe: versionsFor("@openclaw/fs-safe"), + jszip: versionsFor("jszip"), + tar: versionsFor("tar"), + }, + shrinkwrap: { + braceExpansion: readLockedPackage(packages["node_modules/brace-expansion"]), + fsSafe: readLockedPackage(packages["node_modules/@openclaw/fs-safe"]), + hasNestedFsSafeJszip: packages["node_modules/@openclaw/fs-safe/node_modules/jszip"] !== undefined, + hasNestedFsSafeTar: packages["node_modules/@openclaw/fs-safe/node_modules/tar"] !== undefined, + lockfileVersion: shrinkwrap.lockfileVersion ?? null, + rootDependencies: packages[""]?.dependencies ?? {}, + tar: readLockedPackage(packages["node_modules/tar"]), + }, }; -process.stdout.write(${JSON.stringify(EVIDENCE_PREFIX)} + JSON.stringify(evidence) + "\n"); -`; -const ORIGINAL_SUCCESS_MARKER_SOURCE = String.raw` -const fs = require("node:fs"); -const entrypoint = process.argv[1]; -const marker = process.env.NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER; -if (entrypoint && marker) { - let resolved = entrypoint; - try { - resolved = fs.realpathSync(entrypoint); - } catch {} - if (resolved === "/usr/local/lib/node_modules/openclaw/openclaw.mjs") { - process.once("exit", (code) => { - if (code === 0) fs.writeFileSync(marker, "ok\n", { mode: 0o600 }); - }); - } -} +process.stdout.write(${JSON.stringify(EVIDENCE_PREFIX)} + JSON.stringify(evidence) + "\n"); `; -function probeScript(testCase: InstallCase, archivePath: string): string { - const env = Object.entries(testCase.env ?? {}) - .map(([name, value]) => `export ${name}=${shellQuote(value)}`) - .join("\n"); - const command = ["/usr/local/bin/openclaw", ...installArgs(testCase, archivePath)] - .map(shellQuote) - .join(" "); - return `set -uo pipefail -umask 077 -export HOME=${shellQuote(CONTAINER_HOME)} -export npm_config_cache=${shellQuote(`${CONTAINER_HOME}/.npm-cache`)} -export npm_config_fetch_retries=1 -export npm_config_fetch_retry_maxtimeout=15000 -export npm_config_fetch_timeout=15000 -export npm_config_ignore_scripts=true -${env} -set +e -${command} -status=$? -set -e -export NEMOCLAW_E2E_CASE_ID=${shellQuote(testCase.id)} -export NEMOCLAW_E2E_COMMAND_EXIT="$status" -export NEMOCLAW_E2E_STATE_ROOT=${shellQuote(testCase.expectedStateRoot)} -export NEMOCLAW_E2E_OPENCLAW_VERSION="$(/usr/local/bin/openclaw --version 2>/dev/null || true)" -node -e ${shellQuote(PROBE_SOURCE)}`; -} - -function secureDockerRunArgs(options: { - container: string; - fixtureVolume: string; - image: string; - script: string; - volume: string; - hideReplacement?: boolean; -}): string[] { - const args = [ +function secureDockerRunArgs(container: string, image: string): string[] { + return [ "run", "--rm", "--name", - options.container, + container, "--user", "sandbox:sandbox", "--read-only", "--network", - "bridge", + "none", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--pids-limit", - "256", + "64", "--memory", - "2g", + "256m", "--memory-swap", - "2g", + "256m", "--cpus", - "2", + "1", "--ulimit", "nofile=1024:1024", - "--mount", - `type=volume,source=${options.volume},target=${CONTAINER_HOME}`, - "--mount", - `type=volume,source=${options.fixtureVolume},target=/fixture,readonly`, "--tmpfs", - "/tmp:rw,nosuid,nodev,size=256m,mode=1777", + "/tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777", + "--entrypoint", + "node", + image, + "-e", + PROBE_SOURCE, ]; - args.push( - ...(options.hideReplacement - ? ["--mount", `type=tmpfs,target=${REPLACEMENT_ROOT},tmpfs-size=1048576,tmpfs-mode=0555`] - : []), - ); - args.push("--entrypoint", "bash", options.image, "-lc", options.script); - return args; } function parseEvidence(result: DockerCommandResult): ProbeEvidence { @@ -311,142 +287,187 @@ function parseEvidence(result: DockerCommandResult): ProbeEvidence { return JSON.parse((line as string).slice(EVIDENCE_PREFIX.length)) as ProbeEvidence; } -function requireSuccessfulRemediation(testCase: InstallCase, evidence: ProbeEvidence): void { - expect(evidence.caseId).toBe(testCase.id); - expect(evidence.commandExitCode).toBe(0); - expect(evidence.expectedStateRoot).toBe(testCase.expectedStateRoot); - expect(evidence.openClawVersion).toContain("2026.6.10"); - expect(evidence.pluginSpecs).toEqual([REVIEWED_PLUGIN_SPEC]); - expect(evidence.installedAxiosVersion).toBe("1.18.0"); - expect(evidence.manifestAxiosVersion).toBe("1.18.0"); - expect(evidence.shrinkwrapAxiosVersion).toBe("1.18.0"); - expect(evidence.httpsProxyAgentVersion).toBe("5.0.1"); - expect(evidence.agentBaseVersion).toBe("6.0.2"); - expect(evidence.shrinkwrapAgentBaseVersion).toBe("6.0.2"); - expect(evidence.axiosVersions).toContain("1.18.0"); - expect(evidence.axiosVersions).not.toContain("1.16.0"); +function exactEvidence(): ProbeEvidence { + return { + command: { + exitCode: 0, + output: "OpenClaw 2026.6.10", + target: `${OPENCLAW_ROOT}/openclaw.mjs`, + }, + fsSafeHasOptionalDependencies: false, + fsSafeOptionalDependencies: null, + npmLs: { + exitCode: 0, + output: "", + }, + openClaw: { + bundledDependencies: ["@openclaw/fs-safe"], + dependencies: { + "@openclaw/fs-safe": "0.3.0", + jszip: "3.10.1", + tar: TAR_VERSION, + }, + name: "openclaw", + version: "2026.6.10", + }, + packageVersions: { + braceExpansion: [BRACE_EXPANSION_VERSION], + fsSafe: ["0.3.0"], + jszip: ["3.10.1"], + tar: [TAR_VERSION], + }, + shrinkwrap: { + braceExpansion: { + hasOptionalDependencies: false, + integrity: BRACE_EXPANSION_INTEGRITY, + optionalDependencies: null, + resolved: BRACE_EXPANSION_TARBALL, + version: BRACE_EXPANSION_VERSION, + }, + fsSafe: { + hasOptionalDependencies: false, + integrity: FS_SAFE_INTEGRITY, + optionalDependencies: null, + resolved: FS_SAFE_TARBALL, + version: "0.3.0", + }, + hasNestedFsSafeJszip: false, + hasNestedFsSafeTar: false, + lockfileVersion: 3, + rootDependencies: { + "@openclaw/fs-safe": "0.3.0", + jszip: "3.10.1", + tar: TAR_VERSION, + }, + tar: { + hasOptionalDependencies: false, + integrity: TAR_INTEGRITY, + optionalDependencies: null, + resolved: TAR_TARBALL, + version: TAR_VERSION, + }, + }, + }; } -function requireFailedInstallRemoved(evidence: ProbeEvidence): void { - expect(evidence.commandExitCode).not.toBe(0); - expect(evidence.originalInstallSucceeded).toBe(true); - expect(evidence.pluginSpecs).toEqual([]); - expect(evidence.axiosVersions).not.toContain("1.16.0"); +function requireExactRemediation(evidence: ProbeEvidence): void { + expect(evidence.command.exitCode).toBe(0); + expect(evidence.command.output).toMatch(/\b2026\.6\.10\b/u); + expect(evidence.command.target).toBe(`${OPENCLAW_ROOT}/openclaw.mjs`); + expect(evidence.npmLs.exitCode).toBe(0); + expect(evidence.openClaw.name).toBe("openclaw"); + expect(evidence.openClaw.version).toBe("2026.6.10"); + expect(evidence.openClaw.dependencies).toMatchObject({ + "@openclaw/fs-safe": "0.3.0", + jszip: "3.10.1", + tar: TAR_VERSION, + }); + expect(evidence.openClaw.bundledDependencies).toEqual(["@openclaw/fs-safe"]); + expect(evidence.packageVersions).toEqual({ + braceExpansion: [BRACE_EXPANSION_VERSION], + fsSafe: ["0.3.0"], + jszip: ["3.10.1"], + tar: [TAR_VERSION], + }); + expect(evidence.fsSafeHasOptionalDependencies).toBe(false); + expect(evidence.fsSafeOptionalDependencies).toBeNull(); + expect(evidence.shrinkwrap.lockfileVersion).toBe(3); + expect(evidence.shrinkwrap.rootDependencies).toMatchObject({ + "@openclaw/fs-safe": "0.3.0", + jszip: "3.10.1", + tar: TAR_VERSION, + }); + expect(evidence.shrinkwrap.tar).toMatchObject({ + integrity: TAR_INTEGRITY, + resolved: TAR_TARBALL, + version: TAR_VERSION, + }); + expect(evidence.shrinkwrap.braceExpansion).toMatchObject({ + integrity: BRACE_EXPANSION_INTEGRITY, + resolved: BRACE_EXPANSION_TARBALL, + version: BRACE_EXPANSION_VERSION, + }); + expect(evidence.shrinkwrap.fsSafe).toMatchObject({ + hasOptionalDependencies: false, + integrity: FS_SAFE_INTEGRITY, + optionalDependencies: null, + resolved: FS_SAFE_TARBALL, + version: "0.3.0", + }); + expect(evidence.shrinkwrap.hasNestedFsSafeJszip).toBe(false); + expect(evidence.shrinkwrap.hasNestedFsSafeTar).toBe(false); } const configuredImage = resolveConfiguredImage(process.env); const realContainerTest = configuredImage ? test : test.skip; -describe("OpenClaw security revision container E2E contract (#7272)", () => { - test("keeps real Docker execution explicitly opt-in until the revision image lands", () => { +describe("OpenClaw current-image security revision contract (#7272)", () => { + test("keeps real Docker execution explicitly opt-in until its image dependency lands (#7286)", () => { expect(resolveConfiguredImage({})).toBeUndefined(); expect(() => resolveConfiguredImage({ [RUN_ENV]: "1" })).toThrow(IMAGE_ENV); expect(() => resolveConfiguredImage({ [IMAGE_ENV]: "candidate:local" })).toThrow(RUN_ENV); }); - test("covers every supported OpenClaw state selector without shell-derived inputs", () => { - expect(INSTALL_CASES.map(({ id }) => id)).toEqual([ - "profile-prefix", - "profile-suffix", - "dev-prefix", - "dev-suffix", - "custom-state", - ]); - for (const testCase of INSTALL_CASES) { - expect(installArgs(testCase, "/fixture/plugin.tgz")).toContain("/fixture/plugin.tgz"); - expect(testCase.expectedStateRoot.startsWith(CONTAINER_HOME)).toBe(true); - } - }); - - test("builds an isolated least-privilege Docker boundary without host networking", () => { - const args = secureDockerRunArgs({ - container: "security-e2e", - fixtureVolume: "security-e2e-fixture", - image: "candidate:local", - script: "true", - volume: "security-e2e-state", - }); - expect(args).toContain("bridge"); + test("uses an offline read-only least-privilege Docker boundary", () => { + const args = secureDockerRunArgs("security-e2e", "candidate:local"); + expect(args).toContain("none"); expect(args).not.toContain("host"); expect(args).toContain("--read-only"); expect(args).toContain("ALL"); expect(args).toContain("no-new-privileges"); expect(args.join(" ")).not.toContain("docker.sock"); + expect(args).not.toContain("--mount"); }); - test("rejects vulnerable or inconsistent remediation evidence", () => { - const testCase = INSTALL_CASES[0]; - const good: ProbeEvidence = { - agentBaseVersion: "6.0.2", - axiosVersions: ["1.18.0"], - caseId: testCase.id, - commandExitCode: 0, - expectedStateRoot: testCase.expectedStateRoot, - httpsProxyAgentVersion: "5.0.1", - installedAxiosVersion: "1.18.0", - manifestAxiosVersion: "1.18.0", - openClawVersion: "OpenClaw 2026.6.10", - originalInstallSucceeded: null, - pluginSpecs: [REVIEWED_PLUGIN_SPEC], - shrinkwrapAgentBaseVersion: "6.0.2", - shrinkwrapAxiosVersion: "1.18.0", - }; - expect(() => requireSuccessfulRemediation(testCase, good)).not.toThrow(); + test("rejects vulnerable or incomplete installed dependency evidence", () => { + const good = exactEvidence(); + expect(() => requireExactRemediation(good)).not.toThrow(); expect(() => - requireSuccessfulRemediation(testCase, { + requireExactRemediation({ ...good, - axiosVersions: ["1.16.0"], + packageVersions: { ...good.packageVersions, tar: ["7.5.16"] }, }), ).toThrow(); expect(() => - requireSuccessfulRemediation(testCase, { + requireExactRemediation({ ...good, - shrinkwrapAxiosVersion: "1.16.0", + fsSafeOptionalDependencies: { jszip: "^3.10.1", tar: "7.5.13" }, }), ).toThrow(); + expect(() => + requireExactRemediation({ + ...good, + fsSafeHasOptionalDependencies: true, + }), + ).toThrow(); + expect(() => + requireExactRemediation({ + ...good, + shrinkwrap: { ...good.shrinkwrap, hasNestedFsSafeTar: true }, + }), + ).toThrow(); + expect(() => + requireExactRemediation({ ...good, npmLs: { exitCode: 1, output: "invalid graph" } }), + ).toThrow(); }); }); realContainerTest( - "the installed wrapper remediates exact local plugins across state selectors and fails closed (#7272)", - async ({ artifacts, cleanup, docker, secrets }) => { + "the #7286 image ships only the reviewed OpenClaw dependency graph (#7272)", + async ({ artifacts, docker, secrets }) => { const image = configuredImage as string; const probe = new DockerProbe(artifacts, (text, extraValues) => secrets.redact(text, extraValues), ); - const resourcePrefix = safeDockerName(`nemoclaw-security-e2e-${process.pid}-${randomUUID()}`); - const containers: string[] = []; - const volumes: string[] = []; - const npmHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-security-e2e-npm-")); - cleanup.add("remove reviewed plugin archive", () => - fs.rmSync(npmHome, { recursive: true, force: true }), - ); - cleanup.add("remove security revision containers and volumes", async () => { - for (const container of containers) { - const result = await probe.run(["rm", "-f", container], { - artifactName: `cleanup-${container}`, - timeoutMs: 30_000, - }); - requireCondition( - result.exitCode === 0 || result.stderr.includes("No such container"), - resultText(result), - ); - } - for (const volume of volumes) { - await probe.expect(["volume", "rm", "-f", volume], { - artifactName: `cleanup-${volume}`, - timeoutMs: 30_000, - }); - } - }); + const container = `nemoclaw-security-e2e-${process.pid}-${randomUUID()}`.toLowerCase(); await artifacts.target.declare({ id: TARGET_ID, - boundary: "historical-openclaw-image-wrapper", + boundary: "openclaw-2026.6.10-installed-remediation", image, contracts: [ - "effective OpenClaw state selectors receive reviewed plugin remediation", - "post-install remediation failure removes the fresh vulnerable plugin", + "the current OpenClaw image contains the exact remediated dependency graph from #7286", + "the verifier runs offline, read-only, unprivileged, and without host mounts", ], }); await docker.requireDocker(); @@ -455,142 +476,24 @@ realContainerTest( timeoutMs: 30_000, }); - const reviewedArchive = packReviewedNpmArchive({ - env: { - HOME: npmHome, - PATH: process.env.PATH, - npm_config_audit: "false", - npm_config_cache: path.join(npmHome, "cache"), - npm_config_fund: "false", - npm_config_ignore_scripts: "true", - npm_config_userconfig: "/dev/null", - }, - expectedIntegrity: REVIEWED_PLUGIN_INTEGRITY, - label: "OpenClaw security revision E2E fixture", - packageSpec: REVIEWED_PLUGIN_SPEC, - tarballUrl: REVIEWED_PLUGIN_TARBALL, - tempDirectory: npmHome, - }); - cleanup.add("remove packed reviewed plugin", () => removeReviewedNpmArchive(reviewedArchive)); - const fixtureVolume = `${resourcePrefix}-fixture`; - const fixtureLoader = `${resourcePrefix}-fixture-loader`; - const archiveInContainer = "/fixture/reviewed-plugin.tgz"; - const markerSource = path.join(npmHome, "original-success-marker.cjs"); - fs.writeFileSync(markerSource, ORIGINAL_SUCCESS_MARKER_SOURCE, { mode: 0o444 }); - fs.chmodSync(reviewedArchive.archivePath, 0o444); - volumes.push(fixtureVolume); - containers.push(fixtureLoader); - await probe.expect(["volume", "create", fixtureVolume], { - artifactName: "create-fixture-volume", - }); - await probe.expect( - [ - "run", - "-d", - "--name", - fixtureLoader, - "--network", - "none", - "--cap-drop", - "ALL", - "--security-opt", - "no-new-privileges", - "--pids-limit", - "64", - "--memory", - "128m", - "--mount", - `type=volume,source=${fixtureVolume},target=/fixture`, - "--entrypoint", - "bash", - image, - "-lc", - "sleep 300", - ], - { artifactName: "start-fixture-loader", timeoutMs: 30_000 }, - ); - await probe.expect( - ["cp", reviewedArchive.archivePath, `${fixtureLoader}:${archiveInContainer}`], - { - artifactName: "copy-reviewed-plugin-fixture", - timeoutMs: 30_000, - }, - ); - await probe.expect( - ["cp", markerSource, `${fixtureLoader}:/fixture/original-success-marker.cjs`], - { - artifactName: "copy-original-success-marker", - timeoutMs: 30_000, - }, - ); - await probe.expect(["rm", "-f", fixtureLoader], { - artifactName: "stop-fixture-loader", - timeoutMs: 30_000, - }); - containers.splice(containers.indexOf(fixtureLoader), 1); - - for (const testCase of INSTALL_CASES) { - const volume = `${resourcePrefix}-${testCase.id}`; - const container = `${resourcePrefix}-${testCase.id}`; - volumes.push(volume); - containers.push(container); - await probe.expect(["volume", "create", volume], { - artifactName: `create-${testCase.id}-state-volume`, - }); - const result = await probe.run( - secureDockerRunArgs({ - container, - fixtureVolume, - image, - script: probeScript(testCase, archiveInContainer), - volume, - }), - { artifactName: `install-${testCase.id}`, timeoutMs: RUN_TIMEOUT_MS }, - ); - const evidence = parseEvidence(result); - requireSuccessfulRemediation(testCase, evidence); - await artifacts.writeJson(`evidence/${testCase.id}.json`, evidence); - } - - const failureCase: InstallCase = { - id: "post-install-remediation-failure", - args: ["plugins", "install"], - env: { - NEMOCLAW_E2E_ORIGINAL_SUCCESS_MARKER: `${CONTAINER_HOME}/.original-install-succeeded`, - NODE_OPTIONS: "--require=/fixture/original-success-marker.cjs", - }, - expectedStateRoot: `${CONTAINER_HOME}/.openclaw`, - }; - const failureVolume = `${resourcePrefix}-failure`; - const failureContainer = `${resourcePrefix}-failure`; - volumes.push(failureVolume); - containers.push(failureContainer); - await probe.expect(["volume", "create", failureVolume], { - artifactName: "create-failure-state-volume", + const result = await probe.run(secureDockerRunArgs(container, image), { + artifactName: "inspect-openclaw-installed-graph", + timeoutMs: RUN_TIMEOUT_MS, }); - const failureResult = await probe.run( - secureDockerRunArgs({ - container: failureContainer, - fixtureVolume, - hideReplacement: true, - image, - script: probeScript(failureCase, archiveInContainer), - volume: failureVolume, - }), - { artifactName: "post-install-remediation-failure", timeoutMs: RUN_TIMEOUT_MS }, - ); - const failureEvidence = parseEvidence(failureResult); - requireFailedInstallRemoved(failureEvidence); - await artifacts.writeJson("evidence/post-install-remediation-failure.json", failureEvidence); + const evidence = parseEvidence(result); + requireExactRemediation(evidence); + await artifacts.writeJson("evidence/openclaw-installed-graph.json", evidence); await artifacts.target.complete({ id: TARGET_ID, image, assertions: { - failClosedCleanup: true, - stateSelectors: INSTALL_CASES.map(({ id }) => id), + braceExpansionVersion: BRACE_EXPANSION_VERSION, + fsSafeOptionalDependenciesRemoved: true, + openClawVersion: "2026.6.10", + tarVersion: TAR_VERSION, }, }); }, - 20 * 60_000, + 10 * 60_000, ); From 700137b576b3fcbb9fdc60a65eaebf7cdfd4dd27 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 20 Jul 2026 23:41:03 -0700 Subject: [PATCH 4/9] test(images): keep verifier setup linear Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index d1ed0e82cfb..9183e6dd674 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -72,7 +72,12 @@ type ProbeEvidence = Readonly<{ }>; function requireCondition(condition: boolean, message: string): void { - if (!condition) throw new Error(message); + switch (condition) { + case true: + return; + default: + throw new Error(message); + } } function requireSafeImageReference(value: string): string { @@ -93,12 +98,17 @@ function resolveConfiguredImage(env: NodeJS.ProcessEnv): string | undefined { ); const enabled = selected || explicit === "1"; const image = env[IMAGE_ENV]?.trim(); - if (!enabled) { - requireCondition(!image, `${IMAGE_ENV} requires ${RUN_ENV}=1`); - return undefined; + switch (enabled) { + case false: + requireCondition(!image, `${IMAGE_ENV} requires ${RUN_ENV}=1`); + return undefined; + default: + requireCondition( + Boolean(image), + `${IMAGE_ENV} is required when the container E2E is enabled`, + ); + return requireSafeImageReference(image as string); } - requireCondition(Boolean(image), `${IMAGE_ENV} is required when the container E2E is enabled`); - return requireSafeImageReference(image as string); } const PROBE_SOURCE = String.raw` From 02ee0b76226cc915e27f59e3dbc77f8878f317e4 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 20 Jul 2026 23:43:37 -0700 Subject: [PATCH 5/9] test(images): bind verifier security options Signed-off-by: Apurv Kumaria --- test/openclaw-security-revision-container-e2e.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index 9183e6dd674..e0e0fb493ca 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -420,11 +420,16 @@ describe("OpenClaw current-image security revision contract (#7272)", () => { test("uses an offline read-only least-privilege Docker boundary", () => { const args = secureDockerRunArgs("security-e2e", "candidate:local"); - expect(args).toContain("none"); + for (const [option, value] of [ + ["--network", "none"], + ["--cap-drop", "ALL"], + ["--security-opt", "no-new-privileges"], + ] as const) { + const optionIndex = args.indexOf(option); + expect(args.slice(optionIndex, optionIndex + 2)).toEqual([option, value]); + } expect(args).not.toContain("host"); expect(args).toContain("--read-only"); - expect(args).toContain("ALL"); - expect(args).toContain("no-new-privileges"); expect(args.join(" ")).not.toContain("docker.sock"); expect(args).not.toContain("--mount"); }); From cf98ed39d1f5dc2a48265a28ea32c9a6ed7dc4d4 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 20 Jul 2026 23:56:14 -0700 Subject: [PATCH 6/9] test(images): align verifier dependency depth Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index e0e0fb493ca..33e364182be 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -27,6 +27,15 @@ const FS_SAFE_INTEGRITY = "sha512-uIBE441CIt1kIURoP9qRGKZ8LkGyfD9ZzeESjwAd29ZPWtghws/5GR3Pjb67jKdcJHP1I6roNXcvnhzAU7lHlA=="; const FS_SAFE_TARBALL = "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.3.0.tgz"; const RUN_TIMEOUT_MS = 5 * 60_000; +const NPM_LS_ARGS = [ + "ls", + "--global", + "--depth=1", + "openclaw", + "@openclaw/fs-safe", + "tar", + "jszip", +] as const; type LockedPackage = Readonly<{ hasOptionalDependencies: boolean; @@ -43,6 +52,7 @@ type ProbeEvidence = Readonly<{ target: string | null; }>; npmLs: Readonly<{ + args: readonly string[]; exitCode: number; output: string; }>; @@ -188,10 +198,8 @@ const command = spawnSync(entrypoint, ["--version"], { env: { ...process.env, HOME: "/tmp/openclaw-security-revision-home" }, stdio: ["ignore", "pipe", "pipe"], }); -const npmLs = spawnSync( - "npm", - ["ls", "--global", "--all", "openclaw", "@openclaw/fs-safe", "tar", "jszip"], - { +const npmLsArgs = ${JSON.stringify(NPM_LS_ARGS)}; +const npmLs = spawnSync("npm", npmLsArgs, { encoding: "utf8", env: { ...process.env, @@ -200,8 +208,7 @@ const npmLs = spawnSync( }, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], - }, -); +}); let commandTarget = null; try { @@ -220,6 +227,7 @@ const evidence = { ), fsSafeOptionalDependencies: fsSafePackage.optionalDependencies ?? null, npmLs: { + args: npmLsArgs, exitCode: npmLs.status ?? -1, output: String(npmLs.stderr || npmLs.stdout || "").trim().slice(-4000), }, @@ -307,6 +315,7 @@ function exactEvidence(): ProbeEvidence { fsSafeHasOptionalDependencies: false, fsSafeOptionalDependencies: null, npmLs: { + args: [...NPM_LS_ARGS], exitCode: 0, output: "", }, @@ -364,6 +373,7 @@ function requireExactRemediation(evidence: ProbeEvidence): void { expect(evidence.command.exitCode).toBe(0); expect(evidence.command.output).toMatch(/\b2026\.6\.10\b/u); expect(evidence.command.target).toBe(`${OPENCLAW_ROOT}/openclaw.mjs`); + expect(evidence.npmLs.args).toEqual(NPM_LS_ARGS); expect(evidence.npmLs.exitCode).toBe(0); expect(evidence.openClaw.name).toBe("openclaw"); expect(evidence.openClaw.version).toBe("2026.6.10"); @@ -462,7 +472,10 @@ describe("OpenClaw current-image security revision contract (#7272)", () => { }), ).toThrow(); expect(() => - requireExactRemediation({ ...good, npmLs: { exitCode: 1, output: "invalid graph" } }), + requireExactRemediation({ + ...good, + npmLs: { ...good.npmLs, exitCode: 1, output: "invalid graph" }, + }), ).toThrow(); }); }); From afdaed9de79cd84615f76eeeba1f0ad1df3d8519 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Tue, 21 Jul 2026 08:47:09 -0700 Subject: [PATCH 7/9] test(images): verify locked jszip metadata Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index 33e364182be..ec020c79ea6 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -26,6 +26,10 @@ const BRACE_EXPANSION_TARBALL = const FS_SAFE_INTEGRITY = "sha512-uIBE441CIt1kIURoP9qRGKZ8LkGyfD9ZzeESjwAd29ZPWtghws/5GR3Pjb67jKdcJHP1I6roNXcvnhzAU7lHlA=="; const FS_SAFE_TARBALL = "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.3.0.tgz"; +const JSZIP_VERSION = "3.10.1"; +const JSZIP_INTEGRITY = + "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="; +const JSZIP_TARBALL = "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz"; const RUN_TIMEOUT_MS = 5 * 60_000; const NPM_LS_ARGS = [ "ls", @@ -75,6 +79,7 @@ type ProbeEvidence = Readonly<{ fsSafe: LockedPackage; hasNestedFsSafeJszip: boolean; hasNestedFsSafeTar: boolean; + jszip: LockedPackage; lockfileVersion: number | null; rootDependencies: Readonly>; tar: LockedPackage; @@ -248,6 +253,7 @@ const evidence = { fsSafe: readLockedPackage(packages["node_modules/@openclaw/fs-safe"]), hasNestedFsSafeJszip: packages["node_modules/@openclaw/fs-safe/node_modules/jszip"] !== undefined, hasNestedFsSafeTar: packages["node_modules/@openclaw/fs-safe/node_modules/tar"] !== undefined, + jszip: readLockedPackage(packages["node_modules/jszip"]), lockfileVersion: shrinkwrap.lockfileVersion ?? null, rootDependencies: packages[""]?.dependencies ?? {}, tar: readLockedPackage(packages["node_modules/tar"]), @@ -323,7 +329,7 @@ function exactEvidence(): ProbeEvidence { bundledDependencies: ["@openclaw/fs-safe"], dependencies: { "@openclaw/fs-safe": "0.3.0", - jszip: "3.10.1", + jszip: JSZIP_VERSION, tar: TAR_VERSION, }, name: "openclaw", @@ -332,7 +338,7 @@ function exactEvidence(): ProbeEvidence { packageVersions: { braceExpansion: [BRACE_EXPANSION_VERSION], fsSafe: ["0.3.0"], - jszip: ["3.10.1"], + jszip: [JSZIP_VERSION], tar: [TAR_VERSION], }, shrinkwrap: { @@ -352,10 +358,17 @@ function exactEvidence(): ProbeEvidence { }, hasNestedFsSafeJszip: false, hasNestedFsSafeTar: false, + jszip: { + hasOptionalDependencies: false, + integrity: JSZIP_INTEGRITY, + optionalDependencies: null, + resolved: JSZIP_TARBALL, + version: JSZIP_VERSION, + }, lockfileVersion: 3, rootDependencies: { "@openclaw/fs-safe": "0.3.0", - jszip: "3.10.1", + jszip: JSZIP_VERSION, tar: TAR_VERSION, }, tar: { @@ -379,14 +392,14 @@ function requireExactRemediation(evidence: ProbeEvidence): void { expect(evidence.openClaw.version).toBe("2026.6.10"); expect(evidence.openClaw.dependencies).toMatchObject({ "@openclaw/fs-safe": "0.3.0", - jszip: "3.10.1", + jszip: JSZIP_VERSION, tar: TAR_VERSION, }); expect(evidence.openClaw.bundledDependencies).toEqual(["@openclaw/fs-safe"]); expect(evidence.packageVersions).toEqual({ braceExpansion: [BRACE_EXPANSION_VERSION], fsSafe: ["0.3.0"], - jszip: ["3.10.1"], + jszip: [JSZIP_VERSION], tar: [TAR_VERSION], }); expect(evidence.fsSafeHasOptionalDependencies).toBe(false); @@ -394,7 +407,7 @@ function requireExactRemediation(evidence: ProbeEvidence): void { expect(evidence.shrinkwrap.lockfileVersion).toBe(3); expect(evidence.shrinkwrap.rootDependencies).toMatchObject({ "@openclaw/fs-safe": "0.3.0", - jszip: "3.10.1", + jszip: JSZIP_VERSION, tar: TAR_VERSION, }); expect(evidence.shrinkwrap.tar).toMatchObject({ @@ -414,6 +427,11 @@ function requireExactRemediation(evidence: ProbeEvidence): void { resolved: FS_SAFE_TARBALL, version: "0.3.0", }); + expect(evidence.shrinkwrap.jszip).toMatchObject({ + integrity: JSZIP_INTEGRITY, + resolved: JSZIP_TARBALL, + version: JSZIP_VERSION, + }); expect(evidence.shrinkwrap.hasNestedFsSafeJszip).toBe(false); expect(evidence.shrinkwrap.hasNestedFsSafeTar).toBe(false); } @@ -471,6 +489,21 @@ describe("OpenClaw current-image security revision contract (#7272)", () => { shrinkwrap: { ...good.shrinkwrap, hasNestedFsSafeTar: true }, }), ).toThrow(); + for (const compromisedJszip of [ + { integrity: "sha512-unreviewed" }, + { resolved: "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz" }, + { version: "3.10.0" }, + ]) { + expect(() => + requireExactRemediation({ + ...good, + shrinkwrap: { + ...good.shrinkwrap, + jszip: { ...good.shrinkwrap.jszip, ...compromisedJszip }, + }, + }), + ).toThrow(); + } expect(() => requireExactRemediation({ ...good, From 0dcfe127b5c361a941f12704cd36cce08b54e5e3 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Tue, 21 Jul 2026 11:42:36 -0700 Subject: [PATCH 8/9] test(images): reject optional lock metadata Signed-off-by: Apurv Kumaria --- ...aw-security-revision-container-e2e.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index ec020c79ea6..19a138bb592 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -411,12 +411,16 @@ function requireExactRemediation(evidence: ProbeEvidence): void { tar: TAR_VERSION, }); expect(evidence.shrinkwrap.tar).toMatchObject({ + hasOptionalDependencies: false, integrity: TAR_INTEGRITY, + optionalDependencies: null, resolved: TAR_TARBALL, version: TAR_VERSION, }); expect(evidence.shrinkwrap.braceExpansion).toMatchObject({ + hasOptionalDependencies: false, integrity: BRACE_EXPANSION_INTEGRITY, + optionalDependencies: null, resolved: BRACE_EXPANSION_TARBALL, version: BRACE_EXPANSION_VERSION, }); @@ -428,7 +432,9 @@ function requireExactRemediation(evidence: ProbeEvidence): void { version: "0.3.0", }); expect(evidence.shrinkwrap.jszip).toMatchObject({ + hasOptionalDependencies: false, integrity: JSZIP_INTEGRITY, + optionalDependencies: null, resolved: JSZIP_TARBALL, version: JSZIP_VERSION, }); @@ -504,6 +510,25 @@ describe("OpenClaw current-image security revision contract (#7272)", () => { }), ).toThrow(); } + for (const packageName of ["tar", "braceExpansion", "jszip"] as const) { + for (const optionalDependencyState of [ + { hasOptionalDependencies: true }, + { optionalDependencies: { unreviewed: "1.0.0" } }, + ]) { + expect(() => + requireExactRemediation({ + ...good, + shrinkwrap: { + ...good.shrinkwrap, + [packageName]: { + ...good.shrinkwrap[packageName], + ...optionalDependencyState, + }, + }, + }), + ).toThrow(); + } + } expect(() => requireExactRemediation({ ...good, From 48459d489c929076b47b41550b035cc90093bee1 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Tue, 21 Jul 2026 11:48:53 -0700 Subject: [PATCH 9/9] test(images): require immutable verifier image Signed-off-by: Charan Jagwani --- test/openclaw-security-revision-container-e2e.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/openclaw-security-revision-container-e2e.test.ts b/test/openclaw-security-revision-container-e2e.test.ts index 19a138bb592..6dc5a4b8118 100644 --- a/test/openclaw-security-revision-container-e2e.test.ts +++ b/test/openclaw-security-revision-container-e2e.test.ts @@ -98,8 +98,8 @@ function requireCondition(condition: boolean, message: string): void { function requireSafeImageReference(value: string): string { const image = value.trim(); requireCondition( - /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,511}$/u.test(image), - `${IMAGE_ENV} must be a canonical Docker image reference`, + /^[A-Za-z0-9][A-Za-z0-9._/:-]{0,438}@sha256:[0-9a-f]{64}$/iu.test(image), + `${IMAGE_ENV} must be an immutable named Docker image digest`, ); return image; } @@ -450,6 +450,13 @@ describe("OpenClaw current-image security revision contract (#7272)", () => { expect(resolveConfiguredImage({})).toBeUndefined(); expect(() => resolveConfiguredImage({ [RUN_ENV]: "1" })).toThrow(IMAGE_ENV); expect(() => resolveConfiguredImage({ [IMAGE_ENV]: "candidate:local" })).toThrow(RUN_ENV); + expect(() => + resolveConfiguredImage({ [RUN_ENV]: "1", [IMAGE_ENV]: "candidate:local" }), + ).toThrow("immutable named Docker image digest"); + const immutableImage = `nemoclaw-production@sha256:${"a".repeat(64)}`; + expect(resolveConfiguredImage({ [RUN_ENV]: "1", [IMAGE_ENV]: immutableImage })).toBe( + immutableImage, + ); }); test("uses an offline read-only least-privilege Docker boundary", () => {