Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// 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;
|
||||
})();
|
||||
Reference in New Issue
Block a user