Iteration Teil B: Modul „Abhängigkeiten & kritische Pfade"

Referenz: docs/ISMS-Abhaengigkeitskarte-GEFIM.html

- Graph-Ableitung serverseitig (dependency-graph.ts) aus Process,
  ProcessAsset(role), Asset, AssetRelation — keine redundante Haltung.
  Kritikalität je Knoten (BIA / max C·I·A), kritische Kanten (beide
  Endknoten ≥ Schwellwert), kritischer Pfad (DFS), SPOF-Erkennung
  (transitiv abhängige kritische Prozesse ≥ N)
- React-Flow-Canvas (Topology-Look, dunkle Leinwand + Punktraster):
  Custom-Nodes mit Typ-Icon, Kritikalität, Status-Punkt, SPOF-Badge;
  smoothstep-Kanten mit Labels; kritische Kanten rot + Glow
- dagre-Auto-Layout (links Prozesse → rechts Assets); Toolbar mit Suche
  (Fokus/Zentrieren), Toggle „Kritische Pfade", Fit, PNG-Export;
  Knoten-Klick hebt zusammenhängenden Pfad hervor, Rest abgedunkelt
- Analyse-Panel: kritische Prozesse/Kanten, SPOF-Liste, kritischster
  Pfad, Legende
- Eigener Menüpunkt „Abhängigkeiten"; Absprung aus Asset-/Prozess-Detail
- Theme-Tokens für React Flow (Controls, kritische-Kanten-Glow)

Verifiziert: 10 Knoten/9 Kanten korrekt aus Demo-Daten, SPOF (Cloud-
Hoster, Kundendatenbank), kritischster Pfad, Toggle & Such-Fokus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-03 09:53:49 +02:00
co-authored by Claude Opus 4.8
parent 9e54c7788f
commit d9685b65b9
11 changed files with 1002 additions and 3 deletions
+234
View File
@@ -0,0 +1,234 @@
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 (14), 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<DependencyGraph> {
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<string, number>();
for (const a of assets) {
assetCrit.set(a.id, Math.max(a.confidentiality, a.integrity, a.availability));
}
const nodes: GraphNode[] = [];
const critOf = new Map<string, number>();
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,
});
}
// Kanten: Prozess → Asset (Rolle), Asset → Asset (Relation)
const edges: GraphEdge[] = [];
const adjacency = new Map<string, string[]>(); // gerichtete Abhängigkeit source→target
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 s = pid(p.id);
const t = aid(pa.assetId);
edges.push({
id: `${s}->${t}`,
source: s,
target: t,
label: pa.role === "PRIMARY" ? "erzeugt" : "nutzt",
critical: isCritEdge(s, t),
});
addAdj(s, t);
}
}
const assetIds = new Set(assets.map((a) => a.id));
for (const r of relations) {
if (!assetIds.has(r.assetId) || !assetIds.has(r.relatedAssetId)) continue;
const s = aid(r.assetId);
const t = aid(r.relatedAssetId);
edges.push({
id: `${s}->${t}`,
source: s,
target: t,
label: r.type === "depends_on" ? "hängt ab" : r.type,
critical: isCritEdge(s, t),
});
addAdj(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<string, Set<string>>(); // assetNodeId → Set kritischer Prozess-IDs
for (const cp of criticalProcesses) {
const seen = new Set<string>();
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 (DFS über kritische Kanten ab kritischen Prozessen)
const critAdj = new Map<string, string[]>();
for (const e of edges) {
if (!e.critical) continue;
if (!critAdj.has(e.source)) critAdj.set(e.source, []);
critAdj.get(e.source)!.push(e.target);
}
const nameOf = new Map(nodes.map((n) => [n.id, n.name]));
let longest: string[] = [];
const dfs = (node: string, path: string[], visited: Set<string>) => {
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),
},
};
}