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>
206 lines
8.1 KiB
TypeScript
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.");
|