/** * Graph visualization module for DNSRecon * Handles network graph rendering using vis.js */ 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; // Graph options for cybersecurity theme this.options = { nodes: { shape: 'dot', size: 12, font: { size: 11, color: '#c7c7c7', face: 'Roboto Mono, monospace', background: 'rgba(26, 26, 26, 0.8)', strokeWidth: 1, strokeColor: '#000000' }, borderWidth: 2, borderColor: '#444', shadow: { enabled: true, color: 'rgba(0, 0, 0, 0.3)', size: 3, x: 1, y: 1 }, scaling: { min: 8, max: 20 } }, edges: { width: 2, color: { color: '#444', highlight: '#00ff41', hover: '#ff9900' }, 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: 0.8, type: 'arrow' } }, smooth: { enabled: true, type: 'dynamic', roundness: 0.5 }, shadow: { enabled: true, color: 'rgba(0, 0, 0, 0.2)', size: 2, x: 1, y: 1 } }, physics: { enabled: true, stabilization: { enabled: true, iterations: 100, updateInterval: 25 }, barnesHut: { gravitationalConstant: -2000, centralGravity: 0.3, springLength: 95, springConstant: 0.04, damping: 0.09, avoidOverlap: 0.1 }, maxVelocity: 50, minVelocity: 0.1, solver: 'barnesHut', timestep: 0.35, adaptiveTimestep: true }, interaction: { hover: true, hoverConnectedEdges: true, selectConnectedEdges: true, tooltipDelay: 200, hideEdgesOnDrag: false, hideNodesOnDrag: false }, layout: { improvedLayout: true } }; this.setupEventHandlers(); } /** * Initialize the network graph */ 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'; } console.log('Graph initialized successfully'); } catch (error) { console.error('Failed to initialize graph:', error); this.showError('Failed to initialize visualization'); } } /** * Setup network event handlers */ setupNetworkEvents() { if (!this.network) return; // Node click event this.network.on('click', (params) => { if (params.nodes.length > 0) { const nodeId = params.nodes[0]; this.showNodeDetails(nodeId); } }); // Hover events for tooltips this.network.on('hoverNode', (params) => { const nodeId = params.node; const node = this.nodes.get(nodeId); if (node) { this.showTooltip(params.pointer.DOM, node); } }); this.network.on('blurNode', () => { this.hideTooltip(); }); // Stabilization events this.network.on('stabilizationProgress', (params) => { const progress = params.iterations / params.total; this.updateStabilizationProgress(progress); }); this.network.on('stabilizationIterationsDone', () => { this.onStabilizationComplete(); }); } /** * Update graph with new data * @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 const processedNodes = graphData.nodes.map(node => this.processNode(node)); const processedEdges = graphData.edges.map(edge => this.processEdge(edge)); // Update datasets this.nodes.clear(); this.edges.clear(); this.nodes.add(processedNodes); this.edges.add(processedEdges); // Fit the view if this is the first update or graph is small if (processedNodes.length <= 10) { setTimeout(() => this.fitView(), 500); } console.log(`Graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges`); } catch (error) { console.error('Failed to update graph:', error); this.showError('Failed to update visualization'); } } /** * Process node data for visualization * @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), metadata: node.metadata || {} }; // Add type-specific styling if (node.type === 'domain') { processedNode.shape = 'dot'; } else if (node.type === 'ip') { processedNode.shape = 'square'; } else if (node.type === 'certificate') { processedNode.shape = 'diamond'; } else if (node.type === 'asn') { processedNode.shape = 'triangle'; } return processedNode; } /** * Process edge data for visualization * @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 }; 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 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 = `
`; tooltip += `
${node.id}
`; tooltip += `
Type: ${node.type}
`; if (node.metadata && Object.keys(node.metadata).length > 0) { tooltip += `
`; tooltip += `Click for details
`; } tooltip += `
`; return tooltip; } /** * Create edge tooltip * @param {Object} edge - Edge data * @returns {string} HTML tooltip content */ createEdgeTooltip(edge) { let tooltip = `
`; tooltip += `
${edge.label || 'Relationship'}
`; tooltip += `
Confidence: ${(edge.confidence_score * 100).toFixed(1)}%
`; if (edge.source_provider) { tooltip += `
Source: ${edge.source_provider}
`; } if (edge.discovery_timestamp) { const date = new Date(edge.discovery_timestamp); tooltip += `
Discovered: ${date.toLocaleString()}
`; } tooltip += `
`; return tooltip; } /** * Show node details in modal * @param {string} nodeId - Node identifier */ showNodeDetails(nodeId) { const node = this.nodes.get(nodeId); if (!node) return; // Trigger custom event for main application to handle const event = new CustomEvent('nodeSelected', { detail: { nodeId, node } }); document.dispatchEvent(event); } /** * Show tooltip * @param {Object} position - Mouse position * @param {Object} node - Node data */ showTooltip(position, node) { // Tooltip is handled by vis.js automatically // This method is for custom tooltip implementation if needed } /** * Hide tooltip */ hideTooltip() { // Tooltip hiding is handled by vis.js automatically } /** * Update stabilization progress * @param {number} progress - Progress value (0-1) */ updateStabilizationProgress(progress) { // Could show a progress indicator if needed console.log(`Graph stabilization: ${(progress * 100).toFixed(1)}%`); } /** * Handle stabilization completion */ onStabilizationComplete() { console.log('Graph stabilization complete'); } /** * Fit the view to show all nodes */ fitView() { if (this.network) { this.network.fit({ animation: { duration: 1000, easingFunction: 'easeInOutQuad' } }); } } /** * Reset the view to initial state */ resetView() { if (this.network) { this.network.moveTo({ position: { x: 0, y: 0 }, scale: 1, animation: { duration: 1000, easingFunction: 'easeInOutQuad' } }); } } /** * Clear the graph */ clear() { this.nodes.clear(); this.edges.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'; } } /** * Setup control event handlers */ setupEventHandlers() { // Reset view button document.addEventListener('DOMContentLoaded', () => { const resetBtn = document.getElementById('reset-view'); if (resetBtn) { resetBtn.addEventListener('click', () => this.resetView()); } const fitBtn = document.getElementById('fit-view'); if (fitBtn) { fitBtn.addEventListener('click', () => this.fitView()); } }); } /** * Get network statistics * @returns {Object} Statistics object */ getStatistics() { return { nodeCount: this.nodes.length, edgeCount: this.edges.length, //isStabilized: this.network ? this.network.isStabilized() : false }; } /** * Export graph as image (if needed for future implementation) * @param {string} format - Image format ('png', 'jpeg') * @returns {string} Data URL of the image */ exportAsImage(format = 'png') { if (!this.network) return null; // This would require additional vis.js functionality // Placeholder for future implementation console.log('Image export not yet implemented'); return null; } } // Export for use in main.js window.GraphManager = GraphManager;