dnsrecon/static/js/graph.js
2025-09-10 13:53:32 +02:00

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;