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>
257 lines
7.6 KiB
TypeScript
257 lines
7.6 KiB
TypeScript
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,
|
||
});
|
||
}
|
||
|
||
// 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),
|
||
},
|
||
};
|
||
}
|