/** * Graph visualization module for DNSRecon * Handles network graph rendering using vis.js with enhanced Phase 2 features */ class GraphManager { constructor(containerId) { this.container = document.getElementById(containerId); this.network = null; this.nodes = new vis.DataSet(); this.edges = new vis.DataSet(); this.isInitialized = false; this.currentLayout = 'physics'; this.nodeInfoPopup = null; // Enhanced graph options for Phase 2 this.options = { nodes: { shape: 'dot', size: 15, font: { size: 12, color: '#c7c7c7', face: 'Roboto Mono, monospace', background: 'rgba(26, 26, 26, 0.9)', strokeWidth: 2, strokeColor: '#000000' }, borderWidth: 2, borderColor: '#444', shadow: { enabled: true, color: 'rgba(0, 0, 0, 0.5)', size: 5, x: 2, y: 2 }, scaling: { min: 10, max: 30, label: { enabled: true, min: 8, max: 16 } }, chosen: { node: (values, id, selected, hovering) => { values.borderColor = '#00ff41'; values.borderWidth = 3; values.shadow = true; values.shadowColor = 'rgba(0, 255, 65, 0.6)'; values.shadowSize = 10; } } }, edges: { width: 2, color: { color: '#555', highlight: '#00ff41', hover: '#ff9900', inherit: false }, font: { size: 10, color: '#999', face: 'Roboto Mono, monospace', background: 'rgba(26, 26, 26, 0.8)', strokeWidth: 1, strokeColor: '#000000' }, arrows: { to: { enabled: true, scaleFactor: 1, type: 'arrow' } }, smooth: { enabled: true, type: 'dynamic', roundness: 0.6 }, shadow: { enabled: true, color: 'rgba(0, 0, 0, 0.3)', size: 3, x: 1, y: 1 }, chosen: { edge: (values, id, selected, hovering) => { values.color = '#00ff41'; values.width = 4; values.shadow = true; values.shadowColor = 'rgba(0, 255, 65, 0.4)'; } } }, physics: { enabled: true, stabilization: { enabled: true, iterations: 150, updateInterval: 50 }, barnesHut: { gravitationalConstant: -3000, centralGravity: 0.4, springLength: 120, springConstant: 0.05, damping: 0.1, avoidOverlap: 0.2 }, maxVelocity: 30, minVelocity: 0.1, solver: 'barnesHut', timestep: 0.4, adaptiveTimestep: true }, interaction: { hover: true, hoverConnectedEdges: true, selectConnectedEdges: true, tooltipDelay: 300, hideEdgesOnDrag: false, hideNodesOnDrag: false, zoomView: true, dragView: true, multiselect: true }, layout: { improvedLayout: true, randomSeed: 2 } }; this.createNodeInfoPopup(); } /** * Create floating node info popup */ createNodeInfoPopup() { this.nodeInfoPopup = document.createElement('div'); this.nodeInfoPopup.className = 'node-info-popup'; this.nodeInfoPopup.style.display = 'none'; document.body.appendChild(this.nodeInfoPopup); } /** * Initialize the network graph with enhanced features */ initialize() { if (this.isInitialized) { return; } try { 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(); console.log('Enhanced graph initialized successfully'); } catch (error) { console.error('Failed to initialize graph:', error); this.showError('Failed to initialize visualization'); } } /** * Add interactive graph controls */ addGraphControls() { const controlsContainer = document.createElement('div'); controlsContainer.className = 'graph-controls'; controlsContainer.innerHTML = ` `; this.container.appendChild(controlsContainer); // Add control event listeners document.getElementById('graph-fit').addEventListener('click', () => this.fitView()); document.getElementById('graph-reset').addEventListener('click', () => this.resetView()); document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics()); document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering()); } /** * Setup enhanced network event handlers */ setupNetworkEvents() { if (!this.network) return; // Node click event with enhanced details this.network.on('click', (params) => { if (params.nodes.length > 0) { const nodeId = params.nodes[0]; this.showNodeDetails(nodeId); this.highlightNodeConnections(nodeId); } else { this.clearHighlights(); } }); // Enhanced hover events this.network.on('hoverNode', (params) => { const nodeId = params.node; const node = this.nodes.get(nodeId); if (node) { this.showNodeInfoPopup(params.pointer.DOM, node); this.highlightConnectedNodes(nodeId, true); } }); this.network.on('blurNode', (params) => { this.hideNodeInfoPopup(); this.clearHoverHighlights(); }); // Edge hover events this.network.on('hoverEdge', (params) => { const edgeId = params.edge; const edge = this.edges.get(edgeId); if (edge) { this.showEdgeInfo(params.pointer.DOM, edge); } }); this.network.on('blurEdge', () => { this.hideNodeInfoPopup(); }); // Double-click to focus on node this.network.on('doubleClick', (params) => { if (params.nodes.length > 0) { const nodeId = params.nodes[0]; this.focusOnNode(nodeId); } }); // Context menu (right-click) this.network.on('oncontext', (params) => { params.event.preventDefault(); if (params.nodes.length > 0) { this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]); } }); // Stabilization events with progress this.network.on('stabilizationProgress', (params) => { const progress = params.iterations / params.total; this.updateStabilizationProgress(progress); }); this.network.on('stabilizationIterationsDone', () => { this.onStabilizationComplete(); }); // Selection events this.network.on('select', (params) => { console.log('Selected nodes:', params.nodes); console.log('Selected edges:', params.edges); }); } /** * Update graph with new data and enhanced processing * @param {Object} graphData - Graph data from backend */ updateGraph(graphData) { if (!graphData || !graphData.nodes || !graphData.edges) { console.warn('Invalid graph data received'); return; } try { // Initialize if not already done if (!this.isInitialized) { this.initialize(); } // Process nodes with enhanced attributes const processedNodes = graphData.nodes.map(node => this.processNode(node)); const processedEdges = graphData.edges.map(edge => this.processEdge(edge)); // Update datasets with animation const existingNodeIds = this.nodes.getIds(); const existingEdgeIds = this.edges.getIds(); // Add new nodes with fade-in animation const newNodes = processedNodes.filter(node => !existingNodeIds.includes(node.id)); const newEdges = processedEdges.filter(edge => !existingEdgeIds.includes(edge.id)); // Update existing data this.nodes.update(processedNodes); this.edges.update(processedEdges); // Highlight new additions briefly if (newNodes.length > 0 || newEdges.length > 0) { setTimeout(() => this.highlightNewElements(newNodes, newEdges), 100); } // Auto-fit view for small graphs or first update if (processedNodes.length <= 10 || existingNodeIds.length === 0) { setTimeout(() => this.fitView(), 800); } console.log(`Enhanced graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges (${newNodes.length} new nodes, ${newEdges.length} new edges)`); } catch (error) { console.error('Failed to update enhanced graph:', error); this.showError('Failed to update visualization'); } } /** * Process node data with enhanced styling and metadata * @param {Object} node - Raw node data * @returns {Object} Processed node data */ processNode(node) { const processedNode = { id: node.id, label: this.formatNodeLabel(node.id, node.type), title: this.createNodeTooltip(node), color: this.getNodeColor(node.type), size: this.getNodeSize(node.type), borderColor: this.getNodeBorderColor(node.type), shape: this.getNodeShape(node.type), metadata: node.metadata || {}, type: node.type }; // Add confidence-based styling if (node.confidence) { processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5)); } // Add special styling for important nodes if (this.isImportantNode(node)) { processedNode.shadow = { enabled: true, color: 'rgba(0, 255, 65, 0.6)', size: 10, x: 2, y: 2 }; } // Style based on certificate validity if (node.has_valid_cert === true) { processedNode.borderColor = '#00ff41'; // Green for valid cert } else if (node.has_valid_cert === false) { processedNode.borderColor = '#ff9900'; // Amber for expired/no cert processedNode.borderDashes = [5, 5]; } return processedNode; } /** * Process edge data with enhanced styling and metadata * @param {Object} edge - Raw edge data * @returns {Object} Processed edge data */ processEdge(edge) { const confidence = edge.confidence_score || 0; const processedEdge = { id: `${edge.from}-${edge.to}`, from: edge.from, to: edge.to, label: this.formatEdgeLabel(edge.label, confidence), title: this.createEdgeTooltip(edge), width: this.getEdgeWidth(confidence), color: this.getEdgeColor(confidence), dashes: confidence < 0.6 ? [5, 5] : false, metadata: { relationship_type: edge.label, confidence_score: confidence, source_provider: edge.source_provider, discovery_timestamp: edge.discovery_timestamp } }; // Add animation for high-confidence edges if (confidence >= 0.8) { processedEdge.shadow = { enabled: true, color: 'rgba(0, 255, 65, 0.3)', size: 5, x: 1, y: 1 }; } return processedEdge; } /** * Format node label for display * @param {string} nodeId - Node identifier * @param {string} nodeType - Node type * @returns {string} Formatted label */ formatNodeLabel(nodeId, nodeType) { // Truncate long domain names if (nodeId.length > 20) { return nodeId.substring(0, 17) + '...'; } return nodeId; } /** * Format edge label for display * @param {string} relationshipType - Type of relationship * @param {number} confidence - Confidence score * @returns {string} Formatted label */ formatEdgeLabel(relationshipType, confidence) { if (!relationshipType) return ''; const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○'; return `${relationshipType} ${confidenceText}`; } /** * 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 'certificate': '#c7c7c7', // Gray 'asn': '#00aaff' // Blue }; 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', 'certificate': '#999999', 'asn': '#0088cc' }; 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, 'certificate': 10, 'asn': 16 }; return sizes[nodeType] || 12; } /** * Get enhanced node shape based on type * @param {string} nodeType - Node type * @returns {string} Shape name */ getNodeShape(nodeType) { const shapes = { 'domain': 'dot', 'ip': 'square', 'certificate': 'diamond', 'asn': 'triangle' }; return shapes[nodeType] || 'dot'; } /** * Get edge color based on confidence * @param {number} confidence - Confidence score * @returns {string} Edge color */ getEdgeColor(confidence) { if (confidence >= 0.8) { return '#00ff41'; // High confidence - green } else if (confidence >= 0.6) { return '#ff9900'; // Medium confidence - amber } else { return '#666666'; // Low confidence - gray } } /** * Get edge width based on confidence * @param {number} confidence - Confidence score * @returns {number} Edge width */ getEdgeWidth(confidence) { if (confidence >= 0.8) { return 3; } else if (confidence >= 0.6) { return 2; } else { return 1; } } /** * Create node tooltip * @param {Object} node - Node data * @returns {string} HTML tooltip content */ createNodeTooltip(node) { let tooltip = `