Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

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:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+128
View File
@@ -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;
})();