Files
certvia/prisma/import-managed.ts
T
msolarczekandClaude Opus 4.8 bd56a478fc GAP WP3.0: Generisches, editierbares Register-Datenmodell + Editor
Verwaltete Register (GAP-Report §5.2) sind nicht mehr nur benannte Dokumente,
sondern editierbare Register mit Pflichtspalten und Cross-Links.

- Schema: ManagedRegister (code, title, columns JSON, reviewCycle, responsible,
  supplierLink/assetLink) + RegisterRow (values JSON, supplierRef, assetRef).
  Migration + RLS; in TENANT_MODELS.
- import-managed.ts: 7 generische Register (REG-PROJECTS, -SENS-ROLES, -AUDIT-PLAN,
  -EXT-SERVICES, -SW-WHITELIST, -CRIT-SERVICES, -NET) mit definierten Pflichtspalten
  + Cross-Link-Flags; legt Register-Dokument (Bibliothek/Link) UND ManagedRegister-
  Definition idempotent an (RegisterRow bleibt bei Re-Import erhalten).
- actions/register.ts: addRegisterRow/updateRegisterRow/deleteRegisterRow
  (Modul policies, policy:write; Werte gegen die Pflichtspalten, optional Lieferant/Asset).
- components/generic-register.tsx: editierbare Zeilen-Tabelle + Cross-Link-Selects
  (Lieferant = Asset SUPPLIER/IT_SERVICE, Asset = alle); in /policies/[code] eingebunden.

Browser-verifiziert: REG-EXT-SERVICES rendert 7 Spalten + Lieferant/Asset-Selects;
Eintrag anlegen persistiert alle Werte. tsc + lint + build + Guard-Check grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:07:08 +02:00

217 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PrismaClient } from "@prisma/client";
/**
* Seed der verwalteten Register-Tabellen (§7b) und des Anwender-Handbuchs (§9.7).
* Defaults aus den Auftraggeber-Vorgaben FB-80-04 (Risikomatrix) und
* AA-80-20 (Klassifizierung/Handhabung). Idempotent pro Mandant.
*/
const NOW = "2026-07-07T00:00:00.000Z";
const daysFrom = (base: string, d: number) => new Date(new Date(base).getTime() + d * 86400000);
// Klassifizierungs-Handhabungsmatrix (AA-80-20) — Reihenfolge: Offen · Intern · Vertraulich · Streng vertraulich
const CLASSES = [
{ name: "Offen", description: "Öffentlich; keine Schutzanforderungen." },
{ name: "Intern", description: "Nur für Mitarbeitende; geringer Schutzbedarf." },
{ name: "Vertraulich", description: "Begrenzter Personenkreis; hoher Schutzbedarf." },
{ name: "Streng vertraulich", description: "Namentlich Berechtigte; sehr hoher Schutzbedarf." },
];
// [Offen, Intern, Vertraulich, Streng vertraulich]
const ASPECTS: { name: string; category: string; rules: [string, string, string, string] }[] = [
{ name: "Kennzeichnung (Papier/elektronisch)", category: "Kennzeichnung", rules: ["nicht nötig", "„Intern“ empfohlen", "„Vertraulich“ verpflichtend", "„Streng vertraulich“ + Bearbeiter"] },
{ name: "Vervielfältigung", category: "Handhabung", rules: ["frei", "im Bedarfsfall", "nur mit Freigabe", "nur mit Freigabe, protokolliert"] },
{ name: "Weitergabe intern", category: "Weitergabe", rules: ["frei", "an Mitarbeitende", "an Berechtigte, need-to-know", "namentlich Berechtigte, dokumentiert"] },
{ name: "Postweg / Versand", category: "Weitergabe", rules: ["Standard", "Standard", "verschlossen, persönlich/Einschreiben", "Einschreiben eigenhändig, Empfangsbestätigung"] },
{ name: "E-Mail", category: "Weitergabe", rules: ["ohne Auflagen", "intern ohne Auflagen", "extern verschlüsselt (BL-CRY-04)", "immer verschlüsselt, nur an Berechtigte"] },
{ name: "Fax intern/extern", category: "Weitergabe", rules: ["zulässig", "zulässig", "nur mit Ankündigung/Abholung", "unzulässig"] },
{ name: "Mobile Datenträger", category: "Speicherung", rules: ["zulässig", "nur freigegebene", "nur verschlüsselt (BL-CRY-03)", "nur verschlüsselt, freigegeben, protokolliert"] },
{ name: "Internet / Blogs / Social Media", category: "Weitergabe", rules: ["zulässig", "kein interner Bezug", "unzulässig", "unzulässig"] },
{ name: "Entsorgung Papier", category: "Entsorgung", rules: ["Altpapier", "Datentonne", "Aktenvernichter ≥ P-4", "Aktenvernichter ≥ P-5, protokolliert"] },
{ name: "Löschung elektronisch / Hardware", category: "Entsorgung", rules: ["normal löschen", "normal löschen", "sicheres Löschen (BL-DEL-01)", "sicheres Löschen/Vernichten, Nachweis"] },
{ name: "Speicherung (IT-Systeme)", category: "Speicherung", rules: ["beliebig", "interne Systeme", "zugriffsbeschränkte Ablagen", "verschlüsselt, streng zugriffsbeschränkt"] },
{ name: "Homeoffice / mobiles Arbeiten", category: "Nutzung", rules: ["zulässig", "zulässig", "nur mit Sichtschutz, gesperrt bei Abwesenheit", "nur nach Freigabe, keine Papierform"] },
{ name: "Öffentlicher Bereich / Dienstreise", category: "Nutzung", rules: ["zulässig", "diskret", "Sichtschutz, nicht unbeaufsichtigt", "unzulässig (kritische Länder)"] },
{ name: "Verbale Weitergabe", category: "Weitergabe", rules: ["frei", "unter Kollegen", "nur in geschützter Umgebung", "nur namentlich Berechtigte, abhörsicher"] },
];
// Risiko-Bewertungsmatrix (FB-80-04): Schadensausmaß(14) × EW(14) = 116
const RISK_CLASSES = [
{ name: "Niedrig", maxScore: 3, acceptance: "wird akzeptiert", tone: "ok" },
{ name: "Mittel", maxScore: 6, acceptance: "Maßnahmen; Akzeptanz durch Risk Owner", tone: "warn" },
{ name: "Hoch", maxScore: 9, acceptance: "Akzeptanz durch Risikomanagement-Gremium", tone: "orange" },
{ name: "Kritisch", maxScore: 16, acceptance: "Akzeptanz durch Geschäftsführung", tone: "risk" },
];
const EW_LEVELS = [
{ level: 1, label: "Unwahrscheinlich", definition: "Ereignis in absehbarer Zeit kaum zu erwarten (seltener als alle 5 Jahre)." },
{ level: 2, label: "Gelegentlich", definition: "Ereignis denkbar, aber nicht häufig (ca. alle 15 Jahre)." },
{ level: 3, label: "Wahrscheinlich", definition: "Ereignis tritt voraussichtlich ein (mehrmals pro Jahr)." },
{ level: 4, label: "Sehr wahrscheinlich", definition: "Ereignis tritt regelmäßig/häufig ein (monatlich oder öfter)." },
];
const DAMAGE_DIMS: { name: string; levels: Record<string, string> }[] = [
{ name: "Gesetzes-/Vertragsverstöße", levels: { "1": "unerheblich", "2": "geringfügige Verstöße", "3": "erhebliche Verstöße/Bußgelder", "4": "gravierende Rechtsfolgen/Straftatbestand" } },
{ name: "Datenschutz", levels: { "1": "kein Personenbezug", "2": "geringe Betroffenheit", "3": "sensible Daten betroffen", "4": "massenhaft sensible Daten / hohe Betroffenheit" } },
{ name: "Persönliche Unversehrtheit", levels: { "1": "keine", "2": "leichte Beeinträchtigung", "3": "Gesundheitsgefahr", "4": "Gefahr für Leib und Leben" } },
{ name: "Aufgabenerfüllung", levels: { "1": "keine Einschränkung", "2": "geringe Einschränkung", "3": "erhebliche Einschränkung", "4": "Kernaufgaben nicht erfüllbar" } },
{ name: "Innen-/Außenwirkung (Reputation)", levels: { "1": "keine", "2": "intern begrenzt", "3": "öffentlich wahrnehmbar", "4": "nachhaltiger Reputationsschaden" } },
{ name: "Störungs-/Ausfallzeit", levels: { "1": "< 1 Std.", "2": "bis 1 Tag", "3": "bis 1 Woche", "4": "> 1 Woche" } },
{ name: "Finanzielle Auswirkungen", levels: { "1": "bis 10 T€", "2": "bis 50 T€", "3": "bis 200 T€", "4": "über 500 T€" } },
];
// Krypto-Register (VA-07) — Demo-Datensätze inkl. Ablaufüberwachung
const CRYPTO = [
{ dienst: "TLS Web-Portal (portal.gefim.example)", schluessel: "RSA 3072 · Let's Encrypt", algorithmus: "TLS 1.3 / AES-256", ablaufOffset: 45, verantwortlich: "IT-Leitung", speicherort: "Reverse Proxy", baselineRef: "BL-CRY-01" },
{ dienst: "VPN-Gateway", schluessel: "ECDSA P-256 · interne CA", algorithmus: "IKEv2 / AES-256-GCM", ablaufOffset: 12, verantwortlich: "IT-Leitung", speicherort: "Firewall/HSM", baselineRef: "BL-CRY-02" },
{ dienst: "Festplattenverschlüsselung Notebooks", schluessel: "BitLocker · TPM 2.0", algorithmus: "AES-256-XTS", ablaufOffset: null, verantwortlich: "IT-Betrieb", speicherort: "MDM-Recovery", baselineRef: "BL-CRY-03" },
{ dienst: "S/MIME Zertifikate Geschäftsführung", schluessel: "RSA 2048 · öffentliche CA", algorithmus: "S/MIME", ablaufOffset: -8, verantwortlich: "IT-Leitung", speicherort: "Smartcard", baselineRef: "BL-CRY-04" },
];
// Anwender-Handbuch (§9.7) — kuratierte Themen, Werte via {{VARIABLE}} synchron zur Baseline
const HANDBOOK: { category: string; title: string; bodyMd: string; sourceRefs: string[] }[] = [
{
category: "Zugang & Passwörter", title: "Passwörter & Mehr-Faktor-Anmeldung",
bodyMd: "**Das Wichtigste kurz:**\n\n- Nutze Passwörter mit **mindestens {{PW_MIN_LENGTH}} Zeichen** ({{PW_COMPLEXITY}}).\n- Aktiviere **Mehr-Faktor-Authentifizierung (MFA)** über {{TECH_MFA}} für {{MFA_SCOPE}}.\n- Gib Passwörter **nie** weiter und nutze für jeden Dienst ein eigenes.\n- Bei Verdacht auf Kompromittierung: Passwort sofort ändern und melden.",
sourceRefs: ["R08#4.1.2"],
},
{
category: "Zugang & Passwörter", title: "Zugriffsrechte nur was du brauchst",
bodyMd: "- Zugriff erhältst du **nach dem Minimalprinzip** (need-to-know) über {{TOOL_TICKET}}.\n- Nicht mehr benötigte Rechte werden entzogen; deine Rechte werden regelmäßig **überprüft (Rezertifizierung, {{RECERT_FREQ}})**.\n- Sperre deinen Bildschirm bei jedem Verlassen des Arbeitsplatzes.",
sourceRefs: ["R08#4.2.1"],
},
{
category: "Umgang mit Informationen", title: "Daten richtig klassifizieren & handhaben",
bodyMd: "- Behandle Informationen gemäß ihrer **Schutzklasse** (Offen · Intern · Vertraulich · Streng vertraulich).\n- **Vertrauliche** Inhalte extern nur **verschlüsselt** versenden.\n- Dokumente nicht offen liegen lassen (Clean Desk); vertrauliche Ausdrucke sicher vernichten.",
sourceRefs: ["R02"],
},
{
category: "Sicheres Arbeiten", title: "Mobiles Arbeiten & Homeoffice",
bodyMd: "- Nutze nur **freigegebene Geräte** (verwaltet über {{TECH_MDM}}).\n- Achte auf **Sichtschutz** in der Öffentlichkeit; keine vertraulichen Gespräche in Bahn/Café.\n- Verbinde dich extern nur über {{TECH_VPN}} mit MFA.",
sourceRefs: ["R06"],
},
{
category: "Sicheres Arbeiten", title: "E-Mail, Internet & Schadsoftware",
bodyMd: "- Öffne **keine unerwarteten Anhänge/Links**; im Zweifel nachfragen.\n- Melde verdächtige E-Mails (Phishing) über den Meldeweg.\n- {{TECH_MALWARE}} schützt deine Geräte deaktiviere den Schutz nie.",
sourceRefs: ["R10", "R09"],
},
{
category: "Vorfälle", title: "Sicherheitsvorfall? So meldest du ihn",
bodyMd: "**Sofort melden** bei Verdacht auf Vorfall, Datenverlust oder verlorenem Gerät:\n\n- an die {{ROLE_ISB}} bzw. den definierten Meldeweg.\n- Lieber einmal zu viel melden schnelle Meldung begrenzt den Schaden.\n- Nichts vertuschen, nichts eigenmächtig „reparieren“.",
sourceRefs: ["R04"],
},
];
/** Generische verwaltete Register (WP3.0): Code, Titel, Pflichtspalten, Cross-Links. */
const GENERIC_REGISTERS: {
code: string; title: string; description: string; orderIdx: number;
columns: { key: string; label: string }[]; supplierLink?: boolean; assetLink?: boolean;
}[] = [
{ code: "REG-PROJECTS", title: "Register Informationssicherheit in Projekten", orderIdx: 905,
description: "Projekte mit IS-Klassifizierung, Risikobewertung, Maßnahmen und ISB-Einbindung (R01 / VA-19).",
columns: [{ key: "projekt", label: "Projekt" }, { key: "klassifizierung", label: "IS-Klassifizierung" }, { key: "risiko", label: "Risikobewertung" }, { key: "massnahmen", label: "Maßnahmen/Status" }, { key: "isb", label: "ISB-Einbindung" }] },
{ code: "REG-SENS-ROLES", title: "Register sensibler Tätigkeiten", orderIdx: 906,
description: "Sensible Tätigkeitsbereiche/Rollen mit Eignungsnachweisen und Prüftiefe (R05 / VA-14).",
columns: [{ key: "rolle", label: "Rolle/Bereich" }, { key: "nachweise", label: "Geforderte Eignungsnachweise" }, { key: "prueftiefe", label: "Prüftiefe" }, { key: "verantwortlich", label: "Verantwortlich" }, { key: "review", label: "Review" }] },
{ code: "REG-AUDIT-PLAN", title: "Audit-Programm-Register", orderIdx: 907,
description: "Auditprogramm mit Zeitplan, Umfang, geprüften Controls, Prüfern und Ergebnissen (R03 / VA-15).",
columns: [{ key: "zeitplan", label: "Zeitplan" }, { key: "umfang", label: "Umfang" }, { key: "controls", label: "Geprüfte Controls" }, { key: "pruefer", label: "Prüfer" }, { key: "ergebnisse", label: "Ergebnisse" }] },
{ code: "REG-EXT-SERVICES", title: "Register externe IT-/Cloud-/KI-Dienste", orderIdx: 908,
description: "Externe/Cloud-/KI-Dienste mit Schutzbedarf, Datenlokation, Verschlüsselung, Exit und Freigabe (R13/VA-10 + R02/VA-08).",
columns: [{ key: "dienst", label: "Dienst" }, { key: "schutzbedarf", label: "Schutzbedarf" }, { key: "datenlokation", label: "Datenlokation (EU)" }, { key: "verschluesselung", label: "Verschlüsselung" }, { key: "exit", label: "Exit-Strategie" }, { key: "freigabe", label: "Freigabestatus" }, { key: "freigeber", label: "Freigeber" }], supplierLink: true, assetLink: true },
{ code: "REG-SW-WHITELIST", title: "Register Software-Whitelist", orderIdx: 909,
description: "Zugelassene Software mit Version/Patch-Stand, Quelle/Lieferant, Freigabestatus und Review (R02 1.3.4).",
columns: [{ key: "software", label: "Software" }, { key: "version", label: "Version/Patch-Stand" }, { key: "freigabe", label: "Freigabestatus" }, { key: "freigeber", label: "Freigeber" }, { key: "review", label: "Review" }], supplierLink: true, assetLink: true },
{ code: "REG-CRIT-SERVICES", title: "Register kritischer IT-Dienste", orderIdx: 910,
description: "Kritische IT-Dienste mit BIA-Einstufung, RTO/RPO, Abhängigkeiten und Wiederanlaufreihenfolge (R04 5.2.8 / VA-02).",
columns: [{ key: "dienst", label: "Kritischer IT-Dienst" }, { key: "bia", label: "BIA-Einstufung" }, { key: "rto", label: "RTO" }, { key: "rpo", label: "RPO" }, { key: "abhaengigkeiten", label: "Abhängigkeiten" }, { key: "wiederanlauf", label: "Wiederanlaufreihenfolge" }], assetLink: true },
{ code: "REG-NET", title: "Netz-/Netzdienste-Register", orderIdx: 911,
description: "Netzplan/Segmentierung, Netzdienste, Zonen und Review-Turnus (R09 5.1.2 / R10 5.2.7 / VA-07).",
columns: [{ key: "bezeichnung", label: "Netzplan/Segment" }, { key: "netzdienst", label: "Netzdienst" }, { key: "zone", label: "Zone" }, { key: "review", label: "Review" }] },
];
export async function importManaged(prisma: PrismaClient, tenantId: string) {
// Idempotent: alte Datensätze pro Mandant entfernen
await prisma.handlingRule.deleteMany({ where: { tenantId } });
await prisma.handlingAspect.deleteMany({ where: { tenantId } });
await prisma.classificationClass.deleteMany({ where: { tenantId } });
await prisma.cryptoEntry.deleteMany({ where: { tenantId } });
await prisma.riskMatrixClass.deleteMany({ where: { tenantId } });
await prisma.riskEwLevel.deleteMany({ where: { tenantId } });
await prisma.riskDamageDimension.deleteMany({ where: { tenantId } });
await prisma.handbookTopic.deleteMany({ where: { tenantId } });
// Register-/Handbuch-Dokumente in der Bibliothek (interaktiv gerendert)
const registerDocs = [
{ code: "CRYPTO", type: "REGISTER" as const, title: "Verschlüsselungsmechanismen-Register", orderIdx: 902, intro: "Verwaltetes Register der eingesetzten Verschlüsselung (VA-07). Ablaufüberwachung aktiv." },
{ code: "RISKMATRIX", type: "REGISTER" as const, title: "Risiko-Bewertungsmatrix", orderIdx: 903, intro: "Zentrale Bewertungsmatrix Schadensausmaß × Eintrittswahrscheinlichkeit (R03 / VA-09), Defaults nach FB-80-04." },
{ code: "CLASSIFICATION", type: "REGISTER" as const, title: "Klassifizierung & Handhabungsmatrix", orderIdx: 904, intro: "Schutzklassen und Handhabungsregeln (R02 / VA-08), Defaults nach AA-80-20." },
{ code: "HANDBUCH", type: "HANDBUCH" as const, title: "Anwender-Handbuch Informationssicherheit", orderIdx: 950, intro: "Kompakte Zusammenfassung der wichtigsten Regeln für Mitarbeitende mit Deep-Links in die Quelldokumente." },
];
for (const d of registerDocs) {
await prisma.policyDocument.upsert({
where: { tenantId_code: { tenantId, code: d.code } },
update: { title: d.title, type: d.type, orderIdx: d.orderIdx, rawMarkdown: `# ${d.title}\n\n${d.intro}` },
create: { tenantId, code: d.code, type: d.type, title: d.title, version: "1.0", status: "FREIGEGEBEN", orderIdx: d.orderIdx, rawMarkdown: `# ${d.title}\n\n${d.intro}` },
});
}
// Generische verwaltete Register (GAP-Report WP3.0 / §5.2): Dokument (Bibliothek/Link)
// + editierbare ManagedRegister-Definition mit Pflichtspalten + Cross-Links. Zeilen
// (RegisterRow) bleiben bei Re-Import erhalten (nicht-destruktiv).
for (const g of GENERIC_REGISTERS) {
const intro = `${g.description} Pflichtspalten: ${g.columns.map((c) => c.label).join(", ")}.`;
await prisma.policyDocument.upsert({
where: { tenantId_code: { tenantId, code: g.code } },
update: { title: g.title, type: "REGISTER", orderIdx: g.orderIdx, rawMarkdown: `# ${g.title}\n\n${intro}` },
create: { tenantId, code: g.code, type: "REGISTER", title: g.title, version: "1.0", status: "FREIGEGEBEN", orderIdx: g.orderIdx, rawMarkdown: `# ${g.title}\n\n${intro}` },
});
await prisma.managedRegister.upsert({
where: { tenantId_code: { tenantId, code: g.code } },
update: { title: g.title, description: g.description, columns: g.columns, supplierLink: g.supplierLink ?? false, assetLink: g.assetLink ?? false },
create: { tenantId, code: g.code, title: g.title, description: g.description, columns: g.columns, supplierLink: g.supplierLink ?? false, assetLink: g.assetLink ?? false },
});
}
// Krypto-Register
for (let i = 0; i < CRYPTO.length; i++) {
const c = CRYPTO[i];
await prisma.cryptoEntry.create({
data: {
tenantId, dienst: c.dienst, schluessel: c.schluessel, algorithmus: c.algorithmus,
ablaufdatum: c.ablaufOffset === null ? null : daysFrom(NOW, c.ablaufOffset),
verantwortlich: c.verantwortlich, speicherort: c.speicherort, baselineRef: c.baselineRef, orderIdx: i,
},
});
}
// Klassifizierung + Handhabungsmatrix
const classIds: string[] = [];
for (let i = 0; i < CLASSES.length; i++) {
const cl = await prisma.classificationClass.create({ data: { tenantId, name: CLASSES[i].name, description: CLASSES[i].description, orderIdx: i } });
classIds.push(cl.id);
}
for (let a = 0; a < ASPECTS.length; a++) {
const asp = await prisma.handlingAspect.create({ data: { tenantId, name: ASPECTS[a].name, category: ASPECTS[a].category, orderIdx: a } });
for (let c = 0; c < classIds.length; c++) {
await prisma.handlingRule.create({ data: { tenantId, classId: classIds[c], aspectId: asp.id, text: ASPECTS[a].rules[c] } });
}
}
// Risiko-Bewertungsmatrix
for (let i = 0; i < RISK_CLASSES.length; i++) {
const r = RISK_CLASSES[i];
await prisma.riskMatrixClass.create({ data: { tenantId, name: r.name, maxScore: r.maxScore, acceptance: r.acceptance, tone: r.tone, orderIdx: i } });
}
for (const ew of EW_LEVELS) await prisma.riskEwLevel.create({ data: { tenantId, level: ew.level, label: ew.label, definition: ew.definition } });
for (let i = 0; i < DAMAGE_DIMS.length; i++) {
await prisma.riskDamageDimension.create({ data: { tenantId, name: DAMAGE_DIMS[i].name, levels: DAMAGE_DIMS[i].levels, orderIdx: i } });
}
// Anwender-Handbuch
for (let i = 0; i < HANDBOOK.length; i++) {
const h = HANDBOOK[i];
await prisma.handbookTopic.create({ data: { tenantId, category: h.category, title: h.title, bodyMd: h.bodyMd, sourceRefs: h.sourceRefs, orderIdx: i } });
}
return { crypto: CRYPTO.length, classes: CLASSES.length, aspects: ASPECTS.length, damage: DAMAGE_DIMS.length, handbook: HANDBOOK.length };
}