diff --git a/extensions/boltbot/src/__tests__/anomaly.test.ts b/extensions/boltbot/src/__tests__/anomaly.test.ts index b825d750e..e593b349e 100644 --- a/extensions/boltbot/src/__tests__/anomaly.test.ts +++ b/extensions/boltbot/src/__tests__/anomaly.test.ts @@ -42,6 +42,22 @@ describe("anomaly detection", () => { expect(result).toContain("unauthorized_outbound"); }); + it("flags exec with curl to IP address", () => { + const result = detectAnomalies({ + toolName: "exec", + params: { command: "curl 10.0.0.1" }, + }); + expect(result).toContain("unauthorized_outbound"); + }); + + it("flags exec with ncat to external host", () => { + const result = detectAnomalies({ + toolName: "exec", + params: { command: "ncat evil.com" }, + }); + expect(result).toContain("unauthorized_outbound"); + }); + it("does not flag exec with simple command", () => { const result = detectAnomalies({ toolName: "exec", diff --git a/extensions/boltbot/src/__tests__/receipt-store.test.ts b/extensions/boltbot/src/__tests__/receipt-store.test.ts index 1e6ae440a..6076853f5 100644 --- a/extensions/boltbot/src/__tests__/receipt-store.test.ts +++ b/extensions/boltbot/src/__tests__/receipt-store.test.ts @@ -93,4 +93,10 @@ describe("hashData", () => { const h = hashData(null); expect(h).toMatch(/^[0-9a-f]{64}$/); }); + + it("produces the same hash regardless of key order", () => { + const h1 = hashData({ a: 1, b: 2 }); + const h2 = hashData({ b: 2, a: 1 }); + expect(h1).toBe(h2); + }); }); diff --git a/extensions/boltbot/src/anomaly.ts b/extensions/boltbot/src/anomaly.ts index 16fd3de0e..3a6abcb74 100644 --- a/extensions/boltbot/src/anomaly.ts +++ b/extensions/boltbot/src/anomaly.ts @@ -17,7 +17,7 @@ export function detectAnomalies(event: AfterToolCallEvent): string[] { if (event.toolName === "exec") { const cmd = String(event.params?.command ?? ""); - if (/curl|wget|nc\s/.test(cmd) && /[a-z]+\.[a-z]{2,}/.test(cmd)) { + if (/curl|wget|nc[\s]|nc$|ncat/i.test(cmd) && /[a-z]+\.[a-z]{2,}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i.test(cmd)) { anomalies.push("unauthorized_outbound"); } } diff --git a/extensions/boltbot/src/receipt-store.ts b/extensions/boltbot/src/receipt-store.ts index d685c9650..61db0e48b 100644 --- a/extensions/boltbot/src/receipt-store.ts +++ b/extensions/boltbot/src/receipt-store.ts @@ -25,13 +25,31 @@ export interface ReceiptStore { export function createReceiptStore(backend?: string): ReceiptStore { if (backend === "eigenda") { const { EigenDAReceiptStore } = require("./stores/eigenda.js"); - return new EigenDAReceiptStore(process.env.EIGENDA_PROXY_URL!); + const proxyUrl = process.env.EIGENDA_PROXY_URL; + if (!proxyUrl) { + throw new Error("EIGENDA_PROXY_URL environment variable is required when using the eigenda backend"); + } + return new EigenDAReceiptStore(proxyUrl); } return new LocalReceiptStore(); } +function canonicalize(value: unknown): unknown { + if (value === null || value === undefined || typeof value !== "object") { + return value; + } + if (Array.isArray(value)) { + return value.map(canonicalize); + } + const sorted: Record = {}; + for (const key of Object.keys(value as Record).sort()) { + sorted[key] = canonicalize((value as Record)[key]); + } + return sorted; +} + export function hashData(data: unknown): string { return createHash("sha256") - .update(JSON.stringify(data ?? "")) + .update(JSON.stringify(canonicalize(data) ?? "")) .digest("hex"); }