feat: add security report receipts
This commit is contained in:
parent
2bf9fb6467
commit
21d75d10c9
|
|
@ -81,6 +81,9 @@ export type { AgentBenchOptions, AgentBenchReceipt, BenchContenderId, BenchConte
|
|||
export { createDistillationReceipt, writeDistillationReceipt } from "./distillation-receipt.js";
|
||||
export type { DistillationProvider, DistillationReceipt, DistillationReceiptOptions, DistillationTeacherKind } from "./distillation-receipt.js";
|
||||
|
||||
export { createSecurityReportReceipt, writeSecurityReportReceipt } from "./security-report.js";
|
||||
export type { SecurityReportOptions, SecurityReportReceipt, SecurityReportSeverity, SecurityReportVerdict } from "./security-report.js";
|
||||
|
||||
export { auditGoalCompletion, writeGoalAuditReceipt } from "./goal-audit.js";
|
||||
export type { GoalAuditCriterion, GoalAuditReceipt } from "./goal-audit.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createSecurityReportReceipt, writeSecurityReportReceipt } from "./security-report.js";
|
||||
|
||||
describe("security report receipt", () => {
|
||||
it("accepts confirmed findings only with repro and evidence", () => {
|
||||
const receipt = createSecurityReportReceipt({
|
||||
affectedComponent: "security scan",
|
||||
reproCommand: "node dist/index.js security scan .",
|
||||
evidencePath: ".fable/security/report.json",
|
||||
tracePath: ".fable/security/trace.log",
|
||||
verdict: "confirmed",
|
||||
severity: "high",
|
||||
freshnessHours: 12,
|
||||
now: new Date("2026-06-27T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(receipt.schema).toBe("fable.security.report.v1");
|
||||
expect(receipt.decision).toBe("accepted");
|
||||
expect(receipt.freshness.expiresAt).toBe("2026-06-27T12:00:00.000Z");
|
||||
expect(receipt.reasons).toEqual([]);
|
||||
});
|
||||
|
||||
it("sends weak reports to review", () => {
|
||||
const receipt = createSecurityReportReceipt({ affectedComponent: "x", reproCommand: "", evidencePath: "", verdict: "needs_review" });
|
||||
|
||||
expect(receipt.decision).toBe("review");
|
||||
expect(receipt.reasons).toContain("missing repro command");
|
||||
expect(receipt.reasons).toContain("missing evidence path");
|
||||
});
|
||||
|
||||
it("writes a JSON receipt", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "security-report-"));
|
||||
const file = path.join(dir, "receipt.json");
|
||||
|
||||
writeSecurityReportReceipt(file, createSecurityReportReceipt({ affectedComponent: "x", reproCommand: "echo ok", evidencePath: "evidence.json", verdict: "not_reproduced" }));
|
||||
|
||||
expect(fs.readFileSync(file, "utf-8")).toContain("fable.security.report.v1");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
export type SecurityReportVerdict = "confirmed" | "not_reproduced" | "needs_review";
|
||||
export type SecurityReportSeverity = "low" | "medium" | "high" | "critical";
|
||||
|
||||
export interface SecurityReportOptions {
|
||||
affectedComponent: string;
|
||||
reproCommand: string;
|
||||
evidencePath: string;
|
||||
verdict: SecurityReportVerdict;
|
||||
severity?: SecurityReportSeverity;
|
||||
freshnessHours?: number;
|
||||
tracePath?: string;
|
||||
summary?: string;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
export interface SecurityReportReceipt {
|
||||
schema: "fable.security.report.v1";
|
||||
createdAt: string;
|
||||
affectedComponent: string;
|
||||
reproCommand: string;
|
||||
evidencePath: string;
|
||||
tracePath?: string;
|
||||
verdict: SecurityReportVerdict;
|
||||
severity: SecurityReportSeverity;
|
||||
freshness: { hours: number; expiresAt: string };
|
||||
summary: string;
|
||||
decision: "accepted" | "rejected" | "review";
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export function createSecurityReportReceipt(opts: SecurityReportOptions): SecurityReportReceipt {
|
||||
const now = opts.now ?? new Date();
|
||||
const freshnessHours = opts.freshnessHours ?? 24;
|
||||
const reasons: string[] = [];
|
||||
if (!opts.reproCommand.trim()) reasons.push("missing repro command");
|
||||
if (!opts.evidencePath.trim()) reasons.push("missing evidence path");
|
||||
if (opts.verdict === "needs_review") reasons.push("verdict requires human/security review");
|
||||
|
||||
const decision = reasons.length ? "review" : opts.verdict === "confirmed" ? "accepted" : "rejected";
|
||||
return {
|
||||
schema: "fable.security.report.v1",
|
||||
createdAt: now.toISOString(),
|
||||
affectedComponent: opts.affectedComponent,
|
||||
reproCommand: opts.reproCommand,
|
||||
evidencePath: opts.evidencePath,
|
||||
tracePath: opts.tracePath,
|
||||
verdict: opts.verdict,
|
||||
severity: opts.severity ?? "medium",
|
||||
freshness: { hours: freshnessHours, expiresAt: new Date(now.getTime() + freshnessHours * 60 * 60 * 1000).toISOString() },
|
||||
summary: opts.summary ?? "security finding report",
|
||||
decision,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
|
||||
export function writeSecurityReportReceipt(file: string, receipt: SecurityReportReceipt): string {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
return file;
|
||||
}
|
||||
Loading…
Reference in New Issue