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:
co-authored by
Claude Opus 4.8
parent
9e54c7788f
commit
d9685b65b9
@@ -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 (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<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),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user