This commit is contained in:
overcuriousity
2025-09-10 15:17:17 +02:00
parent 696cec0723
commit ce0e11cf0b
16 changed files with 2577 additions and 484 deletions

View File

@@ -64,6 +64,18 @@ body {
gap: 0.5rem;
}
.status-indicator.scanning {
animation: pulse 1.5s infinite;
}
.status-indicator.completed {
background-color: #00ff41;
}
.status-indicator.error {
background-color: #ff6b6b;
}
.status-dot {
width: 8px;
height: 8px;
@@ -266,6 +278,7 @@ input[type="text"]:focus, select:focus {
background-color: #1a1a1a;
border: 1px solid #444;
overflow: hidden;
position: relative;
}
.progress-fill {
@@ -274,6 +287,23 @@ input[type="text"]:focus, select:focus {
width: 0%;
transition: width 0.3s ease;
box-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* Visualization Panel */
@@ -292,6 +322,37 @@ input[type="text"]:focus, select:focus {
position: relative;
background-color: #1a1a1a;
border-top: 1px solid #444;
transition: height 0.3s ease;
}
.graph-container.expanded {
height: 700px;
}
.graph-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
display: flex;
gap: 0.5rem;
}
.graph-control-btn {
background: rgba(42, 42, 42, 0.9);
border: 1px solid #555;
color: #c7c7c7;
padding: 0.5rem;
font-family: 'Roboto Mono', monospace;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
}
.graph-control-btn:hover {
border-color: #00ff41;
color: #00ff41;
background: rgba(42, 42, 42, 1);
}
.graph-placeholder {
@@ -333,6 +394,20 @@ input[type="text"]:focus, select:focus {
border-top: 1px solid #444;
}
.legend-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.legend-title {
font-size: 0.7rem;
color: #00ff41;
text-transform: uppercase;
font-weight: 500;
margin-bottom: 0.25rem;
}
.legend-item {
display: flex;
align-items: center;
@@ -344,6 +419,7 @@ input[type="text"]:focus, select:focus {
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid #444;
}
.legend-edge {
@@ -353,10 +429,16 @@ input[type="text"]:focus, select:focus {
.legend-edge.high-confidence {
background-color: #00ff41;
box-shadow: 0 0 3px rgba(0, 255, 65, 0.5);
}
.legend-edge.medium-confidence {
background-color: #ff9900;
box-shadow: 0 0 3px rgba(255, 153, 0, 0.5);
}
.legend-edge.low-confidence {
background-color: #666666;
}
/* Provider Panel */
@@ -375,9 +457,11 @@ input[type="text"]:focus, select:focus {
background-color: #1a1a1a;
border: 1px solid #444;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.3s ease;
}
.provider-item:hover {
border-color: #555;
}
.provider-name {
@@ -389,6 +473,7 @@ input[type="text"]:focus, select:focus {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-weight: 500;
}
.provider-status.enabled {
@@ -401,12 +486,78 @@ input[type="text"]:focus, select:focus {
color: #e0e0e0;
}
.provider-status.api-key-required {
background-color: #5c4c2c;
color: #e0e0e0;
}
.provider-stats {
font-size: 0.8rem;
color: #999;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-top: 0.5rem;
}
.provider-stat {
display: flex;
justify-content: space-between;
}
.provider-stat-label {
color: #666;
}
.provider-stat-value {
color: #00ff41;
font-weight: 500;
}
.provider-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.node-info-popup {
position: fixed;
background: rgba(42, 42, 42, 0.95);
border: 1px solid #555;
padding: 1rem;
border-radius: 4px;
color: #c7c7c7;
font-family: 'Roboto Mono', monospace;
font-size: 0.8rem;
max-width: 300px;
z-index: 1001;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.node-info-title {
color: #00ff41;
font-weight: bold;
margin-bottom: 0.5rem;
border-bottom: 1px solid #444;
padding-bottom: 0.25rem;
}
.node-info-detail {
margin-bottom: 0.25rem;
display: flex;
justify-content: space-between;
}
.node-info-label {
color: #999;
}
.node-info-value {
color: #c7c7c7;
font-weight: 500;
}
/* Footer */
.footer {
background-color: #0a0a0a;
@@ -437,6 +588,7 @@ input[type="text"]:focus, select:focus {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
animation: fadeIn 0.3s ease-out;
}
.modal-content {
@@ -447,6 +599,18 @@ input[type="text"]:focus, select:focus {
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
animation: slideInDown 0.3s ease-out;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
@@ -480,6 +644,12 @@ input[type="text"]:focus, select:focus {
padding: 1.5rem;
}
.modal-description {
color: #999;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.detail-row {
display: flex;
justify-content: space-between;
@@ -495,6 +665,7 @@ input[type="text"]:focus, select:focus {
.detail-value {
color: #c7c7c7;
word-break: break-word;
}
/* Responsive Design */
@@ -552,6 +723,40 @@ input[type="text"]:focus, select:focus {
pointer-events: none;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 26, 26, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #444;
border-top: 3px solid #00ff41;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 1rem;
color: #999;
font-family: 'Roboto Mono', monospace;
font-size: 0.9rem;
}
.error {
color: #ff6b6b !important;
border-color: #ff6b6b !important;
@@ -598,4 +803,101 @@ input[type="text"]:focus, select:focus {
.amber {
color: #ff9900;
}
.apikey-section {
margin-bottom: 1.5rem;
}
.apikey-section label {
display: block;
margin-bottom: 0.5rem;
color: #c7c7c7;
font-size: 0.9rem;
font-weight: 500;
}
.apikey-section input[type="password"] {
width: 100%;
padding: 0.75rem;
background-color: #1a1a1a;
border: 1px solid #555;
color: #c7c7c7;
font-family: 'Roboto Mono', monospace;
font-size: 0.9rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.apikey-section input[type="password"]:focus {
outline: none;
border-color: #00ff41;
box-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
}
.apikey-help {
font-size: 0.8rem;
color: #666;
margin-top: 0.25rem;
font-style: italic;
}
.message-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1002;
max-width: 400px;
}
.message-toast {
margin-bottom: 10px;
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 0.9rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
animation: slideInRight 0.3s ease-out;
}
.message-toast.success {
background: #2c5c34;
border-left: 4px solid #00ff41;
}
.message-toast.error {
background: #5c2c2c;
border-left: 4px solid #ff6b6b;
}
.message-toast.warning {
background: #5c4c2c;
border-left: 4px solid #ff9900;
}
.message-toast.info {
background: #2c3e5c;
border-left: 4px solid #00aaff;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}

View File

@@ -1,6 +1,6 @@
/**
* Graph visualization module for DNSRecon
* Handles network graph rendering using vis.js
* Handles network graph rendering using vis.js with enhanced Phase 2 features
*/
class GraphManager {
@@ -10,40 +10,57 @@ class GraphManager {
this.nodes = new vis.DataSet();
this.edges = new vis.DataSet();
this.isInitialized = false;
// Graph options for cybersecurity theme
this.currentLayout = 'physics';
this.nodeInfoPopup = null;
// Enhanced graph options for Phase 2
this.options = {
nodes: {
shape: 'dot',
size: 12,
size: 15,
font: {
size: 11,
size: 12,
color: '#c7c7c7',
face: 'Roboto Mono, monospace',
background: 'rgba(26, 26, 26, 0.8)',
strokeWidth: 1,
background: 'rgba(26, 26, 26, 0.9)',
strokeWidth: 2,
strokeColor: '#000000'
},
borderWidth: 2,
borderColor: '#444',
shadow: {
enabled: true,
color: 'rgba(0, 0, 0, 0.3)',
size: 3,
x: 1,
y: 1
color: 'rgba(0, 0, 0, 0.5)',
size: 5,
x: 2,
y: 2
},
scaling: {
min: 8,
max: 20
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: '#444',
color: '#555',
highlight: '#00ff41',
hover: '#ff9900'
hover: '#ff9900',
inherit: false
},
font: {
size: 10,
@@ -56,131 +73,218 @@ class GraphManager {
arrows: {
to: {
enabled: true,
scaleFactor: 0.8,
scaleFactor: 1,
type: 'arrow'
}
},
smooth: {
enabled: true,
type: 'dynamic',
roundness: 0.5
roundness: 0.6
},
shadow: {
enabled: true,
color: 'rgba(0, 0, 0, 0.2)',
size: 2,
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: 100,
updateInterval: 25
iterations: 150,
updateInterval: 50
},
barnesHut: {
gravitationalConstant: -2000,
centralGravity: 0.3,
springLength: 95,
springConstant: 0.04,
damping: 0.09,
avoidOverlap: 0.1
gravitationalConstant: -3000,
centralGravity: 0.4,
springLength: 120,
springConstant: 0.05,
damping: 0.1,
avoidOverlap: 0.2
},
maxVelocity: 50,
maxVelocity: 30,
minVelocity: 0.1,
solver: 'barnesHut',
timestep: 0.35,
timestep: 0.4,
adaptiveTimestep: true
},
interaction: {
hover: true,
hoverConnectedEdges: true,
selectConnectedEdges: true,
tooltipDelay: 200,
tooltipDelay: 300,
hideEdgesOnDrag: false,
hideNodesOnDrag: false
hideNodesOnDrag: false,
zoomView: true,
dragView: true,
multiselect: true
},
layout: {
improvedLayout: true
improvedLayout: true,
randomSeed: 2
}
};
this.setupEventHandlers();
this.createNodeInfoPopup();
}
/**
* Initialize the network graph
* 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';
}
console.log('Graph initialized successfully');
// 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');
}
}
/**
* Setup network event handlers
* Add interactive graph controls
*/
addGraphControls() {
const controlsContainer = document.createElement('div');
controlsContainer.className = 'graph-controls';
controlsContainer.innerHTML = `
<button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
<button class="graph-control-btn" id="graph-reset" title="Reset View">[RESET]</button>
<button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
<button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
`;
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
// 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();
}
});
// Hover events for tooltips
// Enhanced hover events
this.network.on('hoverNode', (params) => {
const nodeId = params.node;
const node = this.nodes.get(nodeId);
if (node) {
this.showTooltip(params.pointer.DOM, node);
this.showNodeInfoPopup(params.pointer.DOM, node);
this.highlightConnectedNodes(nodeId, true);
}
});
this.network.on('blurNode', () => {
this.hideTooltip();
this.network.on('blurNode', (params) => {
this.hideNodeInfoPopup();
this.clearHoverHighlights();
});
// Stabilization events
// 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
* Update graph with new data and enhanced processing
* @param {Object} graphData - Graph data from backend
*/
updateGraph(graphData) {
@@ -188,37 +292,48 @@ class GraphManager {
console.warn('Invalid graph data received');
return;
}
try {
// Initialize if not already done
if (!this.isInitialized) {
this.initialize();
}
// Process nodes
// 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
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);
// 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);
}
console.log(`Graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges`);
// 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 graph:', error);
console.error('Failed to update enhanced graph:', error);
this.showError('Failed to update visualization');
}
}
/**
* Process node data for visualization
* Process node data with enhanced styling and metadata
* @param {Object} node - Raw node data
* @returns {Object} Processed node data
*/
@@ -230,25 +345,32 @@ class GraphManager {
color: this.getNodeColor(node.type),
size: this.getNodeSize(node.type),
borderColor: this.getNodeBorderColor(node.type),
metadata: node.metadata || {}
shape: this.getNodeShape(node.type),
metadata: node.metadata || {},
type: node.type
};
// 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';
// 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
};
}
return processedNode;
}
/**
* Process edge data for visualization
* Process edge data with enhanced styling and metadata
* @param {Object} edge - Raw edge data
* @returns {Object} Processed edge data
*/
@@ -262,12 +384,29 @@ class GraphManager {
title: this.createEdgeTooltip(edge),
width: this.getEdgeWidth(confidence),
color: this.getEdgeColor(confidence),
dashes: confidence < 0.6 ? [5, 5] : false
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
@@ -281,7 +420,7 @@ class GraphManager {
}
return nodeId;
}
/**
* Format edge label for display
* @param {string} relationshipType - Type of relationship
@@ -290,7 +429,7 @@ class GraphManager {
*/
formatEdgeLabel(relationshipType, confidence) {
if (!relationshipType) return '';
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
return `${relationshipType} ${confidenceText}`;
}
@@ -309,7 +448,7 @@ class GraphManager {
};
return colors[nodeType] || '#ffffff';
}
/**
* Get node border color based on type
* @param {string} nodeType - Node type
@@ -324,7 +463,7 @@ class GraphManager {
};
return borderColors[nodeType] || '#666666';
}
/**
* Get node size based on type
* @param {string} nodeType - Node type
@@ -340,6 +479,21 @@ class GraphManager {
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
@@ -354,7 +508,7 @@ class GraphManager {
return '#666666'; // Low confidence - gray
}
}
/**
* Get edge width based on confidence
* @param {number} confidence - Confidence score
@@ -411,7 +565,20 @@ class GraphManager {
tooltip += `</div>`;
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 {string} nodeId - Node identifier
@@ -419,7 +586,7 @@ class GraphManager {
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 }
@@ -428,22 +595,249 @@ class GraphManager {
}
/**
* Show tooltip
* Show enhanced node info popup
* @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
showNodeInfoPopup(position, node) {
if (!this.nodeInfoPopup) return;
const html = `
<div class="node-info-title">${node.id}</div>
<div class="node-info-detail">
<span class="node-info-label">Type:</span>
<span class="node-info-value">${node.type || 'Unknown'}</span>
</div>
${node.metadata && Object.keys(node.metadata).length > 0 ?
'<div class="node-info-detail"><span class="node-info-label">Details:</span><span class="node-info-value">Click for more</span></div>' :
''}
`;
this.nodeInfoPopup.innerHTML = html;
this.nodeInfoPopup.style.display = 'block';
this.nodeInfoPopup.style.left = position.x + 15 + 'px';
this.nodeInfoPopup.style.top = position.y - 10 + 'px';
// Ensure popup stays in viewport
const rect = this.nodeInfoPopup.getBoundingClientRect();
if (rect.right > window.innerWidth) {
this.nodeInfoPopup.style.left = position.x - rect.width - 15 + 'px';
}
if (rect.bottom > window.innerHeight) {
this.nodeInfoPopup.style.top = position.y - rect.height + 10 + 'px';
}
}
/**
* Show edge information tooltip
* @param {Object} position - Mouse position
* @param {Object} edge - Edge data
*/
showEdgeInfo(position, edge) {
if (!this.nodeInfoPopup) return;
const confidence = edge.metadata ? edge.metadata.confidence_score : 0;
const provider = edge.metadata ? edge.metadata.source_provider : 'Unknown';
const html = `
<div class="node-info-title">${edge.metadata ? edge.metadata.relationship_type : 'Relationship'}</div>
<div class="node-info-detail">
<span class="node-info-label">Confidence:</span>
<span class="node-info-value">${(confidence * 100).toFixed(1)}%</span>
</div>
<div class="node-info-detail">
<span class="node-info-label">Provider:</span>
<span class="node-info-value">${provider}</span>
</div>
`;
this.nodeInfoPopup.innerHTML = html;
this.nodeInfoPopup.style.display = 'block';
this.nodeInfoPopup.style.left = position.x + 15 + 'px';
this.nodeInfoPopup.style.top = position.y - 10 + 'px';
}
/**
* Hide tooltip
* Hide node info popup
*/
hideTooltip() {
// Tooltip hiding is handled by vis.js automatically
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
}));
nodeUpdates.push({
id: nodeId,
borderColor: '#00ff41',
borderWidth: 4
});
// Update edge colors
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
};
}
/**
* 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
}));
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
};
});
// Reset highlighted edges
const edgeUpdates = this.highlightedElements.edges.map(id => {
const originalEdge = this.edges.get(id);
return {
id: id,
color: this.getEdgeColor(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5),
width: this.getEdgeWidth(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5)
};
});
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,
shadow: {
enabled: true,
color: 'rgba(0, 255, 65, 0.8)',
size: 15,
x: 2,
y: 2
}
}));
// Briefly highlight new edges
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,
borderColor: this.getNodeBorderColor(node.type),
borderWidth: 2,
shadow: node.shadow || { enabled: false }
}));
const edgeResets = newEdges.map(edge => ({
id: edge.id,
color: this.getEdgeColor(edge.metadata ? edge.metadata.confidence_score : 0.5),
width: this.getEdgeWidth(edge.metadata ? edge.metadata.confidence_score : 0.5)
}));
this.nodes.update(nodeResets);
this.edges.update(edgeResets);
}, 2000);
}
/**
* Update stabilization progress
* @param {number} progress - Progress value (0-1)
@@ -452,14 +846,71 @@ class GraphManager {
// Could show a progress indicator if needed
console.log(`Graph stabilization: ${(progress * 100).toFixed(1)}%`);
}
/**
* 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'
}
});
}
}
/**
* 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() {
// Simple clustering by node type
const clusterOptionsByType = {
joinCondition: (childOptions) => {
return childOptions.type === 'domain';
},
clusterNodeProperties: {
id: 'domain-cluster',
borderWidth: 3,
shape: 'database',
label: 'Domains',
color: '#00ff41'
}
};
if (this.network.clustering.isCluster('domain-cluster')) {
this.network.clustering.openCluster('domain-cluster');
} else {
this.network.clustering.cluster(clusterOptionsByType);
}
}
/**
* Fit the view to show all nodes
*/
@@ -473,7 +924,7 @@ class GraphManager {
});
}
}
/**
* Reset the view to initial state
*/
@@ -489,21 +940,21 @@ class GraphManager {
});
}
}
/**
* 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
@@ -515,25 +966,7 @@ class GraphManager {
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
@@ -545,7 +978,7 @@ class GraphManager {
//isStabilized: this.network ? this.network.isStabilized() : false
};
}
/**
* Export graph as image (if needed for future implementation)
* @param {string} format - Image format ('png', 'jpeg')
@@ -553,7 +986,7 @@ class GraphManager {
*/
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');

View File

@@ -447,11 +447,19 @@ class DNSReconApp {
try {
console.log('Updating status display...');
// Update status text
// Update status text with animation
if (this.elements.scanStatus) {
this.elements.scanStatus.textContent = this.formatStatus(status.status);
console.log('Updated status display:', status.status);
const formattedStatus = this.formatStatus(status.status);
if (this.elements.scanStatus.textContent !== formattedStatus) {
this.elements.scanStatus.textContent = formattedStatus;
this.elements.scanStatus.classList.add('fade-in');
setTimeout(() => this.elements.scanStatus.classList.remove('fade-in'), 300);
}
// Add status-specific classes for styling
this.elements.scanStatus.className = `status-value status-${status.status}`;
}
if (this.elements.targetDisplay) {
this.elements.targetDisplay.textContent = status.target_domain || 'None';
}
@@ -465,9 +473,16 @@ class DNSReconApp {
this.elements.indicatorsDisplay.textContent = status.indicators_processed || 0;
}
// Update progress bar
// Update progress bar with smooth animation
if (this.elements.progressFill) {
this.elements.progressFill.style.width = `${status.progress_percentage}%`;
// Add pulsing animation for active scans
if (status.status === 'running') {
this.elements.progressFill.parentElement.classList.add('scanning');
} else {
this.elements.progressFill.parentElement.classList.remove('scanning');
}
}
// Update session ID
@@ -492,12 +507,16 @@ class DNSReconApp {
case 'running':
this.setUIState('scanning');
this.showSuccess('Scan is running');
// Reset polling frequency for active scans
this.pollFrequency = 2000;
this.updateConnectionStatus('active');
break;
case 'completed':
this.setUIState('completed');
this.stopPolling();
this.showSuccess('Scan completed successfully');
this.updateConnectionStatus('completed');
// Force a final graph update
console.log('Scan completed - forcing final graph update');
setTimeout(() => this.updateGraph(), 100);
@@ -507,20 +526,54 @@ class DNSReconApp {
this.setUIState('failed');
this.stopPolling();
this.showError('Scan failed');
this.updateConnectionStatus('error');
break;
case 'stopped':
this.setUIState('stopped');
this.stopPolling();
this.showSuccess('Scan stopped');
this.updateConnectionStatus('stopped');
break;
case 'idle':
this.setUIState('idle');
this.stopPolling();
this.updateConnectionStatus('idle');
break;
}
}
/**
* Update connection status indicator
* @param {string} status - Connection status
*/
updateConnectionStatus(status) {
if (!this.elements.connectionStatus) return;
const statusColors = {
'idle': '#c7c7c7',
'active': '#00ff41',
'completed': '#00aa2e',
'stopped': '#ff9900',
'error': '#ff6b6b'
};
this.elements.connectionStatus.style.backgroundColor = statusColors[status] || '#c7c7c7';
const statusText = this.elements.connectionStatus.parentElement?.querySelector('.status-text');
if (statusText) {
const statusTexts = {
'idle': 'System Ready',
'active': 'Scanning Active',
'completed': 'Scan Complete',
'stopped': 'Scan Stopped',
'error': 'Connection Error'
};
statusText.textContent = statusTexts[status] || 'System Online';
}
}
/**
* Set UI state based on scan status
@@ -532,10 +585,17 @@ class DNSReconApp {
switch (state) {
case 'scanning':
this.isScanning = true;
if (this.elements.startScan) this.elements.startScan.disabled = true;
if (this.elements.stopScan) this.elements.stopScan.disabled = false;
if (this.elements.startScan) {
this.elements.startScan.disabled = true;
this.elements.startScan.classList.add('loading');
}
if (this.elements.stopScan) {
this.elements.stopScan.disabled = false;
this.elements.stopScan.classList.remove('loading');
}
if (this.elements.targetDomain) this.elements.targetDomain.disabled = true;
if (this.elements.maxDepth) this.elements.maxDepth.disabled = true;
if (this.elements.configureApiKeys) this.elements.configureApiKeys.disabled = true;
break;
case 'idle':
@@ -543,10 +603,17 @@ class DNSReconApp {
case 'failed':
case 'stopped':
this.isScanning = false;
if (this.elements.startScan) this.elements.startScan.disabled = false;
if (this.elements.stopScan) this.elements.stopScan.disabled = true;
if (this.elements.startScan) {
this.elements.startScan.disabled = false;
this.elements.startScan.classList.remove('loading');
}
if (this.elements.stopScan) {
this.elements.stopScan.disabled = true;
this.elements.stopScan.classList.add('loading');
}
if (this.elements.targetDomain) this.elements.targetDomain.disabled = false;
if (this.elements.maxDepth) this.elements.maxDepth.disabled = false;
if (this.elements.configureApiKeys) this.elements.configureApiKeys.disabled = false;
break;
}
}
@@ -580,20 +647,42 @@ class DNSReconApp {
for (const [name, info] of Object.entries(providers)) {
const providerItem = document.createElement('div');
providerItem.className = 'provider-item';
providerItem.className = 'provider-item fade-in';
const status = info.enabled ? 'enabled' : 'disabled';
const statusClass = info.enabled ? 'enabled' : 'disabled';
let statusClass = 'disabled';
let statusText = 'Disabled';
if (info.enabled) {
statusClass = 'enabled';
statusText = 'Enabled';
} else if (info.requires_api_key) {
statusClass = 'api-key-required';
statusText = 'API Key Required';
}
providerItem.innerHTML = `
<div>
<div class="provider-header">
<div class="provider-name">${name.toUpperCase()}</div>
<div class="provider-stats">
Requests: ${info.statistics.total_requests || 0} |
Success Rate: ${(info.statistics.success_rate || 0).toFixed(1)}%
<div class="provider-status ${statusClass}">${statusText}</div>
</div>
<div class="provider-stats">
<div class="provider-stat">
<span class="provider-stat-label">Requests:</span>
<span class="provider-stat-value">${info.statistics.total_requests || 0}</span>
</div>
<div class="provider-stat">
<span class="provider-stat-label">Success Rate:</span>
<span class="provider-stat-value">${(info.statistics.success_rate || 0).toFixed(1)}%</span>
</div>
<div class="provider-stat">
<span class="provider-stat-label">Relationships:</span>
<span class="provider-stat-value">${info.statistics.relationships_found || 0}</span>
</div>
<div class="provider-stat">
<span class="provider-stat-label">Rate Limit:</span>
<span class="provider-stat-value">${info.rate_limit}/min</span>
</div>
</div>
<div class="provider-status ${statusClass}">${status}</div>
`;
this.elements.providerList.appendChild(providerItem);
@@ -614,7 +703,7 @@ class DNSReconApp {
let detailsHtml = '';
detailsHtml += `<div class="detail-row"><span class="detail-label">Identifier:</span><span class="detail-value">${nodeId}</span></div>`;
detailsHtml += `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.metadata.type || 'Unknown'}</span></div>`;
detailsHtml += `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.metadata.type || node.type || 'Unknown'}</span></div>`;
if (node.metadata) {
for (const [key, value] of Object.entries(node.metadata)) {
@@ -624,6 +713,12 @@ class DNSReconApp {
}
}
// Add timestamps if available
if (node.added_timestamp) {
const addedDate = new Date(node.added_timestamp);
detailsHtml += `<div class="detail-row"><span class="detail-label">Added:</span><span class="detail-value">${addedDate.toLocaleString()}</span></div>`;
}
if (this.elements.modalDetails) {
this.elements.modalDetails.innerHTML = detailsHtml;
}
@@ -645,12 +740,24 @@ class DNSReconApp {
* @returns {boolean} True if data has changed
*/
hasGraphChanged(graphData) {
// Simple check based on node and edge counts
// Simple check based on node and edge counts and timestamps
const currentStats = this.graphManager.getStatistics();
const newNodeCount = graphData.nodes ? graphData.nodes.length : 0;
const newEdgeCount = graphData.edges ? graphData.edges.length : 0;
const changed = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
// Check if counts changed
const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
// Also check if we have new timestamp data
const hasNewTimestamp = graphData.statistics &&
graphData.statistics.last_modified &&
graphData.statistics.last_modified !== this.lastGraphTimestamp;
if (hasNewTimestamp) {
this.lastGraphTimestamp = graphData.statistics.last_modified;
}
const changed = countsChanged || hasNewTimestamp;
console.log(`Graph change check: Current(${currentStats.nodeCount}n, ${currentStats.edgeCount}e) vs New(${newNodeCount}n, ${newEdgeCount}e) = ${changed}`);
@@ -816,6 +923,10 @@ class DNSReconApp {
this.showMessage(message, 'info');
}
showWarning(message) {
this.showMessage(message, 'warning');
}
/**
* Show error message
* @param {string} message - Error message
@@ -828,13 +939,7 @@ class DNSReconApp {
* Show connection error
*/
showConnectionError() {
if (this.elements.connectionStatus) {
this.elements.connectionStatus.style.backgroundColor = '#ff6b6b';
}
const statusText = this.elements.connectionStatus?.parentElement?.querySelector('.status-text');
if (statusText) {
statusText.textContent = 'Connection Error';
}
this.updateConnectionStatus('error');
}
/**
@@ -848,24 +953,12 @@ class DNSReconApp {
// Create message element
const messageElement = document.createElement('div');
messageElement.className = `message-toast message-${type}`;
messageElement.style.cssText = `
background: ${this.getMessageColor(type)};
color: #fff;
padding: 12px 20px;
margin-bottom: 10px;
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 0.9rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
border-left: 4px solid ${this.getMessageBorderColor(type)};
animation: slideInRight 0.3s ease-out;
`;
messageElement.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>${message}</span>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px 20px;">
<span style="flex: 1;">${message}</span>
<button onclick="this.parentElement.parentElement.remove()"
style="background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; margin-left: 10px;">×</button>
style="background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; margin-left: 10px; opacity: 0.7;">×</button>
</div>
`;
@@ -888,13 +981,8 @@ class DNSReconApp {
}
// Update connection status to show activity
if (type === 'success' && this.elements.connectionStatus) {
this.elements.connectionStatus.style.backgroundColor = '#00ff41';
setTimeout(() => {
if (this.elements.connectionStatus) {
this.elements.connectionStatus.style.backgroundColor = '#00ff41';
}
}, 2000);
if (type === 'success' && this.consecutiveErrors === 0) {
this.updateConnectionStatus(this.isScanning ? 'active' : 'idle');
}
}