Fundament: ISMS-Module entfernt; Craftvia-Rollen, Module, Navigation, i18n-Split

- ISMS-Routen, Actions, Server-/Lib-Code, Komponenten, Prisma-Modelle, Seeds,
  Importer, Skripte und ISMS-Tests entfernt (Fundament bleibt: Auth, Identity,
  MFA/WebAuthn, RBAC, Audit, Mail, Storage, Backup/DSGVO, Plattform-Admin)
- Schema auf Fundament-Modelle reduziert; TenantSettings generisch (+phone/email)
- TENANT_MODELS (db.ts, backup/topology.ts) und PII-Felder ausgedünnt
- RBAC: Rollen tenant-admin/backoffice/team-lead/technician + Craftvia-Permissions
- Modul-Katalog (customers, sites, teams, work_orders, imports, field, reports,
  emergency, documents, notifications, lotse) + Navigation aus src/lib/nav.ts
- Modul-Routen mit requireModule-Layout und Platzhalterseite
- Message-Katalog je Namespace (messages/<locale>/<namespace>.json), fs-Loader
- check-module-guards: Modul-Key aus src/server/actions/<moduleKey>/
- Provisionierung, Admin-Konsole, Einstellungen, Files-Route, Mail entkoppelt
- Seed minimal (demo/demo2, Nutzer je Rolle); Fundament-Tests auf Role/
  NotificationPreference-Fixtures umgestellt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:35:44 +02:00
co-authored by Claude Opus 5
parent c8e6f30a27
commit 8491c7f173
443 changed files with 1325 additions and 87773 deletions
-102
View File
@@ -1,102 +0,0 @@
// B1 — Bewertungs-Schicht (Schicht 2), framework-neutrale Typen + reine ISO-Regeln.
// Die Belegbasis (ControlEvidence, Schicht 1) ist geteilt; hier wird sie je Framework
// interpretiert. Keine DB-/Node-APIs (rein, testbar).
import type { ControlSpec, ControlEvidence, MaturitySuggestion, OpenPoint } from "./maturity";
export type FrameworkKey = "TISAX" | "ISO_27001";
export type ImplStatus = "umgesetzt" | "teilweise" | "geplant";
/**
* Bewertung eines Controls — je Framework EIN eigener Typ, nie gemischt (Feindesign §3).
* TISAX: Reifegrad 0–3 mit Zielwert. ISO: Umsetzungsstatus, keine Stufung.
*/
export type Verdict =
| { kind: "maturity"; suggestion: MaturitySuggestion; confirmed: number | null; target: 2 | 3 }
| { kind: "status"; applicable: boolean; suggested: ImplStatus; confirmed: ImplStatus | null };
export interface AssessmentRow {
control: string;
title: string;
spec: ControlSpec;
evidence: ControlEvidence; // Schicht 1, identisch für beide Frameworks
verdict: Verdict; // je Framework interpretiert
gaps: OpenPoint[];
}
export interface ReadinessSummary {
framework: FrameworkKey;
band: string;
tone: "risk" | "warn" | "info" | "ok" | "mut";
text: string;
/** Framework-eigene Kennzahl (kein gemeinsamer Nenner, Feindesign §5). */
metricLabel: string; // "Ø-Reifegrad" | "Umsetzungsgrad"
metricValue: string; // "2,3" | "78 %"
total: number;
fulfilled: number;
}
// ── ISO-Bewertung (rein) ──────────────────────────────────────────────────────
/**
* ISO-Vorschlag aus der geteilten Belegbasis (Feindesign §3 IsoStrategy):
* Richtlinie UND Verfahren `validiert` + geforderte Verknüpfungen → `umgesetzt`;
* teilweise belegt → `teilweise`; sonst `geplant`. Kein Reifegrad, keine Stufung.
*/
export function isoSuggestStatus(spec: ControlSpec, ev: ControlEvidence): ImplStatus {
const policyOk = spec.policy.length === 0 ? true : ev.policy === "validiert";
const verfahrenOk = spec.verfahren.length === 0 ? true : ev.verfahren === "validiert";
const linksOk = (!spec.needsAsset || ev.assetLinked) && (!spec.needsRisk || ev.riskLinked);
if (policyOk && verfahrenOk && linksOk) return "umgesetzt";
const anyEvidence = ev.policy !== "fehlt" || (spec.verfahren.length > 0 && ev.verfahren !== "fehlt");
return anyEvidence ? "teilweise" : "geplant";
}
/**
* Offene Punkte eines anwendbaren ISO-Controls (Feindesign §3): fehlende Begründung,
* fehlender Nachweis (Umsetzung nicht belegt) und Status ≠ „umgesetzt".
*/
export function isoGaps(
spec: ControlSpec,
ev: ControlEvidence,
applicable: boolean,
justification: string,
confirmed: ImplStatus | null,
): OpenPoint[] {
if (!applicable) return [];
const out: OpenPoint[] = [];
if (!justification.trim()) {
out.push({ control: spec.control, kind: "nachweis", actual: 0, target: 1, missing: "Begründung/Ausschluss (SoA 6.1.3 d) fehlt", action: `Anwendbarkeit von ${spec.control} begründen` });
}
const suggested = isoSuggestStatus(spec, ev);
if (suggested !== "umgesetzt") {
const miss =
spec.policy.length > 0 && ev.policy !== "validiert" ? "zuständige Richtlinie nicht freigegeben"
: spec.verfahren.length > 0 && ev.verfahren !== "validiert" ? "gefordertes Verfahren nicht freigegeben"
: spec.needsAsset && !ev.assetLinked ? "Asset-Verknüpfung fehlt"
: spec.needsRisk && !ev.riskLinked ? "Risiko-Verknüpfung fehlt"
: "Umsetzung nicht vollständig belegt";
out.push({ control: spec.control, kind: "richtlinie", actual: 0, target: 1, missing: miss, action: `${spec.control}: ${miss} beheben` });
}
if ((confirmed ?? "geplant") !== "umgesetzt") {
out.push({ control: spec.control, kind: "reifegradluecke", actual: confirmed === "teilweise" ? 1 : 0, target: 2, missing: "Umsetzung noch nicht als umgesetzt bestätigt", action: `${spec.control} umsetzen und in der SoA bestätigen` });
}
return out;
}
// ── ISO-Readiness-Bänder (Feindesign §6, Vokabular ohne Reifegrad/AL) ──────────
export interface IsoBand {
band: string;
tone: ReadinessSummary["tone"];
text: string;
}
/** Umsetzungsgrad (Anteil „umgesetzt" an allen anwendbaren Controls) → ISO-Band. */
export function isoBand(fulfilledRatio: number): IsoBand {
const pct = Math.round(fulfilledRatio * 100);
if (pct < 50) return { band: "Aufbau", tone: "risk", text: "Wesentliche Maßnahmen sind noch offen. Fokus: anwendbare Controls umsetzen und die Anwendbarkeit vollständig begründen." };
if (pct < 80) return { band: "In Umsetzung", tone: "warn", text: "Die Struktur steht, es fehlen vor allem Nachweise der Umsetzung. Fokus: Richtlinien freigeben, Verfahren belegen." };
if (pct < 100) return { band: "Zertifizierungsnah", tone: "info", text: "Wenige offene Punkte. Fokus: verbleibende Controls abschließen und Wirksamkeitsnachweise ergänzen." };
return { band: "Zertifizierungsreif", tone: "ok", text: "Alle anwendbaren Controls sind umgesetzt und belegt. Empfehlung: Stichprobenvalidierung vor dem Zertifizierungsaudit." };
}
-34
View File
@@ -1,34 +0,0 @@
// Deutsche Anzeigelabels für Asset-Typen und Informations-Klassifizierungslabels.
//
// Client-safe: nur Daten (Record<enum, string>), keine Server-/Prisma-Importe außer
// den Enum-Typen. Wird im prozessgeführten Wizard (M2) genutzt, um die geseedeten
// Katalog-Vorschläge (`ProcessCatalogEntry.suggestedAssetTypes/suggestedInfoLabels`)
// menschenlesbar als Chips/Defaults auszuspielen — ohne Label-Duplikate je Step.
import type { AssetType, InfoLabel } from "@prisma/client";
/** Träger-/Asset-Typen (SECONDARY-Träger + primäre Werttypen). */
export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
INFORMATION: "Information",
SYSTEM: "System",
APPLICATION: "Anwendung",
LOCATION: "Standort",
SUPPLIER: "Dienstleister",
IT_SERVICE: "IT-Service",
SOFTWARE: "Software",
PROJECT: "Projekt",
PERSON: "Person",
DATA: "Daten",
};
/** Klassifizierungs-/Schutzlabel primärer Informationswerte. */
export const INFO_LABEL_LABELS: Record<InfoLabel, string> = {
NONE: "ohne",
INFO_HIGH: "Info hoch",
INFO_VERY_HIGH: "Info sehr hoch",
PROTOTYPE: "Prototyp",
PERSONAL_DATA: "personenbezogen",
};
export const assetTypeLabel = (t: AssetType): string => ASSET_TYPE_LABELS[t] ?? t;
export const infoLabelLabel = (l: InfoLabel): string => INFO_LABEL_LABELS[l] ?? l;
-23
View File
@@ -1,23 +0,0 @@
import type { TenantDb } from "@/server/db";
/**
* Aktivierung des Audit-Wizards (M4). Der Audit-Wizard ist aus dem Onboarding
* herausgelöst und **opt-in**: Er ist erst nutzbar, wenn er ausdrücklich
* scharfgeschaltet wurde. Als „einfaches Flag" wird die bestehende
* `TenantModule`-Tabelle wiederverwendet (Schlüssel `audit`) — kein neues Modell.
*
* Anders als die regulären Module (Default = an, s. `MODULES`/`requireModule`)
* gilt hier **Default = aus**: nur eine ausdrückliche Zeile mit `enabled = true`
* aktiviert den Wizard. Scharfschalten z. B. über `toggleTenantModule(tenantId,
* AUDIT_READINESS_MODULE_KEY, true)` (Plattform-Admin) oder direkt in der DB.
*
* Offener Punkt (Konzept 4.4): Aktivierung optional zusätzlich an eine
* Coverage-Schwelle knüpfen — hier bewusst manuell gehalten.
*/
export const AUDIT_READINESS_MODULE_KEY = "audit";
/** True, wenn der Audit-Wizard für den (über `db` gebundenen) Mandanten scharfgeschaltet ist. */
export async function isAuditReadinessEnabled(db: TenantDb): Promise<boolean> {
const row = await db.tenantModule.findFirst({ where: { moduleKey: AUDIT_READINESS_MODULE_KEY } });
return row?.enabled === true;
}
-128
View File
@@ -1,128 +0,0 @@
// Cockpit (M3, 1.4): Kapitel/Control → Bereich (Domain) + Default-RACI.
//
// Diese Datei ist die *kanonische Quelle* der globalen Default-Zuordnung. Sie wird
// (a) vom Seed (`prisma/seed.ts`) in die Tabelle `ControlDomainMap` geschrieben und
// (b) als In-App-Fallback verwendet, falls für ein Control keine DB-Zeile existiert.
//
// Client-safe: nur Daten + reine Funktionen, keine Server-/Prisma-Importe.
// Fachliche Korrektur ggü. erstem Entwurf: BCM ist aus Kapitel 3 gelöst und ein
// eigener Bereich (Kontinuität/Krise). Prototyp/Datenschutz sind label-getrieben.
import type { Domain, RaciKind } from "@prisma/client";
/** Deutsche Anzeigelabels der Bereiche (Cockpit-Board). */
export const DOMAIN_LABELS: Record<Domain, string> = {
GOVERNANCE: "Governance",
HR: "Personal",
PHYSICAL: "Physische Sicherheit",
BCM: "Notfallmanagement (BCM)",
IT: "IT",
PROCUREMENT: "Beschaffung",
COMPLIANCE: "Compliance",
DATA_PROTECTION: "Datenschutz",
PROTOTYPE: "Prototypenschutz",
};
/** Stabile Reihenfolge der Bereiche für Filter/Lanes. */
export const DOMAIN_ORDER: Domain[] = [
"GOVERNANCE",
"HR",
"PHYSICAL",
"BCM",
"IT",
"PROCUREMENT",
"COMPLIANCE",
"DATA_PROTECTION",
"PROTOTYPE",
];
export interface ControlDomainRule {
/** Control-ID ("5.2.9") ODER Kapitel-Präfix ("5"). Exaktere Regel gewinnt. */
control: string;
domain: Domain;
raci: RaciKind;
/** Default-verantwortliche/mitwirkende Funktion (ISB, IT_LEAD, …). */
functionKey?: string;
}
// Globale Default-Zuordnung. Erste Zeile je control mit raci=RESPONSIBLE = primärer
// Bereich; weitere Zeilen (CONSULTED/INFORMED) sind Default-Mitwirkende.
export const CONTROL_DOMAIN_DEFAULTS: ControlDomainRule[] = [
// K1 — Governance (ISB + Leitung)
{ control: "1", domain: "GOVERNANCE", raci: "RESPONSIBLE", functionKey: "ISB" },
// K1.6 Krisenmanagement zählt fachlich zu BCM (aus K3 „BCM" gelöst → eigener Bereich)
{ control: "1.6.3", domain: "BCM", raci: "RESPONSIBLE", functionKey: "BCM" },
// K2 — HR / Personal
{ control: "2", domain: "HR", raci: "RESPONSIBLE", functionKey: "HR_LEAD" },
// K3 — physische Sicherheit
{ control: "3", domain: "PHYSICAL", raci: "RESPONSIBLE", functionKey: "FACILITY_LEAD" },
// K4 IAM — IT verantwortlich, HR mitwirkend (Joiner-Mover-Leaver)
{ control: "4", domain: "IT", raci: "RESPONSIBLE", functionKey: "IT_LEAD" },
{ control: "4", domain: "HR", raci: "CONSULTED", functionKey: "HR_LEAD" },
// K5 — IT-Betrieb
{ control: "5", domain: "IT", raci: "RESPONSIBLE", functionKey: "IT_LEAD" },
// K5.2.8 IT-Kontinuitätsplanung zählt fachlich zu BCM
{ control: "5.2.8", domain: "BCM", raci: "RESPONSIBLE", functionKey: "BCM" },
{ control: "5.2.8", domain: "IT", raci: "CONSULTED", functionKey: "IT_LEAD" },
// K6 — Beschaffung/Lieferanten, ISB mitwirkend (Recht/ISB CONSULTED)
{ control: "6", domain: "PROCUREMENT", raci: "RESPONSIBLE", functionKey: "PROCUREMENT_LEAD" },
{ control: "6", domain: "GOVERNANCE", raci: "CONSULTED", functionKey: "ISB" },
// K7 — Compliance, DPO + IT mitwirkend
{ control: "7", domain: "COMPLIANCE", raci: "RESPONSIBLE", functionKey: "COMPLIANCE_LEAD" },
{ control: "7", domain: "DATA_PROTECTION", raci: "CONSULTED", functionKey: "DPO" },
{ control: "7", domain: "IT", raci: "CONSULTED", functionKey: "IT_LEAD" },
// K7.1.2 Schutz personenbezogener Daten → Datenschutz verantwortlich
{ control: "7.1.2", domain: "DATA_PROTECTION", raci: "RESPONSIBLE", functionKey: "DPO" },
// Label-getriebene Sonderbereiche (nicht kapitelgebunden, s. 1.1/1.4)
{ control: "prototype", domain: "PROTOTYPE", raci: "RESPONSIBLE", functionKey: "PROTOTYPE_LEAD" },
{ control: "datenschutz", domain: "DATA_PROTECTION", raci: "RESPONSIBLE", functionKey: "DPO" },
];
/** Erste Zahl-Segment eines Control-Refs als Kapitel-Präfix ("5.2.9" → "5"). */
export function controlChapter(control: string): string {
return String(control ?? "").trim().split(".")[0] ?? "";
}
/**
* Passende Regeln für ein Control aus einer Regelliste ermitteln. Priorität:
* exakter Control-Match vor Kapitel-Präfix-Match. Gibt alle Regeln der besten
* Genauigkeitsstufe zurück (RESPONSIBLE zuerst, dann CONSULTED/INFORMED).
*/
export function matchControlRules(
control: string,
rules: ControlDomainRule[] = CONTROL_DOMAIN_DEFAULTS,
): ControlDomainRule[] {
const ref = String(control ?? "").trim().toLowerCase();
if (!ref) return [];
const exact = rules.filter((r) => r.control.toLowerCase() === ref);
if (exact.length) return sortByRaci(exact);
const chapter = controlChapter(ref);
const byChapter = rules.filter((r) => r.control.toLowerCase() === chapter);
return sortByRaci(byChapter);
}
const RACI_ORDER: Record<RaciKind, number> = {
RESPONSIBLE: 0,
ACCOUNTABLE: 1,
CONSULTED: 2,
INFORMED: 3,
};
function sortByRaci(rules: ControlDomainRule[]): ControlDomainRule[] {
return [...rules].sort((a, b) => RACI_ORDER[a.raci] - RACI_ORDER[b.raci]);
}
/** Primärer Bereich (RESPONSIBLE) eines Controls aus den globalen Defaults. */
export function defaultDomainForControl(control: string): Domain | null {
const rules = matchControlRules(control);
const primary = rules.find((r) => r.raci === "RESPONSIBLE") ?? rules[0];
return primary?.domain ?? null;
}
/** Default-verantwortliche Funktion (functionKey) je Bereich — erste RESPONSIBLE-Regel. */
export const DOMAIN_FUNCTION: Partial<Record<Domain, string>> = (() => {
const map: Partial<Record<Domain, string>> = {};
for (const r of CONTROL_DOMAIN_DEFAULTS) {
if (r.raci === "RESPONSIBLE" && r.functionKey && !map[r.domain]) map[r.domain] = r.functionKey;
}
return map;
})();
-147
View File
@@ -1,147 +0,0 @@
// AUTOGENERIERT von seed/isms-vorlagenpaket-v2/_generate_iso.py — nicht von Hand aendern.
// Quelle: mapping-iso.json + seed/scoping/c5-controls.json. Neu erzeugen: python3 seed/isms-vorlagenpaket-v2/_generate_iso.py
//
// Belegspezifikation je ISO-Control (Feindesign §1/§2, B1): welche Richtlinie/Verfahren
// zustaendig sind und ob Asset-/Risiko-Verknuepfung gefordert ist. needsAsset/needsRisk
// werden ueber ISO_TO_ISA vom ISA-Spec geerbt; ISO-eigene Abschnitte explizit gesetzt.
// Strukturgleich zu ControlSpec (src/lib/maturity.ts) — von der GETEILTEN Belegbasis konsumiert.
export interface IsoControlSpec {
control: string;
title: string;
policy: string[];
verfahren: string[];
needsAsset: boolean;
needsRisk: boolean;
}
const SPECS: Record<string, IsoControlSpec> = {
"4.1": { control: "4.1", title: "Verstehen der Organisation und ihres Kontexts", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"4.2": { control: "4.2", title: "Anforderungen interessierter Parteien", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"4.3": { control: "4.3", title: "Anwendungsbereich des ISMS", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"4.4": { control: "4.4", title: "Informationssicherheits-Managementsystem", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"5.1": { control: "5.1", title: "Führung und Verpflichtung", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"5.2": { control: "5.2", title: "Politik", policy: ["L00"], verfahren: [], needsAsset: false, needsRisk: false },
"5.3": { control: "5.3", title: "Rollen, Verantwortlichkeiten und Befugnisse", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"6.1.1": { control: "6.1.1", title: "Maßnahmen zum Umgang mit Risiken und Chancen", policy: ["R03"], verfahren: ["VA-09"], needsAsset: false, needsRisk: true },
"6.1.2": { control: "6.1.2", title: "Informationssicherheits-Risikobeurteilung", policy: ["R03"], verfahren: ["VA-09"], needsAsset: false, needsRisk: true },
"6.1.3": { control: "6.1.3", title: "Informationssicherheits-Risikobehandlung", policy: ["R03"], verfahren: ["VA-09"], needsAsset: false, needsRisk: true },
"6.2": { control: "6.2", title: "Informationssicherheitsziele und Planung", policy: ["L00"], verfahren: [], needsAsset: false, needsRisk: false },
"6.3": { control: "6.3", title: "Planung von Änderungen", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"7.1": { control: "7.1", title: "Ressourcen", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"7.2": { control: "7.2", title: "Kompetenz", policy: ["R05"], verfahren: [], needsAsset: false, needsRisk: false },
"7.3": { control: "7.3", title: "Bewusstsein", policy: ["R05"], verfahren: ["VA-12"], needsAsset: false, needsRisk: false },
"7.4": { control: "7.4", title: "Kommunikation", policy: ["L00"], verfahren: [], needsAsset: false, needsRisk: false },
"7.5.1": { control: "7.5.1", title: "Dokumentierte Information – Allgemeines", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"7.5.2": { control: "7.5.2", title: "Erstellen und Aktualisieren", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"7.5.3": { control: "7.5.3", title: "Lenkung dokumentierter Information", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"8.1": { control: "8.1", title: "Betriebliche Planung und Steuerung", policy: ["R03"], verfahren: [], needsAsset: false, needsRisk: false },
"8.2": { control: "8.2", title: "Informationssicherheits-Risikobeurteilung (Durchführung)", policy: ["R03"], verfahren: ["VA-09"], needsAsset: false, needsRisk: true },
"8.3": { control: "8.3", title: "Informationssicherheits-Risikobehandlung (Durchführung)", policy: ["R03"], verfahren: ["VA-09"], needsAsset: false, needsRisk: true },
"9.1": { control: "9.1", title: "Überwachung, Messung, Analyse und Bewertung", policy: ["R03"], verfahren: ["VA-22"], needsAsset: false, needsRisk: false },
"9.2": { control: "9.2", title: "Internes Audit", policy: ["R03"], verfahren: ["VA-15"], needsAsset: false, needsRisk: false },
"9.3": { control: "9.3", title: "Managementbewertung", policy: ["R03"], verfahren: ["VA-22"], needsAsset: false, needsRisk: false },
"10.1": { control: "10.1", title: "Fortlaufende Verbesserung", policy: ["R03"], verfahren: ["VA-21"], needsAsset: false, needsRisk: false },
"10.2": { control: "10.2", title: "Nichtkonformität und Korrekturmaßnahmen", policy: ["R03"], verfahren: ["VA-21"], needsAsset: false, needsRisk: false },
"A.5.1": { control: "A.5.1", title: "Informationssicherheitsrichtlinien", policy: ["L00"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.2": { control: "A.5.2", title: "Rollen und Verantwortlichkeiten", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.3": { control: "A.5.3", title: "Aufgabentrennung", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.4": { control: "A.5.4", title: "Verantwortung der Leitung", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.5": { control: "A.5.5", title: "Kontakt mit Behörden", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.6": { control: "A.5.6", title: "Kontakt mit speziellen Interessengruppen", policy: ["R01"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.7": { control: "A.5.7", title: "Bedrohungsinformationen", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.8": { control: "A.5.8", title: "Informationssicherheit im Projektmanagement", policy: ["R01"], verfahren: ["VA-19"], needsAsset: false, needsRisk: false },
"A.5.9": { control: "A.5.9", title: "Inventar der Informationen und Assets", policy: ["R02"], verfahren: ["VA-08"], needsAsset: true, needsRisk: false },
"A.5.10": { control: "A.5.10", title: "Zulässige Nutzung von Informationen und Assets", policy: ["R02"], verfahren: ["VA-08"], needsAsset: false, needsRisk: false },
"A.5.11": { control: "A.5.11", title: "Rückgabe von Assets", policy: ["R11"], verfahren: ["VA-08"], needsAsset: false, needsRisk: false },
"A.5.12": { control: "A.5.12", title: "Klassifizierung von Informationen", policy: ["R02"], verfahren: ["VA-08"], needsAsset: true, needsRisk: false },
"A.5.13": { control: "A.5.13", title: "Kennzeichnung von Informationen", policy: ["R02"], verfahren: ["VA-08"], needsAsset: true, needsRisk: false },
"A.5.14": { control: "A.5.14", title: "Informationsübertragung", policy: ["R09"], verfahren: [], needsAsset: false, needsRisk: false },
"A.5.15": { control: "A.5.15", title: "Zugangssteuerung", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.5.16": { control: "A.5.16", title: "Identitätsmanagement", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.5.17": { control: "A.5.17", title: "Authentisierungsinformationen", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.5.18": { control: "A.5.18", title: "Zugriffsrechte", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.5.19": { control: "A.5.19", title: "Informationssicherheit in Lieferantenbeziehungen", policy: ["R13"], verfahren: ["VA-10"], needsAsset: false, needsRisk: false },
"A.5.20": { control: "A.5.20", title: "Informationssicherheit in Lieferantenvereinbarungen", policy: ["R13"], verfahren: ["VA-10"], needsAsset: false, needsRisk: false },
"A.5.21": { control: "A.5.21", title: "Informationssicherheit in der IKT-Lieferkette", policy: ["R13"], verfahren: ["VA-10"], needsAsset: false, needsRisk: false },
"A.5.22": { control: "A.5.22", title: "Überwachung und Änderung von Lieferantenleistungen", policy: ["R13"], verfahren: ["VA-10"], needsAsset: false, needsRisk: false },
"A.5.23": { control: "A.5.23", title: "Informationssicherheit bei Cloud-Diensten", policy: ["R12"], verfahren: ["VA-11"], needsAsset: false, needsRisk: false },
"A.5.24": { control: "A.5.24", title: "Incident-Management: Planung und Vorbereitung", policy: ["R04"], verfahren: ["VA-01"], needsAsset: false, needsRisk: false },
"A.5.25": { control: "A.5.25", title: "Bewertung und Entscheidung zu Ereignissen", policy: ["R04"], verfahren: ["VA-01"], needsAsset: false, needsRisk: false },
"A.5.26": { control: "A.5.26", title: "Reaktion auf Informationssicherheitsvorfälle", policy: ["R04"], verfahren: ["VA-01"], needsAsset: false, needsRisk: false },
"A.5.27": { control: "A.5.27", title: "Lernen aus Informationssicherheitsvorfällen", policy: ["R04"], verfahren: ["VA-01"], needsAsset: false, needsRisk: false },
"A.5.28": { control: "A.5.28", title: "Sammeln von Beweismaterial", policy: ["R04"], verfahren: ["VA-01"], needsAsset: false, needsRisk: false },
"A.5.29": { control: "A.5.29", title: "Informationssicherheit während einer Störung", policy: ["R04"], verfahren: ["VA-02"], needsAsset: false, needsRisk: false },
"A.5.30": { control: "A.5.30", title: "IKT-Bereitschaft für Business Continuity", policy: ["R04"], verfahren: ["VA-02"], needsAsset: true, needsRisk: false },
"A.5.31": { control: "A.5.31", title: "Rechtliche und vertragliche Anforderungen", policy: ["R14"], verfahren: ["VA-18"], needsAsset: false, needsRisk: false },
"A.5.32": { control: "A.5.32", title: "Geistige Eigentumsrechte", policy: ["R14"], verfahren: ["VA-18"], needsAsset: false, needsRisk: false },
"A.5.33": { control: "A.5.33", title: "Schutz von Aufzeichnungen", policy: ["R14"], verfahren: ["VA-18"], needsAsset: false, needsRisk: false },
"A.5.34": { control: "A.5.34", title: "Datenschutz und Schutz von PII", policy: ["R14"], verfahren: ["VA-18"], needsAsset: false, needsRisk: false },
"A.5.35": { control: "A.5.35", title: "Unabhängige Überprüfung der Informationssicherheit", policy: ["R03"], verfahren: ["VA-15"], needsAsset: false, needsRisk: false },
"A.5.36": { control: "A.5.36", title: "Einhaltung von Richtlinien und Standards", policy: ["R03"], verfahren: ["VA-15"], needsAsset: false, needsRisk: false },
"A.5.37": { control: "A.5.37", title: "Dokumentierte Betriebsabläufe", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.6.1": { control: "A.6.1", title: "Überprüfung (Screening)", policy: ["R05"], verfahren: ["VA-14"], needsAsset: false, needsRisk: false },
"A.6.2": { control: "A.6.2", title: "Arbeitsvertragliche Regelungen", policy: ["R05"], verfahren: ["VA-14"], needsAsset: false, needsRisk: false },
"A.6.3": { control: "A.6.3", title: "Awareness, Ausbildung und Schulung", policy: ["R05"], verfahren: ["VA-12"], needsAsset: false, needsRisk: false },
"A.6.4": { control: "A.6.4", title: "Disziplinarverfahren", policy: ["R05"], verfahren: [], needsAsset: false, needsRisk: false },
"A.6.5": { control: "A.6.5", title: "Verantwortlichkeiten nach Beendigung oder Wechsel", policy: ["R05"], verfahren: ["VA-14"], needsAsset: false, needsRisk: false },
"A.6.6": { control: "A.6.6", title: "Vertraulichkeitsvereinbarungen", policy: ["R05"], verfahren: ["VA-14"], needsAsset: false, needsRisk: false },
"A.6.7": { control: "A.6.7", title: "Remote-Arbeit", policy: ["R06"], verfahren: [], needsAsset: false, needsRisk: false },
"A.6.8": { control: "A.6.8", title: "Meldung von Informationssicherheitsereignissen", policy: ["R04"], verfahren: ["VA-01"], needsAsset: false, needsRisk: false },
"A.7.1": { control: "A.7.1", title: "Physische Sicherheitsperimeter", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.2": { control: "A.7.2", title: "Physische Zutrittssteuerung", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.3": { control: "A.7.3", title: "Sicherung von Büros, Räumen und Einrichtungen", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.4": { control: "A.7.4", title: "Physische Überwachung", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.5": { control: "A.7.5", title: "Schutz vor physischen und umweltbedingten Bedrohungen", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.6": { control: "A.7.6", title: "Arbeiten in Sicherheitsbereichen", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.7": { control: "A.7.7", title: "Aufgeräumter Arbeitsplatz und Bildschirm", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.8": { control: "A.7.8", title: "Platzierung und Schutz von Betriebsmitteln", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.9": { control: "A.7.9", title: "Sicherheit von Assets außerhalb des Geländes", policy: ["R06"], verfahren: [], needsAsset: false, needsRisk: false },
"A.7.10": { control: "A.7.10", title: "Speichermedien", policy: ["R06"], verfahren: ["VA-08"], needsAsset: false, needsRisk: false },
"A.7.11": { control: "A.7.11", title: "Versorgungseinrichtungen", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.12": { control: "A.7.12", title: "Verkabelungssicherheit", policy: ["R07"], verfahren: ["VA-17"], needsAsset: false, needsRisk: false },
"A.7.13": { control: "A.7.13", title: "Wartung von Betriebsmitteln", policy: ["R07"], verfahren: ["VA-04"], needsAsset: false, needsRisk: false },
"A.7.14": { control: "A.7.14", title: "Sichere Entsorgung oder Wiederverwendung", policy: ["R11"], verfahren: ["VA-08"], needsAsset: false, needsRisk: false },
"A.8.1": { control: "A.8.1", title: "Endgeräte der Benutzer", policy: ["R06"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.2": { control: "A.8.2", title: "Privilegierte Zugriffsrechte", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.8.3": { control: "A.8.3", title: "Einschränkung des Informationszugriffs", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.8.4": { control: "A.8.4", title: "Zugriff auf Quellcode", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.5": { control: "A.8.5", title: "Sichere Authentisierung", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.8.6": { control: "A.8.6", title: "Kapazitätssteuerung", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.7": { control: "A.8.7", title: "Schutz vor Malware", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.8": { control: "A.8.8", title: "Management technischer Schwachstellen", policy: ["R10"], verfahren: ["VA-06"], needsAsset: false, needsRisk: false },
"A.8.9": { control: "A.8.9", title: "Konfigurationsmanagement", policy: ["R10"], verfahren: ["VA-04"], needsAsset: false, needsRisk: false },
"A.8.10": { control: "A.8.10", title: "Löschung von Informationen", policy: ["R11"], verfahren: ["VA-08"], needsAsset: false, needsRisk: false },
"A.8.11": { control: "A.8.11", title: "Datenmaskierung", policy: ["R02"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.12": { control: "A.8.12", title: "Verhinderung von Datenlecks", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.13": { control: "A.8.13", title: "Informationssicherung (Backup)", policy: ["R10"], verfahren: ["VA-05"], needsAsset: false, needsRisk: false },
"A.8.14": { control: "A.8.14", title: "Redundanz der Verarbeitungseinrichtungen", policy: ["R04"], verfahren: ["VA-02"], needsAsset: true, needsRisk: false },
"A.8.15": { control: "A.8.15", title: "Protokollierung (Logging)", policy: ["R10"], verfahren: ["VA-13"], needsAsset: false, needsRisk: false },
"A.8.16": { control: "A.8.16", title: "Überwachungsaktivitäten", policy: ["R10"], verfahren: ["VA-13"], needsAsset: false, needsRisk: false },
"A.8.17": { control: "A.8.17", title: "Uhrzeitsynchronisation", policy: ["R10"], verfahren: ["VA-13"], needsAsset: false, needsRisk: false },
"A.8.18": { control: "A.8.18", title: "Nutzung privilegierter Hilfsprogramme", policy: ["R08"], verfahren: ["VA-03"], needsAsset: false, needsRisk: false },
"A.8.19": { control: "A.8.19", title: "Softwareinstallation auf Produktivsystemen", policy: ["R02"], verfahren: ["VA-04"], needsAsset: false, needsRisk: false },
"A.8.20": { control: "A.8.20", title: "Netzwerksicherheit", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.21": { control: "A.8.21", title: "Sicherheit von Netzwerkdiensten", policy: ["R11"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.22": { control: "A.8.22", title: "Netzwerksegmentierung", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.23": { control: "A.8.23", title: "Web-Filterung", policy: ["R10"], verfahren: [], needsAsset: false, needsRisk: false },
"A.8.24": { control: "A.8.24", title: "Nutzung von Kryptographie", policy: ["R09"], verfahren: ["VA-07"], needsAsset: false, needsRisk: false },
"A.8.25": { control: "A.8.25", title: "Sicherer Entwicklungslebenszyklus", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.26": { control: "A.8.26", title: "Sicherheitsanforderungen an Anwendungen", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.27": { control: "A.8.27", title: "Sichere Systemarchitektur und -prinzipien", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.28": { control: "A.8.28", title: "Sichere Programmierung", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.29": { control: "A.8.29", title: "Sicherheitstests in Entwicklung und Abnahme", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.30": { control: "A.8.30", title: "Ausgelagerte Entwicklung", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.31": { control: "A.8.31", title: "Trennung von Entwicklungs-, Test- und Produktionsumgebungen", policy: ["R10"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.32": { control: "A.8.32", title: "Änderungsmanagement", policy: ["R10"], verfahren: ["VA-04"], needsAsset: false, needsRisk: false },
"A.8.33": { control: "A.8.33", title: "Testinformationen", policy: ["R11"], verfahren: ["VA-16"], needsAsset: false, needsRisk: false },
"A.8.34": { control: "A.8.34", title: "Schutz von Systemen bei Audit-Tests", policy: ["R10"], verfahren: ["VA-15"], needsAsset: false, needsRisk: false },
};
/** Belegspezifikation eines ISO-Controls; Fallback fuer unbekannte Controls. */
export function controlSpecIso(control: string): IsoControlSpec {
return SPECS[control] ?? { control, title: `ISO ${control}`, policy: [], verfahren: [], needsAsset: false, needsRisk: false };
}
/** Alle ISO-Controls mit Belegspezifikation (Klauseln 4-10 + Anhang A). */
export const ISO_SPEC_CONTROLS: readonly string[] = Object.keys(SPECS);
-133
View File
@@ -1,133 +0,0 @@
// AUTOGENERIERT von seed/isms-vorlagenpaket-v2/_generate_iso.py — nicht von Hand aendern.
// Quelle: mapping-iso.json. Neu erzeugen: python3 seed/isms-vorlagenpaket-v2/_generate_iso.py
//
// Eigene Map statt Ergaenzung von CONTROL_TITLES: die Schluessel 6.1.1-6.1.3 sind in
// beiden Katalogen belegt (VDA ISA: Lieferanten - ISO: Risikobeurteilung/-behandlung).
export const CONTROL_TITLES_ISO: Record<string, string> = {
"4.1": "Verstehen der Organisation und ihres Kontexts",
"4.2": "Anforderungen interessierter Parteien",
"4.3": "Anwendungsbereich des ISMS",
"4.4": "Informationssicherheits-Managementsystem",
"5.1": "Führung und Verpflichtung",
"5.2": "Politik",
"5.3": "Rollen, Verantwortlichkeiten und Befugnisse",
"6.1.1": "Maßnahmen zum Umgang mit Risiken und Chancen",
"6.1.2": "Informationssicherheits-Risikobeurteilung",
"6.1.3": "Informationssicherheits-Risikobehandlung",
"6.2": "Informationssicherheitsziele und Planung",
"6.3": "Planung von Änderungen",
"7.1": "Ressourcen",
"7.2": "Kompetenz",
"7.3": "Bewusstsein",
"7.4": "Kommunikation",
"7.5.1": "Dokumentierte Information – Allgemeines",
"7.5.2": "Erstellen und Aktualisieren",
"7.5.3": "Lenkung dokumentierter Information",
"8.1": "Betriebliche Planung und Steuerung",
"8.2": "Informationssicherheits-Risikobeurteilung (Durchführung)",
"8.3": "Informationssicherheits-Risikobehandlung (Durchführung)",
"9.1": "Überwachung, Messung, Analyse und Bewertung",
"9.2": "Internes Audit",
"9.3": "Managementbewertung",
"10.1": "Fortlaufende Verbesserung",
"10.2": "Nichtkonformität und Korrekturmaßnahmen",
"A.5.1": "Informationssicherheitsrichtlinien",
"A.5.2": "Rollen und Verantwortlichkeiten",
"A.5.3": "Aufgabentrennung",
"A.5.4": "Verantwortung der Leitung",
"A.5.5": "Kontakt mit Behörden",
"A.5.6": "Kontakt mit speziellen Interessengruppen",
"A.5.7": "Bedrohungsinformationen",
"A.5.8": "Informationssicherheit im Projektmanagement",
"A.5.9": "Inventar der Informationen und Assets",
"A.5.10": "Zulässige Nutzung von Informationen und Assets",
"A.5.11": "Rückgabe von Assets",
"A.5.12": "Klassifizierung von Informationen",
"A.5.13": "Kennzeichnung von Informationen",
"A.5.14": "Informationsübertragung",
"A.5.15": "Zugangssteuerung",
"A.5.16": "Identitätsmanagement",
"A.5.17": "Authentisierungsinformationen",
"A.5.18": "Zugriffsrechte",
"A.5.19": "Informationssicherheit in Lieferantenbeziehungen",
"A.5.20": "Informationssicherheit in Lieferantenvereinbarungen",
"A.5.21": "Informationssicherheit in der IKT-Lieferkette",
"A.5.22": "Überwachung und Änderung von Lieferantenleistungen",
"A.5.23": "Informationssicherheit bei Cloud-Diensten",
"A.5.24": "Incident-Management: Planung und Vorbereitung",
"A.5.25": "Bewertung und Entscheidung zu Ereignissen",
"A.5.26": "Reaktion auf Informationssicherheitsvorfälle",
"A.5.27": "Lernen aus Informationssicherheitsvorfällen",
"A.5.28": "Sammeln von Beweismaterial",
"A.5.29": "Informationssicherheit während einer Störung",
"A.5.30": "IKT-Bereitschaft für Business Continuity",
"A.5.31": "Rechtliche und vertragliche Anforderungen",
"A.5.32": "Geistige Eigentumsrechte",
"A.5.33": "Schutz von Aufzeichnungen",
"A.5.34": "Datenschutz und Schutz von PII",
"A.5.35": "Unabhängige Überprüfung der Informationssicherheit",
"A.5.36": "Einhaltung von Richtlinien und Standards",
"A.5.37": "Dokumentierte Betriebsabläufe",
"A.6.1": "Überprüfung (Screening)",
"A.6.2": "Arbeitsvertragliche Regelungen",
"A.6.3": "Awareness, Ausbildung und Schulung",
"A.6.4": "Disziplinarverfahren",
"A.6.5": "Verantwortlichkeiten nach Beendigung oder Wechsel",
"A.6.6": "Vertraulichkeitsvereinbarungen",
"A.6.7": "Remote-Arbeit",
"A.6.8": "Meldung von Informationssicherheitsereignissen",
"A.7.1": "Physische Sicherheitsperimeter",
"A.7.2": "Physische Zutrittssteuerung",
"A.7.3": "Sicherung von Büros, Räumen und Einrichtungen",
"A.7.4": "Physische Überwachung",
"A.7.5": "Schutz vor physischen und umweltbedingten Bedrohungen",
"A.7.6": "Arbeiten in Sicherheitsbereichen",
"A.7.7": "Aufgeräumter Arbeitsplatz und Bildschirm",
"A.7.8": "Platzierung und Schutz von Betriebsmitteln",
"A.7.9": "Sicherheit von Assets außerhalb des Geländes",
"A.7.10": "Speichermedien",
"A.7.11": "Versorgungseinrichtungen",
"A.7.12": "Verkabelungssicherheit",
"A.7.13": "Wartung von Betriebsmitteln",
"A.7.14": "Sichere Entsorgung oder Wiederverwendung",
"A.8.1": "Endgeräte der Benutzer",
"A.8.2": "Privilegierte Zugriffsrechte",
"A.8.3": "Einschränkung des Informationszugriffs",
"A.8.4": "Zugriff auf Quellcode",
"A.8.5": "Sichere Authentisierung",
"A.8.6": "Kapazitätssteuerung",
"A.8.7": "Schutz vor Malware",
"A.8.8": "Management technischer Schwachstellen",
"A.8.9": "Konfigurationsmanagement",
"A.8.10": "Löschung von Informationen",
"A.8.11": "Datenmaskierung",
"A.8.12": "Verhinderung von Datenlecks",
"A.8.13": "Informationssicherung (Backup)",
"A.8.14": "Redundanz der Verarbeitungseinrichtungen",
"A.8.15": "Protokollierung (Logging)",
"A.8.16": "Überwachungsaktivitäten",
"A.8.17": "Uhrzeitsynchronisation",
"A.8.18": "Nutzung privilegierter Hilfsprogramme",
"A.8.19": "Softwareinstallation auf Produktivsystemen",
"A.8.20": "Netzwerksicherheit",
"A.8.21": "Sicherheit von Netzwerkdiensten",
"A.8.22": "Netzwerksegmentierung",
"A.8.23": "Web-Filterung",
"A.8.24": "Nutzung von Kryptographie",
"A.8.25": "Sicherer Entwicklungslebenszyklus",
"A.8.26": "Sicherheitsanforderungen an Anwendungen",
"A.8.27": "Sichere Systemarchitektur und -prinzipien",
"A.8.28": "Sichere Programmierung",
"A.8.29": "Sicherheitstests in Entwicklung und Abnahme",
"A.8.30": "Ausgelagerte Entwicklung",
"A.8.31": "Trennung von Entwicklungs-, Test- und Produktionsumgebungen",
"A.8.32": "Änderungsmanagement",
"A.8.33": "Testinformationen",
"A.8.34": "Schutz von Systemen bei Audit-Tests",
};
/** Titel einer ISO/IEC-27001-Anforderung; Fallback: „ISO <ref>“. */
export function controlTitleIso(ref: string): string {
return CONTROL_TITLES_ISO[ref] ?? `ISO ${ref}`;
}
-79
View File
@@ -1,79 +0,0 @@
/**
* VDA-ISA-2027 Control-Titel (aus dem UI-Mockup übernommen) für die
* Referenz-/Coverage-Matrix. Fallback: „ISA <ref>“ wenn nicht enthalten.
*
* Die ISO-27001-Titel liegen bewusst in einer eigenen Map (`control-titles-iso.ts`,
* generiert): die Schlüssel 6.1.1–6.1.3 sind in beiden Katalogen belegt
* (VDA ISA: Lieferanten — ISO: Risikobeurteilung/-behandlung). Über `framework`
* wird der richtige Katalog gewählt; ohne Angabe bleibt es beim bisherigen Verhalten.
*/
import { CONTROL_TITLES_ISO } from "./control-titles-iso";
export const CONTROL_TITLES: Record<string, string> = {
"1.2.1": "Steuerung der Informationssicherheit",
"1.2.2": "Organisation der Verantwortlichkeiten",
"1.2.3": "Informationssicherheit in Projekten",
"1.3.1": "Identifikation von Assets",
"1.3.2": "Klassifizierung",
"1.3.3": "Zugelassene Hardware",
"1.3.4": "Zugelassene Software",
"1.4.1": "Risikomanagement",
"1.5.1": "Compliance-Prüfung im IS-Betrieb",
"1.5.2": "Unabhängige Überprüfung",
"1.6.1": "Meldung von Ereignissen",
"1.6.2": "Behandlung von Sicherheitsereignissen",
"1.6.3": "Krisenmanagement",
"2.1.1": "Qualifikation für sensible Tätigkeiten",
"2.1.2": "Vertragliche Verpflichtung",
"2.1.3": "Sensibilisierung und Schulung",
"2.1.4": "Mobiles Arbeiten",
"3.1.1": "Sicherheitszonen und Zutritt",
"3.1.3": "Umgang mit unterstützenden Betriebsmitteln",
"3.1.4": "Mobile Geräte und Datenträger",
"4.1.1": "Identifikationsmittel",
"4.1.2": "Sichere Anmeldung",
"4.1.3": "Konten und Anmeldeinformationen",
"4.2.1": "Zugriffsrechte",
"5.1.1": "Einsatz kryptografischer Verfahren",
"5.1.2": "Schutz bei Uebertragung",
"5.2.1": "Change-Management",
"5.2.2": "Trennung der Umgebungen",
"5.2.3": "Schutz vor Malware",
"5.2.4": "Protokollierung und Auswertung",
"5.2.5": "Schwachstellen- und Patch-Management",
"5.2.6": "Technische Überprüfung",
"5.2.7": "Netzwerksicherheit",
"5.2.8": "Kontinuitätsplanung IT",
"5.2.9": "Backup und Wiederherstellung",
"5.3.1": "Sicherheit bei Beschaffung/Entwicklung",
"5.3.2": "Anforderungen an Netzdienste",
"5.3.3": "Rückgabe und sichere Löschung",
"5.3.4": "Externe/geteilte IT-Dienste und Cloud",
"6.1.1": "Informationssicherheit bei Lieferanten",
"6.1.2": "Vertraulichkeit",
"6.1.3": "Abgrenzung der Verantwortlichkeiten",
"7.1.1": "Regulatorische und vertragliche Compliance",
"7.1.2": "Schutz personenbezogener Daten",
};
export function controlTitle(ref: string, framework?: "ISO_27001" | "TISAX"): string {
if (framework === "ISO_27001") return CONTROL_TITLES_ISO[ref] ?? `ISO ${ref}`;
return CONTROL_TITLES[ref] ?? `ISA ${ref}`;
}
/**
* Numerischer Vergleich gepunkteter Control-Refs (1.2.10 nach 1.2.2).
* ISO-Anhang-A-Refs („A.5.15“) werden hinter die Klausel-Refs („9.1“) sortiert;
* für rein numerische Refs bleibt die Reihenfolge unverändert.
*/
const refParts = (r: string): number[] =>
r.startsWith("A.")
? [1, ...r.slice(2).split(".").map(Number)]
: [0, ...r.split(".").map(Number)];
export function compareControl(a: string, b: string): number {
const pa = refParts(a), pb = refParts(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
}
return 0;
}
-145
View File
@@ -1,145 +0,0 @@
// VDA-ISA-Katalog-Export (Story B7-2, Fachcontent C9 §3/§4). Reine, testbare Logik:
// Sortierung je Prüfziel (IS → Prototyp → Datenschutz), CSV-Serialisierung (Excel-
// öffenbar, UTF-8-BOM) und Reifegrad-Kennzahlen. Die DB-Anbindung (ControlRow aus A7,
// Gaps aus A8) erfolgt im Route-Handler; hier nur die aufbereiteten Export-Zeilen.
export type Pruefziel = "informationssicherheit" | "prototypenschutz" | "datenschutz";
/** Kapitel-/Control-Präfix → Prüfziel (8.x Prototyp, 9.x Datenschutz, sonst IS). */
export function pruefzielOfControl(control: string): Pruefziel {
if (control.startsWith("8.")) return "prototypenschutz";
if (control.startsWith("9.")) return "datenschutz";
return "informationssicherheit";
}
export const PRUEFZIEL_LABEL: Record<Pruefziel, string> = {
informationssicherheit: "Informationssicherheit",
prototypenschutz: "Prototypenschutz",
datenschutz: "Datenschutz",
};
export interface ExportControl {
control: string;
frage: string;
/** Bestätigter Reifegrad 0–3 oder null (= „na"/unbestätigt). */
reifegrad: number | null;
bestaetigt: boolean;
umsetzung: string;
belege: string;
offenePunkte: number;
pruefziel: Pruefziel;
}
const PZ_ORDER: Record<Pruefziel, number> = { informationssicherheit: 0, prototypenschutz: 1, datenschutz: 2 };
/** Sortierung C9 §3: Prüfziel-Reihenfolge, dann Control numerisch. */
export function sortForExport(rows: ExportControl[]): ExportControl[] {
return [...rows].sort(
(a, b) => PZ_ORDER[a.pruefziel] - PZ_ORDER[b.pruefziel] || a.control.localeCompare(b.control, undefined, { numeric: true }),
);
}
const cell = (v: string | number | null): string => {
const s = v === null ? "na" : String(v);
return /[";\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
/** CSV der VDA-ISA-Katalogsicht (C9 §3). Semikolon-getrennt (DE-Excel), UTF-8-BOM. */
export function toCatalogCsv(rows: ExportControl[]): string {
const header = ["Control-ID", "Kontrollfrage/Ziel", "Reifegrad", "Status", "Umsetzung", "Belege", "Offene Punkte", "Prüfziel"];
const lines = [header.join(";")];
for (const r of sortForExport(rows)) {
lines.push([
cell(r.control), cell(r.frage), cell(r.reifegrad), cell(r.bestaetigt ? "bestätigt" : "unbestätigt"),
cell(r.umsetzung), cell(r.belege), cell(r.offenePunkte), cell(PRUEFZIEL_LABEL[r.pruefziel]),
].join(";"));
}
return "" + lines.join("\r\n");
}
// ── ABGABE-Sicht (VDA-ISA-Prüfungsdokumentation) ──────────────────────────────
// Anders als die Katalogsicht (Reifegrad-Überblick) gibt die ABGABE je Control den
// zusammengesetzten „Beschreibung der Umsetzung"-Text (übernommene
// ControlDescription je Anforderung), den bestätigten Reifegrad (ControlAssessment)
// und die Referenz-Dokumentation (Dokumentverweise + Nachweise) aus.
export interface AbgabeControl {
control: string;
frage: string;
pruefziel: Pruefziel;
/** Bestätigter Reifegrad (ControlAssessment.confirmedValue) oder null. */
reifegrad: number | null;
/** Zielreifegrad (ControlAssessment.target) oder null. */
zielReifegrad: number | null;
/** Zusammengesetzte Umsetzungsbeschreibung im ABGABE-Stil (je Anforderung „+ …"). */
umsetzung: string;
/** Referenz Dokumentation: Dokumentverweise + Nachweise, semikolon-getrennt. */
referenzen: string;
}
/**
* Setzt die Umsetzungsbeschreibung eines Controls im ABGABE-Stil zusammen:
* pro (übernommener) Anforderung „+ <Anforderung>" gefolgt vom Umsetzungssatz
* inkl. Dokumentverweis. Anforderungen ohne übernommenen Text entfallen.
*/
export function composeAbgabeText(items: { requirement: string; text: string | null }[]): string {
return items
.filter((i) => i.text)
.map((i) => `+ ${i.requirement}\n${i.text}`)
.join("\n\n");
}
/** Sortierung der ABGABE-Zeilen (C9 §3): Prüfziel IS → Prototyp → Datenschutz, dann Control numerisch. */
export function sortAbgabeRows(rows: AbgabeControl[]): AbgabeControl[] {
return [...rows].sort(
(a, b) => PZ_ORDER[a.pruefziel] - PZ_ORDER[b.pruefziel] || a.control.localeCompare(b.control, undefined, { numeric: true }),
);
}
/** CSV der ABGABE-Sicht (Prüfungsdokumentation). Semikolon-getrennt (DE-Excel), UTF-8-BOM. */
export function toAbgabeCsv(rows: AbgabeControl[]): string {
const ordered = sortAbgabeRows(rows);
const header = ["Control-ID", "Kontrollfrage/Ziel", "Reifegrad", "Zielreifegrad", "Beschreibung der Umsetzung", "Referenz Dokumentation", "Prüfziel"];
const lines = [header.join(";")];
for (const r of ordered) {
lines.push([
cell(r.control), cell(r.frage), cell(r.reifegrad), cell(r.zielReifegrad),
cell(r.umsetzung), cell(r.referenzen), cell(PRUEFZIEL_LABEL[r.pruefziel]),
].join(";"));
}
return "" + lines.join("\r\n");
}
export interface PruefzielKpi {
pruefziel: Pruefziel;
controls: number;
bestaetigt: number;
/** Ø bestätigter Reifegrad („unbestätigt zählt nicht als erfüllt" → 0). */
avg: number | null;
}
export interface ReifegradKennzahlen {
total: number;
bestaetigt: number;
bestaetigtAnteil: number; // 0..1
gesamtAvg: number | null; // unbestätigt = 0
jePruefziel: PruefzielKpi[];
}
/** Reifegrad-Kennzahlen (C9 §1/§3): Ø gesamt + je Prüfziel, „unbestätigt zählt als 0". */
export function kennzahlen(rows: ExportControl[]): ReifegradKennzahlen {
const avgOf = (rs: ExportControl[]): number | null =>
rs.length === 0 ? null : rs.reduce((s, r) => s + (r.bestaetigt && r.reifegrad !== null ? r.reifegrad : 0), 0) / rs.length;
const byPz = (["informationssicherheit", "prototypenschutz", "datenschutz"] as Pruefziel[])
.map((pz) => rows.filter((r) => r.pruefziel === pz))
.filter((rs) => rs.length > 0)
.map<PruefzielKpi>((rs) => ({ pruefziel: rs[0].pruefziel, controls: rs.length, bestaetigt: rs.filter((r) => r.bestaetigt).length, avg: avgOf(rs) }));
const bestaetigt = rows.filter((r) => r.bestaetigt).length;
return {
total: rows.length,
bestaetigt,
bestaetigtAnteil: rows.length === 0 ? 0 : bestaetigt / rows.length,
gesamtAvg: avgOf(rows),
jePruefziel: byPz,
};
}
-114
View File
@@ -1,114 +0,0 @@
/**
* Funktionstrennungs-Regeln FT-01…FT-06 (Story A4-2, Fachcontent C7 §2). Reine,
* seiteneffektfreie Prüfung des Soll-Rollenmodells auf Konflikte/Lücken. Ergebnis:
* Hinweise mit Schweregrad; einige erzeugen eine Aufgabe.
*
* FT-01 (ISB = IT-Leitung) und FT-04 (ISB nicht benannt) sind bereits als Fragebogen-
* Trigger (B1: `isb_equals_it`, `isb_not_named`) verdrahtet — die zugehörigen Findings
* verweisen darauf (reuse). FT-03/FT-05 erzeugen eigene Aufgaben; FT-02/FT-06 sind
* Hinweise ohne Aufgabe (FT-06 mangels IAM-Daten manuell zu prüfen).
*/
export type FtSeverity = "konflikt" | "schwach" | "luecke" | "info";
export interface FtFinding {
code: string; // FT-01 … FT-06
severity: FtSeverity;
title: string;
message: string;
control?: string;
/** Zu erzeugende Aufgabe (falls vorhanden). `reuse` = bereits über einen Fragebogen-Trigger abgedeckt. */
task?: { origin: string; title: string; control?: string; reuse: boolean };
}
export interface FtRoleInput {
roles: { ISB?: string | null; MANAGEMENT?: string | null; IT_LEAD?: string | null; HR_LEAD?: string | null; DPO?: string | null };
isbNamed: boolean; // Fragebogen Q-ROLE-02 (ISB benannt?)
isbIsIt: boolean; // Fragebogen (ISB zugleich IT-Verantwortung?)
isbInternal: boolean; // FLAG_ISB_INTERNAL
personalData: boolean; // FLAG_PERSONAL_DATA
}
/** Leerwert-/Platzhalter-Erkennung für Rollen-Variablen. */
function empty(v?: string | null): boolean {
const s = (v ?? "").trim().toLowerCase();
return s === "" || s === "n.n." || s === "—" || s === "-" || s === "tbd";
}
const sameName = (a?: string | null, b?: string | null) => !empty(a) && !empty(b) && a!.trim().toLowerCase() === b!.trim().toLowerCase();
export function evaluateFt(input: FtRoleInput): FtFinding[] {
const f: FtFinding[] = [];
const { roles } = input;
// FT-01: ISB = IT-Leitung (Selbstkontrolle) — Konflikt + Aufgabe (reuse B1-Trigger).
if (input.isbIsIt || sameName(roles.ISB, roles.IT_LEAD)) {
f.push({
code: "FT-01", severity: "konflikt", control: "1.2.2",
title: "ISB zugleich IT-Verantwortung",
message: "Der ISB muss die IT unabhängig überwachen können — Selbstkontrolle ist unzulässig. Funktionstrennung herstellen oder kompensierende Kontrolle dokumentieren.",
task: { origin: "isb_equals_it", title: "Funktionstrennung ISB/IT herstellen", control: "1.2.2", reuse: true },
});
}
// FT-02: ISB intern und (mutmaßlich) der IT-Leitung unterstellt — schwacher Hinweis.
if (input.isbNamed && input.isbInternal && !input.isbIsIt) {
f.push({
code: "FT-02", severity: "schwach",
title: "Weisungsunabhängigkeit des internen ISB sicherstellen",
message: "Der interne ISB darf nicht der IT-Leitung unterstellt sein; Berichtsweg direkt an die Leitung sicherstellen.",
});
}
// FT-03: ISB = Oberste Leitung — Konflikt + Aufgabe (neu).
if (sameName(roles.ISB, roles.MANAGEMENT)) {
f.push({
code: "FT-03", severity: "konflikt", control: "1.2.1",
title: "ISB zugleich oberste Leitung",
message: "Die Leitung kann ihre eigene ISMS-Verantwortung nicht selbst überwachen. Rolle trennen oder kompensierende Kontrolle dokumentieren.",
task: { origin: "ft-03", title: "Funktionstrennung ISB/Leitung herstellen", control: "1.2.1", reuse: false },
});
}
// FT-04: ISB nicht benannt — Lücke + Aufgabe (reuse B1-Trigger).
if (!input.isbNamed || empty(roles.ISB)) {
f.push({
code: "FT-04", severity: "luecke", control: "1.2.2",
title: "ISB nicht benannt",
message: "Ein:e Informationssicherheitsbeauftragte:r ist eine Muss-Anforderung. Bitte bestellen (siehe ISB-Bestellung).",
task: { origin: "isb_not_named", title: "ISB bestellen", control: "1.2.2", reuse: true },
});
}
// FT-05: DPO fehlt trotz personenbezogener Daten — Lücke + Aufgabe (neu).
if (input.personalData && empty(roles.DPO)) {
f.push({
code: "FT-05", severity: "luecke", control: "7.1.2",
title: "Datenschutzbeauftragte:r fehlt",
message: "Es werden personenbezogene Daten verarbeitet, aber kein DPO ist benannt. Benennung prüfen/vornehmen.",
task: { origin: "ft-05", title: "DSB benennen/prüfen", control: "7.1.2", reuse: false },
});
}
// FT-06: Antragsteller = Genehmiger von Zugriffsrechten (aus IAM-Daten) — mangels
// IAM-Anbindung manuell zu prüfen.
f.push({
code: "FT-06", severity: "info", control: "4.2.1",
title: "Vier-Augen bei Berechtigungsvergabe",
message: "Antragsteller und Genehmiger von Zugriffsrechten müssen getrennt sein (Control 4.2.1). Mangels IAM-Anbindung manuell zu prüfen.",
});
return f;
}
export const FT_SEVERITY_LABEL: Record<FtSeverity, string> = {
konflikt: "Konflikt",
schwach: "Hinweis",
luecke: "Lücke",
info: "Manuell prüfen",
};
export const FT_SEVERITY_TONE: Record<FtSeverity, "risk" | "warn" | "info" | "mut"> = {
konflikt: "risk",
luecke: "warn",
schwach: "info",
info: "mut",
};
-165
View File
@@ -1,165 +0,0 @@
/**
* Gap-Konsolidierung (Story A8) — reine, deterministische Logik nach Fachcontent C8.
* Führt die offenen Punkte aus Schritt 6 (Risiko) und Schritt 7 (Control-Assessment)
* zusammen, dedupliziert sie (C8 §2), priorisiert deterministisch (C8 §1),
* kennzeichnet Quick-Wins (C8 §3) und sortiert innerhalb gleicher Priorität nach
* betroffenen Controls, Risikohöhe und Aufwand.
*/
export type GapPriority = "hoch" | "mittel" | "niedrig";
export type GapEffort = "gering" | "mittel" | "hoch";
export type GapSource = "control" | "risk" | "role";
/** Roh-Offenpunkt aus einer Quelle (Control-Gap, Risiko oberhalb Akzeptanzlinie, Rolle/FT). */
export interface RawGap {
/** Dedup-Schlüssel (C8 §2): (Control+Teilanforderung) bzw. (verknüpfte Maßnahme/Aufgabe). */
id: string;
source: GapSource;
title: string;
/** Empfohlene Maßnahme (Aufgabentitel). */
action: string;
/** Betroffene Controls (für Sortierung + Bezüge). */
controls: string[];
/** Höchster in-Scope-Anforderungstyp des Controls (MUSS/SOLL/HOCH/SEHR HOCH). */
requirementType?: "MUSS" | "SOLL" | "HOCH" | "SEHR HOCH";
/** Art des Control-Gaps (nachweis/richtlinie/verfahren/verknuepfung/reifegradluecke/widerspruch). */
kind?: string;
/** Ist-Reifegrad (Control). */
maturity?: number;
/** Zielreifegrad (Control). */
target?: number;
/** Effektiver Risikowert (residual ?? brutto) bei Risiko-Quellen. */
riskScore?: number;
/** Risiko oberhalb der Akzeptanzlinie (Wert > 9, nicht akzeptiert). */
aboveAcceptance?: boolean;
/** Funktionstrennungs-Konflikt / fehlende Muss-Rolle (C7 FT-01/03/04). */
ftConflict?: boolean;
/** Aufwandsschätzung für Quick-Win-Erkennung. */
effort: GapEffort;
/** Reifegrad-Wirkung beim Schließen (≥ +1 auf mind. ein Control ⇒ Quick-Win-fähig). */
maturityImpact: number;
/** Abhängigkeit von anderer offener Maßnahme (schließt Quick-Win aus). */
hasDependency: boolean;
/** Kanonische Task-Herkunft (für Abgleich/Erzeugung). */
taskOrigin: string;
/** ID einer bereits existierenden Aufgabe (Abgleich B1), sonst null. */
taskId?: string | null;
}
export interface GapItem extends RawGap {
priority: GapPriority;
quickWin: boolean;
/** Beteiligte Quellen nach Merge (z. B. ["risk","control"]). */
sources: GapSource[];
/** Anzahl betroffener Controls (Sortierkriterium 1). */
affectedControls: number;
}
const PRIORITY_RANK: Record<GapPriority, number> = { hoch: 3, mittel: 2, niedrig: 1 };
const EFFORT_RANK: Record<GapEffort, number> = { gering: 0, mittel: 1, hoch: 2 };
export const PRIORITY_LABEL: Record<GapPriority, string> = { hoch: "Hoch", mittel: "Mittel", niedrig: "Niedrig" };
export const PRIORITY_TONE: Record<GapPriority, "risk" | "warn" | "mut"> = { hoch: "risk", mittel: "warn", niedrig: "mut" };
/** Deterministische Priorität eines Offenpunkts (C8 §1, höchste zutreffende Regel gewinnt). */
export function gapPriority(g: RawGap): GapPriority {
const mat = g.maturity ?? 0;
// Hoch
if (g.aboveAcceptance) return "hoch"; // Behandlungsmaßnahme zu Risiko oberhalb der Akzeptanzlinie
if (g.ftConflict) return "hoch"; // fehlende Muss-Rolle / Funktionstrennungs-Konflikt (C7)
if (g.requirementType === "MUSS" && mat < 2) return "hoch"; // offene MUSS-Anforderung, Reifegrad < 2
if (g.requirementType === "HOCH" || g.requirementType === "SEHR HOCH") return "hoch"; // offene HOCH/SEHR-HOCH-Zusatzanforderung
// Mittel
if (g.requirementType === "SOLL") return "mittel"; // offene SOLL-Anforderung
if (mat === 2 && (g.target ?? 3) >= 3) return "mittel"; // Reifegrad 2, aber unter Zielreifegrad 3
if (g.kind === "richtlinie" || g.kind === "verfahren" || g.kind === "nachweis" || g.kind === "verknuepfung") return "mittel";
if (g.kind === "widerspruch") return "hoch"; // Nachweis widerspricht Richtlinie → gedeckelt, dringend
// Niedrig: redaktionell/Optimierung ohne Reifegrad-Wirkung
return "niedrig";
}
/** Quick-Win (C8 §3): geringer Aufwand + Reifegrad-Wirkung ≥ +1 + keine Abhängigkeit. */
export function isQuickWin(g: RawGap): boolean {
return g.effort === "gering" && g.maturityImpact >= 1 && !g.hasDependency;
}
function mergeInto(base: GapItem, extra: RawGap): void {
base.controls = [...new Set([...base.controls, ...extra.controls])];
base.affectedControls = base.controls.length;
if (!base.sources.includes(extra.source)) base.sources.push(extra.source);
// Höchste Priorität gewinnt.
const p = gapPriority(extra);
if (PRIORITY_RANK[p] > PRIORITY_RANK[base.priority]) base.priority = p;
// Bereits existierende Aufgabe bevorzugen (keine neue erzeugen).
if (!base.taskId && extra.taskId) base.taskId = extra.taskId;
// Höhere Risikohöhe / Reifegrad-Wirkung übernehmen (Sortierung).
if ((extra.riskScore ?? 0) > (base.riskScore ?? 0)) base.riskScore = extra.riskScore;
base.maturityImpact = Math.max(base.maturityImpact, extra.maturityImpact);
}
/**
* Konsolidiert Roh-Offenpunkte: (1) exakte Dedup über den Dedup-Schlüssel, (2) quellen-
* übergreifender Merge — ein Risiko-Offenpunkt absorbiert Control-Gaps derselben
* Controls (dieselbe Maßnahme, C8 §2), (3) Sortierung nach Priorität, betroffenen
* Controls, Risikohöhe und Aufwand (Quick-Wins zuerst).
*/
export function consolidate(raws: RawGap[]): GapItem[] {
// (1) Exakte Deduplizierung.
const byId = new Map<string, GapItem>();
for (const r of raws) {
const existing = byId.get(r.id);
if (!existing) {
byId.set(r.id, { ...r, priority: gapPriority(r), quickWin: isQuickWin(r), sources: [r.source], affectedControls: r.controls.length });
} else {
mergeInto(existing, r);
existing.quickWin = existing.quickWin && isQuickWin(r);
}
}
// (2) Quellenübergreifender Merge: Risiko-Offenpunkt zieht Control-Gaps gleicher Controls an.
const items = [...byId.values()];
const riskItems = items.filter((i) => i.source === "risk");
const absorbed = new Set<string>();
for (const risk of riskItems) {
for (const item of items) {
if (item === risk || item.source !== "control" || absorbed.has(item.id)) continue;
if (item.controls.some((c) => risk.controls.includes(c))) {
mergeInto(risk, item);
absorbed.add(item.id);
}
}
}
const merged = items.filter((i) => !absorbed.has(i.id));
// (3) Sortierung (C8 §1 Zusatzgewichtung).
merged.sort((a, b) =>
PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority] ||
b.affectedControls - a.affectedControls ||
(b.riskScore ?? 0) - (a.riskScore ?? 0) ||
EFFORT_RANK[a.effort] - EFFORT_RANK[b.effort] ||
a.id.localeCompare(b.id)
);
return merged;
}
export interface GapSummary {
total: number;
hoch: number;
mittel: number;
niedrig: number;
quickWins: number;
withTask: number;
withoutTask: number;
}
export function summarize(items: GapItem[]): GapSummary {
return {
total: items.length,
hoch: items.filter((i) => i.priority === "hoch").length,
mittel: items.filter((i) => i.priority === "mittel").length,
niedrig: items.filter((i) => i.priority === "niedrig").length,
quickWins: items.filter((i) => i.quickWin).length,
withTask: items.filter((i) => i.taskId).length,
withoutTask: items.filter((i) => !i.taskId).length,
};
}
-278
View File
@@ -1,278 +0,0 @@
/**
* Fristen-/Timer-Logik für das Modul „Vorfälle" (Fachkonzept §6) — IM-B.
*
* Rein & client-safe (keine Server-/DB-Importe): von Server-Actions, Reminder-Job,
* UI und Test gemeinsam genutzt. Alle Fristen leiten sich aus dem
* **Kenntniszeitpunkt** ab (detectedAt bzw. reportedAt).
*
* Regeln (§6):
* - NIS2 Erstmeldung +24 h · Folgemeldung +72 h · Abschlussbericht +1 Monat.
* NIS2-Timer greifen NUR, wenn die NIS2-Kategorie des Mandanten ∈
* {wichtig, wesentlich} UND der Vorfall als nis2Relevant markiert ist.
* - DSGVO (Art. 33) +72 h bei Personenbezug (dsgvoRelevant / personalData).
* - Interne SLA je Severity: Reaktions-/Behebungsfrist (aus der Default-Matrix
* abgeleitet, hier fixer Default — mandantenkonfigurierbar wäre Folge-Story).
*/
import type { IncidentSeverity } from "@/lib/incident-severity";
/** NIS2-Kategorien, die die Meldefristen-Timer scharfschalten (§6/§14.1). */
export const NIS2_MELDEPFLICHTIG = ["wichtig", "wesentlich"] as const;
export const NIS2_ERSTMELDUNG_HOURS = 24;
export const NIS2_FOLGEMELDUNG_HOURS = 72;
export const NIS2_ABSCHLUSS_MONTHS = 1;
export const DSGVO_HOURS = 72;
/** Reihenfolge des Meldung-Tracks (reportStatus, §3 Parallel-Track). */
export const REPORT_STATUS_ORDER = {
none: 0,
pruefung: 1,
erstmeldung: 2,
folgemeldung: 3,
abschluss: 4,
} as const;
export type ReportStatus = keyof typeof REPORT_STATUS_ORDER;
export const REPORT_STATUSES = Object.keys(REPORT_STATUS_ORDER) as ReportStatus[];
/** Linearer Vorwärts-Übergang im Meldung-Track (null = Endzustand). */
export function nextReportStatus(current: ReportStatus): ReportStatus | null {
const order = REPORT_STATUS_ORDER[current];
const next = REPORT_STATUSES.find((s) => REPORT_STATUS_ORDER[s] === order + 1);
return next ?? null;
}
// ── Zeit-Helfer (rein) ───────────────────────────────────────────────────────
export function addHours(d: Date, hours: number): Date {
return new Date(d.getTime() + hours * 3_600_000);
}
/** Kalender-Monat addieren (nicht 30 Tage) — für den NIS2-Abschlussbericht. */
export function addMonths(d: Date, months: number): Date {
const r = new Date(d.getTime());
const day = r.getDate();
r.setMonth(r.getMonth() + months);
// Überlauf am Monatsende (z. B. 31. → Folgemonat) auf den letzten Tag ziehen.
if (r.getDate() < day) r.setDate(0);
return r;
}
/**
* Kenntniszeitpunkt eines Vorfalls: Entdeckung (detectedAt) hat Vorrang, sonst
* die interne Meldung (reportedAt), ersatzweise Eintritt/Anlage. Ab hier laufen
* alle Meldefristen.
*/
export function knowledgeTime(fields: {
detectedAt?: Date | null;
reportedAt?: Date | null;
occurredAt?: Date | null;
createdAt?: Date | null;
}): Date | null {
return fields.detectedAt ?? fields.reportedAt ?? fields.occurredAt ?? fields.createdAt ?? null;
}
/** Ob die NIS2-Timer für diesen Vorfall greifen (Kategorie × Relevanz). */
export function nis2TimersApply(nis2Category: string, nis2Relevant: boolean): boolean {
return (
nis2Relevant && (NIS2_MELDEPFLICHTIG as readonly string[]).includes(nis2Category)
);
}
/** Ob überhaupt eine Meldepflicht (NIS2 oder DSGVO) besteht. */
export function isReportable(input: {
nis2Category: string;
nis2Relevant: boolean;
dsgvoRelevant: boolean;
}): boolean {
return nis2TimersApply(input.nis2Category, input.nis2Relevant) || input.dsgvoRelevant;
}
// ── Meldefristen ─────────────────────────────────────────────────────────────
export type DeadlineInput = {
nis2Category: string;
nis2Relevant: boolean;
/** = personalData (Personenbezug → DSGVO Art. 33). */
dsgvoRelevant: boolean;
knownAt: Date | null;
};
export type IncidentDeadlines = {
erstmeldungDueAt: Date | null;
folgemeldungDueAt: Date | null;
abschlussDueAt: Date | null;
dsgvoDueAt: Date | null;
};
/**
* Berechnet die Meldefristen aus dem Kenntniszeitpunkt. Nicht anwendbare Fristen
* werden auf `null` gesetzt (z. B. keine NIS2-Betroffenheit → keine NIS2-Timer).
*/
export function computeDeadlines(input: DeadlineInput): IncidentDeadlines {
const base = input.knownAt;
const nis2 = base && nis2TimersApply(input.nis2Category, input.nis2Relevant);
const dsgvo = base && input.dsgvoRelevant;
return {
erstmeldungDueAt: nis2 ? addHours(base, NIS2_ERSTMELDUNG_HOURS) : null,
folgemeldungDueAt: nis2 ? addHours(base, NIS2_FOLGEMELDUNG_HOURS) : null,
abschlussDueAt: nis2 ? addMonths(base, NIS2_ABSCHLUSS_MONTHS) : null,
dsgvoDueAt: dsgvo ? addHours(base, DSGVO_HOURS) : null,
};
}
// ── Interne SLA je Severity (Reaktions-/Behebungsfrist) ─────────────────────
export type Sla = { reactionHours: number; resolutionHours: number };
/**
* Default-SLA-Matrix je Schweregrad (§5 → §6). Reaktion = Zeit bis zur ersten
* Bearbeitung, Behebung = Zeit bis zur Eindämmung/Lösung. Fixer Default; die
* mandantenspezifische Konfiguration wäre eine Folge-Story.
*/
export const SLA_BY_SEVERITY: Record<IncidentSeverity, Sla> = {
niedrig: { reactionHours: 48, resolutionHours: 720 }, // 30 Tage
mittel: { reactionHours: 24, resolutionHours: 336 }, // 14 Tage
hoch: { reactionHours: 4, resolutionHours: 72 }, // 3 Tage
kritisch: { reactionHours: 1, resolutionHours: 24 },
};
export function slaForSeverity(severity: IncidentSeverity): Sla {
return SLA_BY_SEVERITY[severity] ?? SLA_BY_SEVERITY.mittel;
}
export function computeSla(
severity: IncidentSeverity,
knownAt: Date | null,
): { reactionDueAt: Date | null; resolutionDueAt: Date | null } {
const sla = slaForSeverity(severity);
return {
reactionDueAt: knownAt ? addHours(knownAt, sla.reactionHours) : null,
resolutionDueAt: knownAt ? addHours(knownAt, sla.resolutionHours) : null,
};
}
// ── Überfällig-Erkennung / verbleibende Zeit ────────────────────────────────
export function remainingMs(due: Date | null, now: Date = new Date()): number | null {
if (!due) return null;
return due.getTime() - now.getTime();
}
export function isOverdue(due: Date | null, now: Date = new Date()): boolean {
if (!due) return false;
return due.getTime() < now.getTime();
}
/** Ob eine Frist „bald fällig" ist (Standard: innerhalb der nächsten 24 h). */
export function isDueSoon(
due: Date | null,
now: Date = new Date(),
withinHours = 24,
): boolean {
const ms = remainingMs(due, now);
if (ms === null) return false;
return ms >= 0 && ms <= withinHours * 3_600_000;
}
// ── Deadline-Items (UI / Dashboard / Reminder) ──────────────────────────────
export type DeadlineKind =
| "erstmeldung"
| "folgemeldung"
| "abschluss"
| "dsgvo"
| "reaction"
| "resolution";
export type DeadlineItem = {
kind: DeadlineKind;
dueAt: Date;
/** Track schon abgeschlossen? (nur für die NIS2-Meldestufen relevant) */
done: boolean;
overdue: boolean;
dueSoon: boolean;
remainingMs: number;
};
/**
* Ist eine NIS2-Meldestufe bereits eingereicht (reportStatus fortgeschritten)?
* Erstmeldung gilt als erledigt, sobald reportStatus ≥ erstmeldung usw.
*/
export function stageDone(reportStatus: string, kind: DeadlineKind): boolean {
const order = REPORT_STATUS_ORDER[reportStatus as ReportStatus] ?? 0;
switch (kind) {
case "erstmeldung":
return order >= REPORT_STATUS_ORDER.erstmeldung;
case "folgemeldung":
return order >= REPORT_STATUS_ORDER.folgemeldung;
case "abschluss":
return order >= REPORT_STATUS_ORDER.abschluss;
default:
return false;
}
}
export type DeadlineSource = {
severity: string;
reportStatus: string;
detectedAt?: Date | null;
reportedAt?: Date | null;
occurredAt?: Date | null;
createdAt?: Date | null;
erstmeldungDueAt?: Date | null;
folgemeldungDueAt?: Date | null;
abschlussDueAt?: Date | null;
dsgvoDueAt?: Date | null;
};
/**
* Baut die vollständige Frist-Liste eines Vorfalls für die Anzeige. Enthält die
* gespeicherten Meldefristen plus die (abgeleiteten) internen SLA-Fristen. Bereits
* eingereichte NIS2-Stufen sind mit `done=true` markiert, aber weiter sichtbar.
*/
export function deadlineItems(inc: DeadlineSource, now: Date = new Date()): DeadlineItem[] {
const known = knowledgeTime(inc);
const sla = computeSla(inc.severity as IncidentSeverity, known);
const raw: { kind: DeadlineKind; dueAt: Date | null }[] = [
{ kind: "erstmeldung", dueAt: inc.erstmeldungDueAt ?? null },
{ kind: "folgemeldung", dueAt: inc.folgemeldungDueAt ?? null },
{ kind: "abschluss", dueAt: inc.abschlussDueAt ?? null },
{ kind: "dsgvo", dueAt: inc.dsgvoDueAt ?? null },
{ kind: "reaction", dueAt: sla.reactionDueAt },
{ kind: "resolution", dueAt: sla.resolutionDueAt },
];
return raw
.filter((r): r is { kind: DeadlineKind; dueAt: Date } => r.dueAt !== null)
.map((r) => {
const done = stageDone(inc.reportStatus, r.kind);
return {
kind: r.kind,
dueAt: r.dueAt,
done,
overdue: !done && isOverdue(r.dueAt, now),
dueSoon: !done && isDueSoon(r.dueAt, now),
remainingMs: r.dueAt.getTime() - now.getTime(),
};
});
}
/**
* Offene Meldefristen (nur NIS2-Meldestufen + DSGVO), die noch nicht eingereicht
* sind — Grundlage für Reminder/Eskalation. Interne SLA-Fristen bleiben außen vor
* (die adressiert das Aufgaben-Modul), es geht um die behördlichen Meldepflichten.
*/
export function openReportDeadlines(inc: DeadlineSource, now: Date = new Date()): DeadlineItem[] {
return deadlineItems(inc, now).filter(
(d) =>
!d.done &&
(d.kind === "erstmeldung" ||
d.kind === "folgemeldung" ||
d.kind === "abschluss" ||
d.kind === "dsgvo"),
);
}
-270
View File
@@ -1,270 +0,0 @@
/**
* Export-/Vorlage-Bausteine für das Modul „Vorfälle" (IM-C, §9/§72) — rein &
* client-safe (keine Server-/DB-Importe), damit Route-Handler, XLSX-Builder und
* Test dieselben Funktionen nutzen.
*
* - Vorfallregister als Zeilen + CSV (Excel-öffenbar).
* - NIS2- und DSGVO-Meldevorlagen: vorbefüllter, herunterladbarer Text aus den
* Vorfalldaten (Zeiten, Kategorie, Betroffenheit, Impact, Meldefristen) für die
* MANUELLE Übermittlung an die Behörde — KEINE Behörden-API.
*/
import type { IncidentCategory, IncidentStatus } from "@/lib/incident";
// Deutsche Anzeigetexte (das Produkt ist deutschsprachig; Exporte/Vorlagen laufen
// serverseitig ohne next-intl-Kontext, daher hier als feste Labels gespiegelt).
export const INCIDENT_CATEGORY_LABEL_DE: Record<IncidentCategory, string> = {
malware: "Schadsoftware",
phishing: "Phishing / Social Engineering",
unauthorized_access: "Unbefugter Zugriff",
data_loss: "Datenabfluss / -verlust",
outage: "Systemausfall / Verfügbarkeit",
physical: "Physisch (Zutritt / Diebstahl)",
misconfiguration: "Fehlbedienung / Konfiguration",
supplier: "Lieferant / Drittpartei",
prototype_customer_data: "Prototyp / Kundendaten",
other: "Sonstiges",
};
export const INCIDENT_STATUS_LABEL_DE: Record<IncidentStatus, string> = {
neu: "Neu",
triage: "Triage",
in_bearbeitung: "In Bearbeitung",
eingedaemmt: "Eingedämmt",
behoben: "Behoben",
abgeschlossen: "Abgeschlossen",
wiedereroeffnet: "Wiedereröffnet",
};
export const REPORT_STATUS_LABEL_DE: Record<string, string> = {
none: "keine Meldepflicht",
pruefung: "Meldepflicht geprüft",
erstmeldung: "Erstmeldung",
folgemeldung: "Folgemeldung",
abschluss: "Abschlussbericht",
};
export const INCIDENT_SOURCE_LABEL_DE: Record<string, string> = {
manual: "Intern (manuell)",
email: "E-Mail",
};
function catLabel(c: string): string {
return INCIDENT_CATEGORY_LABEL_DE[c as IncidentCategory] ?? c;
}
function statusLabel(s: string): string {
return INCIDENT_STATUS_LABEL_DE[s as IncidentStatus] ?? s;
}
/** Datum de-DE (Datum + Uhrzeit), leere Werte als "—". */
export function fmtDateTime(d: Date | string | null | undefined): string {
if (!d) return "—";
const date = typeof d === "string" ? new Date(d) : d;
if (Number.isNaN(date.getTime())) return "—";
return new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", timeStyle: "short" }).format(date);
}
const jn = (b: boolean) => (b ? "ja" : "nein");
// ── Vorfallregister (Liste) ──────────────────────────────────────────────────
/** Eingangsform einer Registerzeile — aus dem Prisma-Incident + Zählwerten. */
export type IncidentRegisterInput = {
refNo: string;
title: string;
category: string;
severity: string;
priority: string;
status: string;
source: string;
occurredAt?: Date | string | null;
detectedAt?: Date | string | null;
reportedAt?: Date | string | null;
ownerName?: string | null;
assigneeName?: string | null;
nis2Relevant: boolean;
dsgvoRelevant: boolean;
personalData: boolean;
prototypeData: boolean;
reportStatus: string;
erstmeldungDueAt?: Date | string | null;
dsgvoDueAt?: Date | string | null;
abschlussDueAt?: Date | string | null;
measureCount: number;
riskCount: number;
assetCount: number;
controlCount: number;
createdAt?: Date | string | null;
};
/** Spaltenüberschriften des Vorfallregisters (Reihenfolge = CSV-/XLSX-Spalten). */
export const INCIDENT_REGISTER_COLUMNS = [
"Kennung",
"Titel",
"Kategorie",
"Schweregrad",
"Priorität",
"Status",
"Quelle",
"Eingetreten",
"Entdeckt",
"Gemeldet (intern)",
"Verantwortlich",
"Bearbeiter",
"NIS2-relevant",
"DSGVO-relevant",
"Personenbezug",
"Meldestatus",
"Erstmeldung-Frist (24h)",
"DSGVO-Frist (72h)",
"Abschlussbericht-Frist",
"Maßnahmen",
"Risiken",
"Assets",
"Controls",
"Erstellt",
] as const;
/** Eine Registerzeile als geordnete Werteliste (parallel zu den Spalten). */
export function incidentRegisterRow(i: IncidentRegisterInput): string[] {
return [
i.refNo,
i.title,
catLabel(i.category),
i.severity,
i.priority,
statusLabel(i.status),
INCIDENT_SOURCE_LABEL_DE[i.source] ?? i.source,
fmtDateTime(i.occurredAt),
fmtDateTime(i.detectedAt),
fmtDateTime(i.reportedAt),
i.ownerName ?? "—",
i.assigneeName ?? "—",
jn(i.nis2Relevant),
jn(i.dsgvoRelevant),
jn(i.personalData),
REPORT_STATUS_LABEL_DE[i.reportStatus] ?? i.reportStatus,
fmtDateTime(i.erstmeldungDueAt),
fmtDateTime(i.dsgvoDueAt),
fmtDateTime(i.abschlussDueAt),
String(i.measureCount),
String(i.riskCount),
String(i.assetCount),
String(i.controlCount),
fmtDateTime(i.createdAt),
];
}
function csvCell(v: string): string {
// Excel-kompatibel: bei Sonderzeichen quoten, innere Quotes verdoppeln.
return /[",;\n\r]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
}
/** Vorfallregister als CSV (Semikolon-getrennt, mit UTF-8-BOM für Excel). */
export function toRegisterCsv(rows: IncidentRegisterInput[]): string {
const lines = [
INCIDENT_REGISTER_COLUMNS.join(";"),
...rows.map((r) => incidentRegisterRow(r).map(csvCell).join(";")),
];
return "" + lines.join("\r\n");
}
// ── Meldevorlagen (NIS2 / DSGVO) ─────────────────────────────────────────────
export type IncidentTemplateInput = {
refNo: string;
title: string;
description?: string | null;
category: string;
severity: string;
organisation?: string | null;
occurredAt?: Date | string | null;
detectedAt?: Date | string | null;
reportedAt?: Date | string | null;
impactC: number;
impactI: number;
impactA: number;
affectedDataCategories: string[];
personalData: boolean;
nis2Relevant: boolean;
reporterName?: string | null;
reporterContact?: string | null;
// Meldefristen (aus dem Kenntniszeitpunkt abgeleitet, IM-B)
erstmeldungDueAt?: Date | string | null;
folgemeldungDueAt?: Date | string | null;
abschlussDueAt?: Date | string | null;
dsgvoDueAt?: Date | string | null;
};
function field(label: string, value: string): string {
return `${label}:\n${value || "—"}\n`;
}
const TEMPLATE_FOOTER =
"\nHinweis: Diese Vorlage wurde aus den erfassten Vorfalldaten vorbefüllt. Die\n" +
"Übermittlung an die zuständige Behörde erfolgt MANUELL — bitte vor dem Versand\n" +
"prüfen, ergänzen und freigeben. (Erstellt mit Certvia.)\n";
/**
* NIS2-Meldevorlage (Art. 23 — Meldekette 24 h / 72 h / 1 Monat). Vorbefüllter
* Fließtext zur manuellen Übermittlung; Fristen aus dem Kenntniszeitpunkt.
*/
export function buildNis2Template(i: IncidentTemplateInput): string {
const impact = `Vertraulichkeit (C): ${i.impactC}/4 · Integrität (I): ${i.impactI}/4 · Verfügbarkeit (A): ${i.impactA}/4`;
return (
"NIS2 — Meldung eines Sicherheitsvorfalls (Art. 23)\n" +
"===================================================\n\n" +
field("Meldende Organisation", i.organisation ?? "—") +
field("Vorfall-Kennung", i.refNo) +
field("Kurzbezeichnung", i.title) +
field("Kategorie", catLabel(i.category)) +
field("Schweregrad", i.severity) +
"\n-- Zeitlicher Ablauf --\n" +
field("Eingetreten am", fmtDateTime(i.occurredAt)) +
field("Entdeckt am (Kenntnis)", fmtDateTime(i.detectedAt)) +
field("Intern gemeldet am", fmtDateTime(i.reportedAt)) +
"\n-- Auswirkung / Betroffenheit --\n" +
field("Schutzziel-Auswirkung", impact) +
field("Betroffene Datenkategorien", i.affectedDataCategories.join(", ")) +
field("Personenbezug betroffen", i.personalData ? "ja (ggf. zusätzlich DSGVO Art. 33)" : "nein") +
"\n-- Beschreibung --\n" +
field("Sachverhalt", i.description ?? "—") +
"\n-- Meldefristen (Kenntniszeitpunkt zugrunde gelegt) --\n" +
field("Früh-/Erstmeldung (24 h)", fmtDateTime(i.erstmeldungDueAt)) +
field("Meldung (72 h)", fmtDateTime(i.folgemeldungDueAt)) +
field("Abschlussbericht (1 Monat)", fmtDateTime(i.abschlussDueAt)) +
"\n-- Ansprechpartner --\n" +
field("Melder", [i.reporterName, i.reporterContact].filter(Boolean).join(", ") || "—") +
TEMPLATE_FOOTER
);
}
/**
* DSGVO-Meldevorlage (Art. 33 — Meldung einer Verletzung des Schutzes
* personenbezogener Daten, 72 h). Vorbefüllter Text zur manuellen Übermittlung.
*/
export function buildDsgvoTemplate(i: IncidentTemplateInput): string {
return (
"DSGVO — Meldung einer Datenschutzverletzung (Art. 33)\n" +
"=====================================================\n\n" +
field("Verantwortlicher (Organisation)", i.organisation ?? "—") +
field("Vorfall-Kennung", i.refNo) +
field("Kurzbezeichnung", i.title) +
field("Kategorie des Vorfalls", catLabel(i.category)) +
"\n-- Art der Verletzung --\n" +
field("Beschreibung der Verletzung", i.description ?? "—") +
field("Kategorien betroffener Daten", i.affectedDataCategories.join(", ")) +
field(
"Schutzziel-Auswirkung",
`Vertraulichkeit (C): ${i.impactC}/4 · Integrität (I): ${i.impactI}/4 · Verfügbarkeit (A): ${i.impactA}/4`,
) +
"\n-- Zeitlicher Ablauf --\n" +
field("Eingetreten am", fmtDateTime(i.occurredAt)) +
field("Bekannt geworden am (Kenntnis)", fmtDateTime(i.detectedAt)) +
field("Meldefrist (72 h ab Kenntnis)", fmtDateTime(i.dsgvoDueAt)) +
"\n-- Ansprechpartner --\n" +
field("Melder / Kontakt", [i.reporterName, i.reporterContact].filter(Boolean).join(", ") || "—") +
"\nHinweis Art. 34: Bei voraussichtlich hohem Risiko für die betroffenen Personen\n" +
"ist zusätzlich eine Benachrichtigung der Betroffenen zu prüfen.\n" +
TEMPLATE_FOOTER
);
}
-232
View File
@@ -1,232 +0,0 @@
/**
* Gebrandeter, druck-/exportierbarer Einzel-Vorfallbericht (§9/§72) als
* eigenständiges HTML-Dokument mit @media print. Nutzt das Dokument-CD
* (document-brand). Rein & datengetrieben — kein DB-/Server-Import, damit
* testbar. DOCX/PDF bleibt Folge (dockt an dasselbe CD an).
*/
import { BRAND } from "@/lib/brand";
import { DOCUMENT_THEME, documentFooterLine } from "@/lib/document-brand";
import {
INCIDENT_CATEGORY_LABEL_DE,
INCIDENT_STATUS_LABEL_DE,
INCIDENT_SOURCE_LABEL_DE,
REPORT_STATUS_LABEL_DE,
fmtDateTime,
} from "@/lib/incident-export";
import type { IncidentCategory, IncidentStatus } from "@/lib/incident";
export type IncidentReportData = {
refNo: string;
title: string;
organisation?: string | null;
description?: string | null;
category: string;
source: string;
status: string;
severity: string;
priority: string;
occurredAt?: Date | string | null;
detectedAt?: Date | string | null;
reportedAt?: Date | string | null;
ownerName?: string | null;
assigneeName?: string | null;
reporterName?: string | null;
reporterContact?: string | null;
impactC: number;
impactI: number;
impactA: number;
urgency: number;
affectedDataCategories: string[];
personalData: boolean;
prototypeData: boolean;
nis2Relevant: boolean;
dsgvoRelevant: boolean;
reportStatus: string;
rootCause?: string | null;
resolution?: string | null;
closingNote?: string | null;
lessonsLearned?: string | null;
measuresEffectiveness?: string | null;
postIncidentReview?: string | null;
deadlines: { label: string; dueAt: Date | string; done: boolean; overdue: boolean }[];
measures: { ref: string; title: string; status: string; owner?: string | null; dueDate?: Date | string | null }[];
risks: { ref: string; title: string }[];
controls: string[];
assets: string[];
processes: string[];
evidence: { title: string; note?: string | null }[];
timeline: { at: Date | string; actor: string; action: string }[];
generatedAt?: Date | string;
};
function esc(v: unknown): string {
return String(v ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
const catLabel = (c: string) => INCIDENT_CATEGORY_LABEL_DE[c as IncidentCategory] ?? c;
const statusLabel = (s: string) => INCIDENT_STATUS_LABEL_DE[s as IncidentStatus] ?? s;
function row(label: string, value: string): string {
return `<tr><th>${esc(label)}</th><td>${value}</td></tr>`;
}
function block(title: string, body: string): string {
if (!body) return "";
return `<section><h2>${esc(title)}</h2>${body}</section>`;
}
function textBlock(label: string, value?: string | null): string {
if (!value) return "";
return `<div class="tb"><p class="lbl">${esc(label)}</p><p class="val">${esc(value).replace(/\n/g, "<br>")}</p></div>`;
}
function list(items: string[]): string {
if (items.length === 0) return `<p class="muted">—</p>`;
return `<ul>${items.map((i) => `<li>${i}</li>`).join("")}</ul>`;
}
/** Erzeugt das vollständige HTML-Dokument des Vorfallberichts. */
export function renderIncidentReportHtml(d: IncidentReportData): string {
const th = DOCUMENT_THEME;
const generated = fmtDateTime(d.generatedAt ?? new Date());
const flags = [
d.nis2Relevant ? "NIS2-relevant" : null,
d.dsgvoRelevant ? "DSGVO (Personenbezug)" : null,
d.prototypeData ? "TISAX (Prototyp/Kundendaten)" : null,
].filter(Boolean) as string[];
const stammdaten =
`<table class="kv">` +
row("Kategorie", esc(catLabel(d.category))) +
row("Kanal / Quelle", esc(INCIDENT_SOURCE_LABEL_DE[d.source] ?? d.source)) +
row("Schweregrad", esc(d.severity)) +
row("Priorität", esc(d.priority)) +
row("Status", esc(statusLabel(d.status))) +
row("Verantwortlich (Incident-Manager)", esc(d.ownerName ?? "—")) +
row("Bearbeiter", esc(d.assigneeName ?? "—")) +
row("Melder", esc([d.reporterName, d.reporterContact].filter(Boolean).join(", ") || "—")) +
row("Eingetreten am", esc(fmtDateTime(d.occurredAt))) +
row("Entdeckt am", esc(fmtDateTime(d.detectedAt))) +
row("Intern gemeldet am", esc(fmtDateTime(d.reportedAt))) +
row("Auswirkung C/I/A · Dringlichkeit", `C${esc(d.impactC)} · I${esc(d.impactI)} · A${esc(d.impactA)} · ${esc(d.urgency)}`) +
row("Betroffene Datenkategorien", esc(d.affectedDataCategories.join(", ") || "—")) +
row("Merkmale", flags.length ? flags.map((f) => `<span class="pill">${esc(f)}</span>`).join(" ") : "—") +
`</table>`;
const reporting =
d.reportStatus !== "none" || d.deadlines.length > 0
? `<table class="kv">` +
row("Meldestatus", esc(REPORT_STATUS_LABEL_DE[d.reportStatus] ?? d.reportStatus)) +
`</table>` +
(d.deadlines.length
? `<table class="grid"><thead><tr><th>Frist</th><th>Fällig</th><th>Stand</th></tr></thead><tbody>` +
d.deadlines
.map(
(dl) =>
`<tr><td>${esc(dl.label)}</td><td>${esc(fmtDateTime(dl.dueAt))}</td><td>${
dl.done ? "eingereicht" : dl.overdue ? '<span class="over">überfällig</span>' : "offen"
}</td></tr>`,
)
.join("") +
`</tbody></table>`
: "")
: "";
const abschluss =
textBlock("Ursache (Root Cause)", d.rootCause) +
textBlock("Lösung / Behebung", d.resolution) +
textBlock("Abschlussnotiz", d.closingNote) +
textBlock("Lessons Learned", d.lessonsLearned) +
textBlock("Wirksamkeit der Maßnahmen", d.measuresEffectiveness) +
textBlock("Post-Incident-Review (→ Management-Review)", d.postIncidentReview);
const measuresList = d.measures.map(
(m) =>
`${esc(m.ref)} ${esc(m.title)} — <em>${esc(m.status)}</em>${m.owner ? `, ${esc(m.owner)}` : ""}${
m.dueDate ? `, fällig ${esc(fmtDateTime(m.dueDate))}` : ""
}`,
);
const verknuepfungen =
`<div class="cols">` +
`<div><h3>Maßnahmen (CAPA)</h3>${list(measuresList)}</div>` +
`<div><h3>Risiken</h3>${list(d.risks.map((r) => `${esc(r.ref)} ${esc(r.title)}`))}</div>` +
`<div><h3>Betroffene Controls</h3>${list(d.controls.map(esc))}</div>` +
`<div><h3>Betroffene Assets</h3>${list(d.assets.map(esc))}</div>` +
`<div><h3>Betroffene Prozesse</h3>${list(d.processes.map(esc))}</div>` +
`<div><h3>Nachweise</h3>${list(d.evidence.map((e) => `${esc(e.title)}${e.note ? ` — ${esc(e.note)}` : ""}`))}</div>` +
`</div>`;
const timeline = d.timeline.length
? `<table class="grid"><thead><tr><th>Zeitpunkt</th><th>Akteur</th><th>Aktion</th></tr></thead><tbody>` +
d.timeline
.map((e) => `<tr><td>${esc(fmtDateTime(e.at))}</td><td>${esc(e.actor)}</td><td>${esc(e.action)}</td></tr>`)
.join("") +
`</tbody></table>`
: "";
return `<!doctype html>
<html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(d.refNo)} · Vorfallbericht</title>
<style>
:root { --text:${th.text}; --muted:${th.textMuted}; --accent:${th.accent}; --accent2:${th.accentStrong}; --rule:${th.rule}; --thead:${th.tableHeaderBackground}; }
* { box-sizing: border-box; }
body { margin:0; background:${th.pageBackground}; color:var(--text); font-family:${th.bodyFont}, "Open Sans", system-ui, sans-serif; font-size:12.5px; line-height:1.5; }
.page { max-width:900px; margin:0 auto; padding:28px 32px 48px; }
header { border-bottom:3px solid var(--accent); padding-bottom:12px; margin-bottom:18px; }
.brand { font-family:${th.headingFont}, "Poppins", sans-serif; font-weight:700; color:var(--accent2); letter-spacing:.02em; font-size:13px; text-transform:uppercase; }
h1 { font-family:${th.headingFont}, "Poppins", sans-serif; font-size:20px; margin:4px 0 2px; }
.sub { color:var(--muted); font-size:11.5px; }
.badges { margin-top:8px; }
.pill { display:inline-block; border:1px solid var(--rule); border-radius:999px; padding:1px 9px; font-size:10.5px; color:var(--accent2); margin-right:4px; }
h2 { font-family:${th.headingFont}, "Poppins", sans-serif; font-size:14px; color:var(--accent); border-bottom:1px solid var(--rule); padding-bottom:4px; margin:22px 0 10px; }
h3 { font-size:11.5px; text-transform:uppercase; letter-spacing:.03em; color:var(--muted); margin:0 0 4px; }
table { width:100%; border-collapse:collapse; }
table.kv th { text-align:left; width:230px; vertical-align:top; color:var(--muted); font-weight:600; padding:3px 10px 3px 0; }
table.kv td { padding:3px 0; vertical-align:top; }
table.grid { margin-top:8px; }
table.grid th { text-align:left; background:var(--thead); border:1px solid var(--rule); padding:5px 8px; font-size:11px; }
table.grid td { border:1px solid var(--rule); padding:5px 8px; vertical-align:top; }
.over { color:var(--accent2); font-weight:600; }
.tb { margin-bottom:10px; }
.tb .lbl { font-weight:600; margin:0 0 2px; }
.tb .val { margin:0; }
.cols { display:grid; grid-template-columns:1fr 1fr; gap:14px 26px; }
ul { margin:2px 0; padding-left:18px; }
li { margin:1px 0; }
.muted { color:var(--muted); margin:2px 0; }
footer { margin-top:34px; padding-top:10px; border-top:1px solid var(--rule); color:var(--muted); font-size:10.5px; display:flex; justify-content:space-between; }
.print-hint { margin-top:14px; }
.print-btn { font:inherit; cursor:pointer; background:var(--accent); color:#fff; border:0; border-radius:8px; padding:8px 14px; }
@media print {
.print-hint { display:none; }
.page { max-width:none; padding:0; }
.cols { grid-template-columns:1fr 1fr; }
h2 { break-after:avoid; }
section, tr { break-inside:avoid; }
}
</style></head>
<body><div class="page">
<header>
<div class="brand">${esc(BRAND.name)} · Vorfallbericht</div>
<h1>${esc(d.refNo)} — ${esc(d.title)}</h1>
<div class="sub">${d.organisation ? esc(d.organisation) + " · " : ""}Erstellt am ${esc(generated)}</div>
<div class="badges"><span class="pill">${esc(d.severity)}</span><span class="pill">${esc(statusLabel(d.status))}</span>${flags
.map((f) => `<span class="pill">${esc(f)}</span>`)
.join("")}</div>
</header>
${d.description ? `<section><h2>Beschreibung</h2><p>${esc(d.description).replace(/\n/g, "<br>")}</p></section>` : ""}
${block("Stammdaten & Bewertung", stammdaten)}
${block("Meldepflicht & Fristen", reporting)}
${block("Behebung & Abschluss", abschluss || `<p class="muted">Noch nicht abgeschlossen.</p>`)}
${block("Verknüpfungen", verknuepfungen)}
${block("Verlauf (Audit-Timeline)", timeline)}
<div class="print-hint"><button class="print-btn" onclick="window.print()">Bericht drucken / als PDF speichern</button></div>
<footer><span>${esc(documentFooterLine("Vertraulich"))}</span><span>${esc(d.refNo)}</span></footer>
</div></body></html>`;
}
-60
View File
@@ -1,60 +0,0 @@
/**
* Severity-/Schweregrad-Matrix für Vorfälle (Fachkonzept §5).
*
* IM-A: fester DEFAULT in Code — Auswirkung × Dringlichkeit → niedrig | mittel |
* hoch | kritisch. Die mandantenspezifische Konfiguration der Matrix ist bewusst
* eine Folge-Story (nicht IM-A).
*
* Achsen (je 0–4):
* - Auswirkung: aus der höchsten Schutzziel-Verletzung C/I/A des Vorfalls
* (impactFromCia = max(C, I, A)); 0 = keine, 4 = existenzbedrohend.
* - Dringlichkeit (urgency): wie schnell reagiert werden muss.
*
* Client-safe (keine Server-Importe) — nutzbar in UI und Actions.
*/
export const INCIDENT_SEVERITIES = ["niedrig", "mittel", "hoch", "kritisch"] as const;
export type IncidentSeverity = (typeof INCIDENT_SEVERITIES)[number];
/** Reihenfolge/Priorität für Sortierung (höher = schwerer). */
export const SEVERITY_ORDER: Record<IncidentSeverity, number> = {
niedrig: 0,
mittel: 1,
hoch: 2,
kritisch: 3,
};
/**
* 5×5-Default-Matrix (Zeile = Auswirkung 0–4, Spalte = Dringlichkeit 0–4).
* Bewusst asymmetrisch: hohe Auswirkung wiegt etwas schwerer als hohe Dringlichkeit.
*/
const MATRIX: IncidentSeverity[][] = [
// urgency: 0 1 2 3 4
/* impact 0 */ ["niedrig", "niedrig", "niedrig", "mittel", "mittel"],
/* impact 1 */ ["niedrig", "niedrig", "mittel", "mittel", "hoch"],
/* impact 2 */ ["niedrig", "mittel", "mittel", "hoch", "hoch"],
/* impact 3 */ ["mittel", "mittel", "hoch", "hoch", "kritisch"],
/* impact 4 */ ["mittel", "hoch", "hoch", "kritisch", "kritisch"],
];
const clamp04 = (n: number) => Math.max(0, Math.min(4, Math.trunc(Number.isFinite(n) ? n : 0)));
/** Auswirkung eines Vorfalls = höchste der drei Schutzziel-Verletzungen (0–4). */
export function impactFromCia(c: number, i: number, a: number): number {
return Math.max(clamp04(c), clamp04(i), clamp04(a));
}
/** Default-Severity aus Auswirkung (0–4) und Dringlichkeit (0–4). */
export function computeSeverity(impact: number, urgency: number): IncidentSeverity {
return MATRIX[clamp04(impact)][clamp04(urgency)];
}
/** Bequemlichkeit: Severity direkt aus C/I/A + Dringlichkeit. */
export function severityFromIncident(
impactC: number,
impactI: number,
impactA: number,
urgency: number,
): IncidentSeverity {
return computeSeverity(impactFromCia(impactC, impactI, impactA), urgency);
}
-116
View File
@@ -1,116 +0,0 @@
/**
* Zentrale Konstanten/Helfer für das Modul „Vorfälle" (client-safe).
* Kategorien (§4-Taxonomie), Status (§3-Lebenszyklus) und die refNo-Anzeige.
*/
import type { IncidentSeverity } from "@/lib/incident-severity";
export const INCIDENT_CATEGORIES = [
"malware",
"phishing",
"unauthorized_access",
"data_loss",
"outage",
"physical",
"misconfiguration",
"supplier",
"prototype_customer_data",
"other",
] as const;
export type IncidentCategory = (typeof INCIDENT_CATEGORIES)[number];
export const INCIDENT_STATUSES = [
"neu",
"triage",
"in_bearbeitung",
"eingedaemmt",
"behoben",
"abgeschlossen",
"wiedereroeffnet",
] as const;
export type IncidentStatus = (typeof INCIDENT_STATUSES)[number];
export const INCIDENT_SOURCES = ["manual", "email"] as const;
export type IncidentSource = (typeof INCIDENT_SOURCES)[number];
export const INCIDENT_PRIORITIES = ["niedrig", "mittel", "hoch", "kritisch"] as const;
/**
* Erlaubte Statusübergänge (§3). Jeder Übergang wird serverseitig geprüft;
* bestimmte Übergänge verlangen zusätzlich Pflichtfelder (siehe TRANSITION_REQUIRES).
*/
export const STATUS_TRANSITIONS: Record<IncidentStatus, IncidentStatus[]> = {
neu: ["triage", "in_bearbeitung"],
triage: ["in_bearbeitung", "eingedaemmt"],
in_bearbeitung: ["eingedaemmt", "behoben"],
eingedaemmt: ["behoben", "in_bearbeitung"],
behoben: ["abgeschlossen", "wiedereroeffnet"],
abgeschlossen: ["wiedereroeffnet"],
wiedereroeffnet: ["triage", "in_bearbeitung"],
};
/**
* Pflichtfelder je Zielstatus (§8). Ein Übergang nach „behoben" verlangt Ursache +
* Lösung; „abgeschlossen" zusätzlich Abschlussnotiz + Lessons Learned.
*/
export const TRANSITION_REQUIRES: Partial<Record<IncidentStatus, string[]>> = {
behoben: ["rootCause", "resolution"],
abgeschlossen: ["rootCause", "resolution", "closingNote", "lessonsLearned"],
};
export function canTransition(from: IncidentStatus, to: IncidentStatus): boolean {
return STATUS_TRANSITIONS[from]?.includes(to) ?? false;
}
/**
* Ob der Nutzer vertrauliche Vorfälle sehen darf (§11): nur Rollen mit
* incident:manage oder incident:close. Der owner sieht seine eigenen zusätzlich
* über den ownerId-Filter (serverseitig in der Liste). Pure/client-safe.
*/
export function canSeeRestricted(session: { user?: { permissions?: string[] } }): boolean {
const p = session.user?.permissions ?? [];
return p.includes("incident:manage") || p.includes("incident:close");
}
/**
* Fehlende Pflichtfelder für einen Zielstatus (§8). Prüft die effektiven Werte
* (bereits gesetzt oder beim Übergang mitgeliefert). Leeres Array = alles erfüllt.
* Pure Funktion — von Action und Test gemeinsam genutzt.
*/
export function missingRequiredFields(
target: IncidentStatus,
values: Record<string, unknown>,
): string[] {
const required = TRANSITION_REQUIRES[target] ?? [];
return required.filter((field) => {
const v = values[field];
return v === null || v === undefined || (typeof v === "string" && v.trim() === "");
});
}
/** Farb-Ton (mockup-ui Pill) je Severity. */
export const SEVERITY_TONE: Record<IncidentSeverity, "mut" | "info" | "warn" | "risk"> = {
niedrig: "mut",
mittel: "info",
hoch: "warn",
kritisch: "risk",
};
/** Farb-Ton je Status. */
export const STATUS_TONE: Record<IncidentStatus, "mut" | "info" | "warn" | "risk" | "ok"> = {
neu: "risk",
triage: "warn",
in_bearbeitung: "info",
eingedaemmt: "info",
behoben: "ok",
abgeschlossen: "mut",
wiedereroeffnet: "warn",
};
/** Farb-Ton je Meldung-Track-Status (reportStatus, §3 Parallel-Track). */
export const REPORT_STATUS_TONE: Record<string, "mut" | "info" | "warn" | "orange" | "ok"> = {
none: "mut",
pruefung: "warn",
erstmeldung: "orange",
folgemeldung: "info",
abschluss: "ok",
};
-22
View File
@@ -1,22 +0,0 @@
/**
* Repräsentative VDA-ISA-2027-Controls (7 Kapitel) für die Shared-Responsibility-
* Matrix (6.1.3). Vollständiger Katalog kommt per Import aus ISA2027-EN.xlsx;
* diese Auswahl dient als Vorschlagsliste (datalist), Freitext bleibt möglich.
*/
export const ISA_CONTROLS: { ref: string; title: string }[] = [
{ ref: "1.6.1", title: "Informationssicherheits-Vorfälle" },
{ ref: "1.6.2", title: "Business Continuity / Notfallmanagement" },
{ ref: "2.1.1", title: "Qualifikation & Sensibilisierung des Personals" },
{ ref: "3.1.1", title: "Physische Sicherheitszonen" },
{ ref: "4.1.1", title: "Identitätsverwaltung" },
{ ref: "4.1.2", title: "Authentisierung / MFA" },
{ ref: "4.1.3", title: "Zugriffsrechte-Verwaltung" },
{ ref: "5.1.1", title: "Änderungsmanagement" },
{ ref: "5.2.1", title: "Netzwerksicherheit / Segmentierung" },
{ ref: "5.2.2", title: "Schutz vor Schadsoftware" },
{ ref: "5.2.3", title: "Protokollierung & Monitoring" },
{ ref: "5.2.4", title: "Schwachstellen-/Patch-Management" },
{ ref: "5.2.5", title: "Datensicherung / Backup" },
{ ref: "5.3.1", title: "Kryptografie / Verschlüsselung" },
{ ref: "7.1.1", title: "Einhaltung gesetzlicher/regulatorischer Vorgaben" },
];
-46
View File
@@ -1,46 +0,0 @@
/**
* ISB-Bestellung (Story A4-1, Fachcontent C7 §3). Rendert den Bestellungs-/Ernennungs-
* Text inline aus den zentralen Variablen — ohne die Seed-Vorlagen (Dev B) anzufassen.
* Platzhalter werden mit ROLE_ISB / ROLE_MANAGEMENT / ORG_NAME / DOC_DATE befüllt.
*/
export interface BestellungVars {
ISB: string;
MANAGEMENT: string;
ORG_NAME: string;
DOC_DATE: string;
}
const val = (s: string | null | undefined, fallback: string) => {
const t = (s ?? "").trim();
return t === "" ? fallback : t;
};
export function renderIsbBestellung(v: Partial<Record<keyof BestellungVars, string | null>>): string {
const isb = val(v.ISB, "N.N.");
const mgmt = val(v.MANAGEMENT, "N.N.");
const org = val(v.ORG_NAME, "die Organisation");
const date = val(v.DOC_DATE, "TT.MM.JJJJ");
return [
"# Bestellung Informationssicherheitsbeauftragte(r)",
"",
"| Feld | Wert |",
"|------|------|",
`| Bestellte Person / Funktion | ${isb} |`,
`| Bestellt durch | ${mgmt} |`,
`| Datum | ${date} |`,
"",
`Die Organisation ${org} bestellt ${isb} mit Wirkung zum ${date} zum/zur Informationssicherheitsbeauftragten.`,
"",
"## Aufgaben und Befugnisse",
"- Aufbau, Pflege und Weiterentwicklung des ISMS; Beratung der Leitung.",
"- Risikomanagement, interne Audits, Meldeweg für Sicherheitsvorfälle.",
`- Melde-/Eskalationsrecht direkt an ${mgmt}; Zugang zu relevanten Informationen/Systemen.`,
"",
"## Freigabe",
"| Rolle | Name | Unterschrift |",
"|-------|------|--------------|",
`| Leitung | ${mgmt} | |`,
`| ISB | ${isb} | |`,
].join("\n");
}
-108
View File
@@ -1,108 +0,0 @@
// AUTOGENERIERT von seed/isms-vorlagenpaket-v2/_generate_iso.py — nicht von Hand aendern.
// Quelle: _iso_crosswalk.json. Neu erzeugen: python3 seed/isms-vorlagenpaket-v2/_generate_iso.py
//
// Zuordnung ISO-Anforderung -> VDA-ISA-Control desselben Bibliotheksabschnitts.
// Zweck: die vorhandenen Umsetzungshinweise (ImplementationHint, nach ISA-Control
// verschluesselt) fuer ISO-Anforderungen wiederverwenden, statt sie zu duplizieren.
// ISO-Anforderungen ohne Eintrag liegen in einem ISO-eigenen Abschnitt — fuer sie
// gibt es (noch) keine Hinweise.
export const ISO_TO_ISA: Record<string, string> = {
"5.1": "1.2.1",
"5.3": "1.2.2",
"6.1.1": "1.4.1",
"6.1.2": "1.4.1",
"7.1": "1.2.2",
"7.2": "2.1.1",
"7.3": "2.1.3",
"8.2": "1.4.1",
"8.3": "1.4.1",
"9.2": "1.5.2",
"A.5.2": "1.2.2",
"A.5.3": "1.2.2",
"A.5.4": "1.2.1",
"A.5.8": "1.2.3",
"A.5.9": "1.3.1",
"A.5.10": "1.3.3",
"A.5.11": "5.3.3",
"A.5.12": "1.3.2",
"A.5.13": "1.3.2",
"A.5.14": "5.1.2",
"A.5.15": "4.2.1",
"A.5.16": "4.1.1",
"A.5.17": "4.1.3",
"A.5.18": "4.2.1",
"A.5.19": "6.1.1",
"A.5.20": "6.1.2",
"A.5.21": "6.1.3",
"A.5.22": "6.1.1",
"A.5.23": "5.3.4",
"A.5.24": "1.6.1",
"A.5.25": "1.6.2",
"A.5.26": "1.6.2",
"A.5.27": "1.6.2",
"A.5.28": "1.6.2",
"A.5.29": "1.6.3",
"A.5.30": "5.2.8",
"A.5.31": "7.1.1",
"A.5.32": "7.1.1",
"A.5.33": "7.1.1",
"A.5.34": "7.1.2",
"A.5.35": "1.5.2",
"A.5.36": "1.5.1",
"A.6.1": "2.1.1",
"A.6.2": "2.1.2",
"A.6.3": "2.1.3",
"A.6.5": "2.1.2",
"A.6.6": "2.1.2",
"A.6.7": "2.1.4",
"A.6.8": "1.6.1",
"A.7.1": "3.1.1",
"A.7.2": "3.1.1",
"A.7.3": "3.1.1",
"A.7.4": "3.1.1",
"A.7.6": "3.1.1",
"A.7.9": "3.1.4",
"A.7.10": "3.1.4",
"A.7.14": "5.3.3",
"A.8.1": "3.1.4",
"A.8.2": "4.2.1",
"A.8.3": "4.2.1",
"A.8.4": "5.3.1",
"A.8.5": "4.1.2",
"A.8.7": "5.2.3",
"A.8.8": "5.2.5",
"A.8.9": "5.2.1",
"A.8.10": "5.3.3",
"A.8.13": "5.2.9",
"A.8.14": "5.2.8",
"A.8.15": "5.2.4",
"A.8.16": "5.2.4",
"A.8.18": "4.2.1",
"A.8.19": "1.3.4",
"A.8.20": "5.2.7",
"A.8.21": "5.3.2",
"A.8.22": "5.2.7",
"A.8.23": "5.2.3",
"A.8.24": "5.1.1",
"A.8.25": "5.3.1",
"A.8.26": "5.3.1",
"A.8.27": "5.3.1",
"A.8.28": "5.3.1",
"A.8.29": "5.3.1",
"A.8.30": "5.3.1",
"A.8.31": "5.2.2",
"A.8.32": "5.2.1",
"A.8.33": "5.3.1",
"A.8.34": "5.2.6",
};
/** ISO-Anforderungen, die in einem ISO-eigenen Abschnitt liegen (kein ISA-Gegenstueck). */
export const ISO_ONLY: readonly string[] = [
"4.1", "4.2", "4.3", "4.4", "5.2", "6.1.3", "6.2", "6.3", "7.4", "7.5.1", "7.5.2", "7.5.3", "8.1", "9.1", "9.3", "10.1", "10.2", "A.5.1", "A.5.5", "A.5.6", "A.5.7", "A.5.37", "A.6.4", "A.7.5", "A.7.7", "A.7.8", "A.7.11", "A.7.12", "A.7.13", "A.8.6", "A.8.11", "A.8.12", "A.8.17",
];
/** ISA-Control, dessen Umsetzungshinweise fuer diese ISO-Anforderung gelten (oder null). */
export function isaControlForIso(ref: string): string | null {
return ISO_TO_ISA[ref] ?? null;
}
-13
View File
@@ -1,13 +0,0 @@
/** Farbklassen für Stufen 1–4 (Schutzbedarf, Schadenshöhe, Kritikalität) — Pillen-Farben aus dem Mockup. */
export function levelColor(level: number): string {
switch (level) {
case 2:
return "bg-[#fbf1de] text-[#a9760f] border-transparent";
case 3:
return "bg-[#fdeadb] text-[#c2681a] border-transparent";
case 4:
return "bg-[#fbe4e4] text-[#d64c4c] border-transparent";
default:
return "bg-[#e7f6ee] text-[#2e9e6b] border-transparent";
}
}
-217
View File
@@ -1,217 +0,0 @@
import c5 from "../../seed/scoping/c5-controls.json";
/**
* Reifegrad-Engine (Story A7-2) — regelbasierte, nachvollziehbare Herleitung eines
* Reifegrad-Vorschlags 0–3 je Control aus dem Belegstatus (C5 §2), der Zielreifegrad
* (C5 §3) und die „offenen Punkte" (C5 §4). Reine, seiteneffektfreie Logik; die
* Belegstände werden vom Aufrufer aus Richtlinien-/Verfahrensdokumenten, Inventar und
* Risikoregister abgeleitet. Der Vorschlag ist nicht bindend — die Bestätigung/
* Überschreibung durch den Bearbeiter ist Pflicht (persistiert als ControlAssessment).
*
* Grundlage: VDA-ISA-Reifegradmodell 0–3, Fachcontent C5. Belegtabelle je Control:
* `seed/scoping/c5-controls.json` (Generierung: scripts/build-c5-controls.ts).
*/
/**
* Neutrale, framework-UNABHÄNGIGE Control-Spezifikation (Belegbasis, Feindesign §1/§2):
* welche Richtlinie/Verfahren zuständig sind und ob Asset-/Risiko-Verknüpfung gefordert
* ist. Eine Tatsache über die Organisation, keine Frage der Norm — daher von TISAX (C5)
* UND ISO (mapping-iso) befüllt und von der GETEILTEN Belegauflösung (loadEvidenceResolver)
* konsumiert. `loadEvidenceResolver` darf `ControlSpec` NICHT um ein Framework erweitern.
*/
export interface ControlSpec {
control: string;
title: string;
/** Zuständige Richtlinie(n): L00 | R01..R14 (TISAX) bzw. R.. (ISO). Leer = keine dedizierte. */
policy: string[];
/** Geforderte Verfahren: VA-xx. Leer = operative Steuerung in der Richtlinie verankert. */
verfahren: string[];
/** Control setzt auf Inventar auf (Asset-Verknüpfung gefordert). */
needsAsset: boolean;
/** Control setzt auf Risikoregister auf (Risiko-Verknüpfung gefordert). */
needsRisk: boolean;
}
/** TISAX-Belegtabelle (C5 §1) = neutrale ControlSpec + Standard-Zielreifegrad (VDA-ISA-Zusatz). */
export interface C5ControlSpec extends ControlSpec {
/** Standard-Zielreifegrad laut Tabelle (informativ; effektiv aus Scope, s. targetMaturity). */
target: number;
}
/** Vollständige C5-Belegtabelle (45 IS-Controls). */
export const C5_CONTROLS = c5 as C5ControlSpec[];
const C5_BY_ID = new Map(C5_CONTROLS.map((c) => [c.control, c]));
/** Generischer Fallback-Spec für Controls ohne §5-Zeile (Proto 8.x / Datenschutz 9.x, C5 §6). */
export function specForControl(control: string): C5ControlSpec {
return (
C5_BY_ID.get(control) ?? {
control,
title: control,
policy: [],
verfahren: [],
needsAsset: false,
needsRisk: false,
target: 2, // §6-Gruppen: Standard-Zielreifegrad 2 (Ausnahmen HOCH → 3, s. targetMaturity).
}
);
}
/** Validierungsstatus eines Belegs (C5 §1): fehlt → verknüpft (unvalidiert) → validiert. */
export type EvidenceStatus = "fehlt" | "verknuepft" | "validiert";
/** Aus den Fachmodulen abgeleiteter Ist-Belegstand eines Controls. */
export interface ControlEvidence {
/** Status der (aggregierten) zuständigen Richtlinie(n). */
policy: EvidenceStatus;
/** Status der geforderten Verfahren (aggregiert, „schwächster" gewinnt). */
verfahren: EvidenceStatus;
/** Asset-Verknüpfung vorhanden (nur relevant, wenn spec.needsAsset). */
assetLinked: boolean;
/** Risiko-Verknüpfung vorhanden (nur relevant, wenn spec.needsRisk). */
riskLinked: boolean;
/**
* Nur für Controls OHNE dediziertes Verfahren (C5 §2, Sonderfall): validierte,
* dokumentierte Umsetzungsregelung/Konfiguration (N-basisch), die für Grad 2 an die
* Stelle von (V) tritt. Für Controls mit Verfahren irrelevant.
*/
implementationRule?: boolean;
/**
* Operativer Wirksamkeitsnachweis (N) validiert UND aktuell (Aktualitätsregel ≤12 M.,
* C5 §1) — wiederkehrender Review/Audit/Test. Grad-3-Trigger. Frühe Onboarding-Phase
* i. d. R. false → Deckelung auf Grad 2.
*/
operationalProof: boolean;
/** Nachweis widerspricht Richtlinie / offene Findings → Deckelung auf max. 1 (C5 §2). */
contradiction?: boolean;
/** Control enthält nur SOLL (MUSS definitionsgemäß erfüllt) → Bewertung startet bei 1 (C5 §2). */
onlySoll?: boolean;
}
export type MaturityRule = "R0" | "R1a" | "R1b" | "R1c" | "R2" | "R3" | "CAP";
export interface MaturitySuggestion {
value: 0 | 1 | 2 | 3;
rule: MaturityRule;
/** Kurzbegründung (nachvollziehbar, an Belege gekoppelt). */
reason: string;
}
/**
* Reifegrad-Vorschlag nach C5 §2 (generische Belegkonstellations-Tabelle) inkl.
* Sonderfälle: Controls ohne Verfahren (V-Slot durch validierte Umsetzungsregelung
* ersetzt), nur-SOLL-Controls (Start bei 1) und Deckelung bei Widerspruch/Findings.
*/
export function suggestMaturity(spec: C5ControlSpec, ev: ControlEvidence): MaturitySuggestion {
// Deckelung: Nachweis widerspricht Richtlinie / offene Findings → max. Grad 1 (C5 §2).
if (ev.contradiction) {
return { value: 1, rule: "CAP", reason: "Nachweis widerspricht Richtlinie bzw. offene Findings — Vorschlag gedeckelt (max. 1)." };
}
const requiresProcedure = spec.verfahren.length > 0;
// Bei Controls ohne dediziertes Verfahren tritt für Grad 2 eine validierte, dokumentierte
// Umsetzungsregelung (implementationRule) an die Stelle von (V); der wiederkehrende
// Wirksamkeitsnachweis (operationalProof) bleibt eigenständiger Grad-3-Trigger.
const procedureOk = requiresProcedure ? ev.verfahren === "validiert" : ev.implementationRule === true;
const assetOk = !spec.needsAsset || ev.assetLinked;
const riskOk = !spec.needsRisk || ev.riskLinked;
const policyValidated = ev.policy === "validiert";
const policyLinked = ev.policy !== "fehlt";
// R3: R2 erfüllt UND mindestens ein validierter, aktueller operativer Wirksamkeitsnachweis.
if (policyValidated && procedureOk && assetOk && riskOk && ev.operationalProof) {
return { value: 3, rule: "R3", reason: "Richtlinie und Verfahren validiert, Verknüpfungen vorhanden und aktueller Wirksamkeitsnachweis." };
}
// R2: Richtlinie validiert UND alle Verfahren validiert UND (falls einschlägig) Asset-/Risiko-Verknüpfung.
if (policyValidated && procedureOk && assetOk && riskOk) {
return { value: 2, rule: "R2", reason: "Vorgehen dokumentiert und gesteuert; Richtlinie und Verfahren validiert." };
}
// R1b: Richtlinie validiert, aber ein gefordertes Verfahren fehlt/unvalidiert (oder Verknüpfung fehlt).
if (policyValidated) {
return { value: 1, rule: "R1b", reason: "Richtlinie validiert, aber gefordertes Verfahren bzw. Verknüpfung fehlt oder ist unvalidiert." };
}
// R1a: zuständige Richtlinie verknüpft, aber nicht validiert.
if (policyLinked) {
return { value: 1, rule: "R1a", reason: "Regelungsabsicht erkennbar (Richtlinie verknüpft), aber nicht validiert/gelebt." };
}
// R1c: nur operativer Nachweis/Umsetzung ohne validierte Richtlinie.
if (ev.operationalProof || ev.implementationRule || ev.verfahren !== "fehlt") {
return { value: 1, rule: "R1c", reason: "Tätigkeit wird durchgeführt, aber nicht dokumentiert gesteuert (keine validierte Richtlinie)." };
}
// nur-SOLL: MUSS definitionsgemäß erfüllt → Bewertung startet bei 1.
if (ev.onlySoll) {
return { value: 1, rule: "R1c", reason: "Nur-SOLL-Control: MUSS-Ebene definitionsgemäß erfüllt (Start bei Grad 1)." };
}
// R0: keine Belege bzw. nur unvalidierte Fragmente ohne zuständige Richtlinie.
return { value: 0, rule: "R0", reason: "Kein belastbarer Nachweis einer geregelten Vorgehensweise." };
}
export interface ScopeForTarget {
/** Assessment-Level aus TenantSettings (A2-1): AL2 oder AL3. */
level: "AL2" | "AL3";
flags: Record<string, boolean>;
}
/**
* Zielreifegrad je Control (C5 §3): Standard 3; Absenkung auf 2 nur bei reinem MUSS-/AL2-Scope
* (kein SOLL). HOCH/SEHR-HOCH-Schutzbedarf erzwingt Grad 3 (keine Absenkung).
*/
export function targetMaturity(scope: ScopeForTarget): 2 | 3 {
const high = scope.flags.FLAG_HIGH_PROTECTION || scope.flags.FLAG_VERY_HIGH_PROTECTION;
if (scope.level === "AL3" || high || scope.flags.FLAG_INCLUDE_SHOULD) return 3;
// reiner AL2-/MUSS-Scope (kein SOLL, kein erhöhter Schutzbedarf): Absenkung auf Grad 2.
return 2;
}
/** Ein offener Punkt (Gap) nach C5 §4 mit fehlender Belegklasse und empfohlener Maßnahme. */
export interface OpenPoint {
control: string;
kind: "reifegradluecke" | "richtlinie" | "verfahren" | "nachweis" | "verknuepfung" | "widerspruch";
actual: number;
target: number;
/** Fehlende Belegklasse / Kurzbeschreibung. */
missing: string;
/** Empfohlene Maßnahme (Aufgabentitel). */
action: string;
}
/**
* Leitet die offenen Punkte (Gap → Aufgabe) nach C5 §4 ab: Reifegradlücke, fehlender
* Pflichtbeleg (Richtlinie/Verfahren), fehlender/veralteter Nachweis bei Zielgrad 3,
* fehlende Asset-/Risiko-Verknüpfung und Widerspruch/Findings.
*/
export function openPoints(spec: C5ControlSpec, ev: ControlEvidence, suggestion: MaturitySuggestion, target: number): OpenPoint[] {
const out: OpenPoint[] = [];
const base = { control: spec.control, actual: suggestion.value, target };
if (ev.contradiction) {
out.push({ ...base, kind: "widerspruch", missing: "Nachweis mit Findings/Widerspruch", action: `Abweichung in Control ${spec.control} beheben` });
}
if (ev.policy !== "validiert" && spec.policy.length > 0) {
out.push({ ...base, kind: "richtlinie", missing: `Richtlinie ${spec.policy.join("/")}`, action: `Richtlinie ${spec.policy.join("/")} erstellen/validieren (Control ${spec.control})` });
}
if (spec.verfahren.length > 0 && ev.verfahren !== "validiert") {
out.push({ ...base, kind: "verfahren", missing: `Verfahren ${spec.verfahren.join("/")}`, action: `Verfahren ${spec.verfahren.join("/")} erstellen/validieren (Control ${spec.control})` });
}
if (spec.needsAsset && !ev.assetLinked) {
out.push({ ...base, kind: "verknuepfung", missing: "Asset-Verknüpfung", action: `Asset-/Inventarverknüpfung für Control ${spec.control} herstellen` });
}
if (spec.needsRisk && !ev.riskLinked) {
out.push({ ...base, kind: "verknuepfung", missing: "Risiko-Verknüpfung", action: `Risikoverknüpfung für Control ${spec.control} herstellen` });
}
if (target >= 3 && !ev.operationalProof && !ev.contradiction) {
out.push({ ...base, kind: "nachweis", missing: "operativer Wirksamkeitsnachweis (aktuell)", action: `Wirksamkeitsnachweis (Review/Audit/Test) für Control ${spec.control} führen` });
}
// Reifegradlücke zuletzt, nur wenn nicht schon durch einen konkreten Pflichtbeleg abgedeckt.
if (suggestion.value < target && out.length === 0) {
out.push({ ...base, kind: "reifegradluecke", missing: `Reifegrad ${suggestion.value} < Ziel ${target}`, action: `Belege/Umsetzung für Control ${spec.control} bis Zielgrad ${target} anheben` });
}
return out;
}
export const MATURITY_LABEL: Record<0 | 1 | 2 | 3, string> = {
0: "Grad 0 — nicht geregelt",
1: "Grad 1 — informell",
2: "Grad 2 — dokumentiert/gesteuert",
3: "Grad 3 — wirksam/integriert",
};
-15
View File
@@ -1,15 +0,0 @@
export function measureRef(refNo: number): string {
return `M-${String(refNo).padStart(3, "0")}`;
}
export const MEASURE_STATUS_TONE = {
OPEN: "mut",
IN_PROGRESS: "info",
DONE: "ok",
} as const;
export const MEASURE_PRIORITY_TONE = {
LOW: "mut",
MEDIUM: "info",
HIGH: "risk",
} as const;
+35 -27
View File
@@ -1,36 +1,44 @@
/**
* Aktivierbare Module je Mandant (§3.4). `nav` verweist auf den Übersetzungs-
* schlüssel des Navigationseintrags, `href` auf die gegatete Route. Das Dashboard
* ist immer aktiv und kein Modul.
* Aktivierbare Module je Mandant (TenantModule). Der Key ist verbindlich:
* - Server-Actions eines Moduls liegen in `src/server/actions/<key>/*.ts` und laufen
* über `moduleGuard("<key>")` (erzwungen durch scripts/check-module-guards.ts).
* - Routen eines Moduls tragen ein `layout.tsx` mit `await requireModule("<key>")`.
* `href` ist die Einstiegsroute (null = kein eigener Navigationseintrag), `nav` der
* Übersetzungsschlüssel in messages/<locale>/modules.json. Das Dashboard ist kein Modul.
* Die Sidebar-Einträge selbst stehen in src/lib/nav.ts.
*/
export interface ModuleDef {
key: string;
name: string; // Anzeigename in der Admin-Konsole
href: string;
nav: string; // Schlüssel in messages nav.*
/** Anzeigename in der Admin-Konsole (Plattform, deutsch) */
name: string;
href: string | null;
/** Schlüssel in messages modules.* */
nav: string;
}
export const MODULES: ModuleDef[] = [
{ key: "onboarding", name: "Onboarding-Wizard", href: "/onboarding", nav: "onboarding" },
{ key: "audit", name: "Audit-Vorbereitung", href: "/audit-readiness", nav: "auditReadiness" },
{ key: "assets", name: "Asset-Inventar", href: "/assets", nav: "assets" },
{ key: "bia", name: "Business Impact Analyse", href: "/processes", nav: "bia" },
{ key: "risk", name: "Risikoanalyse", href: "/risks", nav: "risks" },
{ key: "soa", name: "SoA & Controls", href: "/soa", nav: "soa" },
{ key: "measures", name: "Maßnahmen / Reviews", href: "/measures", nav: "measures" },
{ key: "tasks", name: "Aufgaben & Freigaben", href: "/tasks", nav: "tasks" },
{ key: "incidents", name: "Vorfälle", href: "/incidents", nav: "incidents" },
{ key: "policies", name: "Richtlinien & Verfahren", href: "/policies", nav: "policies" },
{ key: "suppliers", name: "Lieferanten- / IT-Service-Management", href: "/suppliers", nav: "suppliers" },
{ key: "dependencies", name: "Abhängigkeiten", href: "/dependencies", nav: "dependencies" },
{ key: "evidence", name: "Nachweise", href: "/evidence", nav: "evidence" },
{ key: "chat", name: "KI-Chat", href: "/chat", nav: "chat" },
{ key: "review", name: "Management-Review", href: "/review", nav: "review" },
];
export const MODULES = [
{ key: "customers", name: "Kunden", href: "/customers", nav: "customers" },
{ key: "sites", name: "Objekte", href: "/sites", nav: "sites" },
{ key: "teams", name: "Teams", href: "/teams", nav: "teams" },
{ key: "work_orders", name: "Aufträge", href: "/work-orders", nav: "workOrders" },
{ key: "imports", name: "Auftragsimport", href: "/imports", nav: "imports" },
{ key: "field", name: "Einsätze (Mobile)", href: "/m", nav: "field" },
{ key: "reports", name: "Berichte", href: "/reports", nav: "reports" },
{ key: "emergency", name: "Notdienst", href: "/m/emergency", nav: "emergency" },
{ key: "documents", name: "Dokumente", href: "/documents", nav: "documents" },
{ key: "notifications", name: "Benachrichtigungen", href: "/notifications", nav: "notifications" },
{ key: "lotse", name: "Lotse (KI-Assistent)", href: null, nav: "lotse" },
] as const satisfies readonly ModuleDef[];
export const MODULE_KEYS = MODULES.map((m) => m.key);
export type ModuleKey = (typeof MODULES)[number]["key"];
/** Route → Modul-Key (für den serverseitigen requireModule-Guard). */
export const HREF_TO_MODULE: Record<string, string> = Object.fromEntries(
MODULES.map((m) => [m.href, m.key])
export const MODULE_KEYS: readonly ModuleKey[] = MODULES.map((m) => m.key);
export function isModuleKey(key: string): key is ModuleKey {
return (MODULE_KEYS as readonly string[]).includes(key);
}
/** Route → Modul-Key (für Navigation/Gating). */
export const HREF_TO_MODULE: Record<string, ModuleKey> = Object.fromEntries(
MODULES.filter((m) => m.href).map((m) => [m.href as string, m.key]),
);
+65
View File
@@ -0,0 +1,65 @@
import {
LayoutDashboard,
ClipboardList,
FileInput,
Users,
Building2,
UsersRound,
FileText,
FolderOpen,
Settings,
type LucideIcon,
} from "lucide-react";
import type { ModuleKey } from "@/lib/modules";
import type { Permission } from "@/server/rbac";
/**
* Sidebar-Navigation des Backoffice (src/app/(app)/layout.tsx).
*
* Ein Eintrag ist sichtbar, wenn
* - sein Modul (falls gesetzt) für den Mandanten aktiv ist UND
* - die Session mindestens EINE der `permissions` hat (leer = immer).
* Das ist reiner Komfort — Seiten und Actions prüfen Modul + Rechte serverseitig selbst.
*
* Neue Backoffice-Seite ⇒ hier eintragen; `label` ist ein Schlüssel in messages nav.*.
* Mobile-Einträge (/m, /m/emergency) gehören NICHT hierher (eigene Mobile-Navigation).
*/
export interface NavItem {
href: string;
label: string;
icon: LucideIcon;
module?: ModuleKey;
permissions?: readonly Permission[];
section: "main" | "admin";
}
export const NAV_ITEMS: readonly NavItem[] = [
{ href: "/dashboard", label: "dashboard", icon: LayoutDashboard, section: "main" },
{
href: "/work-orders",
label: "workOrders",
icon: ClipboardList,
module: "work_orders",
permissions: ["work_order:read_all", "work_order:read_team"],
section: "main",
},
{ href: "/imports", label: "imports", icon: FileInput, module: "imports", permissions: ["import:write"], section: "main" },
{ href: "/customers", label: "customers", icon: Users, module: "customers", permissions: ["customer:read"], section: "main" },
{ href: "/sites", label: "sites", icon: Building2, module: "sites", permissions: ["site:read"], section: "main" },
{ href: "/teams", label: "teams", icon: UsersRound, module: "teams", permissions: ["team:read"], section: "main" },
{ href: "/reports", label: "reports", icon: FileText, module: "reports", permissions: ["report:read"], section: "main" },
{ href: "/documents", label: "documents", icon: FolderOpen, module: "documents", permissions: ["document:read"], section: "main" },
{ href: "/settings", label: "settings", icon: Settings, permissions: ["tenant:manage"], section: "admin" },
];
/** Filtert die Navigation nach aktiven Modulen und Rechten der Session. */
export function visibleNavItems(
items: readonly NavItem[],
opts: { disabledModules: ReadonlySet<string>; permissions: readonly string[] },
): NavItem[] {
return items.filter(
(item) =>
(!item.module || !opts.disabledModules.has(item.module)) &&
(!item.permissions?.length || item.permissions.some((p) => opts.permissions.includes(p))),
);
}
-86
View File
@@ -1,86 +0,0 @@
/**
* M2 Strukturanalyse — Dedup-Erkennung & Autovervollständigung für (primäre)
* Informations-Assets (IMPL-tisax-wizard-neustruktur.md §2.1).
*
* `normalizeAssetName` erzeugt den Dedup-Schlüssel `Asset.normalizedName`, auf dem
* die `@@unique([tenantId, normalizedName])` Exakt-Duplikate verhindert. Bei einem
* Treffer wird das bestehende Asset VERKNÜPFT (ProcessAsset) statt neu angelegt.
*
* Die Normalisierung ist bewusst reine, DB-agnostische Logik (kein Prisma) — dieselbe
* Funktion läuft im Server (Schreibpfad + searchAssets) und im Client (Combobox), damit
* Vorschau und Persistenz denselben Schlüssel sehen.
*
* Reihenfolge (fix, §2.1):
* trim → Mehrfach-Whitespace kollabieren → Unicode-NFC → toLowerCase("de") →
* Umlaut-Faltung (ä→ae, ö→oe, ü→ue, ß→ss) → Satzzeichen entfernen.
*/
/** Umlaut-/ß-Faltung nach der Kleinschreibung (Reihenfolge relevant). */
function foldUmlauts(s: string): string {
return s
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss");
}
/**
* Erzeugt den normalisierten Dedup-Schlüssel eines Asset-Namens.
* Beispiel: „Kundendaten", „kundendaten", „ Kunden­daten " → alle `kundendaten`.
* Liefert `""` für leere/nur-Satzzeichen-Eingaben (Aufrufer behandelt das als „kein Schlüssel").
*/
export function normalizeAssetName(input: string): string {
if (!input) return "";
let s = input.trim();
// Weiches Trennzeichen / Zero-Width entfernen, bevor Whitespace kollabiert wird.
s = s.replace(/[­​-‍]/g, "");
s = s.replace(/\s+/g, " ");
s = s.normalize("NFC");
s = s.toLocaleLowerCase("de");
s = foldUmlauts(s);
// Satzzeichen/Sonderzeichen entfernen — nur Wortzeichen und Leerzeichen behalten,
// dann erneut Whitespace normalisieren und trimmen.
s = s.replace(/[^\p{L}\p{N} ]+/gu, "");
s = s.replace(/\s+/g, " ").trim();
return s;
}
/**
* Levenshtein-Distanz (DB-agnostische Fuzzy-Nähe, §2.1). Wird für die WEICHE Warnung
* („Meintest du …?") auf der Kandidatenliste genutzt — als Alternative zu Postgres
* `pg_trgm.similarity()` (siehe Integrationsbericht). Iterativ, O(n·m) Zeit, O(min)-Speicher.
*/
export function levenshtein(a: string, b: string): number {
if (a === b) return 0;
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
// Kürzere Zeichenkette als Spaltenachse (weniger Speicher).
if (a.length > b.length) [a, b] = [b, a];
let prev = Array.from({ length: a.length + 1 }, (_, i) => i);
let curr = new Array<number>(a.length + 1);
for (let j = 1; j <= b.length; j++) {
curr[0] = j;
for (let i = 1; i <= a.length; i++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
curr[i] = Math.min(prev[i] + 1, curr[i - 1] + 1, prev[i - 1] + cost);
}
[prev, curr] = [curr, prev];
}
return prev[a.length];
}
/**
* Ähnlichkeit 0..1 auf Basis der normalisierten Levenshtein-Distanz.
* 1 = identisch. Für die weiche Fuzzy-Warnung ab Schwelle (Default 0.82).
*/
export function nameSimilarity(a: string, b: string): number {
const na = normalizeAssetName(a);
const nb = normalizeAssetName(b);
if (!na && !nb) return 1;
const max = Math.max(na.length, nb.length);
if (max === 0) return 1;
return 1 - levenshtein(na, nb) / max;
}
/** Schwelle, ab der ein Kandidat als Nah-Duplikat gewarnt wird (§2.1). */
export const FUZZY_SIMILARITY_THRESHOLD = 0.82;
-45
View File
@@ -1,45 +0,0 @@
/**
* Objekt-Review (Story A3-1). Der Review-Status eines beliebigen Fach-Objekts wird aus
* seiner jüngsten Review-Aufgabe (Task, Typ `validation` bzw. `policy_approval`) abgeleitet
* — keine eigene Statusspalte je Modell. Regel „nur `validiert`/bestätigt zählt" (C9 §1):
* bestätigt ist ein Objekt erst mit abgeschlossener (DONE) Review-Aufgabe.
*/
export type ReviewState = "unbestaetigt" | "in_pruefung" | "bestaetigt" | "zurueckgewiesen";
export interface ReviewTaskLike {
status: string; // OPEN | DONE | REJECTED | CANCELLED
}
/** Review-Status aus der jüngsten Review-Aufgabe (oder keiner). */
export function reviewState(task?: ReviewTaskLike | null): ReviewState {
if (!task) return "unbestaetigt";
switch (task.status) {
case "DONE":
return "bestaetigt";
case "OPEN":
return "in_pruefung";
case "REJECTED":
return "zurueckgewiesen";
default:
return "unbestaetigt"; // CANCELLED / unbekannt
}
}
/** „Nur bestätigt zählt": ist das Objekt bestätigt? */
export function isConfirmed(task?: ReviewTaskLike | null): boolean {
return reviewState(task) === "bestaetigt";
}
export const REVIEW_LABEL: Record<ReviewState, string> = {
unbestaetigt: "Unbestätigt",
in_pruefung: "In Prüfung",
bestaetigt: "Bestätigt",
zurueckgewiesen: "Zurückgewiesen",
};
export const REVIEW_TONE: Record<ReviewState, "mut" | "info" | "ok" | "risk"> = {
unbestaetigt: "mut",
in_pruefung: "info",
bestaetigt: "ok",
zurueckgewiesen: "risk",
};
-63
View File
@@ -1,63 +0,0 @@
// Ableitung aus WizardFacts (Story B3): Fakten → Auswertungskontext → Flags/Aufgaben.
// Nutzt die B2-Regel-Engine. Wird von der Fragebogen-Komponente (bedingte Sichtbarkeit)
// und der saveFacts-Action (Propagation) gemeinsam verwendet. Client-safe (reine Logik).
import type { Answer, EvalResult, RuleContext } from "@/lib/rules/dsl";
import { evaluateRules, makeContext } from "@/lib/rules/engine";
import { ALL_RULES } from "@/lib/rules/feature-rules";
import { DIRECT_FLAG_MAP } from "@/lib/onboarding/questions";
export interface FactLike {
key: string;
value: unknown;
}
/** Fakten → Antwort-Map (key → Answer). */
export function factsToAnswers(facts: FactLike[]): Record<string, Answer> {
const out: Record<string, Answer> = {};
for (const f of facts) out[f.key] = f.value as Answer;
return out;
}
const DEFAULT_PRUEFZIELE = ["informationssicherheit"];
/**
* Kontext für Engine/Anzeige-Bedingungen: Antworten + abgeleitete Flags + Prüfziele.
* `seedFlags` sind zentral vorgegebene Start-Flags (A2-1: `FLAG_HIGH/VERY_HIGH_PROTECTION`
* aus dem Assessment-Level). `pruefziele` kommen zentral aus `WizardScope` (Scoping,
* A2-2) — **einzige Quelle**, nicht aus dem Fragebogen. Prüfziel-Regeln (pz-datenschutz/
* pz-prototypenschutz) leiten daraus FLAG_PERSONAL_DATA/FLAG_PROTOTYPE_PROTECTION ab.
*/
export function deriveContext(
facts: FactLike[],
seedFlags: Record<string, boolean> = {},
pruefziele: string[] = DEFAULT_PRUEFZIELE,
): RuleContext {
const answers = factsToAnswers(facts);
const base = evaluateRules(ALL_RULES, makeContext({ answers, pruefziele, flags: seedFlags }));
const flags = { ...base.flags };
// Direkt gesetzte Flags (nicht über Q-FEAT-Regeln), z. B. FLAG_INCLUDE_SHOULD.
for (const [qid, flag] of Object.entries(DIRECT_FLAG_MAP)) {
if (qid in answers) flags[flag] = answers[qid] === true;
}
// ISB intern/extern aus dem Modus-Fakt.
const modus = answers["Q-ROLE-ISB-MODUS"];
if (modus === "intern" || modus === "extern") {
flags.FLAG_ISB_INTERNAL = modus === "intern";
flags.FLAG_ISB_EXTERNAL = modus === "extern";
}
return { flags, answers, pruefziele };
}
/** Vollauswertung inkl. Aufgaben-Trigger (für saveFacts-Propagation). */
export function deriveResult(
facts: FactLike[],
seedFlags: Record<string, boolean> = {},
pruefziele: string[] = DEFAULT_PRUEFZIELE,
): EvalResult {
const answers = factsToAnswers(facts);
const res = evaluateRules(ALL_RULES, makeContext({ answers, pruefziele, flags: seedFlags }));
// Direkt-Flags in das Ergebnis spiegeln (Konsistenz mit deriveContext).
const ctx = deriveContext(facts, seedFlags, pruefziele);
return { ...res, flags: ctx.flags };
}
-115
View File
@@ -1,115 +0,0 @@
import type { Domain } from "@prisma/client";
/**
* ISMS-Funktionskatalog der Ebene 1 „Fundament" (M1). Anders als das read-only
* Rollen-Soll-Modell (zentrale `ROLE_*`-Variablen, nur in /settings pflegbar)
* beschreibt dies die im Wizard **zuweisbaren** Funktionen (Funktion → User(n)).
*
* `roleVar` verlinkt (falls vorhanden) die korrespondierende Zentralvariable,
* damit die Soll-Benennung als Vorschlag/Abgleich sichtbar bleibt. `domain` ist
* der Default-Bereich der Funktion (Sichtbarkeitssteuerung Ebene 3). `multi`
* kennzeichnet Funktionen mit mehreren Haltern (Asset-/Risk-Owner-Kennzeichnung).
*/
export interface FunctionDef {
key: string;
label: string;
desc: string;
/** Default-Bereich (Ebene-3-Sichtbarkeit). */
domain: Domain | null;
/** Korrespondierende zentrale Rollen-Variable (read-only Soll-Benennung), falls vorhanden. */
roleVar?: "ROLE_MANAGEMENT" | "ROLE_ISB" | "ROLE_IT_LEAD" | "ROLE_HR_LEAD" | "ROLE_DPO";
/** Mehrere Halter zulässig (z. B. Asset-/Risk-Owner). */
multi?: boolean;
/** Übliche Besetzung (Hinweis). */
besetzung: string;
}
export const FUNCTION_DEFS: readonly FunctionDef[] = [
{
key: "MANAGEMENT",
label: "Oberste Leitung",
desc: "Gesamtverantwortung ISMS, Ressourcen, Freigaben, Managementbewertung.",
domain: "GOVERNANCE",
roleVar: "ROLE_MANAGEMENT",
besetzung: "intern",
},
{
key: "ISB",
label: "Informationssicherheitsbeauftragte:r (ISB)",
desc: "Aufbau/Pflege ISMS, Beratung der Leitung, Risikomanagement, Audits; berichtet an die Leitung.",
domain: "GOVERNANCE",
roleVar: "ROLE_ISB",
besetzung: "intern oder extern",
},
{
key: "PM",
label: "Projekt-/Aufgabensteuerung (PM)",
desc: "Steuert die ISMS-Einführung, priorisiert und verfolgt Aufgaben über die Bereiche.",
domain: "GOVERNANCE",
besetzung: "intern",
},
{
key: "IT_LEAD",
label: "IT-Leitung",
desc: "Betrieb, technische Umsetzung der Maßnahmen.",
domain: "IT",
roleVar: "ROLE_IT_LEAD",
besetzung: "intern oder extern",
},
{
key: "HR_LEAD",
label: "Personalleitung",
desc: "Personalsicherheit, On-/Offboarding, Awareness.",
domain: "HR",
roleVar: "ROLE_HR_LEAD",
besetzung: "intern",
},
{
key: "BCM",
label: "BCM-Verantwortliche:r",
desc: "Business Continuity Management: Notfallvorsorge, Wiederanlauf, Tests.",
domain: "BCM",
besetzung: "intern oder extern",
},
{
key: "AUDITOR_INT",
label: "Unabhängige:r interne:r Auditor:in",
desc: "Führt interne Audits durch; muss vom ISB unabhängig sein (keine Selbstprüfung, AL3-Pflicht).",
domain: "COMPLIANCE",
besetzung: "intern oder extern",
},
{
key: "DPO",
label: "Datenschutzbeauftragte:r (DPO)",
desc: "Datenschutz-Compliance (bei personenbezogenen Daten).",
domain: "DATA_PROTECTION",
roleVar: "ROLE_DPO",
besetzung: "intern oder extern",
},
{
key: "ASSET_OWNER",
label: "Asset-Owner",
desc: "Verantwortet einzelne Werte/Assets (Schutzbedarf, Pflege). Mehrfachbesetzung.",
domain: null,
multi: true,
besetzung: "je Asset",
},
{
key: "RISK_OWNER",
label: "Risk-Owner",
desc: "Verantwortet einzelne Risiken (Bewertung, Behandlung, Akzeptanz). Mehrfachbesetzung.",
domain: null,
multi: true,
besetzung: "je Risiko",
},
] as const;
export const FUNCTION_KEYS = FUNCTION_DEFS.map((f) => f.key);
export function getFunctionDef(key: string): FunctionDef | undefined {
return FUNCTION_DEFS.find((f) => f.key === key);
}
export function isFunctionKey(key: string): boolean {
return FUNCTION_KEYS.includes(key);
}
-91
View File
@@ -1,91 +0,0 @@
// Fragenkatalog des Onboarding-Fragebogens (Story B3, Schritt 2 „context").
// Fachliche Grundlage: C2 §2–§7. Abschnitte A–F. Client-safe (nur Daten/Typen).
//
// Wirkung der Antworten läuft über die B2-Regel-Engine (feature-rules.ts):
// der Fragebogen schreibt nur WizardFacts + leitet daraus Flags/Aufgaben ab —
// NIE die gesperrten Zentralvariablen (Organisation/Rollen/Schutzbedarf → /settings).
//
// `showWhen` ist eine DSL-Condition und wird gegen den aus den Fakten abgeleiteten
// Kontext (Flags + Antworten) ausgewertet → bedingte Sichtbarkeit.
import type { Condition } from "@/lib/rules/dsl";
import { Q } from "@/lib/rules/feature-rules";
export type WizardSection = "A" | "B" | "C" | "D" | "E" | "F";
export type WizardQType = "boolean" | "text" | "select";
export interface WizardChoice {
value: string;
label: string;
}
export interface WizardQuestion {
id: string;
section: WizardSection;
label: string;
type: WizardQType;
choices?: WizardChoice[];
/** Vorbelegung (Baseline-Default in Abschnitt E). */
default?: string | boolean;
help?: string;
/** Anzeige-Bedingung (fehlt = immer sichtbar). */
showWhen?: Condition;
}
export const SECTION_TITLES: Record<WizardSection, string> = {
A: "A — Organisation & Geltungsbereich",
B: "B — Rollen",
C: "C — Governance & Betriebsrahmen",
D: "D — Feature-Fragen (steuern Flags, Klauseln, Controls, Risiken)",
E: "E — Technische Baseline (Defaults bestätigen/anpassen)",
F: "F — Dokument-Metadaten",
};
const flagOn = (flag: string): Condition => ({ kind: "flag", flag });
export const WIZARD_QUESTIONS: WizardQuestion[] = [
// — A — Organisation & Geltungsbereich —
// Datenschutz/Prototyp (ehem. Q-ORG-05/06) sind Prüfziele und werden im Scoping-Schritt
// (WizardScope, A2-2) gesetzt — read-only hier, keine zweite Quelle im Fragebogen.
// — B — Rollen —
{ id: Q.ISB_BENANNT, section: "B", type: "boolean", default: true, label: "Ist ein:e Informationssicherheitsbeauftragte:r (ISB) benannt?", help: "Bei 'nein' entsteht ein Aufgabenvorschlag 'ISB bestellen' (Control 1.2.2)." },
{ id: "Q-ROLE-ISB-MODUS", section: "B", type: "select", default: "intern", label: "ISB intern oder extern besetzt?", choices: [{ value: "intern", label: "intern" }, { value: "extern", label: "extern" }], showWhen: { kind: "answer", question: Q.ISB_BENANNT, equals: true } },
{ id: Q.ISB_IST_IT, section: "B", type: "boolean", default: false, label: "Ist der ISB zugleich für die IT-Verantwortung zuständig?", help: "Bei 'ja' entsteht ein Aufgabenvorschlag zur Funktionstrennung (C7)." },
{ id: "Q-ROLE-05-DPO", section: "B", type: "text", label: "Datenschutzbeauftragte:r (Name/Funktion)?", showWhen: flagOn("FLAG_PERSONAL_DATA") },
// — C — Governance & Betriebsrahmen —
{ id: "Q-GOV-05", section: "C", type: "boolean", default: true, label: "SOLL-Anforderungen (Reifegrad 3) einbeziehen?", help: "Blendet alle [SOLL]-Blöcke ein/aus (FLAG_INCLUDE_SHOULD)." },
{ id: "Q-GOV-04", section: "C", type: "select", default: "jährlich", label: "Revisions-/Prüfzyklus?", choices: [{ value: "jährlich", label: "jährlich" }, { value: "halbjährlich", label: "halbjährlich" }, { value: "quartalsweise", label: "quartalsweise" }] },
// — D — Feature-Fragen —
// Schutzbedarf (ehem. Q-FEAT-01) ist zentral: kommt aus dem Assessment-Level (AL2/AL3),
// read-only im Wizard (Story A2-1) — daher keine Fragebogen-Frage mehr.
{ id: Q.CLOUD, section: "D", type: "boolean", default: true, label: "Werden Cloud-Dienste genutzt?", help: "FLAG_CLOUD_USED · Controls 5.3.2/5.3.4/6.1.3 · Risiken R-CLOUD-*." },
{ id: Q.AI, section: "D", type: "boolean", default: false, label: "Werden KI-/GenAI-Dienste genutzt?", help: "FLAG_AI_USED · Control 5.3.4-KI · Risiko R-AI-*." },
{ id: Q.OT, section: "D", type: "boolean", default: false, label: "Produktions-/OT-Umgebung vorhanden?", help: "FLAG_OT_USED · Risiken R-OT-*." },
{ id: Q.DEV_INHOUSE, section: "D", type: "boolean", default: false, label: "Eigene Software-Entwicklung?", help: "FLAG_DEV_INHOUSE · Control 5.3.1 · Risiko R-DEV-*." },
{ id: Q.MOBILE_WORK, section: "D", type: "boolean", default: true, label: "Mobiles Arbeiten / Homeoffice zugelassen?", help: "FLAG_MOBILE_WORK · Control 2.1.4." },
{ id: Q.MOBILE_DEVICES, section: "D", type: "boolean", default: true, label: "Mobile Endgeräte / Datenträger im Einsatz?", help: "FLAG_MOBILE_DEVICES · Control 3.1.4 · Risiko R-PHY-mobile." },
{ id: Q.PKI, section: "D", type: "boolean", default: false, label: "Eigene PKI / Zertifikatsverwaltung?", help: "FLAG_CRYPTO_PKI · Control 5.1.1." },
{ id: Q.EXTERNAL_IT, section: "D", type: "boolean", default: true, label: "Externe IT-Dienstleister genutzt?", help: "FLAG_EXTERNAL_IT · Controls 6.1.1/6.1.3 · Aufgabe Selbstauskunft." },
{ id: Q.CUSTOMER_SYSTEMS, section: "D", type: "boolean", default: false, label: "Zugriff auf Kundensysteme (z. B. OEM)?", help: "FLAG_CUSTOMER_SYSTEMS · Controls 4.1.3/4.2.1/6.1.3." },
// — E — Technische Baseline (Defaults aus variables.schema.json / BL-*) —
{ id: "PW_MIN_LENGTH", section: "E", type: "text", default: "12", label: "Passwort-Mindestlänge (BL-IAM-01)" },
{ id: "MFA_SCOPE", section: "E", type: "text", default: "alle extern erreichbaren Dienste und privilegierten Konten", label: "MFA-Geltungsbereich (BL-IAM-02)" },
{ id: "PATCH_SLA_CRIT", section: "E", type: "text", default: "72 Stunden", label: "Patch-SLA kritisch (BL-OPS-01)" },
{ id: Q.RESTORE_GETESTET, section: "E", type: "boolean", default: true, label: "Wird die Wiederherstellung regelmäßig getestet? (BL-OPS-06)", help: "Bei 'nein' entsteht ein Aufgabenvorschlag 'Restore-Test etablieren' (Control 5.2.9)." },
{ id: "PENTEST_FREQ", section: "E", type: "text", default: "jährlich", label: "Penetrationstest-Frequenz (BL-OPS-08)", showWhen: flagOn("FLAG_ELEVATED_PROTECTION") },
{ id: "TECH_MDM", section: "E", type: "text", default: "das eingesetzte MDM", label: "MDM-Lösung (BL-EP)", showWhen: flagOn("FLAG_MOBILE_DEVICES") },
{ id: "TECH_VPN", section: "E", type: "text", default: "VPN", label: "VPN/Remote-Access-Lösung", showWhen: flagOn("FLAG_MOBILE_WORK") },
// — F — Dokument-Metadaten —
{ id: "DOC_VERSION", section: "F", type: "text", default: "1.0", label: "Dokument-Version" },
{ id: "DOC_STATUS", section: "F", type: "select", default: "Entwurf", label: "Dokument-Status", choices: [{ value: "Entwurf", label: "Entwurf" }, { value: "In Freigabe", label: "In Freigabe" }, { value: "Freigegeben", label: "Freigegeben" }] },
];
/** Booleans, die direkt ein Flag setzen (nicht über die Q-FEAT-Regeln der Engine). */
export const DIRECT_FLAG_MAP: Record<string, string> = {
"Q-GOV-05": "FLAG_INCLUDE_SHOULD",
};
-62
View File
@@ -1,62 +0,0 @@
// Einklink-Punkt für die echten Schritt-Komponenten in die Step-Registry
// (Contracts §5). Muss NACH der Default-Registrierung (registry.ts) laufen — daher als
// eigenes Modul, das page.tsx nach dem Registry-Import als Side-Effect lädt. „Letzte
// Registrierung je key gewinnt", also überschreibt dies die Platzhalter.
//
// TISAX-Neustruktur:
// Ebene 1 „Fundament" (M1): context → scope → policy → roles → criteria
// Ebene 2 „Strukturanalyse" (v3A): processes (Prozesshaus + BIA-Popup je Prozess)
// danach: controls. (gap/readiness wandern in den separaten Audit-Wizard, M4.)
//
// TISAX v3A: Die früheren eigenständigen Schritte information/assets/protection/risks
// sind ENTFALLEN — ihre Funktion lebt jetzt im BIA-Popup je Prozess (Prozess-Matrix).
// Die Standalone-Module /assets, /risks, /processes bleiben unangetastet erreichbar.
import { registerStep } from "@/lib/onboarding/registry";
import { ScopingStep } from "@/app/(app)/onboarding/steps/scoping/step";
import { ContextStep } from "@/app/(app)/onboarding/steps/context/step";
import { RolesStep } from "@/app/(app)/onboarding/steps/roles/step";
import { PoliciesStep } from "@/app/(app)/onboarding/steps/policies/step";
import { CriteriaStep } from "@/app/(app)/onboarding/steps/criteria/step";
import { ProcessesStep } from "@/app/(app)/onboarding/steps/processes/step";
import { ControlsStep } from "@/app/(app)/onboarding/steps/controls/step";
// ── Ebene 1 „Fundament" ──────────────────────────────────────────────────────
// Schritt 1 „context": Fragebogen/Fakten (Story B3).
registerStep({ key: "context", title: "stepTitle.context", order: 1, component: ContextStep });
// Schritt 2 „scope": Geltungsbereich, Assessment-Level, Standorte/Labels — eingefroren.
registerStep({ key: "scope", title: "stepTitle.scope", order: 2, component: ScopingStep });
// Schritt 3 „policy": Leitlinie & Management-Commitment (verlinkt Richtlinien-Modul).
registerStep({
key: "policy",
title: "stepTitle.policy",
order: 3,
guard: (ctx) => ctx.enabledModules.has("policies"),
component: PoliciesStep,
});
// Schritt 4 „roles": Team / Funktionszuordnung (echte Zuweisung Funktion → User(n)).
registerStep({ key: "roles", title: "stepTitle.roles", order: 4, component: RolesStep });
// Schritt 5 „criteria": Risikoakzeptanzkriterien + C/I/A-Schutzbedarfsskala.
registerStep({ key: "criteria", title: "stepTitle.criteria", order: 5, component: CriteriaStep });
// ── Ebene 2 „Strukturanalyse": prozessgeführter Flow ─────────────────────────
// Schritt 6 „processes": Prozesshaus (Bahnen Management/Kern/Unterstützung). Je
// Prozess-Kachel öffnet ein geführtes BIA-Popup (Informationswert → Träger →
// Schutzbedarf → Risiken → Abschluss). Ersetzt die früheren Einzelschritte.
registerStep({
key: "processes",
title: "stepTitle.processes",
order: 6,
guard: (ctx) => ctx.enabledModules.has("bia"),
component: ProcessesStep,
});
// ── Umsetzung ────────────────────────────────────────────────────────────────
registerStep({ key: "controls", title: "stepTitle.controls", order: 7, component: ControlsStep });
// M4: Die früheren Schritte gap und readiness sind aus dem Onboarding herausgelöst
// und bilden jetzt den separaten, scharfschaltbaren Audit-Wizard (/audit-readiness).
-91
View File
@@ -1,91 +0,0 @@
import type { ComponentType } from "react";
import type { ObjectReviewStatus } from "@prisma/client";
import { PlaceholderStep } from "@/app/(app)/onboarding/steps/placeholder";
/**
* Step-Registry des Onboarding-Wizards (Contracts §5 — Owner: Dev A, B konsumiert).
* Stabile Schnittstelle: `registerStep({ key, title, order, guard, component })`.
*
* TISAX-Neustruktur: Ebene 1 „Fundament" (M1) ordnet die Setup-Schritte neu —
* `context` → `scope` → `policy` → `roles` → `criteria`; `scope`/`policy` lösen die
* früheren Keys `scoping`/`policies` ab. Danach folgt der prozessgeführte Schritt
* `processes` (Prozesshaus) und zuletzt `controls`.
*
* TISAX v3A: Die früheren eigenständigen Schritte `information`, `assets`, `protection`
* und `risks` sind ENTFALLEN — ihre Funktion lebt jetzt im BIA-Popup je Prozess
* (Kunde: „an der Prozess-Matrix langhangeln, nicht losgelöst"). Die zugehörigen
* Standalone-Module (`/assets`, `/risks`, `/processes`) bleiben unangetastet erreichbar.
*
* M4: Die Schritte `gap` und `readiness` sind aus dem Onboarding herausgelöst und
* bilden jetzt den separaten, scharfschaltbaren Audit-Wizard
* (`src/lib/audit-readiness/*`, Route `/audit-readiness`).
*/
export const STEP_KEYS = [
"context",
"scope",
"policy",
"roles",
"criteria",
// Ebene 2 „Strukturanalyse": prozessgeführter Flow (Prozesshaus + BIA-Popup je Prozess).
"processes",
"controls",
// gap/readiness sind in den Audit-Wizard (/audit-readiness) ausgelagert (M4).
] as const;
export type StepKey = (typeof STEP_KEYS)[number];
/** Kontext, den `guard` zur Sichtbarkeitsentscheidung erhält (erweiterbar, z. B. Scope-Flags in A2). */
export interface StepCtx {
enabledModules: Set<string>;
}
/** Props, die jede Schritt-Komponente erhält. Schritte laden weitere Daten selbst (RSC). */
export interface StepProps {
stepKey: StepKey;
status: ObjectReviewStatus;
}
export interface StepDef {
key: StepKey;
/** i18n-Schlüssel unter `onboarding.stepTitle.*`. */
title: string;
/** 1-basierte Reihenfolge (fixe Wizard-Nummerierung). */
order: number;
/** true = Schritt sichtbar/aktiv. Fehlt = immer sichtbar. */
guard?: (ctx: StepCtx) => boolean;
component: ComponentType<StepProps>;
}
const registry = new Map<StepKey, StepDef>();
/** Registriert (oder überschreibt) einen Schritt. Letzte Registrierung je `key` gewinnt. */
export function registerStep(def: StepDef): void {
registry.set(def.key, def);
}
/** Default-Registrierung aller Schritte mit Platzhalter-Komponenten (A1-1). */
for (const [i, key] of STEP_KEYS.entries()) {
registerStep({
key,
title: `stepTitle.${key}`,
order: i + 1,
// Der Richtlinien-Schritt „policy" ist nur bei aktivem Richtlinien-Modul sichtbar.
guard: key === "policy" ? (ctx) => ctx.enabledModules.has("policies") : undefined,
component: PlaceholderStep,
});
}
export function getStep(key: StepKey): StepDef | undefined {
return registry.get(key);
}
/** Alle Schritte in fixer Reihenfolge. */
export function getOrderedSteps(): StepDef[] {
return [...registry.values()].sort((a, b) => a.order - b.order);
}
/** Sichtbare Schritte für den gegebenen Kontext (guard entscheidet über Ausblenden). */
export function getVisibleSteps(ctx: StepCtx): StepDef[] {
return getOrderedSteps().filter((s) => !s.guard || s.guard(ctx));
}
-70
View File
@@ -1,70 +0,0 @@
import type { ObjectReviewStatus } from "@prisma/client";
import type { StepDef, StepKey } from "@/lib/onboarding/registry";
/**
* State-Machine je Wizard-Schritt (Story A1-1/F2, Contracts §2) auf Basis des
* generalisierten `ObjectReviewStatus`:
* Bearbeiter: offen → in_bearbeitung → zur_validierung
* Validator: zur_validierung → validiert | zur_validierung → zurueckgewiesen
* Bearbeiter: zurueckgewiesen → in_bearbeitung (Nacharbeit)
* „Nur `validiert` zählt als bestätigt." Das „Weiter"-Gate ist gesperrt, solange
* der Vorgänger-Schritt nicht `validiert` ist.
*/
export function isValidated(status: ObjectReviewStatus): boolean {
return status === "validiert";
}
/** Nächster Status einer **Bearbeiter**-Aktion (advance) — oder null, wenn nicht anwendbar. */
export function advanceTarget(status: ObjectReviewStatus): ObjectReviewStatus | null {
switch (status) {
case "offen":
return "in_bearbeitung";
case "in_bearbeitung":
return "zur_validierung";
case "zurueckgewiesen":
return "in_bearbeitung"; // Nacharbeit
default:
return null; // zur_validierung / validiert: keine Bearbeiter-Aktion
}
}
/** Ist der Schritt aktuell einer Validierungs-Entscheidung zugänglich? */
export function isAwaitingValidation(status: ObjectReviewStatus): boolean {
return status === "zur_validierung";
}
export type StatusOf = (key: StepKey) => ObjectReviewStatus;
/**
* Bearbeitung/Navigation ist von der Validierung **entkoppelt**: jeder sichtbare Schritt
* ist jederzeit zugänglich und bearbeitbar — auch wenn Vorgänger noch nicht `validiert`
* sind. So lässt sich der komplette Wizard parallel durcharbeiten, während die Freigaben
* (Validierung) unabhängig und zeitversetzt erfolgen. Der frühere „Weiter"-Gate-Zwang
* (alle Vorgänger validiert) bleibt als weiche Anzeige erhalten (`predecessorsValidated`),
* blockiert aber nicht mehr.
*/
export function isStepAccessible(): boolean {
return true;
}
/**
* Weiche Signalfunktion: sind alle sichtbaren Vorgänger bereits `validiert`? Dient nur
* noch der Anzeige (z. B. Hinweis „Freigaben stehen noch aus"), nicht der Sperre.
*/
export function predecessorsValidated(steps: StepDef[], statusOf: StatusOf, index: number): boolean {
return steps.slice(0, index).every((s) => isValidated(statusOf(s.key)));
}
/** Resume-Punkt: erster nicht-`validiert`er sichtbarer Schritt (oder letzter, wenn alle fertig). */
export function firstIncompleteIndex(steps: StepDef[], statusOf: StatusOf): number {
const i = steps.findIndex((s) => !isValidated(statusOf(s.key)));
return i === -1 ? Math.max(steps.length - 1, 0) : i;
}
/** Fortschritt in Prozent (validierte sichtbare Schritte / gesamt). */
export function progressPercent(steps: StepDef[], statusOf: StatusOf): number {
if (steps.length === 0) return 0;
const done = steps.filter((s) => isValidated(statusOf(s.key))).length;
return Math.round((done / steps.length) * 100);
}
-260
View File
@@ -1,260 +0,0 @@
import Handlebars from "handlebars";
import { marked } from "marked";
import sanitizeHtml from "sanitize-html";
/**
* Rendering-Engine für Richtlinien & Verfahren (VDA-ISA 2027 Vorlagenpaket).
*
* Pipeline (§7 der Spezifikation):
* 1. {{LINK:…}} vorab auflösen (Doppelpunkt/#-Ziele würde Handlebars sonst brechen).
* 2. Handlebars mit noEscape:true → verschachtelte {{#if FLAG}} und {{VARIABLE}}.
* 3. Lesemodus-Nachbearbeitung: Hidden-Anker entfernen, wiederholtes
* „Umsetzung bei <Org>" reduzieren, zentral verwaltete Abschnitte
* unterdrücken (§7a.3), BL-Referenzen entfernen (Wert bleibt, §7).
* 4. Markdown → HTML (marked).
*/
export type RenderContext = Record<string, string | boolean>;
/**
* HTML-Sonderzeichen in einen ungefährlichen Text kodieren (F-03).
* Wird auf alle String-Variablenwerte angewandt, BEVOR sie über Handlebars
* (`noEscape: true`) roh in das Markdown gelangen. So kann ein Variablenwert
* kein rohes HTML (`<img onerror=…>`) in die spätere marked-Ausgabe einschleusen.
*/
export function escapeHtml(s: string): string {
return s.replace(
/[<>&"']/g,
(c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&#39;" }[c]!),
);
}
/**
* Baut den Render-Kontext (Variablen + Flags) aus den PolicyVariable-Zeilen.
* String-Werte werden HTML-kodiert (F-03), Boolean-Flags bleiben Boolean, damit
* die `{{#if FLAG}}`-Logik unverändert funktioniert.
*/
export function buildContext(vars: { key: string; kind: string; value: string }[]): RenderContext {
const ctx: RenderContext = {};
for (const v of vars) ctx[v.key] = v.kind === "boolean" ? v.value === "true" : escapeHtml(v.value);
// Framework-Flags (gemeinsame Bibliothek ISO/TISAX): Fehlen sie — Bestandsmandant, dessen
// Vorlagenpaket noch nicht neu importiert wurde —, gilt die bisherige TISAX-Sicht. Ohne diesen
// Vorbelegungsschritt wuerden die {{#if FLAG_FW_TISAX}}-Bloecke bei ihnen leer rendern.
if (ctx.FLAG_FW_TISAX === undefined) ctx.FLAG_FW_TISAX = true;
if (ctx.FLAG_FW_ISO27001 === undefined) ctx.FLAG_FW_ISO27001 = false;
return ctx;
}
/**
* Setzt die effektiven Schutzbedarf-/TISAX-Flags (Delta-Update):
* - HIGH ist im TISAX-Modell stets aktiv (AL2-Baseline schließt HOHER ein).
* - VERY_HIGH = Dokument-Override (AL2/AL3) sonst globaler Wert (AL3-Schalter).
* - ELEVATED = HIGH || VERY_HIGH (hier stets true) — wird abgeleitet, nie manuell.
*/
export function applyProtection(ctx: RenderContext, override?: string | null): RenderContext {
const global = ctx.FLAG_VERY_HIGH_PROTECTION === true;
const veryHigh = override === "AL3" ? true : override === "AL2" ? false : global;
ctx.FLAG_HIGH_PROTECTION = true;
ctx.FLAG_VERY_HIGH_PROTECTION = veryHigh;
ctx.FLAG_ELEVATED_PROTECTION = true;
return ctx;
}
/** Effektiver TISAX-Level für Anzeige: AL3 wenn VERY_HIGH aktiv, sonst AL2. */
export function effectiveLevel(globalVeryHigh: boolean, override?: string | null): "AL2" | "AL3" {
const veryHigh = override === "AL3" ? true : override === "AL2" ? false : globalVeryHigh;
return veryHigh ? "AL3" : "AL2";
}
// Abschnitte, die das Tool zentral verwaltet und daher im Lese-/Exportmodus ausblendet (§7a.3)
const SUPPRESS_SECTIONS = [
"Verwandte Dokumente",
"Nachweise",
"Überprüfung und Aktualisierung",
"Verbindlichkeit",
];
/** {{LINK:ZIEL}} → interner Deep-Link nach /policies. */
export function resolveLink(target: string): { href: string; label: string } {
const [code, anchor] = target.split("#");
const q = (c: string) => `/policies/${encodeURIComponent(c)}${anchor ? `#${anchor}` : ""}`;
switch (code) {
case "NACHWEISREGISTER":
return { href: "/policies/NACHWEIS", label: "Nachweisregister" };
case "ISA_MAPPING":
return { href: "/policies?view=coverage", label: "ISA-Mapping-Matrix" };
case "BASELINE":
return { href: "/policies/BASELINE", label: "Technische Sicherheits-Baseline" };
// In Fachmodule überführte Register (GAP-Konsolidierung) — kein eigenes Dokument mehr.
case "REG-EXT-SERVICES":
return { href: "/suppliers?tab=services", label: "IT-Services (Lieferantenmodul)" };
case "REG-SW-WHITELIST":
return { href: "/suppliers?tab=software", label: "Software-Freigaben (Lieferantenmodul)" };
case "REG-CRIT-SERVICES":
return { href: "/assets?view=critical", label: "Kritische IT-Dienste (Assetinventar)" };
case "REG-PROJECTS":
return { href: "/assets?type=PROJECT", label: "Projekte (Assetinventar)" };
default:
return { href: q(code), label: anchor ? `${code} §${anchor}` : code };
}
}
function resolveLinks(md: string): string {
return md.replace(/\{\{LINK:([^}]+)\}\}/g, (_m, target: string) => {
const { href, label } = resolveLink(target.trim());
return `[${label}](${href})`;
});
}
function stripHiddenAnchors(md: string): string {
return md.replace(/<!--[\s\S]*?-->/g, "");
}
/** §7a.1 — wiederkehrende Zwischenüberschrift „Umsetzung bei <Org>" auf ein dezentes „Umsetzung:" reduzieren. */
function reduceImplHeadings(md: string): string {
return md.replace(/^\*\*Umsetzung bei .+?\*\*\s*$/gm, "_Umsetzung:_");
}
/** §7a.3 — zentral verwaltete Abschnitte ausblenden (## bis zur nächsten ##-Überschrift). */
function suppressSections(md: string): string {
const lines = md.split("\n");
const out: string[] = [];
let skipping = false;
for (const line of lines) {
const h2 = line.match(/^##\s+(?:\d+\.?\s*)?(.+?)\s*$/);
if (h2) {
const title = h2[1].trim();
skipping = SUPPRESS_SECTIONS.some((s) => title.toLowerCase().startsWith(s.toLowerCase()));
}
if (!skipping) out.push(line);
}
return out.join("\n");
}
/**
* §7 — Baseline-Referenzen (BL-…) im Lesemodus entfernen; nur der konkrete
* Variablenwert bleibt. Behandelt Klammerzusätze „(BL-IAM-01)", Kombiformen
* „(BL-CRY-01/04)", einleitende Wendungen „nach/gemäß/laut BL-…" und Resttokens.
*/
function stripBaselineTokens(md: string): string {
// "(… BL-XXX-01 …)" — Klammern, die BL-Referenzen enthalten, ganz entfernen
md = md.replace(/\s*\([^()]*BL-[A-Z]+-\d+[^()]*\)/g, "");
// "nach/gemäß/laut BL-XXX-01" (auch /04-Kombi)
md = md.replace(/\s*(?:nach|gemäß|gemaess|laut|per)\s+BL-[A-Z]+-\d+(?:\/\d+)?/gi, "");
// verbleibende bloße Tokens
md = md.replace(/\s*BL-[A-Z]+-\d+(?:\/\d+)?/g, "");
return md;
}
/** Nachsichtiger Fallback-Renderer (rekursiv, verschachtelungsfähig) für defekte Vorlagen. */
function lenientRender(md: string, ctx: RenderContext): string {
const ifPat = /\{\{#if (\w+)\}\}((?:(?!\{\{#if )(?!\{\{\/if\}\})[\s\S])*?)\{\{\/if\}\}/g;
let prev = "";
while (prev !== md) {
prev = md;
md = md.replace(ifPat, (_m, flag: string, inner: string) => (ctx[flag] ? inner : ""));
}
return md.replace(/\{\{(\w+)\}\}/g, (_m, key: string) => (key in ctx ? String(ctx[key]) : ""));
}
export interface RenderOptions {
readMode?: boolean; // Default true — Lesefassung ohne Anker/BL/verwaltete Abschnitte
stripBaseline?: boolean; // Default = readMode; für das Baseline-Register selbst false
}
/** Vorlagen-Markdown → gerenderte Markdown-Lesefassung. */
export function renderPolicyMarkdown(raw: string, ctx: RenderContext, opts: RenderOptions = {}): string {
const readMode = opts.readMode ?? true;
const stripBaseline = opts.stripBaseline ?? readMode;
let md = resolveLinks(raw);
try {
const tpl = Handlebars.compile(md, { noEscape: true });
md = tpl(ctx);
} catch {
// Defensiv: beschädigte Tokens dürfen keine ganze Seite zerlegen —
// nachsichtiger, verschachtelungsfähiger Fallback (analog _verify.py).
md = lenientRender(md, ctx);
}
if (readMode) {
md = stripHiddenAnchors(md);
md = reduceImplHeadings(md);
md = suppressSections(md);
if (stripBaseline) md = stripBaselineTokens(md);
}
return md.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
}
/**
* Sanitizing-Allowlist für die gerenderte Richtlinien-HTML (F-03).
*
* Wirksamste Einzelmaßnahme: das von `marked` erzeugte HTML wird gegen eine
* strikte Tag-/Attribut-Allowlist gefiltert, bevor es per
* `dangerouslySetInnerHTML` eingebunden wird. Damit werden rohes HTML aus dem
* Vorlagen-Markdown, aus Template-Variablen und aus dem Upload-Titel entschärft.
*
* Bibliothekswahl: `sanitize-html` statt `dompurify` + `jsdom`. Das Rendering
* läuft ausschließlich serverseitig (alle Aufrufer von `renderPolicyHtml` sind
* Server-Komponenten, kein `"use client"`), daher ist die serverseitig gebaute,
* jsdom-freie und deutlich schlankere `sanitize-html`-Bibliothek die passendere
* Wahl gegenüber der schweren jsdom-Abhängigkeit.
*
* Erlaubt ist genau das Vokabular, das das Vorlagenpaket erzeugt: Überschriften,
* Absätze, Listen, Textauszeichnung, Tabellen (inkl. Ausrichtung/Spannen),
* Links, Trennlinien. URIs sind auf http(s)/mailto sowie interne, relative Ziele
* (`/…`, `#…`) begrenzt — `javascript:` & Co. werden verworfen.
*/
const SANITIZE_OPTS: sanitizeHtml.IOptions = {
allowedTags: [
"h1", "h2", "h3", "h4", "h5", "h6",
"p", "br", "hr",
"ul", "ol", "li",
"strong", "em", "b", "i", "del", "s", "code", "pre", "blockquote",
"table", "thead", "tbody", "tfoot", "tr", "th", "td",
"a",
],
allowedAttributes: {
a: ["href", "title", "name"],
th: ["colspan", "rowspan", "align"],
td: ["colspan", "rowspan", "align"],
},
// Nur sichere Schemata; relative Ziele (/… , #…) erlaubt sanitize-html per Default.
allowedSchemes: ["http", "https", "mailto"],
allowProtocolRelative: false,
// Kommentare/verbotene Tags samt Inhalt verwerfen statt Text durchreichen.
disallowedTagsMode: "discard",
};
/** Vorlagen-Markdown → sanitisierte HTML (Lesemodus). */
export function renderPolicyHtml(raw: string, ctx: RenderContext, opts: RenderOptions = {}): string {
const md = renderPolicyMarkdown(raw, ctx, opts);
const html = marked.parse(md, { async: false }) as string;
// Ausgabekodierung/Sanitizing als letzte, autoritative Schicht (F-03).
return sanitizeHtml(html, SANITIZE_OPTS);
}
/**
* Zerlegt ein Dokument in Titel, Info-Tabelle (Kopf) und Rumpf (ab erster
* `## `-Überschrift). Ermöglicht die einklappbare Dokumenten-Info (§7a.5).
*/
export function splitPolicyDoc(raw: string): { title: string; infoMd: string; bodyMd: string } {
const titleM = raw.match(/^#\s+(.+?)\s*$/m);
const title = titleM ? titleM[1].trim() : "";
const idx = raw.search(/^##\s+/m);
const head = idx >= 0 ? raw.slice(0, idx) : raw;
const bodyMd = idx >= 0 ? raw.slice(idx) : "";
const tableM = head.match(/(\|[^\n]*\|\n(?:\|[^\n]*\|\n?)+)/);
return { title, infoMd: tableM ? tableM[1] : "", bodyMd };
}
/**
* Prüft eine gerenderte Fassung auf Rückstände (offene {{…}}, «MISS»-Marker).
* Referenz: `_verify.py`-Gate über alle Flag-Kombinationen.
*/
export function findRenderResidue(rendered: string): string[] {
const issues: string[] = [];
const open = rendered.match(/\{\{[^}]*\}\}/g);
if (open) issues.push(...open);
return issues;
}
-64
View File
@@ -1,64 +0,0 @@
/**
* Klartext-Namen für die internen Richtlinien- und Verfahrenscodes (Fachcontent-Legende
* C5 §5). Dient dazu, im UI die internen Identifier (L00, R01–R14, VA-01…VA-19) durch
* menschenlesbare Bezeichnungen zu ersetzen, statt die Codes anzuzeigen.
*/
const POLICY_NAMES: Record<string, string> = {
L00: "Informationssicherheits-Leitlinie",
R01: "ISMS-Organisation & Rollen",
R02: "Umgang mit Informationswerten",
R03: "Risikomanagement & Wirksamkeitsprüfung",
R04: "Incident-, Notfall- & Kontinuitätsmanagement",
R05: "Personalsicherheit",
R06: "Mobiles Arbeiten & mobile Geräte",
R07: "Physische Sicherheit & Zonenkonzept",
R08: "Zugriffssteuerung (IAM)",
R09: "Kryptografie & Kommunikationssicherheit",
R10: "IT-Betriebssicherheit",
R11: "Systementwicklung & Beschaffung",
R12: "Cloud, externe IT-Dienste & KI",
R13: "Lieferantenmanagement",
R14: "Compliance, Recht & Datenschutz",
};
const VERFAHREN_NAMES: Record<string, string> = {
"VA-01": "Ereignisbehandlung",
"VA-02": "Notfall- & Kontinuitätsmanagement",
"VA-03": "Benutzer- & Berechtigungsverwaltung",
"VA-04": "Change-Management",
"VA-05": "Backup & Wiederherstellung",
"VA-06": "Schwachstellen-, Patch- & Audit-Management",
"VA-07": "Kryptografie & sichere Übertragung",
"VA-08": "Asset-Inventarisierung & Klassifizierung",
"VA-09": "Risikomanagement",
"VA-10": "Lieferantensteuerung",
"VA-11": "Cloud- & KI-Freigabe",
"VA-12": "Schulung & Sensibilisierung",
"VA-13": "Protokollierung & Monitoring",
"VA-14": "Personalprozess",
"VA-15": "Interne Audits & Compliance-Prüfung",
"VA-16": "Sichere Entwicklung & Beschaffung",
"VA-17": "Zutrittsmanagement",
"VA-18": "Rechts- & Datenschutzkataster",
"VA-19": "Projektklassifizierung",
};
/** Klartext-Name einer Richtlinie (L00/R01…R14); fällt auf den Code zurück. */
export function policyTitle(code: string): string {
return POLICY_NAMES[code] ?? code;
}
/** Klartext-Name eines Verfahrens (VA-xx); fällt auf den Code zurück. */
export function verfahrenTitle(code: string): string {
return VERFAHREN_NAMES[code] ?? code;
}
/** Mehrere Codes als lesbare, kommagetrennte Namensliste. */
export function policyTitles(codes: string[]): string {
return codes.map(policyTitle).join(", ");
}
export function verfahrenTitles(codes: string[]): string {
return codes.map(verfahrenTitle).join(", ");
}
-15
View File
@@ -1,15 +0,0 @@
/**
* Abgrenzung „zentraler" Richtlinien-Variablen (nur in den Einstellungen pflegbar,
* im Richtlinien-Editor gesperrt): Organisation (Unternehmensname/Scope, …),
* verantwortliche Rollen (ROLE_*) und die Schutzbedarf-Flags. Alles andere
* (dokument-/paketspezifische Variablen, eigene) bleibt im Editor pflegbar.
*
* Reine Funktion ohne Server-/Render-Abhängigkeiten (auch in Actions nutzbar).
*/
export const CENTRAL_VARIABLE_GROUPS = new Set(["Organisation", "Rollen"]);
export const CENTRAL_VARIABLE_KEYS = new Set(["FLAG_HIGH_PROTECTION", "FLAG_VERY_HIGH_PROTECTION"]);
export function isCentralVariable(v: { key: string; groupName?: string | null }): boolean {
if (v.groupName && CENTRAL_VARIABLE_GROUPS.has(v.groupName)) return true;
return CENTRAL_VARIABLE_KEYS.has(v.key);
}
-118
View File
@@ -1,118 +0,0 @@
// Assessment-Readiness — Reifegrad-Interpretation + dynamische nächste Schritte
// (Story B7-1, Fachcontent C9 §1/§2). Client-safe, rein/testbar. Die Aggregation
// der Control-Reifegrade speist sich aus dem Control-Assessment (Schritt 7, Story A7);
// bis A7 vorliegt, liefert `computeReadiness` ohne Assessments `avgReifegrad = null`
// und blendet nur die schon berechenbaren Kennzahlen/Schritte ein.
export type ReadinessBand = "Aufbau" | "Etabliert im Aufbau" | "Assessment-nah" | "Assessment-reif";
export interface BandDef {
band: ReadinessBand;
tone: "risk" | "warn" | "info" | "ok";
text: string;
}
/** C9 §1 — Interpretations-Textbänder nach Gesamtreifegrad (Ø 0–3). */
export const REIFEGRAD_BANDS: Record<ReadinessBand, BandDef> = {
"Aufbau": { band: "Aufbau", tone: "risk", text: "Das ISMS befindet sich im Aufbau. Wesentliche Richtlinien/Verfahren sind noch zu erstellen oder freizugeben. Fokus: Muss-Anforderungen und Grundstruktur." },
"Etabliert im Aufbau": { band: "Etabliert im Aufbau", tone: "warn", text: "Grundlegende Prozesse sind vorhanden und teils dokumentiert. Für Assessment-Reife fehlen v. a. Nachweise der gelebten Anwendung und Validierungen." },
"Assessment-nah": { band: "Assessment-nah", tone: "info", text: "Das ISMS ist überwiegend etabliert. Wenige offene Punkte und Nachweise trennen von der Assessment-Reife (AL-Ziel). Fokus: Hoch-Punkte schließen, Wirksamkeitsnachweise ergänzen." },
"Assessment-reif": { band: "Assessment-reif", tone: "ok", text: "Die bewerteten Controls erfüllen den Zielreifegrad. Empfehlung: Stichprobenvalidierung und Aktualität der Nachweise vor dem Assessment sicherstellen." },
};
/** Ø-Reifegrad → Textband (C9 §1: <1,5 · 1,5–<2,5 · 2,5–<3,0 · =3,0). */
export function interpretationBand(avg: number): BandDef {
if (avg < 1.5) return REIFEGRAD_BANDS["Aufbau"];
if (avg < 2.5) return REIFEGRAD_BANDS["Etabliert im Aufbau"];
if (avg < 3.0) return REIFEGRAD_BANDS["Assessment-nah"];
return REIFEGRAD_BANDS["Assessment-reif"];
}
/** Eine bewertete Teilanforderung/Control (aus Schritt 7, Story A7). */
export interface ControlAssessment {
control: string;
chapter: string; // z. B. "1" | "8" | "9"
reifegrad: number; // 0–3
bestaetigt: boolean; // validiert (A3) — nur bestätigte zählen als erfüllt
}
/**
* Ø-Reifegrad (C9 §1): „unbestätigt zählt nicht als erfüllt" → unbestätigte Controls
* gehen mit 0 in den Schnitt ein. Ohne Assessments → null.
*/
export function averageReifegrad(assessments: ControlAssessment[]): number | null {
if (assessments.length === 0) return null;
const sum = assessments.reduce((s, a) => s + (a.bestaetigt ? a.reifegrad : 0), 0);
return sum / assessments.length;
}
/** Ø-Reifegrad je Kapitel (nur Kapitel mit Assessments). */
export function averageByChapter(assessments: ControlAssessment[]): { chapter: string; avg: number; count: number }[] {
const byCh = new Map<string, ControlAssessment[]>();
for (const a of assessments) {
const list = byCh.get(a.chapter) ?? [];
list.push(a);
byCh.set(a.chapter, list);
}
return [...byCh.entries()]
.map(([chapter, list]) => ({ chapter, avg: averageReifegrad(list) ?? 0, count: list.length }))
.sort((a, b) => a.chapter.localeCompare(b.chapter, undefined, { numeric: true }));
}
export interface NextStepsInput {
/** Offene Aufgaben mit Priorität „hoch". */
openHighCount: number;
/** Objekte, die auf Validierung warten (unbestätigt). */
unvalidatedCount: number;
/** Prüfziel Prototypenschutz aktiv und 8.x noch unter Ziel. */
prototypeGap: boolean;
/** Operative Nachweis-Upload-Iteration noch offen. */
evidenceUploadPending: boolean;
}
/** C9 §2 — dynamische, regelbasierte nächste Schritte. */
export function buildNextSteps(i: NextStepsInput): string[] {
const steps: string[] = [];
if (i.openHighCount > 0) steps.push(`Vor dem Assessment zwingend: die ${i.openHighCount} Hoch-Punkte im Maßnahmenplan abarbeiten (siehe Aufgaben).`);
if (i.unvalidatedCount > 0) steps.push(`${i.unvalidatedCount} Objekte warten auf Validierung durch ISB/Berater — bis dahin als „unbestätigt" gewertet.`);
if (i.prototypeGap) steps.push("Prototypenspezifische Nachweise ergänzen (physische Schutzmaßnahmen, Zutritt, Transport).");
if (i.evidenceUploadPending) steps.push("Operative Nachweise (Screenshots/Protokolle) in der Folgeiteration hochladen.");
return steps;
}
export interface ReadinessInput {
assessments: ControlAssessment[];
bestaetigtAnteil: number; // 0..1 Anteil bestätigter Controls
offenJePrioritaet: { hoch: number; mittel: number; niedrig: number };
pruefzielAbdeckung: { pruefziel: string; anforderungen: number }[];
zielReifegrad: number; // i. d. R. 3
nextSteps: NextStepsInput;
}
export interface ReadinessSummary {
avgReifegrad: number | null;
band: BandDef | null;
byChapter: { chapter: string; avg: number; count: number }[];
bestaetigtAnteil: number;
offenJePrioritaet: { hoch: number; mittel: number; niedrig: number };
pruefzielAbdeckung: { pruefziel: string; anforderungen: number }[];
zielReifegrad: number;
nextSteps: string[];
/** true, solange keine Control-Bewertungen vorliegen (A7 ausstehend). */
assessmentPending: boolean;
}
export function computeReadiness(input: ReadinessInput): ReadinessSummary {
const avg = averageReifegrad(input.assessments);
return {
avgReifegrad: avg,
band: avg === null ? null : interpretationBand(avg),
byChapter: averageByChapter(input.assessments),
bestaetigtAnteil: input.bestaetigtAnteil,
offenJePrioritaet: input.offenJePrioritaet,
pruefzielAbdeckung: input.pruefzielAbdeckung,
zielReifegrad: input.zielReifegrad,
nextSteps: buildNextSteps(input.nextSteps),
assessmentPending: input.assessments.length === 0,
};
}
-37
View File
@@ -1,37 +0,0 @@
// Client-safe Helfer der Dokumentenlenkung (AP5): Prüfzyklus → nächster Prüftermin
// und Versions-Bump. Keine Node-/DB-APIs.
/** Prüfzyklen und ihr Abstand in Monaten. */
export const REVIEW_CYCLES: Record<string, number> = {
monatlich: 1,
quartalsweise: 3,
halbjährlich: 6,
jährlich: 12,
"2-jährlich": 24,
};
export const REVIEW_CYCLE_KEYS = Object.keys(REVIEW_CYCLES);
/** Nächsten Prüftermin aus Startdatum + Zyklus berechnen (null, wenn kein/kein bekannter Zyklus). */
export function computeNextReview(from: Date, cycle: string | null): Date | null {
if (!cycle) return null;
const months = REVIEW_CYCLES[cycle];
if (!months) return null;
const d = new Date(from);
d.setMonth(d.getMonth() + months);
return d;
}
/** „Prüfung fällig", wenn ein Termin gesetzt und erreicht/überschritten ist. */
export function isReviewDue(nextReviewAt: Date | null, now: Date = new Date()): boolean {
return !!nextReviewAt && new Date(nextReviewAt).getTime() <= now.getTime();
}
/** Minor-Version erhöhen: „1.0" → „1.1", „2.9" → „2.10", „3" → „3.1". */
export function bumpMinorVersion(version: string): string {
const parts = version.split(".");
if (parts.length < 2) return `${version}.1`;
const last = parseInt(parts[parts.length - 1], 10);
if (isNaN(last)) return `${version}.1`;
parts[parts.length - 1] = String(last + 1);
return parts.join(".");
}
-37
View File
@@ -1,37 +0,0 @@
/** Score-Bänder der 5×5-Matrix (Farben/Stufen aus dem Mockup). */
export type RiskLevelKey = "low" | "medium" | "elevated" | "high" | "critical";
export function riskLevel(score: number): RiskLevelKey {
if (score >= 20) return "critical";
if (score >= 15) return "high";
if (score >= 10) return "elevated";
if (score >= 5) return "medium";
return "low";
}
/** Zellen-Hintergrund der Heatmap — kräftige Ampelfarben (auf Dunkel kontrastreich). */
export const RISK_CELL_COLORS: Record<RiskLevelKey, string> = {
low: "#2ea86b",
medium: "#e3b427",
elevated: "#e2802e",
high: "#e0553f",
critical: "#d63c5e",
};
/** Sättigungsstarke Farbskala grün→rot für den Risiko-Score-Balken. */
export const RISK_SCALE_GRADIENT =
"linear-gradient(90deg,#2ec76b 0%,#e3b427 32%,#e2802e 60%,#e0553f 82%,#d63c5e 100%)";
/** Pillen-Ton je Stufe — gemäß Heatmap-Skala: Grün → Gelb → Orange → Rot. */
export const RISK_PILL_TONE: Record<RiskLevelKey, "ok" | "warn" | "orange" | "risk"> = {
low: "ok",
medium: "warn",
elevated: "orange",
high: "risk",
critical: "risk",
};
export function riskRef(refNo: number): string {
return `R-${String(refNo).padStart(3, "0")}`;
}
-69
View File
@@ -1,69 +0,0 @@
// Regel-/Mapping-DSL (Story F3, Contracts §7 / Backlog-Foundation #3).
// Vertrag: Bedingung(Antwort | Scope | Flag) → Wirkung(Klausel ein/aus · Control ·
// Risiko/Asset-Bezug · Aufgabe). Rein deklarativ + serialisierbar; die Auswertung
// erfolgt deterministisch in engine.ts. Keine UI, keine Seiteneffekte.
/** Antwortwert einer Wizard-Frage (Q-*). */
export type Answer = string | number | boolean | null;
/** Auswertungskontext: gesetzte Flags, Antworten je Frage, aktive Prüfziele. */
export interface RuleContext {
/** FLAG_* → true/false (z. B. aus dem Fragebogen abgeleitet). */
flags: Record<string, boolean>;
/** Frage-ID (Q-*) → Antwort. */
answers: Record<string, Answer>;
/** Aktive Prüfziele: informationssicherheit | prototypenschutz | datenschutz. */
pruefziele: string[];
}
/**
* Bedingung. Atomar (flag/answer/pruefziel) oder zusammengesetzt (all/any/not).
* `equals` bei flag ist optional (Default: true).
*/
export type Condition =
| { kind: "flag"; flag: string; equals?: boolean }
| { kind: "answer"; question: string; equals: Answer }
| { kind: "answerIn"; question: string; oneOf: Answer[] }
| { kind: "pruefziel"; value: string }
| { kind: "all"; of: Condition[] }
| { kind: "any"; of: Condition[] }
| { kind: "not"; of: Condition };
/**
* Wirkung auf die vier Kanäle (C2 §1). Alle Felder optional; die Engine
* aggregiert die Wirkungen aller zutreffenden Regeln.
*/
export interface RuleEffect {
/** Kanal Flag → steuert {{#if FLAG_X}}-Klauseln (ein/aus). */
setFlags?: Record<string, boolean>;
/** Kanal Control: in den Scope geschaltete Controls (Anforderungs-IDs). */
controls?: string[];
/** Kanal Risiko: vorgeschlagene Katalog-Risiken (R-*). */
risks?: string[];
/** Kanal Asset: betroffene Asset-Bezüge (optional). */
assets?: string[];
/** Kanal Aufgabe: Trigger-IDs aus src/lib/task-triggers.ts. */
tasks?: string[];
}
/** Eine Regel: wenn `when` zutrifft, gilt `effect`. */
export interface Rule {
id: string;
/** Fachliche Referenz (z. B. "C2 §5 Q-FEAT-02"). */
source: string;
when: Condition;
effect: RuleEffect;
}
/** Aggregiertes Auswertungsergebnis über alle zutreffenden Regeln. */
export interface EvalResult {
/** Vereinigte gesetzte Flags (inkl. abgeleitetem FLAG_ELEVATED_PROTECTION). */
flags: Record<string, boolean>;
controls: string[];
risks: string[];
assets: string[];
/** Trigger-IDs für Aufgabenvorschläge (proposeTasksFromTriggers). */
tasks: string[];
/** IDs der Regeln, die ausgelöst haben (für Nachvollziehbarkeit/Tests). */
matched: string[];
}
-62
View File
@@ -1,62 +0,0 @@
// Regel-Engine (Story B2). Wertet die DSL-Regeln (dsl.ts) deterministisch gegen
// einen RuleContext aus und aggregiert die Wirkungen der vier Kanäle. Rein,
// seiteneffektfrei, unit-testbar (scripts/test-rules.ts).
import type { Condition, EvalResult, Rule, RuleContext } from "./dsl";
/** Wertet eine Bedingung gegen den Kontext aus. Unbekannte Antworten = false. */
export function evaluateCondition(cond: Condition, ctx: RuleContext): boolean {
switch (cond.kind) {
case "flag":
return (ctx.flags[cond.flag] ?? false) === (cond.equals ?? true);
case "answer":
return ctx.answers[cond.question] === cond.equals;
case "answerIn":
return cond.oneOf.includes(ctx.answers[cond.question] ?? null);
case "pruefziel":
return ctx.pruefziele.includes(cond.value);
case "all":
return cond.of.every((c) => evaluateCondition(c, ctx));
case "any":
return cond.of.some((c) => evaluateCondition(c, ctx));
case "not":
return !evaluateCondition(cond.of, ctx);
}
}
const uniq = (xs: string[]) => Array.from(new Set(xs));
/**
* Wertet alle Regeln aus und aggregiert die Wirkungen. FLAG_ELEVATED_PROTECTION
* wird abgeleitet (HIGH || VERY_HIGH) und nie aus Regeln direkt gesetzt.
* Reihenfolge-stabil: Ergebnislisten in Regel-Reihenfolge, dedupliziert.
*/
export function evaluateRules(rules: Rule[], ctx: RuleContext): EvalResult {
const flags: Record<string, boolean> = { ...ctx.flags };
const controls: string[] = [];
const risks: string[] = [];
const assets: string[] = [];
const tasks: string[] = [];
const matched: string[] = [];
for (const rule of rules) {
if (!evaluateCondition(rule.when, ctx)) continue;
matched.push(rule.id);
const e = rule.effect;
if (e.setFlags) for (const [k, v] of Object.entries(e.setFlags)) flags[k] = v;
if (e.controls) controls.push(...e.controls);
if (e.risks) risks.push(...e.risks);
if (e.assets) assets.push(...e.assets);
if (e.tasks) tasks.push(...e.tasks);
}
// Abgeleitetes Flag (Contracts §3): HIGH oder VERY_HIGH aktiv.
flags.FLAG_ELEVATED_PROTECTION = Boolean(flags.FLAG_HIGH_PROTECTION || flags.FLAG_VERY_HIGH_PROTECTION);
return { flags, controls: uniq(controls), risks: uniq(risks), assets: uniq(assets), tasks: uniq(tasks), matched };
}
/** Bequemer Kontext-Builder mit Defaults. */
export function makeContext(partial?: Partial<RuleContext>): RuleContext {
return { flags: {}, answers: {}, pruefziele: ["informationssicherheit"], ...partial };
}
-141
View File
@@ -1,141 +0,0 @@
// Regeldaten (Story B2) — maschinenlesbare Form der C2-Antwort→Wirkung-Matrix.
// Quelle: C2 §5 (Q-FEAT-01…10, Feature-Flags → Klausel/Control/Risiko/Aufgabe),
// C2 §2 (Q-ORG-05/06, Prüfziel-steuernde Flags) und C2 §8 (Aufgaben-Trigger).
// Trigger-IDs referenzieren src/lib/task-triggers.ts (Story B1).
import type { Rule } from "./dsl";
// Stabile Antwort-Keys, die der Fragebogen (B3) füllt. Zentral, damit B3 und
// die Regeln denselben Schlüssel verwenden.
export const Q = {
// Q-FEAT-01 (Schutzbedarf) entfällt als Fragebogen-Antwort: FLAG_HIGH/VERY_HIGH_PROTECTION
// kommen ausschließlich aus dem zentralen Assessment-Level (Story A2-1, Contracts §3).
CLOUD: "Q-FEAT-02",
AI: "Q-FEAT-03",
OT: "Q-FEAT-04",
DEV_INHOUSE: "Q-FEAT-05",
MOBILE_WORK: "Q-FEAT-06",
MOBILE_DEVICES: "Q-FEAT-07",
PKI: "Q-FEAT-08",
EXTERNAL_IT: "Q-FEAT-09",
CUSTOMER_SYSTEMS: "Q-FEAT-10",
// Q-ORG-05/06 (Datenschutz/Prototyp) entfallen als Fragebogen-Antwort: die Prüfziele
// kommen zentral aus WizardScope (Scoping, A2-2) — keine zweite Quelle.
ISB_BENANNT: "Q-ROLE-02_ISB_BENANNT", // boolean
ISB_IST_IT: "Q-ROLE_ISB_IST_IT", // boolean (ISB = IT-Verantwortung)
RESTORE_GETESTET: "Q-BL-11_RESTORE_GETESTET", // boolean
} as const;
/** C2 §5 + §2: Feature-Antworten → Flag/Control/Risiko-Wirkung.
* Hinweis (A2-1): FLAG_HIGH/VERY_HIGH_PROTECTION werden NICHT aus dem Fragebogen
* abgeleitet, sondern zentral aus dem Assessment-Level geseedet (protectionFlags). */
export const FEATURE_RULES: Rule[] = [
{
id: "feat02-cloud",
source: "C2 §5 Q-FEAT-02",
when: { kind: "answer", question: Q.CLOUD, equals: true },
effect: { setFlags: { FLAG_CLOUD_USED: true }, controls: ["5.3.2", "5.3.4", "6.1.3"], risks: ["R-CLOUD"] },
},
{
id: "feat03-ai",
source: "C2 §5 Q-FEAT-03",
when: { kind: "answer", question: Q.AI, equals: true },
effect: { setFlags: { FLAG_AI_USED: true }, controls: ["5.3.4-KI"], risks: ["R-AI"] },
},
{
id: "feat04-ot",
source: "C2 §5 Q-FEAT-04",
when: { kind: "answer", question: Q.OT, equals: true },
effect: { setFlags: { FLAG_OT_USED: true }, risks: ["R-OT"] },
},
{
id: "feat05-dev",
source: "C2 §5 Q-FEAT-05",
when: { kind: "answer", question: Q.DEV_INHOUSE, equals: true },
effect: { setFlags: { FLAG_DEV_INHOUSE: true }, controls: ["5.3.1"], risks: ["R-DEV"] },
},
{
id: "feat06-mobile-work",
source: "C2 §5 Q-FEAT-06",
when: { kind: "answer", question: Q.MOBILE_WORK, equals: true },
effect: { setFlags: { FLAG_MOBILE_WORK: true }, controls: ["2.1.4"] },
},
{
id: "feat07-mobile-devices",
source: "C2 §5 Q-FEAT-07",
when: { kind: "answer", question: Q.MOBILE_DEVICES, equals: true },
effect: { setFlags: { FLAG_MOBILE_DEVICES: true }, controls: ["3.1.4"], risks: ["R-PHY-mobile"] },
},
{
id: "feat08-pki",
source: "C2 §5 Q-FEAT-08",
when: { kind: "answer", question: Q.PKI, equals: true },
effect: { setFlags: { FLAG_CRYPTO_PKI: true }, controls: ["5.1.1"] },
},
{
id: "feat09-external-it",
source: "C2 §5 Q-FEAT-09",
when: { kind: "answer", question: Q.EXTERNAL_IT, equals: true },
effect: { setFlags: { FLAG_EXTERNAL_IT: true }, controls: ["6.1.1", "6.1.3"], risks: ["R-SUP"] },
},
{
id: "feat10-customer-systems",
source: "C2 §5 Q-FEAT-10",
when: { kind: "answer", question: Q.CUSTOMER_SYSTEMS, equals: true },
effect: { setFlags: { FLAG_CUSTOMER_SYSTEMS: true }, controls: ["4.1.3", "4.2.1", "6.1.3"] },
},
// Prüfziel-getriebene Wirkung — Single Source: WizardScope (Scoping-Schritt, A2-2).
// Ersetzt die früheren Q-ORG-05/06-Fragebogenfragen (Doppelquelle vermieden).
{
id: "pz-datenschutz",
source: "Scoping/WizardScope — Prüfziel Datenschutz (C2 §2)",
when: { kind: "pruefziel", value: "datenschutz" },
effect: { setFlags: { FLAG_PERSONAL_DATA: true }, controls: ["7.1.2"], risks: ["R-DSGVO"] },
},
{
id: "pz-prototypenschutz",
source: "Scoping/WizardScope — Prüfziel Prototypenschutz (C2 §2)",
when: { kind: "pruefziel", value: "prototypenschutz" },
effect: { setFlags: { FLAG_PROTOTYPE_PROTECTION: true } },
},
];
/** C2 §8: Antworten → Aufgaben-Trigger (IDs aus task-triggers.ts). */
export const TASK_RULES: Rule[] = [
{
id: "task-isb-not-named",
source: "C2 §8 Q-ROLE-02",
when: { kind: "answer", question: Q.ISB_BENANNT, equals: false },
effect: { tasks: ["isb_not_named"] },
},
{
id: "task-isb-equals-it",
source: "C2 §8 Q-ROLE-02/03",
when: { kind: "answer", question: Q.ISB_IST_IT, equals: true },
effect: { tasks: ["isb_equals_it"] },
},
{
id: "task-external-it",
source: "C2 §8 Q-FEAT-09",
when: { kind: "answer", question: Q.EXTERNAL_IT, equals: true },
effect: { tasks: ["external_it_yes"] },
},
{
id: "task-cloud-ai-approval",
source: "C2 §8 Q-FEAT-02/03",
when: { kind: "any", of: [
{ kind: "answer", question: Q.CLOUD, equals: true },
{ kind: "answer", question: Q.AI, equals: true },
] },
effect: { tasks: ["cloud_ai_without_approval"] },
},
{
id: "task-no-restore-test",
source: "C2 §8 Q-BL-11",
when: { kind: "answer", question: Q.RESTORE_GETESTET, equals: false },
effect: { tasks: ["no_restore_test"] },
},
];
/** Vollständiges Regelwerk (Feature- + Aufgaben-Regeln). */
export const ALL_RULES: Rule[] = [...FEATURE_RULES, ...TASK_RULES];
-78
View File
@@ -1,78 +0,0 @@
import c1 from "../../seed/scoping/c1-scope.json";
/**
* Scope-Filter (Story A2-2). Reine, seiteneffektfreie Logik: gegeben die aktiven
* Prüfziele und die gesetzten Flags liefert er die im Scope liegenden Anforderungen
* nach der C1-Methodik.
*
* Datengrundlage: `seed/scoping/c1-scope.json` (aus `C1_Scoping-AL-Pruefziel.md`,
* `id` = `mapping.json`-`id`). Generierung: `scripts/build-c1-scope.ts`.
*
* Gate-Logik:
* 1. **Prüfziel** — die Anforderung ist nur im Scope, wenn ihr Prüfziel aktiv ist
* (Informationssicherheit stets; Prototypenschutz/Datenschutz nur bei Auswahl,
* Kapitel 8.x bzw. 9.x).
* 2. **Flag-Bedingung** — SOLL → `FLAG_INCLUDE_SHOULD`, HOCH → `FLAG_HIGH_PROTECTION`,
* SEHR HOCH → `FLAG_VERY_HIGH_PROTECTION`. Diese Schutzbedarf-Flags kommen zentral
* aus dem Assessment-Level (A2-1: HIGH ist AL2-Baseline, VERY_HIGH nur AL3).
*
* Hinweis: Die C1-Spalten AL2/AL3 sind informativ mitgeführt; gegated wird über die
* Flag-Bedingung, weil im Produktmodell die AL2-Baseline HOCH einschließt
* (Admin: „AL2 — MUSS·SOLL·HOCH / AL3 — zusätzlich SEHR HOCH").
*/
export type Pruefziel = "informationssicherheit" | "prototypenschutz" | "datenschutz";
export type RequirementType = "MUSS" | "SOLL" | "HOCH" | "SEHR HOCH";
export interface C1Row {
id: string;
control: string;
type: RequirementType;
al2: boolean;
al3: boolean;
pruefziel: Pruefziel;
condition: string | null;
}
/** Vollständige C1-Anforderungsliste (412 Zeilen). */
export const C1_ROWS = c1 as C1Row[];
export interface ScopeInput {
/** Aktive Prüfziele. Informationssicherheit ist immer aktiv. */
pruefziele: Pruefziel[];
/** Gesetzte Feature-/Schutzbedarf-Flags (HIGH/VERY_HIGH aus dem Assessment-Level, INCLUDE_SHOULD aus dem Fragebogen). */
flags: Record<string, boolean>;
}
/** Liegt eine Anforderung im aktuellen Scope? */
export function isRequirementActive(r: C1Row, input: ScopeInput): boolean {
if (!input.pruefziele.includes(r.pruefziel)) return false; // Prüfziel-Gate (Kapitel)
if (r.condition && !input.flags[r.condition]) return false; // Flag-Bedingung (SOLL/HOCH/SEHR HOCH)
return true;
}
/** Alle aktiven Anforderungen im Scope. */
export function activeRequirements(input: ScopeInput, rows: C1Row[] = C1_ROWS): C1Row[] {
return rows.filter((r) => isRequirementActive(r, input));
}
export interface ScopeSummary {
total: number;
byType: Record<RequirementType, number>;
byPruefziel: Record<string, number>;
controls: number;
}
/** Kennzahlen des aktiven Scopes (für die Wizard-Vorschau). */
export function scopeSummary(input: ScopeInput, rows: C1Row[] = C1_ROWS): ScopeSummary {
const active = activeRequirements(input, rows);
const byType: Record<RequirementType, number> = { MUSS: 0, SOLL: 0, HOCH: 0, "SEHR HOCH": 0 };
const byPruefziel: Record<string, number> = {};
const controls = new Set<string>();
for (const r of active) {
byType[r.type]++;
byPruefziel[r.pruefziel] = (byPruefziel[r.pruefziel] ?? 0) + 1;
controls.add(r.control);
}
return { total: active.length, byType, byPruefziel, controls: controls.size };
}
-78
View File
@@ -1,78 +0,0 @@
// Client-safe Helfer für die Anwendbarkeitserklärung (SoA, AP3). Keine Node-/DB-APIs,
// damit Seite (Client-Badges) und Export (Server) dieselbe Logik teilen.
export const SOA_STATUS = ["umgesetzt", "teilweise", "geplant"] as const;
export type SoaStatus = (typeof SOA_STATUS)[number];
export const SOA_STATUS_LABEL: Record<string, string> = {
umgesetzt: "umgesetzt",
teilweise: "teilweise",
geplant: "geplant",
};
/** Für die Vollständigkeitsprüfung nötige Felder einer SoA-Zeile. */
export interface SoaCompletable {
applicable: boolean;
justification: string;
}
/**
* Vollständigkeit nach ISO/IEC 27001 6.1.3 d): jede Zeile — ob einbezogen ODER
* ausgeschlossen — braucht eine Begründung. Ohne Begründung gilt der Control als
* unvollständig (im Zertifizierungsaudit angreifbar).
*/
export function isSoaEntryComplete(e: SoaCompletable): boolean {
return e.justification.trim().length > 0;
}
/** Sortierschlüssel für Control-IDs wie „A.5.15" (numerisch je Segment). */
export function soaControlSortKey(control: string): number[] {
return control.replace(/^A\./, "").split(".").map((s) => parseInt(s.replace(/\D/g, ""), 10) || 0);
}
export function compareSoaControls(a: string, b: string): number {
const ka = soaControlSortKey(a); const kb = soaControlSortKey(b);
for (let i = 0; i < Math.max(ka.length, kb.length); i++) {
if ((ka[i] ?? 0) !== (kb[i] ?? 0)) return (ka[i] ?? 0) - (kb[i] ?? 0);
}
return a.localeCompare(b);
}
// ── Export (CSV/XLSX teilen dieselben Spalten/Zeilen) ─────────────────────────
export const SOA_EXPORT_COLUMNS = [
"Control", "Titel", "Anwendbar", "Begründung (Einbezug/Ausschluss)",
"Herkunft/Quelle", "Umsetzungsstatus", "Richtlinie", "Verantwortlich", "Nachweis", "Vollständig",
] as const;
/** Anzeigefertige Eingabe für eine Export-Zeile (Server hat IDs bereits aufgelöst). */
export interface SoaExportInput {
control: string;
title: string | null;
applicable: boolean;
justification: string;
source: string | null;
implementationStatus: string;
policyCode: string | null;
ownerName: string | null;
evidenceLabel: string | null;
}
export function soaExportRow(e: SoaExportInput): string[] {
return [
e.control,
e.title ?? "",
e.applicable ? "ja" : "nein",
e.justification,
e.source ?? "",
SOA_STATUS_LABEL[e.implementationStatus] ?? e.implementationStatus,
e.policyCode ?? "",
e.ownerName ?? "",
e.evidenceLabel ?? "",
isSoaEntryComplete(e) ? "ja" : "NEIN",
];
}
/** CSV aus Spalten + Zeilen (RFC-4180-Quoting). */
export function toSoaCsv(rows: SoaExportInput[]): string {
const esc = (v: string) => (/[",\n;]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
const lines = [SOA_EXPORT_COLUMNS.join(";"), ...rows.map((r) => soaExportRow(r).map(esc).join(";"))];
return "" + lines.join("\r\n");
}
-45
View File
@@ -1,45 +0,0 @@
import type { Prisma } from "@prisma/client";
/**
* Gemeinsame Prisma-Includes für das Lieferanten- und IT-Service-Cockpit.
* Werden sowohl auf /suppliers als auch auf /assets verwendet, damit die
* Fach-Popups an beiden Stellen mit identischer Datenform gerendert werden.
*/
export const SUPPLIER_INCLUDE = {
supplierProfile: true,
contracts: true,
ndas: true,
evidence: true,
assessments: true,
subcontractors: true,
decisions: true,
maturity: true,
riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true, score: true } } } },
relationsFrom: { include: { relatedAsset: { select: { name: true, confidentiality: true, integrity: true, availability: true } } } },
relationsTo: { include: { asset: { select: { name: true, confidentiality: true, integrity: true, availability: true } } } },
} satisfies Prisma.AssetInclude;
export const SERVICE_INCLUDE = {
serviceProfile: { include: { provider: { select: { id: true, name: true } } } },
raci: { orderBy: { controlRef: "asc" } },
riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true, score: true } } } },
relationsFrom: { include: { relatedAsset: { select: { id: true, name: true, type: true, confidentiality: true, integrity: true, availability: true } } } },
relationsTo: { include: { asset: { select: { id: true, name: true } } } },
processAssets: { include: { process: { select: { id: true, name: true } } } },
} satisfies Prisma.AssetInclude;
export const SOFTWARE_INCLUDE = {
softwareProfile: { include: { provider: { select: { id: true, name: true } } } },
riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true, score: true } } } },
relationsFrom: { include: { relatedAsset: { select: { id: true, name: true, type: true, confidentiality: true, integrity: true, availability: true } } } },
relationsTo: { include: { asset: { select: { id: true, name: true } } } },
} satisfies Prisma.AssetInclude;
export const PROJECT_INCLUDE = {
projectProfile: true,
owner: { select: { id: true, name: true } },
riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true, score: true } } } },
relationsFrom: { include: { relatedAsset: { select: { id: true, name: true, type: true, confidentiality: true, integrity: true, availability: true } } } },
relationsTo: { include: { asset: { select: { id: true, name: true } } } },
processAssets: { include: { process: { select: { id: true, name: true } } } },
} satisfies Prisma.AssetInclude;
-193
View File
@@ -1,193 +0,0 @@
/**
* Anforderungs-Engine für Lieferanten (VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)).
* Schutzbedarf → Anforderungsstufe; flacher Anforderungskatalog mit
* Status-Ableitung aus den hinterlegten Objekten; Gate-Logik & Konformität.
*/
export function supplierRef(refNo: number): string {
return `L-${String(refNo).padStart(3, "0")}`;
}
export function serviceRef(refNo: number): string {
return `S-${String(refNo).padStart(3, "0")}`;
}
export function softwareRef(refNo: number): string {
return `SW-${String(refNo).padStart(3, "0")}`;
}
export function projectRef(refNo: number): string {
return `P-${String(refNo).padStart(3, "0")}`;
}
/**
* Hängt einen Query-Parameter an einen Basis-Pfad an und wählt automatisch
* das richtige Trennzeichen (`?` oder `&`). Erlaubt, Detail-/Bearbeiten-Popups
* sowohl auf /suppliers als auch auf /assets zu öffnen (backHref-Muster).
*/
export function withQuery(base: string, key: string, val: string): string {
return `${base}${base.includes("?") ? "&" : "?"}${key}=${val}`;
}
export const SUPPLIER_LIFECYCLE_TONE = {
ACTIVE: "ok",
ONBOARDING: "info",
UNDER_REVIEW: "warn",
OFFBOARDED: "mut",
} as const;
const DAY = 24 * 3600 * 1000;
export function isExpired(d: Date | null | undefined): boolean {
return !!d && d.getTime() < Date.now();
}
export function isExpiring(d: Date | null | undefined, days = 90): boolean {
return !!d && d.getTime() - Date.now() < days * DAY;
}
export function isReviewDue(d: Date | null | undefined, days = 30): boolean {
return !!d && d.getTime() < Date.now() + days * DAY;
}
/** Schutzbedarf 1(normal)/2(hoch)/3(sehr hoch) aus der höchsten C/I/A-Stufe. */
export type ProtectionLevel = 1 | 2 | 3;
export function protectionLevel(maxCia: number): ProtectionLevel {
if (maxCia >= 4) return 3;
if (maxCia >= 3) return 2;
return 1;
}
export const PROTECTION_LABEL: Record<ProtectionLevel, string> = {
1: "normal",
2: "hoch",
3: "sehr hoch",
};
/** Kontext für die Statusableitung — aus dem Lieferanten-Datensatz berechnet. */
export type ReqContext = {
hasValidNda: boolean;
ndaMonitored: boolean;
hasSecurityContract: boolean;
customerReqPassed: boolean;
hasFlowdown: boolean;
hasAssessment: boolean;
hasReviewedAssessment: boolean;
hasAdequateEvidence: boolean;
complianceMonitored: boolean;
hasLinkedRisk: boolean;
hasManagementDecision: boolean;
hasAudit: boolean;
};
export type Tier = "must" | "should" | "high" | "veryhigh";
export type ReqStatus = "met" | "partial" | "open" | "na";
export type ReqTheme = "contract" | "assessment" | "transparency";
export type Requirement = {
key: string;
label: string;
control: "6.1.1" | "6.1.2" | "6.1.3";
tier: Tier;
theme: ReqTheme;
met: (c: ReqContext) => boolean;
refLabel?: (c: ReqContext) => string | null;
};
export const REQUIREMENTS: Requirement[] = [
// Vertrag & Geheimhaltung
{ key: "nda_valid", label: "NDA abgeschlossen und gültig", control: "6.1.2", tier: "must", theme: "contract", met: (c) => c.hasValidNda },
{ key: "infosec_contract", label: "Vertragliche InfoSec-Vereinbarung geschlossen", control: "6.1.1", tier: "must", theme: "contract", met: (c) => c.hasSecurityContract },
{ key: "customer_req_passed", label: "Kundenanforderungen an Partner weitergegeben", control: "6.1.1", tier: "must", theme: "contract", met: (c) => c.customerReqPassed },
{ key: "flowdown", label: "Flow-down an Subunternehmer verpflichtet", control: "6.1.1", tier: "should", theme: "contract", met: (c) => c.hasFlowdown },
{ key: "nda_monitoring", label: "NDA-Fristen-Monitoring aktiv", control: "6.1.2", tier: "should", theme: "contract", met: (c) => c.ndaMonitored },
// Bewertung & Nachweise
{ key: "risk_assessment", label: "Sicherheits-Risikobewertung durchgeführt", control: "6.1.1", tier: "must", theme: "assessment", met: (c) => c.hasAssessment || c.hasLinkedRisk },
{ key: "reports_reviewed", label: "Service-Reports & Dokumente geprüft", control: "6.1.1", tier: "should", theme: "assessment", met: (c) => c.hasReviewedAssessment },
{ key: "adequate_evidence", label: "Nachweis angemessenes Sicherheitsniveau (Zertifikat/Audit)", control: "6.1.1", tier: "high", theme: "assessment", met: (c) => c.hasAdequateEvidence },
{ key: "compliance_monitored", label: "Compliance-Grad dokumentiert & anlassbezogen überwacht", control: "6.1.1", tier: "high", theme: "assessment", met: (c) => c.complianceMonitored },
// Transparenz / sehr hoch
{ key: "customer_transparency", label: "Kundentransparenz zu Lieferkettenrisiken erfüllt", control: "6.1.1", tier: "veryhigh", theme: "transparency", met: (c) => c.customerReqPassed },
];
const TIER_MIN_LEVEL: Record<Tier, ProtectionLevel> = { must: 1, should: 1, high: 2, veryhigh: 3 };
export function tierActive(tier: Tier, level: ProtectionLevel): boolean {
return level >= TIER_MIN_LEVEL[tier];
}
export function requirementStatus(req: Requirement, ctx: ReqContext, level: ProtectionLevel): ReqStatus {
if (!tierActive(req.tier, level)) return "na";
return req.met(ctx) ? "met" : "open";
}
/** Gate: sehr hoher Schutzbedarf ohne gültiges Audit/Label → Kompensation nötig. */
export function gateActive(level: ProtectionLevel, ctx: ReqContext): boolean {
return level === 3 && !ctx.hasAudit;
}
export function gateSatisfied(ctx: ReqContext): boolean {
return ctx.hasAudit || (ctx.hasManagementDecision && ctx.hasLinkedRisk);
}
export type Conformity = "conform" | "partial" | "gaps" | "action";
export function conformity(level: ProtectionLevel, ctx: ReqContext): Conformity {
if (gateActive(level, ctx) && !gateSatisfied(ctx)) return "action";
const active = REQUIREMENTS.filter((r) => tierActive(r.tier, level));
const met = active.filter((r) => r.met(ctx)).length;
if (met === active.length) return "conform";
if (met >= Math.ceil(active.length * 0.6)) return "partial";
return "gaps";
}
export const CONFORMITY_TONE: Record<Conformity, "ok" | "warn" | "risk"> = {
conform: "ok",
partial: "warn",
gaps: "risk",
action: "risk",
};
export const CONFORMITY_LABEL: Record<Conformity, string> = {
conform: "Konform",
partial: "Teilweise",
gaps: "Lücken offen",
action: "Handlung nötig",
};
/** Berechneter Reifegrad (0–3) aus Erfüllungsgrad der aktiven Anforderungen. */
export function computedMaturity(level: ProtectionLevel, ctx: ReqContext): number {
const active = REQUIREMENTS.filter((r) => tierActive(r.tier, level));
if (active.length === 0) return 0;
const met = active.filter((r) => r.met(ctx)).length;
return Math.round((met / active.length) * 3 * 10) / 10;
}
export const TARGET_MATURITY = 3.0;
type CtxInput = {
contracts: { securityClauses: boolean; flowdown: boolean; customerRequirementsPassed: boolean }[];
ndas: { validTo: Date | null }[];
evidence: { kind: string; adequacyChecked: boolean; validTo: Date | null }[];
assessments: { result: string | null; nextReview: Date | null }[];
subcontractors: { flowdownObligation: boolean }[];
nextReview: Date | null;
linkedRiskCount: number;
decisionCount: number;
};
/** Baut den ReqContext aus den hinterlegten Lieferanten-Objekten. */
export function buildReqContext(d: CtxInput): ReqContext {
const hasAudit = d.evidence.some(
(e) =>
(e.kind === "AUDIT_REPORT" || e.kind === "TISAX_LABEL") &&
e.adequacyChecked &&
!isExpired(e.validTo)
);
const hasAdequateEvidence = d.evidence.some((e) => e.adequacyChecked && !isExpired(e.validTo));
return {
hasValidNda: d.ndas.some((n) => !isExpired(n.validTo)),
ndaMonitored: d.ndas.length > 0 && !!d.nextReview,
hasSecurityContract: d.contracts.some((c) => c.securityClauses),
customerReqPassed: d.contracts.some((c) => c.customerRequirementsPassed),
hasFlowdown: d.contracts.some((c) => c.flowdown) || d.subcontractors.some((s) => s.flowdownObligation),
hasAssessment: d.assessments.length > 0,
hasReviewedAssessment: d.assessments.some((a) => !!a.result),
hasAdequateEvidence,
complianceMonitored: d.assessments.some((a) => !!a.nextReview),
hasLinkedRisk: d.linkedRiskCount > 0,
hasManagementDecision: d.decisionCount > 0,
hasAudit,
};
}
-131
View File
@@ -1,131 +0,0 @@
// Aufgaben-Trigger-Katalog (Story B1) — fachliche Grundlage: C2 §8.
// Bestimmte Wizard-Antworten/Zurückweisungen erzeugen einen Aufgaben-*Vorschlag*
// (status PROPOSED), den der Bearbeiter bestätigt oder verwirft. Dieser Katalog
// ist die maschinenlesbare Form der C2-§8-Tabelle; die vollständige Antwort→Wirkung-
// Matrix (alle vier DSL-Kanäle) folgt in der Regel-Engine (Story B2, src/lib/rules/**).
//
// `origin = "wizard:<id>"` macht die Erzeugung idempotent: pro Trigger existiert je
// Mandant höchstens ein offener Vorschlag/Aufgabe.
import type { Domain } from "@prisma/client";
import { defaultDomainForControl } from "./control-domain";
import type { TaskLinks, TaskPriority, TaskResources, TaskType } from "./tasks";
export interface GapTrigger {
/** Stabile ID → origin "wizard:<id>". */
id: string;
/** Wizard-Schritt, aus dem der Trigger typischerweise stammt (3/4/6/7 + Zurückweisung). */
step: number;
/** Auslösende Antwort/Bedingung (fachlich, C2 §8). */
condition: string;
taskType: TaskType;
title: string;
priority: TaskPriority;
/** Verknüpfungen (Control/Dokument/…) für die Nachweislage. */
links: TaskLinks;
/** Referenz (Control/Baseline/Verfahren) für die Anzeige. */
source: string;
/** Vorbelegte Ressourcen-Hinweise; im Vorschlag editierbar. */
resourcesHint?: TaskResources;
/**
* Kontext-Verteiler (M3): expliziter Ziel-Bereich für die aus diesem Trigger
* erzeugte Aufgabe. Hat Vorrang vor der Ableitung aus dem Control-Bezug und ist
* für Trigger OHNE (eindeutigen) Control-Bezug die einzige Quelle des Bereichs.
* Wird i. d. R. aus {@link FACT_DOMAIN_MAP} übernommen.
*/
domain?: Domain;
}
/**
* Kontext-Verteiler (M3): explizites Fragebogen-Fakt/-Trigger → Bereich (Domain)-Mapping.
*
* Hintergrund: Kontextgetriebene Gaps sollen direkt im fachlich richtigen Bereichs-Board
* landen. Für Trigger mit Control-Bezug reicht die Ableitung über `domainForControl`
* (Control → Bereich). Trigger, die aus einem Fragebogen-FAKT statt aus einem Control
* entstehen (z. B. „Cloud/KI ohne Freigabeverfahren" hängt an einem Verfahrensdokument,
* nicht an einem Control), bekommen ihren Bereich hier explizit. Schlüssel = Trigger-ID
* (= stabile Fakt-Identität, s. `GAP_TRIGGERS[].id`).
*
* Nur Fakten eintragen, deren Bereich NICHT sauber aus dem Control-Kapitel folgt.
*/
export const FACT_DOMAIN_MAP: Record<string, Domain> = {
// Cloud-/KI-Freigabeverfahren (VA-11) ist IT-getrieben, hat aber keinen Control-Bezug
// in den Links → Bereich explizit auf IT setzen.
cloud_ai_without_approval: "IT",
};
export const GAP_TRIGGERS: GapTrigger[] = [
{
id: "isb_not_named",
step: 3,
condition: "Q-ROLE-02 – ISB nicht benannt",
taskType: "organizational",
title: "ISB (Informationssicherheitsbeauftragte:r) bestellen",
priority: "hoch",
links: { control: "1.2.2" },
source: "Control 1.2.2",
resourcesHint: { personnel: "Oberste Leitung benennt ISB", time: "kurzfristig" },
},
{
id: "isb_equals_it",
step: 3,
condition: "Q-ROLE-02/03 – ISB = IT-Verantwortung",
taskType: "organizational",
title: "Funktionstrennung ISB ≠ IT herstellen oder kompensieren",
priority: "hoch",
links: { control: "1.2.2" },
source: "C7 (Funktionstrennung) · Control 1.2.2",
resourcesHint: { personnel: "Leitung/ISB", time: "mit nächstem Review" },
},
{
id: "external_it_yes",
step: 7,
condition: "Q-FEAT-09 – externe IT-Dienstleister = ja",
taskType: "evidence_provide",
title: "Dienstleister-Selbstauskunft / TISAX-Nachweis einholen",
priority: "mittel",
links: { control: "6.1.1" },
source: "Control 6.1.1",
resourcesHint: { personnel: "Einkauf/ISB", tool: "Ticketsystem" },
},
{
id: "cloud_ai_without_approval",
step: 4,
condition: "Q-FEAT-02/03 – Cloud/KI = ja, ohne Freigabeverfahren",
taskType: "document_create",
title: "Cloud-/KI-Freigabeverfahren aktivieren",
priority: "mittel",
links: { document: "VA-11" },
source: "VA-11",
resourcesHint: { personnel: "ISB", time: "vor Produktivnutzung" },
// Kein Control-Bezug in den Links → Bereich explizit (Kontext-Verteiler).
domain: FACT_DOMAIN_MAP.cloud_ai_without_approval,
},
{
id: "no_restore_test",
step: 7,
condition: "Q-BL-11 – kein Wiederherstellungstest",
taskType: "technical",
title: "Restore-Test etablieren",
priority: "hoch",
links: { control: "5.2.9" },
source: "BL-OPS-06 · Control 5.2.9",
resourcesHint: { personnel: "IT-Betrieb", time: "quartalsweise etablieren" },
},
];
export const originForTrigger = (id: string) => `wizard:${id}`;
export const triggerById = (id: string): GapTrigger | null =>
GAP_TRIGGERS.find((t) => t.id === id) ?? null;
/**
* Kontext-Verteiler (M3), client-safe/statische Variante: Ziel-Bereich eines Triggers.
* Priorität: expliziter `trigger.domain` (Fakt→Domain) vor der Ableitung aus dem
* Control-Bezug (`defaultDomainForControl`). Die Task-Erzeugung nutzt zusätzlich die
* DB-gestützte Auflösung (`domainForControl`, inkl. Tenant-Override) mit derselben
* Priorität; diese Funktion dient statischer Anzeige/Vorschau ohne DB-Zugriff.
*/
export function domainForTrigger(trg: GapTrigger): Domain | null {
return trg.domain ?? defaultDomainForControl(trg.links.control ?? "");
}
-95
View File
@@ -1,95 +0,0 @@
// Gemeinsame Task-Typen (Contracts §1, Story F1). Client-safe: nur Typen +
// Literal-Konstanten, keine Server-Logik — importierbar von Dev A's Wizard-Gates
// (die `createTask` aufrufen) und vom Aufgaben-Modul.
//
// Hinweis: `owner` aus Contracts §1 bildet auf die bestehende Spalte
// `Task.assigneeId` ab (eine verantwortliche Person). `status` setzt die Action
// serverseitig (Default "OPEN") — es ist kein Eingabefeld.
export const TASK_TYPES = [
"document_create",
"evidence_provide",
"technical",
"organizational",
"validation",
"policy_approval",
] as const;
export type TaskType = (typeof TASK_TYPES)[number];
export const TASK_PRIORITIES = ["hoch", "mittel", "niedrig"] as const;
export type TaskPriority = (typeof TASK_PRIORITIES)[number];
// Status-Lebenszyklus. "PROPOSED" = automatisch erzeugter Vorschlag (Story B1),
// der vom Bearbeiter bestätigt (→ OPEN) oder verworfen (→ DISCARDED) wird.
// "IN_PROGRESS" = mittlere Kanban-Spalte (Board-Refactor); war bislang nur in
// Board/Actions verwendet, aber hier nicht gelistet → jetzt konsistent.
export const TASK_STATUSES = ["PROPOSED", "OPEN", "IN_PROGRESS", "DONE", "REJECTED", "CANCELLED", "DISCARDED"] as const;
export type TaskStatus = (typeof TASK_STATUSES)[number];
/** Deutsche Anzeigelabels je Aufgabentyp (Contracts §1). */
export const TASK_TYPE_LABELS: Record<TaskType, string> = {
document_create: "Dokument erstellen",
evidence_provide: "Nachweis erbringen",
technical: "Technisch",
organizational: "Organisatorisch",
validation: "Validierung",
policy_approval: "Richtlinien-Freigabe",
};
// Cockpit (M3, 1.6): Nachweis-/Evidence-Register. Client-safe Konstanten für UI +
// Server-Validierung. „audit-ready" (Wirksamkeit) verlangt einen solchen Nachweis
// zusätzlich zur Vier-Augen-Freigabe (Auto-Completion-Deckel 2.2).
export const EVIDENCE_KINDS = ["record", "protocol", "screenshot", "export"] as const;
export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
/** Deutsche Anzeigelabels je Nachweisart. */
export const EVIDENCE_KIND_LABELS: Record<EvidenceKind, string> = {
record: "Aufzeichnung",
protocol: "Protokoll",
screenshot: "Screenshot",
export: "Export",
};
export function isEvidenceKind(v: unknown): v is EvidenceKind {
return typeof v === "string" && (EVIDENCE_KINDS as readonly string[]).includes(v);
}
/** Ressourcenabschätzung je Aufgabe (Contracts §1). Alle Felder optional. */
export interface TaskResources {
tool?: string;
budget?: string;
personnel?: string;
time?: string;
}
/** Polymorphe Verknüpfungen einer Aufgabe (Contracts §1). IDs/Refs, alle optional. */
export interface TaskLinks {
control?: string;
risk?: string;
document?: string;
asset?: string;
}
/**
* Eingabe für `createTask` (Contracts §1). Von Wizard-Gates/Triggern aufgerufen.
* `owner` → `Task.assigneeId`; `status` wird serverseitig gesetzt (kein Feld hier).
*/
export interface CreateTaskInput {
type: TaskType;
title: string;
description?: string | null;
origin: string;
owner?: string | null;
dueDate?: Date | string | null;
priority?: TaskPriority;
resources?: TaskResources;
links?: TaskLinks;
}
export function isTaskType(v: unknown): v is TaskType {
return typeof v === "string" && (TASK_TYPES as readonly string[]).includes(v);
}
export function isTaskPriority(v: unknown): v is TaskPriority {
return typeof v === "string" && (TASK_PRIORITIES as readonly string[]).includes(v);
}