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 { createDistillationReceipt, writeDistillationReceipt } from "./distillation-receipt.js";
|
||||||
export type { DistillationProvider, DistillationReceipt, DistillationReceiptOptions, DistillationTeacherKind } 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 { auditGoalCompletion, writeGoalAuditReceipt } from "./goal-audit.js";
|
||||||
export type { GoalAuditCriterion, GoalAuditReceipt } 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