db96c4a25e
ALERT_WEBHOOK_URL (secret) → HttpAlertSender sinon NullAlertSender. Gatekeeper émet onFirstProvisionalExec (1ère exéc PROVISOIRE) et route onBlockedAttempt vers une alerte. Scheduler quotidien digest + rappel J-1, câblé Phase 2 + arrêt propre. 53 tests verts. Palier de risque : reversible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import { AssetRepository } from "../src/gatekeeper/repository.js";
|
|
import { Gatekeeper } from "../src/gatekeeper/gatekeeper.js";
|
|
import { runAlertCycleOnce } from "../src/alerts/scheduler.js";
|
|
import { createAsset, PROVISIONAL_TTL_MS } from "../src/gatekeeper/assets.js";
|
|
import { createLogger } from "../src/audit/log.js";
|
|
import type { AlertSender, AlertEvent } from "../src/alerts/types.js";
|
|
import type { ToolSpec } from "../src/agent/types.js";
|
|
|
|
const log = createLogger("silent");
|
|
|
|
class CapturingSender implements AlertSender {
|
|
events: AlertEvent[] = [];
|
|
async send(e: AlertEvent): Promise<void> {
|
|
this.events.push(e);
|
|
}
|
|
}
|
|
|
|
let repo: AssetRepository;
|
|
beforeEach(() => (repo = new AssetRepository(":memory:")));
|
|
afterEach(() => repo.close());
|
|
|
|
describe("Gatekeeper first_provisional_exec", () => {
|
|
it("alerte à la 1ʳᵉ exécution d'un asset PROVISOIRE puis plus", () => {
|
|
const onFirstProvisionalExec = vi.fn();
|
|
const gk = new Gatekeeper(repo, log, { onFirstProvisionalExec });
|
|
// asset provisoire pré-existant, exécutable via la capacité privilégiée
|
|
const id = "tool:portainer.redeploy";
|
|
repo.create(createAsset({ id, type: "tool", version: "1.0.0", riskTier: "reversible" }));
|
|
repo.updateStatus(id, "provisoire");
|
|
|
|
const spec: ToolSpec = {
|
|
name: "portainer.redeploy",
|
|
description: "",
|
|
parameters: {},
|
|
server: "portainer",
|
|
readOnly: false,
|
|
riskTier: "privileged",
|
|
};
|
|
expect(gk.authorizeTool(spec).allowed).toBe(true);
|
|
expect(gk.authorizeTool(spec).allowed).toBe(true);
|
|
expect(onFirstProvisionalExec).toHaveBeenCalledOnce(); // une seule fois
|
|
expect(repo.get(id)?.execCount).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("runAlertCycleOnce", () => {
|
|
it("émet J-1 + digest quand des assets sont en attente", async () => {
|
|
const now = 2_000_000_000;
|
|
// provisoire qui expire dans ~1h → J-1
|
|
repo.create(createAsset({ id: "soon", type: "tool", version: "1.0.0", riskTier: "reversible", now: now - PROVISIONAL_TTL_MS + 3_600_000 }));
|
|
// privilégié bloqué → compte dans le digest
|
|
repo.create(createAsset({ id: "blk", type: "stack-portainer", version: "1.0.0", riskTier: "privileged", now }));
|
|
|
|
const sender = new CapturingSender();
|
|
await runAlertCycleOnce(repo, sender, now);
|
|
|
|
const kinds = sender.events.map((e) => e.kind);
|
|
expect(kinds).toContain("countdown_j1");
|
|
expect(kinds).toContain("daily_digest");
|
|
});
|
|
|
|
it("n'émet rien sans asset en attente", async () => {
|
|
const sender = new CapturingSender();
|
|
await runAlertCycleOnce(repo, sender, Date.now());
|
|
expect(sender.events).toHaveLength(0);
|
|
});
|
|
});
|