import type { TenantDb } from "./db"; /** * Ableitung des Abhängigkeitsgraphen (SPEC §4.11) aus Process, * ProcessAsset(role), Asset und AssetRelation. Keine redundante * Datenhaltung — der Graph wird bei jedem Aufruf frisch berechnet. * * Kritikalität je Knoten: * - Prozess: BIA-Kritikalität (1–4), sonst 1 * - Asset: max(C, I, A) * Kritische Kante: beide Endknoten-Kritikalität ≥ Schwellwert. * Kritischer Pfad: von kritischen Prozessen entlang kritischer Kanten (DFS). * SPOF: Asset, von dem ≥ spofMin kritische Prozesse (transitiv) abhängen. */ export type GraphNodeKind = | "process" | "INFORMATION" | "SYSTEM" | "APPLICATION" | "LOCATION" | "SUPPLIER" | "PERSON" | "DATA"; export type GraphNode = { id: string; entity: "process" | "asset"; kind: GraphNodeKind; name: string; sub: string | null; criticality: number; critical: boolean; spof: boolean; dependentCriticalProcesses: number; }; export type GraphEdge = { id: string; source: string; target: string; label: string; critical: boolean; }; export type DependencyGraph = { nodes: GraphNode[]; edges: GraphEdge[]; threshold: number; analysis: { spofs: { id: string; name: string; count: number }[]; criticalProcessCount: number; criticalEdgeCount: number; longestCriticalPath: string[]; // Knoten-Namen }; }; export async function buildDependencyGraph( db: TenantDb, opts: { threshold?: number; spofMin?: number } = {} ): Promise { const threshold = opts.threshold ?? 3; const spofMin = opts.spofMin ?? 2; const [processes, assets, relations] = await Promise.all([ db.process.findMany({ include: { bia: { select: { criticality: true } }, processAssets: { select: { assetId: true, role: true } }, }, }), db.asset.findMany({ select: { id: true, name: true, type: true, confidentiality: true, integrity: true, availability: true, }, }), db.assetRelation.findMany({ select: { assetId: true, relatedAssetId: true, type: true } }), ]); const pid = (id: string) => `p:${id}`; const aid = (id: string) => `a:${id}`; const assetCrit = new Map(); for (const a of assets) { assetCrit.set(a.id, Math.max(a.confidentiality, a.integrity, a.availability)); } const nodes: GraphNode[] = []; const critOf = new Map(); for (const p of processes) { const c = p.bia?.criticality ?? 1; critOf.set(pid(p.id), c); nodes.push({ id: pid(p.id), entity: "process", kind: "process", name: p.name, sub: null, criticality: c, critical: c >= threshold, spof: false, dependentCriticalProcesses: 0, }); } for (const a of assets) { const c = assetCrit.get(a.id) ?? 1; critOf.set(aid(a.id), c); nodes.push({ id: aid(a.id), entity: "asset", kind: a.type, name: a.name, sub: null, criticality: c, critical: c >= threshold, spof: false, dependentCriticalProcesses: 0, }); } // Zwei Sichten: // - adjacency (Analyse): Abhängigkeitsrichtung (Abhängiger → Abhängigkeit), // Basis für kritischen Pfad, SPOF, transitive Abhängigkeiten. // - edges (Anzeige): links = unterstützende Abhängigkeit, Mitte = Prozess, // rechts = erzeugtes primäres Asset. Pfeile fließen sauber von links nach // rechts, daher zeigen unterstützende Kanten VON der Abhängigkeit zum // Nutzer/Prozess. const edges: GraphEdge[] = []; const adjacency = new Map(); const addAdj = (s: string, t: string) => { if (!adjacency.has(s)) adjacency.set(s, []); adjacency.get(s)!.push(t); }; const isCritEdge = (s: string, t: string) => (critOf.get(s) ?? 1) >= threshold && (critOf.get(t) ?? 1) >= threshold; for (const p of processes) { for (const pa of p.processAssets) { const proc = pid(p.id); const asset = aid(pa.assetId); // Analyse: Prozess ist auf alle zugeordneten Assets angewiesen (SPOF/Pfad) addAdj(proc, asset); if (pa.role === "PRIMARY") { // erzeugtes Asset rechts vom Prozess edges.push({ id: `${proc}->${asset}`, source: proc, target: asset, label: "erzeugt", critical: isCritEdge(proc, asset), }); } else { // unterstützendes Asset links vom Prozess (Kante zeigt zum Prozess) edges.push({ id: `${asset}->${proc}`, source: asset, target: proc, label: "unterstützt", critical: isCritEdge(asset, proc), }); } } } const assetIds = new Set(assets.map((a) => a.id)); for (const r of relations) { if (!assetIds.has(r.assetId) || !assetIds.has(r.relatedAssetId)) continue; // Analyse: asset hängt von relatedAsset ab addAdj(aid(r.assetId), aid(r.relatedAssetId)); // Anzeige: Abhängigkeit (relatedAsset) links, Nutzer (asset) rechts const s = aid(r.relatedAssetId); const t = aid(r.assetId); edges.push({ id: `${s}->${t}`, source: s, target: t, label: "unterstützt", critical: isCritEdge(s, t), }); } // Transitive Abhängigkeiten je kritischem Prozess → SPOF-Zählung je Asset const criticalProcesses = nodes.filter((n) => n.entity === "process" && n.critical); const dependents = new Map>(); // assetNodeId → Set kritischer Prozess-IDs for (const cp of criticalProcesses) { const seen = new Set(); const stack = [...(adjacency.get(cp.id) ?? [])]; while (stack.length) { const cur = stack.pop()!; if (seen.has(cur)) continue; seen.add(cur); if (!dependents.has(cur)) dependents.set(cur, new Set()); dependents.get(cur)!.add(cp.id); for (const next of adjacency.get(cur) ?? []) stack.push(next); } } const spofs: { id: string; name: string; count: number }[] = []; for (const n of nodes) { if (n.entity !== "asset") continue; const count = dependents.get(n.id)?.size ?? 0; n.dependentCriticalProcesses = count; if (count >= spofMin) { n.spof = true; spofs.push({ id: n.id, name: n.name, count }); } } spofs.sort((a, b) => b.count - a.count); // Längster kritischer Pfad (Abhängigkeitskette): DFS über die Analyse-Adjazenz, // beschränkt auf kritische Knoten, ab kritischen Prozessen. const critAdj = new Map(); for (const [s, targets] of adjacency) { if ((critOf.get(s) ?? 1) < threshold) continue; const critTargets = targets.filter((t) => (critOf.get(t) ?? 1) >= threshold); if (critTargets.length) critAdj.set(s, critTargets); } const nameOf = new Map(nodes.map((n) => [n.id, n.name])); let longest: string[] = []; const dfs = (node: string, path: string[], visited: Set) => { const next = critAdj.get(node) ?? []; if (next.length === 0) { if (path.length > longest.length) longest = [...path]; return; } let extended = false; for (const nx of next) { if (visited.has(nx)) continue; extended = true; visited.add(nx); dfs(nx, [...path, nx], visited); visited.delete(nx); } if (!extended && path.length > longest.length) longest = [...path]; }; for (const cp of criticalProcesses) { dfs(cp.id, [cp.id], new Set([cp.id])); } return { nodes, edges, threshold, analysis: { spofs, criticalProcessCount: criticalProcesses.length, criticalEdgeCount: edges.filter((e) => e.critical).length, longestCriticalPath: longest.map((id) => nameOf.get(id) ?? id), }, }; }