From 3951b9e52113f198ad1db0ae3db5ebd4feab5981 Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Wed, 24 Sep 2025 11:36:27 +0200 Subject: [PATCH] fix correlation provider issues --- core/graph_manager.py | 3 +- core/scanner.py | 6 +- providers/correlation_provider.py | 74 +- static/css/main.css | 4 +- static/js/graph.js | 1085 +++++------------------------ 5 files changed, 225 insertions(+), 947 deletions(-) diff --git a/core/graph_manager.py b/core/graph_manager.py index 735f50e..57b066a 100644 --- a/core/graph_manager.py +++ b/core/graph_manager.py @@ -163,7 +163,8 @@ class GraphManager: 'to': target, 'label': attrs.get('relationship_type', ''), 'source_provider': attrs.get('source_provider', ''), - 'discovery_timestamp': attrs.get('discovery_timestamp') + 'discovery_timestamp': attrs.get('discovery_timestamp'), + 'raw_data': attrs.get('raw_data', {}) }) return { diff --git a/core/scanner.py b/core/scanner.py index 3d88b26..ea5e095 100644 --- a/core/scanner.py +++ b/core/scanner.py @@ -929,7 +929,7 @@ class Scanner: # Re-enqueue the node for full processing is_ip = _is_valid_ip(node_id) - eligible_providers = self._get_eligible_providers(node_id, is_ip, False) + eligible_providers = self._get_eligible_providers(node_id, is_ip, False, is_extracted=True) for provider in eligible_providers: provider_name = provider.get_name() priority = self._get_priority(provider_name) @@ -1133,7 +1133,7 @@ class Scanner: self.logger.logger.warning(f"Error initializing provider states for {target}: {e}") - def _get_eligible_providers(self, target: str, is_ip: bool, dns_only: bool) -> List: + def _get_eligible_providers(self, target: str, is_ip: bool, dns_only: bool, is_extracted: bool = False) -> List: """ FIXED: Improved provider eligibility checking with better filtering. """ @@ -1145,7 +1145,7 @@ class Scanner: # Check if the target is part of a large entity is_in_large_entity = False - if self.graph.graph.has_node(target): + if self.graph.graph.has_node(target) and not is_extracted: metadata = self.graph.graph.nodes[target].get('metadata', {}) if 'large_entity_id' in metadata: is_in_large_entity = True diff --git a/providers/correlation_provider.py b/providers/correlation_provider.py index db8cd1b..c34d8bd 100644 --- a/providers/correlation_provider.py +++ b/providers/correlation_provider.py @@ -1,4 +1,4 @@ -# DNScope/providers/correlation_provider.py +# dnsrecon-reduced/providers/correlation_provider.py import re from typing import Dict, Any, List @@ -24,6 +24,10 @@ class CorrelationProvider(BaseProvider): self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}') self.EXCLUDED_KEYS = [ 'cert_source', + 'a_records', + 'mx_records', + 'ns_records', + 'ptr_records', 'cert_issuer_ca_id', 'cert_common_name', 'cert_validity_period_days', @@ -38,6 +42,8 @@ class CorrelationProvider(BaseProvider): 'updated_timestamp', 'discovery_timestamp', 'query_timestamp', + 'shodan_ip_str', + 'shodan_a_record', ] def get_name(self) -> str: @@ -83,7 +89,7 @@ class CorrelationProvider(BaseProvider): def _find_correlations(self, node_id: str) -> ProviderResult: """ Find correlations for a given node with enhanced filtering and error handling. - UPDATED: Enhanced with discovery timestamps for time-based edge coloring. + UPDATED: Enhanced with discovery timestamps for time-based edge coloring and list value processing. """ result = ProviderResult() discovery_time = datetime.now(timezone.utc) @@ -109,38 +115,46 @@ class CorrelationProvider(BaseProvider): attr_value = attr.get('value') attr_provider = attr.get('provider', 'unknown') - # Enhanced filtering logic - should_exclude = self._should_exclude_attribute(attr_name, attr_value) - - if should_exclude: - continue + # Prepare a list of values to iterate over + values_to_process = [] + if isinstance(attr_value, list): + values_to_process.extend(attr_value) + else: + values_to_process.append(attr_value) - # Build correlation index - if attr_value not in self.correlation_index: - self.correlation_index[attr_value] = { - 'nodes': set(), - 'sources': [] + for value_item in values_to_process: + # Enhanced filtering logic + should_exclude = self._should_exclude_attribute(attr_name, value_item) + + if should_exclude: + continue + + # Build correlation index + if value_item not in self.correlation_index: + self.correlation_index[value_item] = { + 'nodes': set(), + 'sources': [] + } + + self.correlation_index[value_item]['nodes'].add(node_id) + + source_info = { + 'node_id': node_id, + 'provider': attr_provider, + 'attribute': attr_name, + 'path': f"{attr_provider}_{attr_name}" } - self.correlation_index[attr_value]['nodes'].add(node_id) + # Avoid duplicate sources + existing_sources = [s for s in self.correlation_index[value_item]['sources'] + if s['node_id'] == node_id and s['path'] == source_info['path']] + if not existing_sources: + self.correlation_index[value_item]['sources'].append(source_info) - source_info = { - 'node_id': node_id, - 'provider': attr_provider, - 'attribute': attr_name, - 'path': f"{attr_provider}_{attr_name}" - } - - # Avoid duplicate sources - existing_sources = [s for s in self.correlation_index[attr_value]['sources'] - if s['node_id'] == node_id and s['path'] == source_info['path']] - if not existing_sources: - self.correlation_index[attr_value]['sources'].append(source_info) - - # Create correlation if we have multiple nodes with this value - if len(self.correlation_index[attr_value]['nodes']) > 1: - self._create_correlation_relationships(attr_value, self.correlation_index[attr_value], result, discovery_time) - correlations_found += 1 + # Create correlation if we have multiple nodes with this value + if len(self.correlation_index[value_item]['nodes']) > 1: + self._create_correlation_relationships(value_item, self.correlation_index[value_item], result, discovery_time) + correlations_found += 1 # Log correlation results if correlations_found > 0: diff --git a/static/css/main.css b/static/css/main.css index f0aae48..18a74d1 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -401,7 +401,7 @@ input[type="text"]:focus, select:focus { gap: 0.3rem; position: absolute; top: 10px; - left: 10px; + right: 10px; background: rgba(26, 26, 26, 0.9); padding: 0.5rem; border-radius: 6px; @@ -1406,7 +1406,7 @@ input[type="password"]:focus { .graph-controls { position: relative; top: auto; - left: auto; + right: auto; margin-bottom: 1rem; min-width: auto; } diff --git a/static/js/graph.js b/static/js/graph.js index d2cec72..b315302 100644 --- a/static/js/graph.js +++ b/static/js/graph.js @@ -2,7 +2,7 @@ /** * Graph visualization module for DNScope * Handles network graph rendering using vis.js with proper large entity node hiding - * UPDATED: Added time-based blue gradient edge coloring system + * UPDATED: Fixed time-based blue gradient edge coloring system and simplified logic. */ const contextMenuCSS = ` .graph-context-menu { @@ -106,22 +106,16 @@ class GraphManager { this.history = []; this.filterPanel = null; this.initialTargetIds = new Set(); - // Track large entity members for proper hiding this.largeEntityMembers = new Set(); this.isScanning = false; - - // Manual refresh button for polling optimization this.manualRefreshButton = null; - this.manualRefreshHandler = null; // Store the handler - - // Time-based gradient settings - this.timeOfInterest = new Date(); // Default to now - this.edgeTimestamps = new Map(); // Store edge ID -> timestamp mapping + this.manualRefreshHandler = null; + this.timeOfInterest = new Date(); + this.edgeTimestamps = new Map(); - // Gradient colors: grey-ish dark to retina-melting light blue this.gradientColors = { - dark: '#6b7280', // Grey-ish dark - light: '#00bfff' // Retina-melting light blue + dark: '#6b7280', + light: '#00bfff' }; this.options = { @@ -226,19 +220,17 @@ class GraphManager { randomSeed: 2 } }; - if (typeof document !== 'undefined') { - const style = document.createElement('style'); - style.textContent = contextMenuCSS; - document.head.appendChild(style); - } + + if (typeof document !== 'undefined') { + const style = document.createElement('style'); + style.textContent = contextMenuCSS; + document.head.appendChild(style); + } this.createNodeInfoPopup(); this.createContextMenu(); document.body.addEventListener('click', () => this.hideContextMenu()); } - /** - * Create floating node info popup - */ createNodeInfoPopup() { this.nodeInfoPopup = document.createElement('div'); this.nodeInfoPopup.className = 'node-info-popup'; @@ -246,11 +238,7 @@ class GraphManager { document.body.appendChild(this.nodeInfoPopup); } - /** - * Create context menu - */ createContextMenu() { - // Remove existing context menu if it exists const existing = document.getElementById('graph-context-menu'); if (existing) { existing.remove(); @@ -261,7 +249,6 @@ class GraphManager { this.contextMenu.className = 'graph-context-menu'; this.contextMenu.style.display = 'none'; - // Prevent body click listener from firing when clicking the menu itself this.contextMenu.addEventListener('click', (event) => { event.stopPropagation(); }); @@ -269,31 +256,20 @@ class GraphManager { document.body.appendChild(this.contextMenu); } - /** - * Initialize the network graph - */ initialize() { - if (this.isInitialized) { - return; - } + if (this.isInitialized) return; try { - const data = { - nodes: this.nodes, - edges: this.edges - }; - + const data = { nodes: this.nodes, edges: this.edges }; this.network = new vis.Network(this.container, data, this.options); this.setupNetworkEvents(); this.isInitialized = true; - // Hide placeholder const placeholder = this.container.querySelector('.graph-placeholder'); if (placeholder) { placeholder.style.display = 'none'; } - // Add graph controls this.addGraphControls(); this.addFilterPanel(); @@ -304,15 +280,10 @@ class GraphManager { } } - /** - * Add interactive graph controls with time of interest control - * UPDATED: Added time-based edge coloring controls - */ addGraphControls() { const controlsContainer = document.createElement('div'); controlsContainer.className = 'graph-controls'; - // Format current date/time for the input const currentDateTime = this.formatDateTimeForInput(this.timeOfInterest); controlsContainer.innerHTML = ` @@ -336,31 +307,23 @@ class GraphManager { this.container.appendChild(controlsContainer); - // Add control event listeners document.getElementById('graph-fit').addEventListener('click', () => this.fitView()); document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics()); document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering()); document.getElementById('graph-unhide').addEventListener('click', () => this.unhideAll()); document.getElementById('graph-revert').addEventListener('click', () => this.revertLastAction()); - // Time of interest control document.getElementById('time-of-interest').addEventListener('change', (e) => { this.timeOfInterest = new Date(e.target.value); - console.log('Time of interest updated:', this.timeOfInterest); this.updateEdgeColors(); }); - // Manual refresh button - handler will be set by main app this.manualRefreshButton = document.getElementById('graph-manual-refresh'); - // If a handler was set before the button existed, attach it now if (this.manualRefreshButton && this.manualRefreshHandler) { this.manualRefreshButton.addEventListener('click', this.manualRefreshHandler); } } - /** - * Format date for datetime-local input - */ formatDateTimeForInput(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); @@ -370,92 +333,41 @@ class GraphManager { return `${year}-${month}-${day}T${hours}:${minutes}`; } - /** - * Extract relevant timestamp from edge raw_data based on provider - */ extractEdgeTimestamp(edge) { const rawData = edge.raw_data || {}; - const provider = edge.source_provider || ''; - // Check for standardized relevance_timestamp first if (rawData.relevance_timestamp) { return new Date(rawData.relevance_timestamp); } - // Provider-specific timestamp extraction - switch (provider.toLowerCase()) { - case 'shodan': - // Use last_seen timestamp for Shodan - if (rawData.last_seen) { - return new Date(rawData.last_seen); - } - break; - - case 'crtsh': - // Use certificate issue date (not_before) for certificates - if (rawData.cert_not_before) { - return new Date(rawData.cert_not_before); - } - break; - - case 'dns': - case 'correlation': - default: - // Use discovery timestamp for DNS and correlation - if (edge.discovery_timestamp) { - return new Date(edge.discovery_timestamp); - } - break; - } - - // Fallback to discovery timestamp or current time if (edge.discovery_timestamp) { return new Date(edge.discovery_timestamp); } - return new Date(); // Default to now if no timestamp available + return new Date(); } - /** - * Calculate time-based blue gradient color - */ - calculateTimeGradientColor(timestamp) { + calculateTimeGradientColor(timestamp, maxTimeDiff) { if (!timestamp || !this.timeOfInterest) { - return this.gradientColors.dark; // Default to dark grey + return this.gradientColors.dark; } - // Calculate time difference in milliseconds const timeDiff = Math.abs(timestamp.getTime() - this.timeOfInterest.getTime()); - // Find maximum time difference across all edges for normalization - let maxTimeDiff = 0; - this.edgeTimestamps.forEach((edgeTimestamp) => { - const diff = Math.abs(edgeTimestamp.getTime() - this.timeOfInterest.getTime()); - if (diff > maxTimeDiff) { - maxTimeDiff = diff; - } - }); - if (maxTimeDiff === 0) { - return this.gradientColors.light; // All timestamps are the same + return this.gradientColors.light; } - // Calculate gradient position (0 = closest to time of interest, 1 = furthest) const gradientPosition = timeDiff / maxTimeDiff; - // Interpolate between light blue (close) and dark grey (far) return this.interpolateColor( - this.gradientColors.light, // Close to time of interest - this.gradientColors.dark, // Far from time of interest + this.gradientColors.light, + this.gradientColors.dark, gradientPosition ); } - /** - * Interpolate between two hex colors - */ interpolateColor(color1, color2, factor) { - // Parse hex colors const hex1 = color1.replace('#', ''); const hex2 = color2.replace('#', ''); @@ -467,57 +379,45 @@ class GraphManager { const g2 = parseInt(hex2.substring(2, 4), 16); const b2 = parseInt(hex2.substring(4, 6), 16); - // Interpolate const r = Math.round(r1 + (r2 - r1) * factor); const g = Math.round(g1 + (g2 - g1) * factor); const b = Math.round(b1 + (b2 - b1) * factor); - // Convert back to hex return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; } - /** - * Update all edge colors based on current time of interest - */ updateEdgeColors() { const edgeUpdates = []; - + let maxTimeDiff = 0; + this.edgeTimestamps.forEach((edgeTimestamp) => { + const diff = Math.abs(edgeTimestamp.getTime() - this.timeOfInterest.getTime()); + if (diff > maxTimeDiff) { + maxTimeDiff = diff; + } + }); + this.edges.forEach((edge) => { const timestamp = this.edgeTimestamps.get(edge.id); - const color = this.calculateTimeGradientColor(timestamp); + const color = this.calculateTimeGradientColor(timestamp, maxTimeDiff); edgeUpdates.push({ id: edge.id, - color: { - color: color, - highlight: '#00ff41', - hover: '#ff9900' - } + color: { color: color, highlight: '#00ff41', hover: '#ff9900' } }); }); if (edgeUpdates.length > 0) { this.edges.update(edgeUpdates); - console.log(`Updated ${edgeUpdates.length} edge colors based on time gradient`); } } - /** - * Set the manual refresh button click handler - * @param {Function} handler - Function to call when manual refresh is clicked - */ setManualRefreshHandler(handler) { this.manualRefreshHandler = handler; - // If the button already exists, attach the handler if (this.manualRefreshButton && typeof handler === 'function') { this.manualRefreshButton.addEventListener('click', handler); } } - /** - * Show or hide the manual refresh button - * @param {boolean} show - Whether to show the button - */ showManualRefreshButton(show) { if (this.manualRefreshButton) { this.manualRefreshButton.style.display = show ? 'inline-block' : 'none'; @@ -530,33 +430,20 @@ class GraphManager { this.container.appendChild(this.filterPanel); } - /** - * Setup network event handlers - */ setupNetworkEvents() { if (!this.network) return; - // FIXED: Right-click context menu this.container.addEventListener('contextmenu', (event) => { event.preventDefault(); - - // Get coordinates relative to the canvas - const pointer = { - x: event.offsetX, - y: event.offsetY - }; - + const pointer = { x: event.offsetX, y: event.offsetY }; const nodeId = this.network.getNodeAt(pointer); - if (nodeId) { - // Pass the original client event for positioning this.showContextMenu(nodeId, event); } else { this.hideContextMenu(); } }); - // Node click event with details this.network.on('click', (params) => { this.hideContextMenu(); if (params.nodes.length > 0) { @@ -575,27 +462,16 @@ class GraphManager { } }); - // Hover events this.network.on('hoverNode', (params) => { - const nodeId = params.node; - const node = this.nodes.get(nodeId); - if (node) { - this.highlightConnectedNodes(nodeId, true); - } - }); - - // Stabilization events with progress - this.network.on('stabilizationProgress', (params) => { - const progress = params.iterations / params.total; + this.highlightConnectedNodes(params.node, true); }); this.network.on('stabilizationIterationsDone', () => { this.onStabilizationComplete(); }); - // Click away to hide context menu document.addEventListener('click', (e) => { - if (!this.contextMenu.contains(e.target)) { + if (this.contextMenu && !this.contextMenu.contains(e.target)) { this.hideContextMenu(); } }); @@ -628,65 +504,65 @@ class GraphManager { const nodeMap = new Map(graphData.nodes.map(node => [node.id, node])); - // FIXED: Process all nodes first, then apply hiding logic correctly - const processedNodes = graphData.nodes.map(node => { - const processed = this.processNode(node); - - // FIXED: Only hide if node is still a large entity member - if (node.metadata && node.metadata.large_entity_id) { - processed.hidden = true; - } else { - // FIXED: Ensure extracted nodes are visible - processed.hidden = false; - } - - return processed; - }); - - const processedEdges = graphData.edges.map(edge => { + // --- START: TWO-PASS LOGIC FOR ACCURATE GRADIENTS --- + + // 1. First Pass: Re-route edges and gather all timestamps to find the time range + const rawEdges = graphData.edges.map(edge => { let fromNode = nodeMap.get(edge.from); let toNode = nodeMap.get(edge.to); let fromId = edge.from; let toId = edge.to; - // FIXED: Only re-route if nodes are STILL in large entities - if (fromNode && fromNode.metadata && fromNode.metadata.large_entity_id) { + if (fromNode?.metadata?.large_entity_id) { fromId = fromNode.metadata.large_entity_id; } - if (toNode && toNode.metadata && toNode.metadata.large_entity_id) { + if (toNode?.metadata?.large_entity_id) { toId = toNode.metadata.large_entity_id; } - // Avoid self-referencing edges from re-routing - if (fromId === toId) { - return null; + if (fromId === toId) return null; + return { ...edge, from: fromId, to: toId }; + }).filter(Boolean); + + this.edgeTimestamps.clear(); + rawEdges.forEach(edge => { + const edgeId = `${edge.from}-${edge.to}-${edge.label}`; + this.edgeTimestamps.set(edgeId, this.extractEdgeTimestamp(edge)); + }); + + // 2. Calculate the global maxTimeDiff for this update + let maxTimeDiff = 0; + this.edgeTimestamps.forEach((edgeTimestamp) => { + const diff = Math.abs(edgeTimestamp.getTime() - this.timeOfInterest.getTime()); + if (diff > maxTimeDiff) { + maxTimeDiff = diff; } + }); - const reRoutedEdge = { ...edge, from: fromId, to: toId }; - return this.processEdge(reRoutedEdge); - }).filter(Boolean); // Remove nulls from self-referencing edges + // 3. Second Pass: Process nodes and edges with the correct time context + const processedNodes = graphData.nodes.map(node => { + const processed = this.processNode(node); + processed.hidden = !!node.metadata?.large_entity_id; + return processed; + }); + const processedEdges = rawEdges.map(edge => this.processEdge(edge, maxTimeDiff)); - const existingNodeIds = this.nodes.getIds(); - const existingEdgeIds = this.edges.getIds(); + // --- END: TWO-PASS LOGIC --- - const newNodes = processedNodes.filter(node => !existingNodeIds.includes(node.id)); - const newEdges = processedEdges.filter(edge => !existingEdgeIds.includes(edge.id)); - - // FIXED: Update all nodes to ensure extracted nodes become visible this.nodes.update(processedNodes); this.edges.update(processedEdges); - // Update edge timestamps and colors for time-based gradient - this.updateEdgeTimestampsAndColors(graphData.edges); - this.updateFilterControls(); this.applyAllFilters(); + const newNodes = processedNodes.filter(node => !this.nodes.get(node.id)); + const newEdges = processedEdges.filter(edge => !this.edges.get(edge.id)); + if (newNodes.length > 0 || newEdges.length > 0) { setTimeout(() => this.highlightNewElements(newNodes, newEdges), 100); } - if (this.nodes.length <= 10 || existingNodeIds.length === 0) { + if (this.nodes.length <= 10 || this.nodes.getIds().length === 0) { setTimeout(() => this.fitView(), 800); } @@ -695,20 +571,25 @@ class GraphManager { this.showError('Failed to update visualization'); } } - - /** - * Update edge timestamps and apply time-based gradient colors - */ - updateEdgeTimestampsAndColors(edgeData) { - // Extract timestamps from raw edge data - edgeData.forEach(edge => { - const edgeId = `${edge.from}-${edge.to}-${edge.label}`; - const timestamp = this.extractEdgeTimestamp(edge); - this.edgeTimestamps.set(edgeId, timestamp); - }); + + processEdge(edge, maxTimeDiff) { + const edgeId = `${edge.from}-${edge.to}-${edge.label}`; + const timestamp = this.edgeTimestamps.get(edgeId); + const timeGradientColor = this.calculateTimeGradientColor(timestamp, maxTimeDiff); - // Update edge colors based on new timestamps - this.updateEdgeColors(); + return { + id: edgeId, + from: edge.from, + to: edge.to, + label: edge.label, + title: this.createEdgeTooltip(edge), + color: { color: timeGradientColor, highlight: '#00ff41', hover: '#ff9900' }, + metadata: { + relationship_type: edge.label, + source_provider: edge.source_provider, + discovery_timestamp: edge.discovery_timestamp + } + }; } analyzeCertificateInfo(attributes) { @@ -718,14 +599,10 @@ class GraphManager { for (const attr of attributes) { const attrName = (attr.name || '').toLowerCase(); - const attrProvider = (attr.provider || '').toLowerCase(); const attrValue = attr.value; - // Look for certificate attributes from crtsh provider - if (attrProvider === 'crtsh' || attrName.startsWith('cert_')) { + if (attrName.startsWith('cert_')) { hasCertificates = true; - - // Check certificate validity using raw attribute names if (attrName === 'cert_is_currently_valid') { if (attrValue === true) { hasValidCertificates = true; @@ -733,13 +610,6 @@ class GraphManager { hasExpiredCertificates = true; } } - // Check for expiry indicators - else if (attrName === 'cert_expires_soon' && attrValue === true) { - hasExpiredCertificates = true; - } - else if (attrName.includes('expired') && attrValue === true) { - hasExpiredCertificates = true; - } } } @@ -751,12 +621,6 @@ class GraphManager { }; } - /** - * UPDATED: Helper method to find an attribute by name in the standardized attributes list - * @param {Array} attributes - List of StandardAttribute objects - * @param {string} name - Attribute name to find - * @returns {Object|null} The attribute object if found, null otherwise - */ findAttributeByName(attributes, name) { if (!Array.isArray(attributes)) { return null; @@ -764,11 +628,6 @@ class GraphManager { return attributes.find(attr => attr.name === name) || null; } - /** - * UPDATED: Process node data with styling and metadata for the flat data model - * @param {Object} node - Raw node data with standardized attributes - * @returns {Object} Processed node data - */ processNode(node) { const processedNode = { id: node.id, @@ -786,26 +645,20 @@ class GraphManager { }; if (node.max_depth_reached) { - processedNode.borderColor = '#ff0000'; // Red border for max depth + processedNode.borderColor = '#ff0000'; } - // FIXED: Certificate-based domain coloring if (node.type === 'domain' && Array.isArray(node.attributes)) { const certInfo = this.analyzeCertificateInfo(node.attributes); - if (certInfo.hasExpiredOnly) { - // Red for domains with only expired/invalid certificates processedNode.color = '#ff6b6b'; processedNode.borderColor = '#cc5555'; } else if (!certInfo.hasCertificates) { - // Grey for domains with no certificates processedNode.color = '#c7c7c7'; processedNode.borderColor = '#999999'; } - // Green for valid certificates (default color) } - // Handle merged correlation objects if (node.type === 'correlation_object') { const correlationValueAttr = this.findAttributeByName(node.attributes, 'correlation_value'); const value = correlationValueAttr ? correlationValueAttr.value : 'Unknown'; @@ -818,48 +671,6 @@ class GraphManager { return processedNode; } - /** - * Process edge data with styling, metadata, and time-based gradient colors - * @param {Object} edge - Raw edge data - * @returns {Object} Processed edge data - */ - processEdge(edge) { - const edgeId = `${edge.from}-${edge.to}-${edge.label}`; - - // Extract timestamp for this edge - const timestamp = this.extractEdgeTimestamp(edge); - this.edgeTimestamps.set(edgeId, timestamp); - - // Calculate time-based gradient color - const timeGradientColor = this.calculateTimeGradientColor(timestamp); - - const processedEdge = { - id: edgeId, - from: edge.from, - to: edge.to, - label: edge.label, // Correctly access the label directly - title: this.createEdgeTooltip(edge), - color: { - color: timeGradientColor, - highlight: '#00ff41', - hover: '#ff9900' - }, - metadata: { - relationship_type: edge.label, - source_provider: edge.source_provider, - discovery_timestamp: edge.discovery_timestamp - } - }; - - return processedEdge; - } - - /** - * Format node label for display - * @param {string} nodeId - Node identifier - * @param {string} nodeType - Node type - * @returns {string} Formatted label - */ formatNodeLabel(nodeId, nodeType) { if (typeof nodeId !== 'string') return ''; if (nodeId.length > 20) { @@ -868,80 +679,38 @@ class GraphManager { return nodeId; } - - - /** - * Get node color based on type - * @param {string} nodeType - Node type - * @returns {string} Color value - */ getNodeColor(nodeType) { const colors = { - 'domain': '#00ff41', // Green - 'ip': '#ff9900', // Amber - 'isp': '#00aaff', // Blue - 'ca': '#ff6b6b', // Red - 'large_entity': '#ff6b6b', // Red for large entities - 'correlation_object': '#9620c0ff' + 'domain': '#00ff41', 'ip': '#ff9900', 'isp': '#00aaff', + 'ca': '#ff6b6b', 'large_entity': '#ff6b6b', 'correlation_object': '#9620c0ff' }; return colors[nodeType] || '#ffffff'; } - /** - * Get node border color based on type - * @param {string} nodeType - Node type - * @returns {string} Border color value - */ getNodeBorderColor(nodeType) { const borderColors = { - 'domain': '#00aa2e', - 'ip': '#cc7700', - 'isp': '#0088cc', - 'ca': '#cc5555', - 'correlation_object': '#c235c9ff' + 'domain': '#00aa2e', 'ip': '#cc7700', 'isp': '#0088cc', + 'ca': '#cc5555', 'correlation_object': '#c235c9ff' }; return borderColors[nodeType] || '#666666'; } - /** - * Get node size based on type - * @param {string} nodeType - Node type - * @returns {number} Node size - */ getNodeSize(nodeType) { const sizes = { - 'domain': 12, - 'ip': 14, - 'isp': 16, - 'ca': 16, - 'correlation_object': 8, - 'large_entity': 25 + 'domain': 12, 'ip': 14, 'isp': 16, 'ca': 16, + 'correlation_object': 8, 'large_entity': 25 }; return sizes[nodeType] || 12; } - /** - * Get node shape based on type - * @param {string} nodeType - Node type - * @returns {string} Shape name - */ getNodeShape(nodeType) { const shapes = { - 'domain': 'dot', - 'ip': 'square', - 'isp': 'triangle', - 'ca': 'diamond', - 'correlation_object': 'hexagon', - 'large_entity': 'dot' + 'domain': 'dot', 'ip': 'square', 'isp': 'triangle', 'ca': 'diamond', + 'correlation_object': 'hexagon', 'large_entity': 'dot' }; return shapes[nodeType] || 'dot'; } - /** - * Create edge tooltip with correct provider information and timestamp - * @param {Object} edge - Edge data - * @returns {string} HTML tooltip content - */ createEdgeTooltip(edge) { let tooltip = `
`; tooltip += `
${edge.label || 'Relationship'}
`; @@ -951,205 +720,85 @@ class GraphManager { } if (edge.discovery_timestamp) { - const date = new Date(edge.discovery_timestamp); - tooltip += `
Discovered: ${date.toLocaleString()}
`; + const discoveryDate = new Date(edge.discovery_timestamp); + tooltip += `
Discovered: ${discoveryDate.toLocaleString()}
`; } - - // Add timestamp information for time-based coloring + const edgeId = `${edge.from}-${edge.to}-${edge.label}`; - const timestamp = this.edgeTimestamps.get(edgeId); - if (timestamp) { - tooltip += `
Data from: ${timestamp.toLocaleString()}
`; + const relevanceTimestamp = this.edgeTimestamps.get(edgeId); + if (relevanceTimestamp) { + tooltip += `
Data from: ${relevanceTimestamp.toLocaleString()}
`; } tooltip += `
`; return tooltip; } - /** - * Determine if node is important based on connections or metadata - * @param {Object} node - Node data - * @returns {boolean} True if node is important - */ - isImportantNode(node) { - // Mark nodes as important based on criteria - if (node.type === 'domain' && node.id.includes('www.')) return false; - if (node.metadata && node.metadata.connection_count > 3) return true; - if (node.type === 'asn') return true; - return false; - } - - /** - * Show node details in modal - * @param {Object} node - Node object - */ showNodeDetails(node) { - // Trigger custom event for main application to handle - const event = new CustomEvent('nodeSelected', { - detail: { node } - }); + const event = new CustomEvent('nodeSelected', { detail: { node } }); document.dispatchEvent(event); } - /** - * Hide node info popup - */ - hideNodeInfoPopup() { - if (this.nodeInfoPopup) { - this.nodeInfoPopup.style.display = 'none'; - } - } - - /** - * Highlight node connections - * @param {string} nodeId - Node to highlight - */ highlightNodeConnections(nodeId) { const connectedNodes = this.network.getConnectedNodes(nodeId); const connectedEdges = this.network.getConnectedEdges(nodeId); - // Update node colors - const nodeUpdates = connectedNodes.map(id => ({ - id: id, - borderColor: '#ff9900', - borderWidth: 3 - })); + const nodeUpdates = connectedNodes.map(id => ({ id: id, borderColor: '#ff9900', borderWidth: 3 })); + nodeUpdates.push({ id: nodeId, borderColor: '#00ff41', borderWidth: 4 }); - nodeUpdates.push({ - id: nodeId, - borderColor: '#00ff41', - borderWidth: 4 - }); - - // Update edge colors - const edgeUpdates = connectedEdges.map(id => ({ - id: id, - color: { color: '#ff9900' }, - width: 3 - })); + const edgeUpdates = connectedEdges.map(id => ({ id: id, color: { color: '#ff9900' }, width: 3 })); this.nodes.update(nodeUpdates); this.edges.update(edgeUpdates); - // Store for cleanup - this.highlightedElements = { - nodes: connectedNodes.concat([nodeId]), - edges: connectedEdges - }; + this.highlightedElements = { nodes: connectedNodes.concat([nodeId]), edges: connectedEdges }; } - /** - * Highlight connected nodes on hover - * @param {string} nodeId - Node ID - * @param {boolean} highlight - Whether to highlight or unhighlight - */ highlightConnectedNodes(nodeId, highlight) { const connectedNodes = this.network.getConnectedNodes(nodeId); const connectedEdges = this.network.getConnectedEdges(nodeId); - if (highlight) { - // Dim all other elements this.dimUnconnectedElements([nodeId, ...connectedNodes], connectedEdges); } } - /** - * Dim elements not connected to the specified nodes - * @param {Array} nodeIds - Node IDs to keep highlighted - * @param {Array} edgeIds - Edge IDs to keep highlighted - */ dimUnconnectedElements(nodeIds, edgeIds) { const allNodes = this.nodes.get(); const allEdges = this.edges.get(); - const nodeUpdates = allNodes.map(node => ({ - id: node.id, - opacity: nodeIds.includes(node.id) ? 1 : 0.3 - })); - - const edgeUpdates = allEdges.map(edge => ({ - id: edge.id, - opacity: edgeIds.includes(edge.id) ? 1 : 0.1 - })); + const nodeUpdates = allNodes.map(node => ({ id: node.id, opacity: nodeIds.includes(node.id) ? 1 : 0.3 })); + const edgeUpdates = allEdges.map(edge => ({ id: edge.id, opacity: edgeIds.includes(edge.id) ? 1 : 0.1 })); this.nodes.update(nodeUpdates); this.edges.update(edgeUpdates); } - /** - * Clear all highlights - */ clearHighlights() { if (this.highlightedElements) { - // Reset highlighted nodes const nodeUpdates = this.highlightedElements.nodes.map(id => { const originalNode = this.nodes.get(id); - return { - id: id, - borderColor: this.getNodeBorderColor(originalNode.type), - borderWidth: 2 - }; + return { id: id, borderColor: this.getNodeBorderColor(originalNode.type), borderWidth: 2 }; }); - // Reset highlighted edges to time-based colors const edgeUpdates = this.highlightedElements.edges.map(id => { const timestamp = this.edgeTimestamps.get(id); const color = this.calculateTimeGradientColor(timestamp); - return { - id: id, - color: { - color: color, - highlight: '#00ff41', - hover: '#ff9900' - } - }; + return { id: id, color: { color: color, highlight: '#00ff41', hover: '#ff9900' } }; }); this.nodes.update(nodeUpdates); this.edges.update(edgeUpdates); - this.highlightedElements = null; } } - /** - * Clear hover highlights - */ - clearHoverHighlights() { - const allNodes = this.nodes.get(); - const allEdges = this.edges.get(); - - const nodeUpdates = allNodes.map(node => ({ id: node.id, opacity: 1 })); - const edgeUpdates = allEdges.map(edge => ({ id: edge.id, opacity: 1 })); - - this.nodes.update(nodeUpdates); - this.edges.update(edgeUpdates); - } - - /** - * Highlight newly added elements - * @param {Array} newNodes - New nodes - * @param {Array} newEdges - New edges - */ highlightNewElements(newNodes, newEdges) { - // Briefly highlight new nodes - const nodeHighlights = newNodes.map(node => ({ - id: node.id, - borderColor: '#00ff41', - borderWidth: 4 - })); - - // Briefly highlight new edges - const edgeHighlights = newEdges.map(edge => ({ - id: edge.id, - color: '#00ff41', - width: 4 - })); + const nodeHighlights = newNodes.map(node => ({ id: node.id, borderColor: '#00ff41', borderWidth: 4 })); + const edgeHighlights = newEdges.map(edge => ({ id: edge.id, color: '#00ff41', width: 4 })); this.nodes.update(nodeHighlights); this.edges.update(edgeHighlights); - // Reset after animation setTimeout(() => { const nodeResets = newNodes.map(node => ({ id: node.id, @@ -1157,18 +806,10 @@ class GraphManager { borderWidth: 2, })); - // Reset edges to time-based colors const edgeResets = newEdges.map(edge => { const timestamp = this.edgeTimestamps.get(edge.id); const color = this.calculateTimeGradientColor(timestamp); - return { - id: edge.id, - color: { - color: color, - highlight: '#00ff41', - hover: '#ff9900' - } - }; + return { id: edge.id, color: { color: color, highlight: '#00ff41', hover: '#ff9900' } }; }); this.nodes.update(nodeResets); @@ -1176,153 +817,82 @@ class GraphManager { }, 2000); } - /** - * Handle stabilization completion - */ onStabilizationComplete() { console.log('Graph stabilization complete'); } - /** - * Focus view on specific node - * @param {string} nodeId - Node to focus on - */ focusOnNode(nodeId) { const nodePosition = this.network.getPositions([nodeId]); if (nodePosition[nodeId]) { this.network.moveTo({ position: nodePosition[nodeId], scale: 1.5, - animation: { - duration: 1000, - easingFunction: 'easeInOutQuart' - } + animation: { duration: 1000, easingFunction: 'easeInOutQuart' } }); } } - /** - * Toggle physics simulation - */ togglePhysics() { const currentPhysics = this.network.physics.physicsEnabled; this.network.setOptions({ physics: !currentPhysics }); - const button = document.getElementById('graph-physics'); if (button) { button.textContent = currentPhysics ? '[PHYSICS OFF]' : '[PHYSICS ON]'; - button.style.color = currentPhysics ? '#ff9900' : '#00ff41'; } } - /** - * Toggle node clustering - */ toggleClustering() { if (this.network.isCluster('domain-cluster')) { this.network.openCluster('domain-cluster'); } else { - const clusterOptions = { - joinCondition: (nodeOptions) => { - return nodeOptions.type === 'domain'; - }, - clusterNodeProperties: { - id: 'domain-cluster', - label: 'Domains', - shape: 'database', - color: '#00ff41', - borderWidth: 3, - } - }; - this.network.cluster(clusterOptions); - } - } - - /** - * Fit the view to show all nodes - */ - fitView() { - if (this.network) { - this.network.fit({ - animation: { - duration: 1000, - easingFunction: 'easeInOutQuad' - } + this.network.cluster({ + joinCondition: (nodeOptions) => nodeOptions.type === 'domain', + clusterNodeProperties: { id: 'domain-cluster', label: 'Domains', shape: 'database', color: '#00ff41' } }); } } - /** - * Clear the graph - */ + fitView() { + if (this.network) { + this.network.fit({ animation: { duration: 1000, easingFunction: 'easeInOutQuad' } }); + } + } + clear() { this.nodes.clear(); this.edges.clear(); this.edgeTimestamps.clear(); this.history = []; - this.largeEntityMembers.clear(); - this.initialTargetIds.clear(); - - // Show placeholder const placeholder = this.container.querySelector('.graph-placeholder'); if (placeholder) { placeholder.style.display = 'flex'; } } - /** - * Show error message - * @param {string} message - Error message - */ showError(message) { const placeholder = this.container.querySelector('.graph-placeholder .placeholder-text'); if (placeholder) { placeholder.textContent = `Error: ${message}`; - placeholder.style.color = '#ff6b6b'; } } - /* * @param {Set} excludedNodeIds - Node IDs to exclude from analysis (for simulation) - * @param {Set} excludedEdgeTypes - Edge types to exclude from traversal - * @param {Set} excludedNodeTypes - Node types to exclude from traversal - * @returns {Object} Analysis results with reachable/unreachable nodes - */ analyzeGraphReachability(excludedNodeIds = new Set(), excludedEdgeTypes = new Set(), excludedNodeTypes = new Set()) { - console.log("Performing comprehensive reachability analysis..."); - - const analysis = { - reachableNodes: new Set(), - unreachableNodes: new Set(), - isolatedClusters: [], - affectedNodes: new Set() - }; - + const analysis = { reachableNodes: new Set(), unreachableNodes: new Set() }; if (this.nodes.length === 0) return analysis; - // Build adjacency list excluding specified elements const adjacencyList = {}; this.nodes.getIds().forEach(id => { - if (!excludedNodeIds.has(id)) { - adjacencyList[id] = []; - } + if (!excludedNodeIds.has(id)) adjacencyList[id] = []; }); this.edges.forEach(edge => { - const edgeType = edge.metadata?.relationship_type || ''; - if (!excludedEdgeTypes.has(edgeType) && - !excludedNodeIds.has(edge.from) && - !excludedNodeIds.has(edge.to)) { - - if (adjacencyList[edge.from]) { - adjacencyList[edge.from].push(edge.to); - } + if (!excludedEdgeTypes.has(edge.metadata?.relationship_type || '') && + !excludedNodeIds.has(edge.from) && !excludedNodeIds.has(edge.to)) { + if (adjacencyList[edge.from]) adjacencyList[edge.from].push(edge.to); } }); - // BFS traversal from initial targets const traversalQueue = []; - - // Start from initial targets that aren't excluded this.initialTargetIds.forEach(rootId => { if (!excludedNodeIds.has(rootId)) { const node = this.nodes.get(rootId); @@ -1335,11 +905,9 @@ class GraphManager { } }); - // BFS to find all reachable nodes let queueIndex = 0; while (queueIndex < traversalQueue.length) { const currentNode = traversalQueue[queueIndex++]; - for (const neighbor of (adjacencyList[currentNode] || [])) { if (!analysis.reachableNodes.has(neighbor)) { const node = this.nodes.get(neighbor); @@ -1351,115 +919,33 @@ class GraphManager { } } - // Identify unreachable nodes (maintaining forensic integrity) Object.keys(adjacencyList).forEach(nodeId => { if (!analysis.reachableNodes.has(nodeId)) { analysis.unreachableNodes.add(nodeId); } }); - // Find isolated clusters among unreachable nodes - analysis.isolatedClusters = this.findIsolatedClusters( - Array.from(analysis.unreachableNodes), - adjacencyList - ); - - /*console.log(`Reachability analysis complete:`, { - reachable: analysis.reachableNodes.size, - unreachable: analysis.unreachableNodes.size, - clusters: analysis.isolatedClusters.length - });*/ - return analysis; } - - /** - * Find isolated clusters within a set of nodes - * Used for forensic analysis to identify disconnected subgraphs - */ - findIsolatedClusters(nodeIds, adjacencyList) { - const visited = new Set(); - const clusters = []; - - for (const nodeId of nodeIds) { - if (!visited.has(nodeId)) { - const cluster = []; - const stack = [nodeId]; - - while (stack.length > 0) { - const current = stack.pop(); - if (!visited.has(current)) { - visited.add(current); - cluster.push(current); - - // Add unvisited neighbors within the unreachable set - for (const neighbor of (adjacencyList[current] || [])) { - if (nodeIds.includes(neighbor) && !visited.has(neighbor)) { - stack.push(neighbor); - } - } - } - } - - if (cluster.length > 0) { - clusters.push(cluster); - } - } - } - - return clusters; - } - - /** - * ENHANCED: Get comprehensive graph statistics with forensic information - * Updates the existing getStatistics() method - */ - getStatistics() { - const basicStats = { - nodeCount: this.nodes.length, - edgeCount: this.edges.length, - }; - - // Add forensic statistics - const visibleNodes = this.nodes.get({ filter: node => !node.hidden }); - const hiddenNodes = this.nodes.get({ filter: node => node.hidden }); - - return { - ...basicStats, - forensicStatistics: { - visibleNodes: visibleNodes.length, - hiddenNodes: hiddenNodes.length, - initialTargets: this.initialTargetIds.size, - integrityStatus: visibleNodes.length > 0 && this.initialTargetIds.size > 0 ? 'INTACT' : 'COMPROMISED' - } - }; - } updateFilterControls() { if (!this.filterPanel) return; const nodeTypes = new Set(this.nodes.get().map(n => n.type)); const edgeTypes = new Set(this.edges.get().map(e => e.metadata.relationship_type)); - // Wrap both columns in a single container with vertical layout let filterHTML = '
'; - - // Nodes section filterHTML += '

Nodes

'; nodeTypes.forEach(type => { const label = type === 'correlation_object' ? 'latent correlations' : type; - const isChecked = type !== 'correlation_object'; - filterHTML += ``; + filterHTML += ``; }); filterHTML += '
'; - // Edges section filterHTML += '

Edges

'; edgeTypes.forEach(type => { filterHTML += ``; }); - filterHTML += '
'; - - filterHTML += '
'; // Close filter-container + filterHTML += ''; this.filterPanel.innerHTML = filterHTML; this.filterPanel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { @@ -1467,14 +953,9 @@ class GraphManager { }); } - /** - * ENHANCED: Apply filters using consolidated reachability analysis - * Replaces the existing applyAllFilters() method - */ applyAllFilters() { if (this.nodes.length === 0) return; - // Get filter criteria from UI const excludedNodeTypes = new Set(); this.filterPanel?.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => { excludedNodeTypes.add(cb.value); @@ -1485,15 +966,9 @@ class GraphManager { excludedEdgeTypes.add(cb.value); }); - // Perform comprehensive analysis const analysis = this.analyzeGraphReachability(new Set(), excludedEdgeTypes, excludedNodeTypes); - // Apply visibility updates - const nodeUpdates = this.nodes.map(node => ({ - id: node.id, - hidden: !analysis.reachableNodes.has(node.id) - })); - + const nodeUpdates = this.nodes.map(node => ({ id: node.id, hidden: !analysis.reachableNodes.has(node.id) })); const edgeUpdates = this.edges.map(edge => ({ id: edge.id, hidden: excludedEdgeTypes.has(edge.metadata?.relationship_type || '') || @@ -1503,357 +978,145 @@ class GraphManager { this.nodes.update(nodeUpdates); this.edges.update(edgeUpdates); - - console.log(`Enhanced filters applied. Visible nodes: ${analysis.reachableNodes.size}`); } - /** - * ENHANCED: Hide node with forensic integrity using reachability analysis - * Replaces the existing hideNodeAndOrphans() method - */ hideNodeWithReachabilityAnalysis(nodeId) { - console.log(`Hiding node ${nodeId} with reachability analysis...`); - - // Simulate hiding this node and analyze impact - const excludedNodes = new Set([nodeId]); - const analysis = this.analyzeGraphReachability(excludedNodes); - - // Nodes that will become unreachable (should be hidden) + const analysis = this.analyzeGraphReachability(new Set([nodeId])); const nodesToHide = [nodeId, ...Array.from(analysis.unreachableNodes)]; - - // Store history for potential revert - const historyData = { - nodeIds: nodesToHide, - operation: 'hide_with_reachability', - timestamp: Date.now() - }; + const historyData = { nodeIds: nodesToHide, operation: 'hide', timestamp: Date.now() }; const updates = nodesToHide.map(id => ({ id: id, hidden: true })); this.nodes.update(updates); this.addToHistory('hide', historyData); - - return { - hiddenNodes: nodesToHide, - isolatedClusters: analysis.isolatedClusters - }; } - /** - * ENHANCED: Delete node with forensic integrity using reachability analysis - * Replaces the existing deleteNodeAndOrphans() method - */ async deleteNodeWithReachabilityAnalysis(nodeId) { - console.log(`Deleting node ${nodeId} with reachability analysis...`); - - // Simulate deletion and analyze impact - const excludedNodes = new Set([nodeId]); - const analysis = this.analyzeGraphReachability(excludedNodes); - - // Nodes that will become unreachable (should be deleted) + const analysis = this.analyzeGraphReachability(new Set([nodeId])); const nodesToDelete = [nodeId, ...Array.from(analysis.unreachableNodes)]; - // Collect forensic data before deletion const historyData = { nodes: nodesToDelete.map(id => this.nodes.get(id)).filter(Boolean), edges: [], operation: 'delete_with_reachability', - timestamp: Date.now(), - forensicAnalysis: { - originalTarget: nodeId, - cascadeNodes: nodesToDelete.length - 1, - isolatedClusters: analysis.isolatedClusters.length, - clusterSizes: analysis.isolatedClusters.map(cluster => cluster.length) - } + timestamp: Date.now() }; - // Collect affected edges nodesToDelete.forEach(id => { const connectedEdgeIds = this.network.getConnectedEdges(id); - const edges = this.edges.get(connectedEdgeIds); - historyData.edges.push(...edges); + historyData.edges.push(...this.edges.get(connectedEdgeIds)); }); - - // Remove duplicates from edges historyData.edges = Array.from(new Map(historyData.edges.map(e => [e.id, e])).values()); - // Perform backend deletion with forensic logging - let operationFailed = false; - for (const targetNodeId of nodesToDelete) { try { - const response = await fetch(`/api/graph/node/${targetNodeId}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - forensicContext: { - operation: 'reachability_cascade_delete', - originalTarget: nodeId, - analysisTimestamp: historyData.timestamp - } - }) - }); - - const result = await response.json(); - if (!result.success) { - console.error(`Backend deletion failed for node ${targetNodeId}:`, result.error); - operationFailed = true; - break; - } - - console.log(`Node ${targetNodeId} deleted from backend with forensic context`); + const response = await fetch(`/api/graph/node/${targetNodeId}`, { method: 'DELETE' }); + if (!response.ok) throw new Error(`Backend deletion failed for ${targetNodeId}`); this.nodes.remove({ id: targetNodeId }); - } catch (error) { - console.error(`API error during deletion of node ${targetNodeId}:`, error); - operationFailed = true; - break; + this.nodes.update(historyData.nodes); + this.edges.update(historyData.edges); + return { success: false, error: "Backend deletion failed, UI reverted" }; } } - - // Handle operation results - if (!operationFailed) { - this.addToHistory('delete', historyData); - return { - success: true, - deletedNodes: nodesToDelete, - forensicAnalysis: historyData.forensicAnalysis - }; - } else { - // Revert UI changes if backend operations failed - use update instead of add - console.log("Reverting UI changes due to backend failure"); - this.nodes.update(historyData.nodes); - this.edges.update(historyData.edges); - - return { - success: false, - error: "Backend deletion failed, UI reverted" - }; - } + this.addToHistory('delete', historyData); + return { success: true, deletedNodes: nodesToDelete }; } - /** - * Show context menu for a node - * @param {string} nodeId - The ID of the node - * @param {Event} event - The contextmenu event - */ showContextMenu(nodeId, event) { - console.log('Showing context menu for node:', nodeId); const node = this.nodes.get(nodeId); - - // Create menu items - let menuItems = ` - `; this.contextMenu.innerHTML = menuItems; - - // Position the menu this.contextMenu.style.left = `${event.clientX}px`; this.contextMenu.style.top = `${event.clientY}px`; this.contextMenu.style.display = 'block'; - // Ensure menu stays within viewport const rect = this.contextMenu.getBoundingClientRect(); - if (rect.right > window.innerWidth) { - this.contextMenu.style.left = `${event.clientX - rect.width}px`; - } - if (rect.bottom > window.innerHeight) { - this.contextMenu.style.top = `${event.clientY - rect.height}px`; - } + if (rect.right > window.innerWidth) this.contextMenu.style.left = `${event.clientX - rect.width}px`; + if (rect.bottom > window.innerHeight) this.contextMenu.style.top = `${event.clientY - rect.height}px`; - // Add event listeners to menu items this.contextMenu.querySelectorAll('li').forEach(item => { item.addEventListener('click', (e) => { - if (e.currentTarget.hasAttribute('disabled')) { // Prevent action if disabled - e.stopPropagation(); - return; - } + if (e.currentTarget.hasAttribute('disabled')) return; e.stopPropagation(); - const action = e.currentTarget.dataset.action; - const nodeId = e.currentTarget.dataset.nodeId; - this.performContextMenuAction(action, nodeId); + this.performContextMenuAction(e.currentTarget.dataset.action, e.currentTarget.dataset.nodeId); this.hideContextMenu(); }); }); } - /** - * Hide the context menu - */ hideContextMenu() { - if (this.contextMenu) { - this.contextMenu.style.display = 'none'; - } + if (this.contextMenu) this.contextMenu.style.display = 'none'; } - /** - * UPDATED: Enhanced context menu actions using new methods - * Updates the existing performContextMenuAction() method - */ performContextMenuAction(action, nodeId) { switch (action) { - case 'focus': - this.focusOnNode(nodeId); - break; - - case 'iterate': - const event = new CustomEvent('iterateScan', { - detail: { nodeId } - }); - document.dispatchEvent(event); - break; - - case 'hide': - // Use enhanced method with reachability analysis - this.hideNodeWithReachabilityAnalysis(nodeId); - break; - - case 'delete': - // Use enhanced method with reachability analysis - this.deleteNodeWithReachabilityAnalysis(nodeId); - break; - + case 'focus': this.focusOnNode(nodeId); break; + case 'iterate': document.dispatchEvent(new CustomEvent('iterateScan', { detail: { nodeId } })); break; + case 'hide': this.hideNodeWithReachabilityAnalysis(nodeId); break; + case 'delete': this.deleteNodeWithReachabilityAnalysis(nodeId); break; case 'details': const node = this.nodes.get(nodeId); - if (node) { - this.showNodeDetails(node); - } + if (node) this.showNodeDetails(node); break; - - default: - console.warn('Unknown action:', action); } } - /** - * Add an operation to the history stack - * @param {string} type - The type of operation ('hide', 'delete') - * @param {Object} data - The data needed to revert the operation - */ addToHistory(type, data) { this.history.push({ type, data }); } - /** - * Revert the last action - */ async revertLastAction() { const lastAction = this.history.pop(); - if (!lastAction) { - console.log('No actions to revert.'); - return; - } - + if (!lastAction) return; + switch (lastAction.type) { case 'hide': - // Revert hiding nodes by un-hiding them - const updates = lastAction.data.nodeIds.map(id => ({ id: id, hidden: false })); - this.nodes.update(updates); + this.nodes.update(lastAction.data.nodeIds.map(id => ({ id: id, hidden: false }))); break; case 'delete': try { const response = await fetch('/api/graph/revert', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(lastAction) }); - const result = await response.json(); - - if (result.success) { - console.log('Delete action reverted successfully on backend.'); - // Re-add all nodes and edges from the history to the local view - use update instead of add - this.nodes.update(lastAction.data.nodes); - this.edges.update(lastAction.data.edges); - } else { - console.error('Failed to revert delete action on backend:', result.error); - // Push the action back onto the history stack if the API call failed - this.history.push(lastAction); - } + if (!response.ok) throw new Error('Backend revert failed'); + this.nodes.update(lastAction.data.nodes); + this.edges.update(lastAction.data.edges); } catch (error) { - console.error('Error during revert API call:', error); this.history.push(lastAction); + this.showError('Failed to revert the last action.'); } break; } } - /** - * FIXED: Unhide all hidden nodes, excluding large entity members and disconnected nodes. - * This prevents orphaned large entity members from appearing as free-floating nodes. - */ unhideAll() { const allHiddenNodes = this.nodes.get({ filter: (node) => { - // Skip nodes that are part of a large entity - if (node.metadata && node.metadata.large_entity_id) { - return false; - } - - // Skip nodes that are not hidden - if (node.hidden !== true) { - return false; - } - - // Skip nodes that have no edges (would appear disconnected) - const nodeId = node.id; - const hasIncomingEdges = this.edges.get().some(edge => edge.to === nodeId && !edge.hidden); - const hasOutgoingEdges = this.edges.get().some(edge => edge.from === nodeId && !edge.hidden); - - if (!hasIncomingEdges && !hasOutgoingEdges) { - console.log(`Skipping disconnected node ${nodeId} from unhide`); - return false; - } - - return true; + if (node.metadata?.large_entity_id || node.hidden !== true) return false; + const hasVisibleEdges = this.edges.get().some(edge => (edge.to === node.id || edge.from === node.id) && !edge.hidden); + return hasVisibleEdges; } }); if (allHiddenNodes.length > 0) { - console.log(`Unhiding ${allHiddenNodes.length} nodes with valid connections`); - const updates = allHiddenNodes.map(node => ({ id: node.id, hidden: false })); - this.nodes.update(updates); - } else { - console.log('No eligible nodes to unhide'); + this.nodes.update(allHiddenNodes.map(node => ({ id: node.id, hidden: false }))); } } - } -// Export for use in main.js window.GraphManager = GraphManager; \ No newline at end of file