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>
This commit is contained in:
@@ -100,6 +100,34 @@ const HANDBOOK: { category: string; title: string; bodyMd: string; sourceRefs: s
|
||||
},
|
||||
];
|
||||
|
||||
/** 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 } });
|
||||
@@ -117,12 +145,6 @@ export async function importManaged(prisma: PrismaClient, tenantId: string) {
|
||||
{ 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." },
|
||||
// Verwaltete Register (GAP-Report §5.2). Vorerst als benannte Register-Dokumente mit
|
||||
// dokumentierten Pflichtspalten; die editierbare Datenpflege (Attribute/Cross-Links) folgt (WP3.0).
|
||||
{ code: "REG-PROJECTS", type: "REGISTER" as const, title: "Register Informationssicherheit in Projekten", orderIdx: 905, intro: "Projekte mit IS-Klassifizierung, Risikobewertung, abgeleiteten Maßnahmen/Status und ISB-Einbindung (R01 / VA-19). Pflichtspalten: Projekt, IS-Klassifizierung, Risikobewertung, Maßnahmen/Status, ISB-Einbindung." },
|
||||
{ code: "REG-SENS-ROLES", type: "REGISTER" as const, title: "Register sensibler Tätigkeiten", orderIdx: 906, intro: "Sensible Tätigkeitsbereiche/Rollen mit geforderten Eignungsnachweisen und Prüftiefe (R05 / VA-14). Pflichtspalten: Rolle/Bereich, geforderte Eignungsnachweise, Prüftiefe, Verantwortlich, Review." },
|
||||
{ code: "REG-AUDIT-PLAN", type: "REGISTER" as const, title: "Audit-Programm-Register", orderIdx: 907, intro: "Auditprogramm mit Zeitplan, Umfang, geprüften Controls, Prüfern und Ergebnissen (R03 / VA-15). Pflichtspalten: Zeitplan, Umfang, geprüfte Controls, Prüfer, Ergebnisse." },
|
||||
{ code: "REG-EXT-SERVICES", type: "REGISTER" as const, title: "Register externe IT-/Cloud-/KI-Dienste", orderIdx: 908, intro: "Externe/Cloud-/KI-Dienste mit Schutzbedarf, Datenlokation (EU), Verschlüsselung, Exit-Strategie, Quelle/Lieferant und Freigabestatus (R13/VA-10 + R02/VA-08). Cross-Link an Lieferantensteuerung und Asset-Inventar." },
|
||||
];
|
||||
for (const d of registerDocs) {
|
||||
await prisma.policyDocument.upsert({
|
||||
@@ -132,6 +154,23 @@ export async function importManaged(prisma: PrismaClient, tenantId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "managed_registers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenant_id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"columns" JSONB NOT NULL DEFAULT '[]',
|
||||
"review_cycle" TEXT,
|
||||
"responsible" TEXT,
|
||||
"supplier_link" BOOLEAN NOT NULL DEFAULT false,
|
||||
"asset_link" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "managed_registers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "register_rows" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenant_id" TEXT NOT NULL,
|
||||
"register_id" TEXT NOT NULL,
|
||||
"values" JSONB NOT NULL DEFAULT '{}',
|
||||
"supplier_ref" TEXT,
|
||||
"asset_ref" TEXT,
|
||||
"order_idx" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "register_rows_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "managed_registers_tenant_id_idx" ON "managed_registers"("tenant_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "managed_registers_tenant_id_code_key" ON "managed_registers"("tenant_id", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "register_rows_tenant_id_register_id_idx" ON "register_rows"("tenant_id", "register_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "register_rows" ADD CONSTRAINT "register_rows_register_id_fkey" FOREIGN KEY ("register_id") REFERENCES "managed_registers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
||||
-- RLS für die neuen mandantenbezogenen Tabellen.
|
||||
DO $$
|
||||
DECLARE t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['managed_registers','register_rows'] LOOP
|
||||
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
|
||||
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -1029,3 +1029,44 @@ model HandbookTopic {
|
||||
@@index([tenantId])
|
||||
@@map("handbook_topics")
|
||||
}
|
||||
|
||||
// Generisches verwaltetes Register (GAP-Report WP3.0): benannte Register mit
|
||||
// Register-ID (REG-…), definierten Pflichtspalten, Review-Turnus und optionalen
|
||||
// Cross-Links an Lieferanten (R13/VA-10) und Asset-Inventar (R02/VA-08).
|
||||
model ManagedRegister {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
code String // REG-PROJECTS, REG-SW-WHITELIST, …
|
||||
title String
|
||||
description String?
|
||||
columns Json @default("[]") // [{ key, label }] Pflichtspalten
|
||||
reviewCycle String? @map("review_cycle")
|
||||
responsible String?
|
||||
supplierLink Boolean @default(false) @map("supplier_link") // Cross-Link Lieferant (Asset)
|
||||
assetLink Boolean @default(false) @map("asset_link") // Cross-Link Asset
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
rows RegisterRow[]
|
||||
|
||||
@@unique([tenantId, code])
|
||||
@@index([tenantId])
|
||||
@@map("managed_registers")
|
||||
}
|
||||
|
||||
model RegisterRow {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
registerId String @map("register_id")
|
||||
values Json @default("{}") // { spaltenKey: wert }
|
||||
supplierRef String? @map("supplier_ref") // Asset-ID (Lieferant)
|
||||
assetRef String? @map("asset_ref") // Asset-ID
|
||||
orderIdx Int @default(0) @map("order_idx")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
register ManagedRegister @relation(fields: [registerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId, registerId])
|
||||
@@map("register_rows")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user