mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(security): fingerprint full policy warnings
This commit is contained in:
@@ -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,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: {
|
||||
|
||||
Reference in New Issue
Block a user