import { memo, useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type WheelEvent as ReactWheelEvent } from 'react'; import { useApp } from '../../app/AppProvider'; import { FloatingResultsWindow } from '../linked-results/FloatingResultsWindow'; import { linkResultSheets } from '../../profiles/purple-knight/result-linker'; import { linkPingCastleSectionSheets } from '../../profiles/pingcastle/pingcastle-section-links'; import { inlineResultTableSheets } from '../linked-results/inline-result-tables'; import { resultMessageFor, resultOutcomeFor, resultSectionsFor, resultStatusFieldFor } from '../linked-results/result-message'; import { useFilteredRecords } from '../search-filter/use-filtered-records'; import { SearchFilterBar, SearchTextControl } from '../search-filter/SearchFilterBar'; import { deriveDynamicFacets } from '../search-filter/dynamic-facets'; import { ViewBookmarks } from '../view-bookmarks/ViewBookmarks'; import type { PathSnapshotState, ViewBookmark } from '../view-bookmarks/view-bookmarks'; import { toolIdForRecord } from '../../importers/status-normalization'; import { toolProfileDefinitions } from '../../profiles/profile-definition-catalog'; import { activeMitreMatrix, buildFindingPathItems, MITRE_MATRIX_CHANGED_EVENT, type FindingPathItem, type MitreGraphData } from './mitre-graph'; import { buildObsidianGraph, type ObsidianGraphNode } from './obsidian-graph'; import { hiddenPathfinderFacetLabels } from './pathfinder-filter-trail'; import { FILTERED_GRAPH_SELECTION_ID, graphSelectionUnionNodeIds, selectedPathNodeIds } from './category-branch'; import { graphFacetSelectionForNode, relationNodeIdForFacetFilters, type GraphFacetSelection } from './graph-filter-bridge'; const severityLabels = { critical: 'Critical', high: 'High', medium: 'Medium', low: 'Low', info: 'Info', unknown: 'Unknown' } as const; const graphNodeKindLabels = { finding: 'Finding', category: 'Category', defense: 'D3FEND', target: 'Target', technique: 'Technique', tactic: 'Tactic' } as const; // Fensterunterscheidungsfarben fuer gleichzeitig geoeffnete Result-Fenster. // Enthielten zwei violette Toene (#7c3aed, #6d28d9); ersetzt, weil violett als // UI-Rolle vollstaendig entfernt ist (Nutzerentscheidung 2026-09-20, AGENTS.md // "UI segments and tiles"). const resultColors = ['#0e7490', '#0f766e', '#b45309', '#be123c', '#1d4ed8', '#b91c6c']; interface PathSnapshotRequest { id: string; path: PathSnapshotState; } interface ObsidianGraphViewProps { active: boolean; onOpenFinding(): void; pathSnapshotRequest?: PathSnapshotRequest; onPathSnapshotApplied?(): void; } interface ResultWindowState { recordId: string; toolId?: string; title: string; color: string; zIndex: number; minimized: boolean; } interface CameraState { x: number; y: number; scale: number; } interface PointerOperation { kind: 'pan' | 'node'; pointerId: number; moved: boolean; nodeId?: string; startX: number; startY: number; camera: CameraState; nodeStart?: { x: number; y: number }; } interface ExtractViewState { camera: CameraState; pinnedNodeIds: string[]; selectedNodeId: string; markedSelectionIds: string[]; } const clamp = (value: number, minimum: number, maximum: number) => Math.min(maximum, Math.max(minimum, value)); const graphLabel = (label: string) => label.length > 44 ? `${label.slice(0, 41).trimEnd()}…` : label; function fittedCameraFor(graphNodes: Array<{ x: number; y: number }>, width: number, height: number): CameraState { if (!graphNodes.length) return { x: 0, y: 0, scale: 1 }; const xs = graphNodes.map(node => node.x); const ys = graphNodes.map(node => node.y); const minX = Math.min(...xs) - 170; const maxX = Math.max(...xs) + 170; const minY = Math.min(...ys) - 90; const maxY = Math.max(...ys) + 90; const scale = clamp(Math.min(width / (maxX - minX), height / (maxY - minY)) * 0.88, 0.12, 1.8); return { x: -((minX + maxX) / 2) * scale, y: -((minY + maxY) / 2) * scale, scale }; } export const ObsidianGraphView = memo(ObsidianGraphContent); function ObsidianGraphContent({ active, onOpenFinding, pathSnapshotRequest, onPathSnapshotApplied }: ObsidianGraphViewProps) { const app = useApp(); const { dataset, activeSheet, activeSheetId, activeProfile, titleColumn, reviews, actionOnly, facetFilters, pathFindingIds, search: workSearch, statusFilter, setFacetFilter, setSelectedRecord, setView, toggleWorkspace, updateReview } = app; const [graphData, setGraphData] = useState(activeMitreMatrix); useEffect(() => { const refreshMatrix = () => setGraphData(activeMitreMatrix()); globalThis.addEventListener(MITRE_MATRIX_CHANGED_EVENT, refreshMatrix); return () => globalThis.removeEventListener(MITRE_MATRIX_CHANGED_EVENT, refreshMatrix); }, []); const [selectedNodeId, setSelectedNodeId] = useState(''); const [pinnedNodeIds, setPinnedNodeIds] = useState([]); const [hoveredNodeId, setHoveredNodeId] = useState(''); const [showText, setShowText] = useState(true); const [camera, setCamera] = useState({ x: 0, y: 0, scale: 1 }); const [dimensions, setDimensions] = useState({ width: 1200, height: 720 }); const [nodeOverrides, setNodeOverrides] = useState>({}); const [markedSelectionIds, setMarkedSelectionIds] = useState([]); const [extractedSelectionIds, setExtractedSelectionIds] = useState([]); const [resultWindows, setResultWindows] = useState([]); const [queuedPathSnapshot, setQueuedPathSnapshot] = useState(); const [resetViewRevision, setResetViewRevision] = useState(0); const surfaceRef = useRef(null); const pointerOperation = useRef(null); const nextColor = useRef(0); const nextZIndex = useRef(2100); const lastFittedGraphSignature = useRef(''); const extractViewBefore = useRef(undefined); const fittedExtractSignature = useRef(''); const graphFacetSelectionRef = useRef(undefined); const handledResetViewRevision = useRef(0); const cameraRef = useRef(camera); const cameraAnimationFrame = useRef(undefined); cameraRef.current = camera; const marked = Object.values(reviews).filter(review => review.include).length; const visibleRecords = useFilteredRecords('deviation'); const graphFacets = useMemo( () => deriveDynamicFacets(activeSheet, dataset, activeProfile, reviews), [activeProfile, activeSheet, dataset, reviews] ); const items = useMemo( () => buildFindingPathItems(visibleRecords, graphData, titleColumn, activeSheet?.toolId ?? dataset?.profileId), [activeSheet?.toolId, dataset?.profileId, graphData, titleColumn, visibleRecords] ); const graph = useMemo(() => buildObsidianGraph(items, graphData), [items, graphData]); const graphSignature = useMemo(() => graph.nodes.map(node => node.id).join('|'), [graph.nodes]); const nodes = useMemo(() => graph.nodes.map(node => ({ ...node, ...(nodeOverrides[node.id] ?? {}) })), [graph.nodes, nodeOverrides]); const markedBranchNodeIds = useMemo( () => graphSelectionUnionNodeIds(graph, markedSelectionIds), [graph, markedSelectionIds] ); const extractedNodeIds = useMemo( () => extractedSelectionIds.length ? new Set(selectedPathNodeIds(graph, extractedSelectionIds[0])) : undefined, [extractedSelectionIds, graph] ); const displayedNodes = useMemo( () => extractedNodeIds ? nodes.filter(node => extractedNodeIds.has(node.id)) : nodes, [extractedNodeIds, nodes] ); const displayedEdges = useMemo( () => extractedSelectionIds.length ? graph.edges.filter(edge => edge.source === extractedSelectionIds[0] || edge.target === extractedSelectionIds[0]) : graph.edges, [extractedSelectionIds, graph.edges] ); const labelOffsets = useMemo(() => { const placed: Array<{ left: number; right: number; top: number; bottom: number }> = []; const offsets: Record = {}; if (!showText) return offsets; const candidates = [0, -22, 22, -44, 44, -66, 66, -88, 88, -110, 110, -132, 132, -154, 154, -176, 176, -198, 198, -220, 220]; const ordered = displayedNodes.filter(node => { const screenX = dimensions.width / 2 + camera.x + node.x * camera.scale; const screenY = dimensions.height / 2 + camera.y + node.y * camera.scale; return screenX >= 6 && screenX <= dimensions.width - 6 && screenY >= 6 && screenY <= dimensions.height - 6; }).sort((left, right) => Number(left.kind === 'finding') - Number(right.kind === 'finding') || left.y - right.y); for (const node of ordered) { const label = graphLabel(node.label); const width = Math.max(48, Math.min(352, label.length * 8.6)); const anchorX = dimensions.width / 2 + camera.x + node.x * camera.scale; const anchorY = dimensions.height / 2 + camera.y + node.y * camera.scale; const gap = node.radius * camera.scale + 6; const left = node.x > 0 ? anchorX - gap - width : anchorX + gap; const right = left + width; const offset = candidates.find(candidate => { const top = anchorY + candidate - 15; const bottom = top + 20; return top >= 6 && bottom <= dimensions.height - 6 && placed.every(rect => right + 4 <= rect.left || left >= rect.right + 4 || bottom + 3 <= rect.top || top >= rect.bottom + 3); }) ?? 0; offsets[node.id] = offset; placed.push({ left, right, top: anchorY + offset - 15, bottom: anchorY + offset + 5 }); } return offsets; }, [camera.scale, camera.x, camera.y, dimensions.height, dimensions.width, displayedNodes, showText]); const nodeLookup = useMemo(() => new Map(nodes.map(node => [node.id, node])), [nodes]); const findingLookup = useMemo(() => new Map(items.map(item => [item.id, item])), [items]); const selectedNode = nodeLookup.get(selectedNodeId); const filteredRelationNodeId = useMemo( () => relationNodeIdForFacetFilters(nodes, graphFacets, facetFilters), [facetFilters, graphFacets, nodes] ); const defaultStatusFilter = activeProfile.defaults(activeSheet).statusFilter; const hasActiveGraphFilter = Boolean( workSearch || Object.keys(facetFilters).length || statusFilter !== defaultStatusFilter || actionOnly || pathFindingIds.length ); const selectedRelationNodeId = [...pinnedNodeIds].reverse() .find(nodeId => nodeLookup.get(nodeId)?.kind !== 'finding') ?? ''; const actionBranchNodeId = extractedSelectionIds[0] || selectedRelationNodeId || filteredRelationNodeId || (hasActiveGraphFilter && nodes.length ? FILTERED_GRAPH_SELECTION_ID : ''); const actionSelectionNodeIds = actionBranchNodeId ? [actionBranchNodeId] : []; const actionBranchNode = nodeLookup.get(actionBranchNodeId); const actionSelectionIsMarked = actionSelectionNodeIds.length > 0 && actionSelectionNodeIds.length === markedSelectionIds.length && actionSelectionNodeIds.every(nodeId => markedSelectionIds.includes(nodeId)); const hoveredNode = nodeLookup.get(hoveredNodeId); const selectedFinding = selectedNode?.itemId ? findingLookup.get(selectedNode.itemId) : undefined; const detailNode = selectedNode ?? hoveredNode; const detailFinding = detailNode?.itemId ? findingLookup.get(detailNode.itemId) : undefined; const detailToolId = detailFinding ? toolIdForRecord(detailFinding.record, activeSheet?.toolId ?? dataset?.profileId) : undefined; const detailToolName = toolProfileDefinitions.find(profile => profile.id === detailToolId)?.name; const detailStatus = detailFinding ? resultStatusFieldFor(detailFinding.record, detailToolId) : undefined; const detailRuleId = detailFinding?.record.fields.find(field => field.label.trim().toLowerCase() === 'source rule id' && field.value.trim()); const activePathNodeIds = hoveredNodeId ? [hoveredNodeId] : pinnedNodeIds; const pathSnapshot = useMemo(() => { const nodeIds = new Set(pinnedNodeIds); for (const edge of graph.edges) { if (pinnedNodeIds.includes(edge.source) || pinnedNodeIds.includes(edge.target)) { nodeIds.add(edge.source); nodeIds.add(edge.target); } } return { activeNodeIds: [...pinnedNodeIds], nodeIds: [...nodeIds] }; }, [graph.edges, pinnedNodeIds]); const connectionCount = detailNode ? graph.edges.filter(edge => edge.source === detailNode.id || edge.target === detailNode.id).length : 0; const updateGraphFacetSelection = (selection: GraphFacetSelection, selected: boolean) => { const previous = graphFacetSelectionRef.current; if (!selected) { const currentValues = facetFilters[selection.facetId]?.values; if (currentValues?.length === 1 && currentValues[0] === selection.value) setFacetFilter(selection.facetId, null); graphFacetSelectionRef.current = undefined; return; } if (previous && previous.facetId !== selection.facetId) { const previousValues = facetFilters[previous.facetId]?.values; if (previousValues?.length === 1 && previousValues[0] === previous.value) setFacetFilter(previous.facetId, null); } setFacetFilter(selection.facetId, { values: [selection.value] }); graphFacetSelectionRef.current = selection; }; const togglePinnedNode = (nodeId: string) => { const node = nodeLookup.get(nodeId); if (!node) return; const isCurrentlyPinned = pinnedNodeIds.includes(nodeId); const graphFacetSelection = node.kind !== 'finding' && !extractedSelectionIds.length ? graphFacetSelectionForNode(node, graphFacets) : undefined; if (graphFacetSelection) { updateGraphFacetSelection(graphFacetSelection, !isCurrentlyPinned); } setPinnedNodeIds(current => { const isPinned = current.includes(nodeId); const next = isPinned ? current.filter(id => id !== nodeId) : node.kind !== 'finding' && !extractedSelectionIds.length ? [...current.filter(id => nodeLookup.get(id)?.kind === 'finding'), nodeId] : [...current, nodeId]; if (!isPinned && node.kind !== 'finding' && !extractedSelectionIds.length) setMarkedSelectionIds([]); setSelectedNodeId(active => isPinned ? (active === nodeId ? (next.at(-1) ?? '') : active) : nodeId); return next; }); }; useEffect(() => { const surface = surfaceRef.current; if (!active || !surface || typeof ResizeObserver === 'undefined') return; const observer = new ResizeObserver(entries => { const rect = entries[0]?.contentRect; if (rect) setDimensions(current => { const width = Math.max(1, rect.width); const height = Math.max(1, rect.height); return current.width === width && current.height === height ? current : { width, height }; }); }); observer.observe(surface); return () => observer.disconnect(); }, [active]); const stopCameraAnimation = useCallback(() => { if (cameraAnimationFrame.current !== undefined) globalThis.cancelAnimationFrame(cameraAnimationFrame.current); cameraAnimationFrame.current = undefined; }, []); const animateCameraTo = useCallback((target: CameraState, duration = 620) => { stopCameraAnimation(); const start = cameraRef.current; if (typeof globalThis.matchMedia === 'function' && globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches) { cameraRef.current = target; setCamera(target); return; } const startedAt = globalThis.performance.now(); const tick = (timestamp: number) => { const progress = clamp((timestamp - startedAt) / duration, 0, 1); const eased = progress * progress * progress * (progress * (progress * 6 - 15) + 10); const breath = Math.sin(Math.PI * progress) * 0.032; const logarithmicScale = Math.exp(Math.log(start.scale) + (Math.log(target.scale) - Math.log(start.scale)) * eased); const next = { x: start.x + (target.x - start.x) * eased, y: start.y + (target.y - start.y) * eased, scale: logarithmicScale * (1 - breath) }; cameraRef.current = next; setCamera(next); if (progress < 1) cameraAnimationFrame.current = globalThis.requestAnimationFrame(tick); else cameraAnimationFrame.current = undefined; }; cameraAnimationFrame.current = globalThis.requestAnimationFrame(tick); }, [stopCameraAnimation]); const fitGraphTo = useCallback((width: number, height: number) => { setCamera(fittedCameraFor(displayedNodes, width, height)); }, [displayedNodes]); const fitGraph = useCallback( () => animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height), 520), [animateCameraTo, dimensions.height, dimensions.width, displayedNodes] ); useEffect(() => { setNodeOverrides({}); setPinnedNodeIds(current => current.filter(id => graph.nodes.some(node => node.id === id))); setSelectedNodeId(current => graph.nodes.some(node => node.id === current) ? current : ''); setMarkedSelectionIds(current => current.filter(id => id === FILTERED_GRAPH_SELECTION_ID || graph.nodes.some(node => node.id === id))); setExtractedSelectionIds(current => current.filter(id => graph.nodes.some(node => node.id === id))); }, [graphSignature]); useEffect(() => { if (active && pathSnapshotRequest) setQueuedPathSnapshot(pathSnapshotRequest); }, [active, pathSnapshotRequest]); useEffect(() => { if (!active || !queuedPathSnapshot) return; const availableNodeIds = new Set(graph.nodes.map(node => node.id)); const restored = queuedPathSnapshot.path.activeNodeIds.filter(id => availableNodeIds.has(id)); setPinnedNodeIds(restored); setSelectedNodeId(restored.at(-1) ?? ''); setQueuedPathSnapshot(undefined); onPathSnapshotApplied?.(); }, [active, graph.nodes, graphSignature, onPathSnapshotApplied, queuedPathSnapshot]); useEffect(() => { if (!active || lastFittedGraphSignature.current === graphSignature) return; const frame = globalThis.requestAnimationFrame(() => { const rect = surfaceRef.current?.getBoundingClientRect(); if (!rect || rect.width <= 1 || rect.height <= 1) return; lastFittedGraphSignature.current = graphSignature; setDimensions({ width: rect.width, height: rect.height }); fitGraphTo(rect.width, rect.height); }); return () => globalThis.cancelAnimationFrame(frame); }, [active, fitGraphTo, graphSignature]); useEffect(() => { if (!active) return; const extractSignature = extractedSelectionIds.join('|'); if (!extractSignature) { fittedExtractSignature.current = ''; return; } if (fittedExtractSignature.current === extractSignature) return; fittedExtractSignature.current = extractSignature; const frame = globalThis.requestAnimationFrame(() => animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height), 520)); return () => globalThis.cancelAnimationFrame(frame); }, [active, animateCameraTo, dimensions.height, dimensions.width, displayedNodes, extractedSelectionIds]); useEffect(() => { if (!active || !resetViewRevision || handledResetViewRevision.current === resetViewRevision) return; handledResetViewRevision.current = resetViewRevision; const frame = globalThis.requestAnimationFrame(() => animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height))); return () => globalThis.cancelAnimationFrame(frame); }, [active, animateCameraTo, dimensions.height, dimensions.width, displayedNodes, resetViewRevision]); useEffect(() => { if (!active) stopCameraAnimation(); }, [active, stopCameraAnimation]); useEffect(() => stopCameraAnimation, [stopCameraAnimation]); const linksFor = (item: FindingPathItem) => dataset && activeSheet ? [ ...linkResultSheets(item.record, dataset.sheets, activeSheetId), ...linkPingCastleSectionSheets(item.record, dataset.sheets), ...(resultMessageFor(item.record, activeSheet.toolId ?? dataset.profileId) ? [] : inlineResultTableSheets(item.record)) ] : []; const sectionsFor = (item: FindingPathItem) => resultSectionsFor(item.record, activeSheet?.toolId ?? dataset?.profileId); const resultWindowFor = (recordId: string) => resultWindows.find(item => item.recordId === recordId); const activateWindow = (recordId: string) => setResultWindows(current => current.map(item => item.recordId === recordId ? { ...item, zIndex: ++nextZIndex.current } : item)); const openResults = (item: FindingPathItem) => { if (resultWindowFor(item.id)) return setResultWindows(current => current.filter(candidate => candidate.recordId !== item.id)); const sheets = linksFor(item); if (!sheets.length && !sectionsFor(item).length) return; const total = sheets.reduce((sum, sheet) => sum + sheet.analysis.records.length, 0); setResultWindows(current => [...current, { recordId: item.id, toolId: toolIdForRecord(item.record, activeSheet?.toolId ?? dataset?.profileId), title: sheets.length ? `Results (${total.toLocaleString('en-US')}) – ${item.title}` : `Result – ${item.title}`, color: resultColors[nextColor.current++ % resultColors.length], zIndex: ++nextZIndex.current, minimized: false }]); }; const openFinding = (item: FindingPathItem) => { setSelectedRecord(item.id); setView('records'); onOpenFinding(); }; const zoomBy = (factor: number) => { const current = cameraRef.current; animateCameraTo({ ...current, scale: clamp(current.scale * factor, 0.08, 4) }, 280); }; const handleWheel = (event: ReactWheelEvent) => { event.preventDefault(); stopCameraAnimation(); const rect = event.currentTarget.getBoundingClientRect(); const nextScale = clamp(camera.scale * Math.exp(-event.deltaY * 0.0012), 0.08, 4); const pointerX = event.clientX - rect.left - dimensions.width / 2; const pointerY = event.clientY - rect.top - dimensions.height / 2; const graphX = (pointerX - camera.x) / camera.scale; const graphY = (pointerY - camera.y) / camera.scale; setCamera({ x: pointerX - graphX * nextScale, y: pointerY - graphY * nextScale, scale: nextScale }); }; const startPan = (event: ReactPointerEvent) => { if (event.button !== 0) return; stopCameraAnimation(); event.currentTarget.setPointerCapture(event.pointerId); pointerOperation.current = { kind: 'pan', pointerId: event.pointerId, moved: false, startX: event.clientX, startY: event.clientY, camera }; }; const startNodeDrag = (event: ReactPointerEvent, node: ObsidianGraphNode) => { if (event.button !== 0) return; stopCameraAnimation(); event.stopPropagation(); event.currentTarget.setPointerCapture(event.pointerId); pointerOperation.current = { kind: 'node', pointerId: event.pointerId, moved: false, nodeId: node.id, startX: event.clientX, startY: event.clientY, camera, nodeStart: { x: node.x, y: node.y } }; }; const movePointer = (event: ReactPointerEvent) => { const operation = pointerOperation.current; if (!operation || operation.pointerId !== event.pointerId) return; const dx = event.clientX - operation.startX; const dy = event.clientY - operation.startY; if (Math.hypot(dx, dy) > 3) operation.moved = true; if (operation.kind === 'pan') setCamera({ ...operation.camera, x: operation.camera.x + dx, y: operation.camera.y + dy }); else if (operation.nodeId && operation.nodeStart) { const graphDx = dx / operation.camera.scale; const graphDy = dy / operation.camera.scale; setNodeOverrides(current => ({ ...current, [operation.nodeId!]: { x: operation.nodeStart!.x + graphDx, y: operation.nodeStart!.y + graphDy } })); } }; const endPointer = (event: ReactPointerEvent) => { const operation = pointerOperation.current; if (!operation || operation.pointerId !== event.pointerId) return; if (operation.kind === 'node' && operation.nodeId && !operation.moved) { togglePinnedNode(operation.nodeId); } pointerOperation.current = null; }; const toggleMarkedSelection = () => { if (!actionSelectionNodeIds.length) return; setMarkedSelectionIds(actionSelectionIsMarked ? [] : [...actionSelectionNodeIds]); }; const extractSelection = () => { if (!selectedNodeId) return; const pathNodeIds = selectedPathNodeIds(graph, selectedNodeId); if (!pathNodeIds.length) return; extractViewBefore.current = { camera: cameraRef.current, pinnedNodeIds, selectedNodeId, markedSelectionIds }; setPinnedNodeIds([selectedNodeId]); setMarkedSelectionIds([]); setExtractedSelectionIds([selectedNodeId]); }; const exitSelectionExtract = () => { setExtractedSelectionIds([]); const savedView = extractViewBefore.current; extractViewBefore.current = undefined; if (savedView) { animateCameraTo(savedView.camera, 520); setPinnedNodeIds(savedView.pinnedNodeIds); setSelectedNodeId(savedView.selectedNodeId); setMarkedSelectionIds(savedView.markedSelectionIds); } }; const resetGraphView = () => { setPinnedNodeIds([]); setSelectedNodeId(''); setHoveredNodeId(''); setMarkedSelectionIds([]); setExtractedSelectionIds([]); setNodeOverrides({}); setShowText(true); extractViewBefore.current = undefined; fittedExtractSignature.current = ''; setResetViewRevision(current => current + 1); }; return
{displayedNodes.length.toLocaleString('en-US')} nodes · {displayedEdges.length.toLocaleString('en-US')} links
{actionBranchNode ? graphNodeKindLabels[actionBranchNode.kind] : actionBranchNodeId === FILTERED_GRAPH_SELECTION_ID ? 'Filters' : 'Selection'}
{displayedNodes.map(node => { const isSelected = pinnedNodeIds.includes(node.id); const isMarkedBranch = markedBranchNodeIds.includes(node.id); const isMuted = markedBranchNodeIds.length > 0 && !isMarkedBranch && !isSelected; const isSelectionMuted = !markedBranchNodeIds.length && pinnedNodeIds.length > 0 && !isSelected; const labelOnLeft = node.x > 0; const labelOffsetY = labelOffsets[node.id] ?? 0; const labelText = graphLabel(node.label); const labelNodeScreenX = dimensions.width / 2 + camera.x + node.x * camera.scale; const labelNodeScreenY = dimensions.height / 2 + camera.y + node.y * camera.scale; const labelWidth = Math.max(32, labelText.length * 7.2); const labelAnchorX = labelNodeScreenX + (labelOnLeft ? -1 : 1) * (node.radius * camera.scale + 6); const labelLeft = labelOnLeft ? labelAnchorX - labelWidth : labelAnchorX; const labelRight = labelOnLeft ? labelAnchorX : labelAnchorX + labelWidth; const labelTop = labelNodeScreenY + labelOffsetY - 15; const labelBottom = labelNodeScreenY + labelOffsetY + 5; const viewportInset = 6; const nodeIsInViewport = labelNodeScreenX >= viewportInset && labelNodeScreenX <= dimensions.width - viewportInset && labelNodeScreenY >= viewportInset && labelNodeScreenY <= dimensions.height - viewportInset; const labelIsFullyVisible = labelLeft >= viewportInset && labelRight <= dimensions.width - viewportInset && labelTop >= viewportInset && labelBottom <= dimensions.height - viewportInset; const showInlineLabel = showText && nodeIsInViewport && labelIsFullyVisible; const finding = node.itemId ? findingLookup.get(node.itemId) : undefined; return startNodeDrag(event, node)} onPointerEnter={() => setHoveredNodeId(node.id)} onPointerLeave={() => setHoveredNodeId(current => current === node.id ? '' : current)} onDoubleClick={() => finding && openFinding(finding)} onKeyDown={event => { if (event.key === 'Enter' || event.key === ' ') togglePinnedNode(node.id); }}> {isMarkedBranch && !isSelected && } {isSelected && } {node.kind !== 'finding' && } {showInlineLabel && {labelText}} ; })} {!nodes.length &&
No matching graph nodes.Change the shared filters or open a scan with MITRE mappings.
}
{resultWindows.map((item, index) => { if (item.minimized && resultWindows.some(window => !window.minimized)) return null; const finding = items.find(candidate => candidate.id === item.recordId); const sheets = finding ? linksFor(finding) : []; const sections = finding ? sectionsFor(finding) : []; return activateWindow(item.recordId)} onClose={() => setResultWindows(current => current.filter(candidate => candidate.recordId !== item.recordId))} onOpenRecord={finding ? () => openFinding(finding) : undefined} actionLabel="To Report" actionTitle="Jump to this finding’s record in the Report" minimized={item.minimized} onToggleMinimize={() => setResultWindows(current => current.map(candidate => candidate.recordId === item.recordId ? { ...candidate, minimized: !candidate.minimized } : candidate))} />; })}
; }