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:
committed by
gsxdsm
parent
abb0f63aae
commit
cbd3c961d1
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user