565 lines
17 KiB
JavaScript
565 lines
17 KiB
JavaScript
/**
|
|
* 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 = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
|
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${node.id}</div>`;
|
|
tooltip += `<div style="color: #999; margin-bottom: 2px;">Type: ${node.type}</div>`;
|
|
|
|
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
|
tooltip += `<div style="color: #999; margin-top: 4px; border-top: 1px solid #444; padding-top: 4px;">`;
|
|
tooltip += `Click for details</div>`;
|
|
}
|
|
|
|
tooltip += `</div>`;
|
|
return tooltip;
|
|
}
|
|
|
|
/**
|
|
* Create edge tooltip
|
|
* @param {Object} edge - Edge data
|
|
* @returns {string} HTML tooltip content
|
|
*/
|
|
createEdgeTooltip(edge) {
|
|
let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
|
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
|
|
tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
|
|
|
|
if (edge.source_provider) {
|
|
tooltip += `<div style="color: #999; margin-bottom: 2px;">Source: ${edge.source_provider}</div>`;
|
|
}
|
|
|
|
if (edge.discovery_timestamp) {
|
|
const date = new Date(edge.discovery_timestamp);
|
|
tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
|
|
}
|
|
|
|
tooltip += `</div>`;
|
|
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; |