fix(boltbot): address code review findings

- Deterministic hashing: canonicalize object keys before SHA-256
- Validate EIGENDA_PROXY_URL before constructing EigenDA store
- Improve anomaly regex: detect IP targets, ncat, case-insensitive
This commit is contained in:
duy 2026-01-29 12:58:36 -08:00
parent 1096cc16e6
commit 30e42178c1
4 changed files with 43 additions and 3 deletions

View File

@ -42,6 +42,22 @@ describe("anomaly detection", () => {
expect(result).toContain("unauthorized_outbound"); 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", () => { it("does not flag exec with simple command", () => {
const result = detectAnomalies({ const result = detectAnomalies({
toolName: "exec", toolName: "exec",

View File

@ -93,4 +93,10 @@ describe("hashData", () => {
const h = hashData(null); const h = hashData(null);
expect(h).toMatch(/^[0-9a-f]{64}$/); 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);
});
}); });

View File

@ -17,7 +17,7 @@ export function detectAnomalies(event: AfterToolCallEvent): string[] {
if (event.toolName === "exec") { if (event.toolName === "exec") {
const cmd = String(event.params?.command ?? ""); 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"); anomalies.push("unauthorized_outbound");
} }
} }

View File

@ -25,13 +25,31 @@ export interface ReceiptStore {
export function createReceiptStore(backend?: string): ReceiptStore { export function createReceiptStore(backend?: string): ReceiptStore {
if (backend === "eigenda") { if (backend === "eigenda") {
const { EigenDAReceiptStore } = require("./stores/eigenda.js"); 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(); 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<string, unknown> = {};
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
sorted[key] = canonicalize((value as Record<string, unknown>)[key]);
}
return sorted;
}
export function hashData(data: unknown): string { export function hashData(data: unknown): string {
return createHash("sha256") return createHash("sha256")
.update(JSON.stringify(data ?? "")) .update(JSON.stringify(canonicalize(data) ?? ""))
.digest("hex"); .digest("hex");
} }