Files
craftvia/scripts/test-incident-deadlines.ts
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

206 lines
8.1 KiB
TypeScript

// Reiner Test der Fristen-/Timer-Logik für Vorfälle (IM-B, Fachkonzept §6).
//
// Prüft:
// 1. Fristen aus dem Kenntniszeitpunkt (detectedAt/reportedAt) korrekt berechnet.
// 2. NIS2-Timer greifen NUR bei nis2Category ∈ {wichtig, wesentlich} UND nis2Relevant.
// 3. DSGVO-Frist (72 h) greift bei Personenbezug (dsgvoRelevant).
// 4. Interne SLA je Severity (Reaktions-/Behebungsfrist).
// 5. Überfällig-Erkennung + offene/erledigte Meldestufen (reportStatus).
//
// Lauf: npx tsx scripts/test-incident-deadlines.ts (rein, keine DB).
import {
addHours,
addMonths,
computeDeadlines,
computeSla,
deadlineItems,
isDueSoon,
isOverdue,
isReportable,
knowledgeTime,
nextReportStatus,
nis2TimersApply,
openReportDeadlines,
remainingMs,
slaForSeverity,
stageDone,
NIS2_ERSTMELDUNG_HOURS,
NIS2_FOLGEMELDUNG_HOURS,
} from "../src/lib/incident-deadlines";
let failures = 0;
const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
const HOUR = 3_600_000;
const known = new Date("2026-03-10T08:00:00.000Z");
// ── 1. Kenntniszeitpunkt-Priorität ──────────────────────────────────────────
{
const detected = new Date("2026-03-10T08:00:00Z");
const reported = new Date("2026-03-11T08:00:00Z");
ok(
knowledgeTime({ detectedAt: detected, reportedAt: reported })?.getTime() === detected.getTime(),
"knowledgeTime: detectedAt hat Vorrang vor reportedAt",
);
ok(
knowledgeTime({ detectedAt: null, reportedAt: reported })?.getTime() === reported.getTime(),
"knowledgeTime: ohne detectedAt greift reportedAt",
);
ok(
knowledgeTime({ detectedAt: null, reportedAt: null, occurredAt: null, createdAt: known })?.getTime() ===
known.getTime(),
"knowledgeTime: Fallback auf createdAt",
);
}
// ── 1b. Fristen aus Kenntniszeitpunkt (NIS2 wesentlich) ─────────────────────
{
const d = computeDeadlines({
nis2Category: "wesentlich",
nis2Relevant: true,
dsgvoRelevant: false,
knownAt: known,
});
ok(
d.erstmeldungDueAt?.getTime() === known.getTime() + NIS2_ERSTMELDUNG_HOURS * HOUR,
"NIS2 Erstmeldung = Kenntnis + 24 h",
);
ok(
d.folgemeldungDueAt?.getTime() === known.getTime() + NIS2_FOLGEMELDUNG_HOURS * HOUR,
"NIS2 Folgemeldung = Kenntnis + 72 h",
);
ok(d.abschlussDueAt !== null, "NIS2 Abschlussbericht gesetzt");
ok(
d.abschlussDueAt!.getMonth() === addMonths(known, 1).getMonth(),
"NIS2 Abschlussbericht = Kenntnis + 1 Monat (Monat +1)",
);
ok(d.dsgvoDueAt === null, "ohne Personenbezug keine DSGVO-Frist");
}
// ── 2. NIS2-Timer nur bei passender Kategorie UND Relevanz ──────────────────
{
ok(nis2TimersApply("wichtig", true), "nis2TimersApply: wichtig + relevant → true");
ok(nis2TimersApply("wesentlich", true), "nis2TimersApply: wesentlich + relevant → true");
ok(!nis2TimersApply("keine", true), "nis2TimersApply: keine → false (auch wenn relevant)");
ok(!nis2TimersApply("wesentlich", false), "nis2TimersApply: nicht relevant → false");
const noCat = computeDeadlines({
nis2Category: "keine",
nis2Relevant: true,
dsgvoRelevant: false,
knownAt: known,
});
ok(
noCat.erstmeldungDueAt === null && noCat.folgemeldungDueAt === null && noCat.abschlussDueAt === null,
"computeDeadlines: nis2Category=keine → keine NIS2-Fristen",
);
const notRelevant = computeDeadlines({
nis2Category: "wesentlich",
nis2Relevant: false,
dsgvoRelevant: false,
knownAt: known,
});
ok(
notRelevant.erstmeldungDueAt === null,
"computeDeadlines: nis2Relevant=false → keine NIS2-Fristen (trotz Kategorie)",
);
}
// ── 3. DSGVO bei Personenbezug ──────────────────────────────────────────────
{
const d = computeDeadlines({
nis2Category: "keine",
nis2Relevant: false,
dsgvoRelevant: true,
knownAt: known,
});
ok(d.dsgvoDueAt?.getTime() === known.getTime() + 72 * HOUR, "DSGVO-Frist = Kenntnis + 72 h");
ok(d.erstmeldungDueAt === null, "DSGVO ohne NIS2: keine NIS2-Fristen");
ok(
isReportable({ nis2Category: "keine", nis2Relevant: false, dsgvoRelevant: true }),
"isReportable: nur DSGVO → meldepflichtig",
);
ok(
!isReportable({ nis2Category: "keine", nis2Relevant: true, dsgvoRelevant: false }),
"isReportable: NIS2-relevant aber Kategorie keine → nicht meldepflichtig",
);
}
// ── 4. Interne SLA je Severity ──────────────────────────────────────────────
{
ok(slaForSeverity("kritisch").reactionHours === 1, "SLA kritisch: Reaktion 1 h");
ok(slaForSeverity("kritisch").resolutionHours === 24, "SLA kritisch: Behebung 24 h");
ok(
slaForSeverity("niedrig").reactionHours > slaForSeverity("hoch").reactionHours,
"SLA: niedrigere Severity → längere Reaktionsfrist",
);
const sla = computeSla("hoch", known);
ok(
sla.reactionDueAt?.getTime() === addHours(known, 4).getTime(),
"computeSla hoch: Reaktionsfrist = Kenntnis + 4 h",
);
ok(computeSla("mittel", null).reactionDueAt === null, "computeSla ohne Kenntnis → null");
}
// ── 5. Überfällig-Erkennung + Meldestufen ───────────────────────────────────
{
const now = new Date("2026-03-11T10:00:00.000Z"); // 26 h nach Kenntnis
ok(isOverdue(addHours(known, 24), now), "isOverdue: 24-h-Frist ist nach 26 h überfällig");
ok(!isOverdue(addHours(known, 72), now), "isOverdue: 72-h-Frist noch nicht überfällig");
ok(remainingMs(addHours(known, 72), now)! > 0, "remainingMs: 72-h-Frist hat positive Restzeit");
ok(isDueSoon(addHours(known, 30), now), "isDueSoon: Frist in 4 h ist bald fällig");
ok(!isDueSoon(addHours(known, 240), now), "isDueSoon: Frist in >24 h ist nicht bald fällig");
ok(stageDone("erstmeldung", "erstmeldung"), "stageDone: reportStatus=erstmeldung → Erstmeldung erledigt");
ok(!stageDone("pruefung", "erstmeldung"), "stageDone: reportStatus=pruefung → Erstmeldung offen");
ok(stageDone("folgemeldung", "erstmeldung"), "stageDone: reportStatus=folgemeldung → Erstmeldung erledigt");
const inc = {
severity: "hoch",
reportStatus: "pruefung",
detectedAt: known,
reportedAt: null,
occurredAt: null,
createdAt: known,
erstmeldungDueAt: addHours(known, 24),
folgemeldungDueAt: addHours(known, 72),
abschlussDueAt: addMonths(known, 1),
dsgvoDueAt: null,
};
const items = deadlineItems(inc, now);
const erst = items.find((i) => i.kind === "erstmeldung")!;
ok(erst.overdue, "deadlineItems: offene Erstmeldung nach 26 h ist überfällig");
ok(items.some((i) => i.kind === "reaction"), "deadlineItems: interne SLA-Reaktionsfrist enthalten");
const open = openReportDeadlines(inc, now);
ok(
open.every((i) => !i.done) && open.some((i) => i.kind === "erstmeldung"),
"openReportDeadlines: nur offene Meldestufen, Erstmeldung enthalten",
);
const submitted = openReportDeadlines({ ...inc, reportStatus: "folgemeldung" }, now);
ok(
!submitted.some((i) => i.kind === "erstmeldung" || i.kind === "folgemeldung"),
"openReportDeadlines: eingereichte Stufen (reportStatus=folgemeldung) fallen raus",
);
}
// ── 6. Meldung-Track Vorwärts-Übergänge ─────────────────────────────────────
{
ok(nextReportStatus("pruefung") === "erstmeldung", "nextReportStatus: pruefung → erstmeldung");
ok(nextReportStatus("erstmeldung") === "folgemeldung", "nextReportStatus: erstmeldung → folgemeldung");
ok(nextReportStatus("folgemeldung") === "abschluss", "nextReportStatus: folgemeldung → abschluss");
ok(nextReportStatus("abschluss") === null, "nextReportStatus: abschluss ist Endzustand");
}
if (failures) {
console.error(`\n✗ ${failures} Prüfung(en) fehlgeschlagen.`);
process.exit(1);
}
console.log("\n✓ Alle Fristen-/Timer-Prüfungen bestanden.");