fix(security): fingerprint full policy warnings

This commit is contained in:
jesse-merhi
2026-08-11 04:18:35 +10:00
parent 54368acb05
commit e5bb6e955b
4 changed files with 135 additions and 29 deletions
@@ -325,8 +325,12 @@ describe("legacy file install scan compatibility", () => {
it("continues after one acknowledgement and a fresh evaluation of the same warning", async () => {
const onInstallPolicyWarning = vi.fn().mockResolvedValue({ status: "approved" });
runInstallPolicyMock
.mockResolvedValueOnce({ warning: { reason: "review this plugin" } })
.mockResolvedValueOnce({ warning: { reason: "review this plugin" } });
.mockResolvedValueOnce({
warning: { reason: "review this plugin", fingerprint: "warning-a" },
})
.mockResolvedValueOnce({
warning: { reason: "review this plugin", fingerprint: "warning-a" },
});
const result = await scanFileInstallSourceRuntime({
filePath: "/tmp/payload.js",
@@ -347,9 +351,11 @@ describe("legacy file install scan compatibility", () => {
it("requires approval again when policy re-evaluation returns a changed warning", async () => {
const onInstallPolicyWarning = vi.fn().mockResolvedValue({ status: "approved" });
runInstallPolicyMock
.mockResolvedValueOnce({ warning: { reason: "review this plugin" } })
.mockResolvedValueOnce({
warning: { reason: "review the new finding" },
warning: { reason: "review this plugin", fingerprint: "warning-a" },
})
.mockResolvedValueOnce({
warning: { reason: "review the new finding", fingerprint: "warning-b" },
findings: [
{
ruleId: "changed-warning",
@@ -376,9 +382,41 @@ describe("legacy file install scan compatibility", () => {
expect(runInstallPolicyMock).toHaveBeenCalledTimes(2);
});
it("requires approval again when the full warning changes behind identical display values", async () => {
const onInstallPolicyWarning = vi.fn().mockResolvedValue({ status: "approved" });
const displayedWarning = {
warning: { reason: "same bounded reason", fingerprint: "full-warning-a" },
findings: [
{
ruleId: "same-bounded-finding",
severity: "warn" as const,
message: "same bounded message",
},
],
};
runInstallPolicyMock.mockResolvedValueOnce(displayedWarning).mockResolvedValueOnce({
...displayedWarning,
warning: { ...displayedWarning.warning, fingerprint: "full-warning-b" },
});
const result = await scanFileInstallSourceRuntime({
filePath: "/tmp/payload.js",
logger: {},
onInstallPolicyWarning,
pluginId: "payload",
});
expect(result?.blocked?.reason).toContain("The policy warning changed after approval.");
expect(result?.blocked?.reason).toContain("same bounded reason");
expect(onInstallPolicyWarning).toHaveBeenCalledTimes(1);
expect(runInstallPolicyMock).toHaveBeenCalledTimes(2);
});
it("keeps a block from policy re-evaluation terminal", async () => {
runInstallPolicyMock
.mockResolvedValueOnce({ warning: { reason: "review this plugin" } })
.mockResolvedValueOnce({
warning: { reason: "review this plugin", fingerprint: "warning-a" },
})
.mockResolvedValueOnce({
blocked: { code: "security_scan_blocked", reason: "now blocked" },
});
@@ -395,7 +433,7 @@ describe("legacy file install scan compatibility", () => {
it("keeps the deprecated unsafe flag inert when policy warns", async () => {
runInstallPolicyMock.mockResolvedValue({
warning: { reason: "review this plugin" },
warning: { reason: "review this plugin", fingerprint: "warning-a" },
});
const result = await scanFileInstallSourceRuntime({
@@ -424,7 +462,9 @@ describe("legacy file install scan compatibility", () => {
it("keeps a block from acknowledged policy re-evaluation terminal", async () => {
runInstallPolicyMock
.mockResolvedValueOnce({ warning: { reason: "review this plugin" } })
.mockResolvedValueOnce({
warning: { reason: "review this plugin", fingerprint: "warning-a" },
})
.mockResolvedValueOnce({
blocked: { code: "security_scan_blocked", reason: "now blocked" },
});
@@ -442,7 +482,7 @@ describe("legacy file install scan compatibility", () => {
it("distinguishes an exhausted noninteractive approval from cancellation", async () => {
runInstallPolicyMock.mockResolvedValue({
warning: { reason: "review the dependency warning" },
warning: { reason: "review the dependency warning", fingerprint: "warning-a" },
findings: [
{
ruleId: "dependency-warning",
@@ -473,7 +513,7 @@ describe("legacy file install scan compatibility", () => {
it("renders warning details as one readable review notice", async () => {
const warnings: string[] = [];
runInstallPolicyMock.mockResolvedValue({
warning: { reason: "review this plugin" },
warning: { reason: "review this plugin", fingerprint: "warning-a" },
findings: [{ ruleId: "context", severity: "info", message: "Informational context." }],
});
+1 -4
View File
@@ -1,7 +1,6 @@
// Runtime bridge for plugin install security scanning.
import fs from "node:fs/promises";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
@@ -776,9 +775,7 @@ async function runOperatorInstallPolicy(params: {
});
}
if (reevaluated?.warning) {
const warningUnchanged =
reevaluated.warning.reason === result.warning.reason &&
isDeepStrictEqual(reevaluated.findings ?? [], result.findings ?? []);
const warningUnchanged = reevaluated.warning.fingerprint === result.warning.fingerprint;
if (!warningUnchanged) {
return {
blocked: {