fix(FN-4915): harden fn_secret_get secret audit payload guard

Fusion-Task-Id: FN-4915
Fusion-Task-Lineage: d5c49dae-6069-4998-b632-aca52cc312dc
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 13:46:11 -07:00
committed by gsxdsm
parent abb0f63aae
commit cbd3c961d1
2 changed files with 47 additions and 12 deletions

View File

@@ -6,9 +6,14 @@ const listSecretsMock = vi.hoisted(() => vi.fn());
const approvalCreateMock = vi.hoisted(() => vi.fn());
const approvalFindLatestByDedupeKeyMock = vi.hoisted(() => vi.fn());
const recordRunAuditEventMock = vi.hoisted(() => vi.fn());
const assertNoSecretPlaintextMock = vi.hoisted(() => vi.fn());
vi.mock("@fusion/dashboard", () => ({ registerGithubTrackingHook: vi.fn() }));
vi.mock("@fusion/engine", () => ({ createFnAgent: vi.fn(), fetchWebContent: vi.fn() }));
vi.mock("@fusion/engine", () => ({
createFnAgent: vi.fn(),
fetchWebContent: vi.fn(),
assertNoSecretPlaintext: assertNoSecretPlaintextMock,
}));
vi.mock("@fusion/core", async () => {
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
@@ -52,6 +57,7 @@ describe("extension fn_secret_get", () => {
resolveSecretAccessPolicyMock.mockReturnValue({ policy: "auto", source: "secret" });
approvalFindLatestByDedupeKeyMock.mockReturnValue(null);
approvalCreateMock.mockReturnValue({ id: "apr-1", status: "pending" });
assertNoSecretPlaintextMock.mockImplementation(() => {});
});
it("returns value for auto policy", async () => {
@@ -61,8 +67,15 @@ describe("extension fn_secret_get", () => {
const result = await tool.execute("id", { key: "API_KEY" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1", runId: "run-1" });
expect(result.details.value).toBe("secret-value");
expect(approvalCreateMock).not.toHaveBeenCalled();
expect(recordRunAuditEventMock.mock.calls[0][0].mutationType).toBe("secret:read");
expect(JSON.stringify(recordRunAuditEventMock.mock.calls[0][0])).not.toContain("secret-value");
const event = recordRunAuditEventMock.mock.calls[0][0];
expect(event.mutationType).toBe("secret:read");
expect(event.metadata).toEqual({ key: "API_KEY", scope: "project" });
expect(event.metadata).not.toHaveProperty("plaintextValue");
expect(event.metadata).not.toHaveProperty("value");
expect(event.metadata).not.toHaveProperty("ciphertext");
expect(event.metadata).not.toHaveProperty("nonce");
expect(event.metadata).not.toHaveProperty("decrypted");
expect(JSON.stringify(event)).not.toContain("secret-value");
});
it("returns pending_approval for prompt policy", async () => {
@@ -75,6 +88,22 @@ describe("extension fn_secret_get", () => {
expect(approvalCreateMock).toHaveBeenCalled();
});
it("skips audit emission if metadata guard throws", async () => {
assertNoSecretPlaintextMock.mockImplementation(() => {
throw new Error("secret audit metadata may not include plaintext fields");
});
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const tools = new Map<string, any>();
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
const tool = tools.get("fn_secret_get");
const result = await tool.execute("id", { key: "API_KEY" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1", runId: "run-1" });
expect(result.details.value).toBe("secret-value");
expect(recordRunAuditEventMock).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
it("returns denied for deny policy and not found when missing", async () => {
const tools = new Map<string, any>();
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);

View File

@@ -42,6 +42,7 @@ import {
ExperimentFinalizeStateError,
type FinalizePlanOverride,
fetchWebContent,
assertNoSecretPlaintext,
} from "@fusion/engine";
import * as dashboard from "@fusion/dashboard";
import { resolve, basename, extname, join } from "node:path";
@@ -121,15 +122,20 @@ function emitSecretAudit(
metadata?: Record<string, unknown>,
): void {
if (!ctx.runId || !ctx.agentId) return;
store.recordRunAuditEvent({
runId: ctx.runId,
agentId: ctx.agentId,
taskId: ctx.taskId,
domain: "filesystem",
mutationType,
target,
metadata,
});
try {
assertNoSecretPlaintext(metadata);
store.recordRunAuditEvent({
runId: ctx.runId,
agentId: ctx.agentId,
taskId: ctx.taskId,
domain: "filesystem",
mutationType,
target,
metadata,
});
} catch (error) {
console.warn("[fusion-extension] secret audit emission skipped", error);
}
}
/**