Files
certvia/src/server/dependency-graph.ts
T
MartinandClaude Opus 4.8 18bd82e54a Abhängigkeitsgraph: klarere Anordnung (Abhängigkeiten links, Prozess
Mitte, erzeugte Assets rechts)

- Anzeige-Kanten rollenabhängig orientiert: primäre/erzeugte Assets
  rechts vom Prozess, unterstützende Abhängigkeiten links (Pfeile
  fließen sauber links→rechts, Label "erzeugt" bzw. "unterstützt")
- Analyse-Adjazenz (SPOF, kritischer Pfad, transitive Abhängigkeiten)
  von der Anzeige entkoppelt und über alle Asset-Bezüge berechnet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:09:06 +02:00

257 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
});
}
// 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<string, string[]>();
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<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 (Abhängigkeitskette): DFS über die Analyse-Adjazenz,
// beschränkt auf kritische Knoten, ab kritischen Prozessen.
const critAdj = new Map<string, string[]>();
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<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),
},
};
}