Assessor_2.0/src/features/mitre-graph/category-branch.ts

80 lines
3.6 KiB
TypeScript

import type { ObsidianGraphModel } from './obsidian-graph';
export const FILTERED_GRAPH_SELECTION_ID = '__filtered-graph-selection__';
function oppositeNode(edge: { source: string; target: string }, nodeId: string): string | undefined {
if (edge.source === nodeId) return edge.target;
if (edge.target === nodeId) return edge.source;
return undefined;
}
export function relationBranchNodeIds(graph: ObsidianGraphModel, rootId: string): string[] {
const kindById = new Map(graph.nodes.map(node => [node.id, node.kind]));
const rootKind = kindById.get(rootId);
if (!rootKind || rootKind === 'finding') return [];
const selected = new Set<string>([rootId]);
const findingAnchors = rootKind === 'tactic'
? [rootId, ...graph.edges.flatMap(edge => {
const candidate = oppositeNode(edge, rootId);
return candidate && kindById.get(candidate) === 'technique' ? [candidate] : [];
})]
: [rootId];
findingAnchors.forEach(id => selected.add(id));
const findingIds = graph.edges.flatMap(edge => findingAnchors.flatMap(anchorId => {
const candidate = oppositeNode(edge, anchorId);
return candidate && kindById.get(candidate) === 'finding' ? [candidate] : [];
}));
findingIds.forEach(id => selected.add(id));
// Alle direkten Beziehungen der zur Wurzel gehoerenden Findings werden
// aufgenommen, auch weitere Endpunkte derselben Art wie die Wurzel. Das ist
// insbesondere fuer Findings mit mehreren Kategorien oder D3FEND-Zuordnungen
// wichtig. Ein gemeinsam genutzter Relationsknoten darf die Auswahl trotzdem
// nicht rekursiv in weitere Findings ausdehnen.
graph.edges.forEach(edge => {
const sourceFinding = findingIds.includes(edge.source);
const targetFinding = findingIds.includes(edge.target);
if (!sourceFinding && !targetFinding) return;
const candidate = sourceFinding ? edge.target : edge.source;
selected.add(candidate);
});
// Techniques besitzen eine zusaetzliche, fachlich gewollte Ebene zu ihren
// Tactics. Dort endet die Traversierung bewusst.
const techniqueIds = [...selected].filter(id => kindById.get(id) === 'technique');
graph.edges.forEach(edge => {
const techniqueId = techniqueIds.includes(edge.source) ? edge.source : techniqueIds.includes(edge.target) ? edge.target : undefined;
if (!techniqueId) return;
const candidate = oppositeNode(edge, techniqueId);
if (candidate && kindById.get(candidate) === 'tactic' && (rootKind !== 'tactic' || candidate === rootId)) selected.add(candidate);
});
return graph.nodes.map(node => node.id).filter(id => selected.has(id));
}
export function graphSelectionNodeIds(graph: ObsidianGraphModel, selectionId: string): string[] {
return selectionId === FILTERED_GRAPH_SELECTION_ID
? graph.nodes.map(node => node.id)
: relationBranchNodeIds(graph, selectionId);
}
export function graphSelectionUnionNodeIds(graph: ObsidianGraphModel, selectionIds: string[]): string[] {
const selected = new Set<string>();
for (const selectionId of selectionIds) {
if (selectionId === FILTERED_GRAPH_SELECTION_ID) return graph.nodes.map(node => node.id);
const branchNodeIds = relationBranchNodeIds(graph, selectionId);
if (branchNodeIds.length) {
branchNodeIds.forEach(nodeId => selected.add(nodeId));
continue;
}
if (!graph.nodes.some(node => node.id === selectionId)) continue;
selected.add(selectionId);
graph.edges.forEach(edge => {
if (edge.source === selectionId) selected.add(edge.target);
if (edge.target === selectionId) selected.add(edge.source);
});
}
return graph.nodes.map(node => node.id).filter(nodeId => selected.has(nodeId));
}