/** * Main application logic for DNSRecon web interface * Handles UI interactions, API communication, and data flow */ class DNSReconApp { constructor() { console.log('DNSReconApp constructor called'); this.graphManager = null; this.scanStatus = 'idle'; this.pollInterval = null; this.currentSessionId = null; this.elements = {}; this.isScanning = false; this.lastGraphUpdate = null; this.init(); } /** * Initialize the application */ init() { console.log('DNSReconApp init called'); document.addEventListener('DOMContentLoaded', () => { console.log('DOM loaded, initializing application...'); try { this.initializeElements(); this.setupEventHandlers(); this.initializeGraph(); this.updateStatus(); this.loadProviders(); this.initializeEnhancedModals(); console.log('DNSRecon application initialized successfully'); } catch (error) { console.error('Failed to initialize DNSRecon application:', error); this.showError(`Initialization failed: ${error.message}`); } }); } /** * Initialize DOM element references */ initializeElements() { console.log('Initializing DOM elements...'); this.elements = { // Form elements targetInput: document.getElementById('target-input'), maxDepth: document.getElementById('max-depth'), startScan: document.getElementById('start-scan'), addToGraph: document.getElementById('add-to-graph'), stopScan: document.getElementById('stop-scan'), exportResults: document.getElementById('export-results'), configureSettings: document.getElementById('configure-settings'), // Status elements scanStatus: document.getElementById('scan-status'), targetDisplay: document.getElementById('target-display'), depthDisplay: document.getElementById('depth-display'), relationshipsDisplay: document.getElementById('relationships-display'), progressCompact: document.getElementById('progress-compact'), progressFill: document.getElementById('progress-fill'), // Provider elements providerList: document.getElementById('provider-list'), // Node Modal elements nodeModal: document.getElementById('node-modal'), modalTitle: document.getElementById('modal-title'), modalDetails: document.getElementById('modal-details'), modalClose: document.getElementById('modal-close'), // Settings Modal elements settingsModal: document.getElementById('settings-modal'), settingsModalClose: document.getElementById('settings-modal-close'), apiKeyInputs: document.getElementById('api-key-inputs'), saveApiKeys: document.getElementById('save-api-keys'), resetApiKeys: document.getElementById('reset-api-keys'), // Other elements sessionId: document.getElementById('session-id'), connectionStatus: document.getElementById('connection-status'), }; // Verify critical elements exist const requiredElements = ['targetInput', 'startScan', 'scanStatus']; for (const elementName of requiredElements) { if (!this.elements[elementName]) { throw new Error(`Required element '${elementName}' not found in DOM`); } } console.log('DOM elements initialized successfully'); this.createMessageContainer(); } /** * Create a message container for showing user feedback */ createMessageContainer() { // Check if message container already exists let messageContainer = document.getElementById('message-container'); if (!messageContainer) { messageContainer = document.createElement('div'); messageContainer.id = 'message-container'; messageContainer.className = 'message-container'; messageContainer.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 1000; max-width: 400px; `; document.body.appendChild(messageContainer); console.log('Message container created'); } } /** * Setup event handlers */ setupEventHandlers() { console.log('Setting up event handlers...'); try { // Form interactions this.initializeModalFunctionality(); this.elements.startScan.addEventListener('click', (e) => { console.log('Start scan button clicked'); e.preventDefault(); this.startScan(); }); this.elements.addToGraph.addEventListener('click', (e) => { e.preventDefault(); this.startScan(false); }); this.elements.stopScan.addEventListener('click', (e) => { console.log('Stop scan button clicked'); e.preventDefault(); this.stopScan(); }); this.elements.exportResults.addEventListener('click', (e) => { console.log('Export results button clicked'); e.preventDefault(); this.exportResults(); }); this.elements.configureSettings.addEventListener('click', () => this.showSettingsModal()); // Enter key support for target domain input this.elements.targetInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !this.isScanning) { console.log('Enter key pressed in domain input'); this.startScan(); } }); // Node Modal interactions if (this.elements.modalClose) { this.elements.modalClose.addEventListener('click', () => this.hideModal()); } if (this.elements.nodeModal) { this.elements.nodeModal.addEventListener('click', (e) => { if (e.target === this.elements.nodeModal) this.hideModal(); }); } // Settings Modal interactions if (this.elements.settingsModalClose) { this.elements.settingsModalClose.addEventListener('click', () => this.hideSettingsModal()); } if (this.elements.settingsModal) { this.elements.settingsModal.addEventListener('click', (e) => { if (e.target === this.elements.settingsModal) this.hideSettingsModal(); }); } if (this.elements.saveApiKeys) { this.elements.saveApiKeys.addEventListener('click', () => this.saveApiKeys()); } if (this.elements.resetApiKeys) { this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys()); } // ** FIX: Listen for the custom event from the graph ** document.addEventListener('nodeSelected', (e) => { this.showNodeModal(e.detail.node); }); // Listen for the new iterateScan event from the graph context menu document.addEventListener('iterateScan', (e) => { if (this.isScanning) { this.showWarning('A scan is already in progress.'); return; } const { nodeId } = e.detail; console.log(`Received iterateScan event for node: ${nodeId}`); this.elements.targetInput.value = nodeId; this.startScan(false, nodeId); // Pass nodeId to force rescan }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.hideModal(); this.hideSettingsModal(); } }); // Window events window.addEventListener('beforeunload', () => { if (this.isScanning) { return 'A scan is currently in progress. Are you sure you want to leave?'; } }); console.log('Event handlers set up successfully'); } catch (error) { console.error('Failed to setup event handlers:', error); throw error; } } /** * Initialize graph visualization */ initializeGraph() { try { console.log('Initializing graph manager...'); this.graphManager = new GraphManager('network-graph'); console.log('Graph manager initialized successfully'); } catch (error) { console.error('Failed to initialize graph manager:', error); this.showError('Failed to initialize graph visualization'); } } /** * Start scan with error handling */ async startScan(clearGraph = true, forceRescanTarget = null) { console.log('=== STARTING SCAN ==='); try { const target = this.elements.targetInput.value.trim(); const maxDepth = parseInt(this.elements.maxDepth.value); console.log(`Target: "${target}", Max depth: ${maxDepth}`); // Validation if (!target) { console.log('Validation failed: empty target'); this.showError('Please enter a target domain or IP'); this.elements.targetInput.focus(); return; } if (!this.isValidTarget(target)) { console.log(`Validation failed: invalid target format for "${target}"`); this.showError('Please enter a valid domain name (e.g., example.com) or IP address (e.g., 8.8.8.8)'); this.elements.targetInput.focus(); return; } console.log('Validation passed, setting UI state to scanning...'); this.setUIState('scanning'); this.showInfo('Starting reconnaissance scan...'); console.log('Making API call to start scan...'); const requestData = { target: target, max_depth: maxDepth, clear_graph: clearGraph, force_rescan_target: forceRescanTarget }; console.log('Request data:', requestData); const response = await this.apiCall('/api/scan/start', 'POST', requestData); console.log('API response received:', response); if (response.success) { this.currentSessionId = response.scan_id; this.showSuccess('Reconnaissance scan started successfully'); if (clearGraph) { this.graphManager.clear(); } console.log(`Scan started for ${target} with depth ${maxDepth}`); // Start polling immediately with faster interval for responsiveness this.startPolling(1000); // Force an immediate status update console.log('Forcing immediate status update...'); setTimeout(() => { this.updateStatus(); this.updateGraph(); }, 100); } else { throw new Error(response.error || 'Failed to start scan'); } } catch (error) { console.error('Failed to start scan:', error); this.showError(`Failed to start scan: ${error.message}`); this.setUIState('idle'); } } /** * Scan stop with immediate UI feedback */ async stopScan() { try { console.log('Stopping scan...'); // Immediately disable stop button and show stopping state if (this.elements.stopScan) { this.elements.stopScan.disabled = true; this.elements.stopScan.innerHTML = '[STOPPING]Stopping...'; } // Show immediate feedback this.showInfo('Stopping scan...'); const response = await this.apiCall('/api/scan/stop', 'POST'); if (response.success) { this.showSuccess('Scan stop requested'); console.log('Scan stop requested successfully'); // Force immediate status update setTimeout(() => { this.updateStatus(); }, 100); // Continue polling for a bit to catch the status change this.startPolling(500); // Fast polling to catch status change // Stop fast polling after 10 seconds setTimeout(() => { if (this.scanStatus === 'stopped' || this.scanStatus === 'idle') { this.stopPolling(); } }, 10000); } else { throw new Error(response.error || 'Failed to stop scan'); } } catch (error) { console.error('Failed to stop scan:', error); this.showError(`Failed to stop scan: ${error.message}`); // Re-enable stop button on error if (this.elements.stopScan) { this.elements.stopScan.disabled = false; this.elements.stopScan.innerHTML = '[STOP]Terminate Scan'; } } } /** * Export scan results */ async exportResults() { try { console.log('Exporting results...'); // Create a temporary link to trigger download const link = document.createElement('a'); link.href = '/api/export'; link.download = ''; // Let server determine filename document.body.appendChild(link); link.click(); document.body.removeChild(link); this.showSuccess('Results export initiated'); console.log('Results export initiated'); } catch (error) { console.error('Failed to export results:', error); this.showError(`Failed to export results: ${error.message}`); } } /** * Start polling for scan updates with configurable interval */ startPolling(interval = 2000) { console.log('=== STARTING POLLING ==='); if (this.pollInterval) { console.log('Clearing existing poll interval'); clearInterval(this.pollInterval); } this.pollInterval = setInterval(() => { console.log('--- Polling tick ---'); this.updateStatus(); this.updateGraph(); this.loadProviders(); }, interval); console.log(`Polling started with ${interval}ms interval`); } /** * Stop polling for updates */ stopPolling() { console.log('=== STOPPING POLLING ==='); if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } } /** * Status update with better error handling */ async updateStatus() { try { console.log('Updating status...'); const response = await this.apiCall('/api/scan/status'); console.log('Status response:', response); if (response.success && response.status) { const status = response.status; console.log('Current scan status:', status.status); console.log('Current progress:', status.progress_percentage + '%'); console.log('Graph stats:', status.graph_statistics); this.updateStatusDisplay(status); // Handle status changes if (status.status !== this.scanStatus) { console.log(`*** STATUS CHANGED: ${this.scanStatus} -> ${status.status} ***`); this.handleStatusChange(status.status, status.task_queue_size); } this.scanStatus = status.status; } else { console.error('Status update failed:', response); // Don't show error for status updates to avoid spam } } catch (error) { console.error('Failed to update status:', error); this.showConnectionError(); } } /** * Update graph from server */ async updateGraph() { try { console.log('Updating graph...'); const response = await this.apiCall('/api/graph'); console.log('Graph response:', response); if (response.success) { const graphData = response.graph; console.log('Graph data received:'); console.log('- Nodes:', graphData.nodes ? graphData.nodes.length : 0); console.log('- Edges:', graphData.edges ? graphData.edges.length : 0); if (graphData.nodes) { graphData.nodes.forEach(node => { console.log(` Node: ${node.id} (${node.type})`); }); } if (graphData.edges) { graphData.edges.forEach(edge => { console.log(` Edge: ${edge.from} -> ${edge.to} (${edge.label})`); }); } // Only update if data has changed if (this.hasGraphChanged(graphData)) { console.log('*** GRAPH DATA CHANGED - UPDATING VISUALIZATION ***'); this.graphManager.updateGraph(graphData); this.lastGraphUpdate = Date.now(); // Update relationship count in status const edgeCount = graphData.edges ? graphData.edges.length : 0; if (this.elements.relationshipsDisplay) { this.elements.relationshipsDisplay.textContent = edgeCount; } } else { console.log('Graph data unchanged, skipping update'); } } else { console.error('Graph update failed:', response); } } catch (error) { console.error('Failed to update graph:', error); // Don't show error for graph updates to avoid spam } } /** * Update status display elements * @param {Object} status - Status object from server */ updateStatusDisplay(status) { try { console.log('Updating status display...'); // Update status text with animation if (this.elements.scanStatus) { 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'; } if (this.elements.depthDisplay) { this.elements.depthDisplay.textContent = `${status.current_depth}/${status.max_depth}`; } // Update progress bar and compact display if (this.elements.progressFill) { const completed = status.indicators_completed || 0; const enqueued = status.task_queue_size || 0; const totalTasks = completed + enqueued; const progressPercentage = totalTasks > 0 ? (completed / totalTasks) * 100 : 0; this.elements.progressFill.style.width = `${progressPercentage}%`; if (this.elements.progressCompact) { this.elements.progressCompact.textContent = `${completed}/${totalTasks} - ${Math.round(progressPercentage)}%`; } // 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 display with user session info if (this.elements.sessionId) { const scanSessionId = this.currentSessionId; const userSessionId = status.user_session_id; if (scanSessionId && userSessionId) { this.elements.sessionId.textContent = `Session: ${userSessionId.substring(0, 8)}... | Scan: ${scanSessionId}`; } else if (userSessionId) { this.elements.sessionId.textContent = `User Session: ${userSessionId.substring(0, 8)}...`; } else { this.elements.sessionId.textContent = 'Session: Loading...'; } } this.setUIState(status.status, status.task_queue_size); console.log('Status display updated successfully'); } catch (error) { console.error('Error updating status display:', error); } } /** * Handle status changes with improved state synchronization * @param {string} newStatus - New scan status */ handleStatusChange(newStatus, task_queue_size) { console.log(`=== STATUS CHANGE: ${this.scanStatus} -> ${newStatus} ===`); switch (newStatus) { case 'running': this.setUIState('scanning', task_queue_size); this.showSuccess('Scan is running'); // Increase polling frequency for active scans this.startPolling(1000); // Poll every 1 second for running scans this.updateConnectionStatus('active'); break; case 'completed': this.setUIState('completed', task_queue_size); this.stopPolling(); this.showSuccess('Scan completed successfully'); this.updateConnectionStatus('completed'); this.loadProviders(); // Force a final graph update console.log('Scan completed - forcing final graph update'); setTimeout(() => this.updateGraph(), 100); break; case 'failed': this.setUIState('failed', task_queue_size); this.stopPolling(); this.showError('Scan failed'); this.updateConnectionStatus('error'); this.loadProviders(); break; case 'stopped': this.setUIState('stopped', task_queue_size); this.stopPolling(); this.showSuccess('Scan stopped'); this.updateConnectionStatus('stopped'); this.loadProviders(); break; case 'idle': this.setUIState('idle', task_queue_size); this.stopPolling(); this.updateConnectionStatus('idle'); break; default: console.warn(`Unknown status: ${newStatus}`); 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'; } } /** * UI state management with immediate button updates */ setUIState(state, task_queue_size) { console.log(`Setting UI state to: ${state}`); const isQueueEmpty = task_queue_size === 0; switch (state) { case 'scanning': this.isScanning = true; if (this.graphManager) { this.graphManager.isScanning = true; } if (this.elements.startScan) { this.elements.startScan.disabled = true; this.elements.startScan.classList.add('loading'); this.elements.startScan.innerHTML = '[SCANNING]Scanning...'; } if (this.elements.addToGraph) { this.elements.addToGraph.disabled = true; this.elements.addToGraph.classList.add('loading'); } if (this.elements.stopScan) { this.elements.stopScan.disabled = false; this.elements.stopScan.classList.remove('loading'); this.elements.stopScan.innerHTML = '[STOP]Terminate Scan'; } if (this.elements.targetInput) this.elements.targetInput.disabled = true; if (this.elements.maxDepth) this.elements.maxDepth.disabled = true; if (this.elements.configureSettings) this.elements.configureSettings.disabled = true; break; case 'idle': case 'completed': case 'failed': case 'stopped': this.isScanning = false; if (this.graphManager) { this.graphManager.isScanning = false; } if (this.elements.startScan) { this.elements.startScan.disabled = !isQueueEmpty; this.elements.startScan.classList.remove('loading'); this.elements.startScan.innerHTML = '[RUN]Start Reconnaissance'; } if (this.elements.addToGraph) { this.elements.addToGraph.disabled = !isQueueEmpty; this.elements.addToGraph.classList.remove('loading'); } if (this.elements.stopScan) { this.elements.stopScan.disabled = true; this.elements.stopScan.innerHTML = '[STOP]Terminate Scan'; } if (this.elements.targetInput) this.elements.targetInput.disabled = false; if (this.elements.maxDepth) this.elements.maxDepth.disabled = false; if (this.elements.configureSettings) this.elements.configureSettings.disabled = false; break; } } /** * Load provider information */ async loadProviders() { try { console.log('Loading providers...'); const response = await this.apiCall('/api/providers'); if (response.success) { this.updateProviderDisplay(response.providers); this.buildApiKeyModal(response.providers); console.log('Providers loaded successfully'); } } catch (error) { console.error('Failed to load providers:', error); } } /** * Update provider display * @param {Object} providers - Provider information */ updateProviderDisplay(providers) { if (!this.elements.providerList) return; this.elements.providerList.innerHTML = ''; for (const [name, info] of Object.entries(providers)) { const providerItem = document.createElement('div'); providerItem.className = 'provider-item fade-in'; 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 = `
${info.display_name}
${statusText}
Requests: ${info.statistics.total_requests || 0}
Success Rate: ${(info.statistics.success_rate || 0).toFixed(1)}%
Relationships: ${info.statistics.relationships_found || 0}
Rate Limit: ${info.rate_limit}/min
`; this.elements.providerList.appendChild(providerItem); } } /** * Enhanced node details HTML generation with better visual hierarchy * File: static/js/main.js (replace generateNodeDetailsHtml method) */ generateNodeDetailsHtml(node) { if (!node) return '
Details not available.
'; let detailsHtml = ''; return detailsHtml; } generateStandardNodeDetails(node) { let html = ''; // Relationships sections html += this.generateRelationshipsSection(node); // Enhanced attributes section with special certificate handling if (node.attributes && Object.keys(node.attributes).length > 0) { const { certificates, ...otherAttributes } = node.attributes; // Handle certificates separately with enhanced display if (certificates) { html += this.generateCertificateSection({ certificates }); } // Handle other attributes normally if (Object.keys(otherAttributes).length > 0) { html += this.generateAttributesSection(otherAttributes); } } // Description section html += this.generateDescriptionSection(node); // Metadata section (collapsed by default) html += this.generateMetadataSection(node); return html; } /** * Enhanced certificate section generation using existing styles */ generateCertificateSection(attributes) { const certificates = attributes.certificates; if (!certificates || typeof certificates !== 'object') { return ''; } let html = ` '; return html; } /** * Generate latest certificate info using existing attribute list */ generateLatestCertificateInfo(latest) { const isValid = latest.is_currently_valid; const statusText = isValid ? 'Valid' : 'Invalid/Expired'; const statusColor = isValid ? '#00ff41' : '#ff6b6b'; let html = `
Most Recent Certificate
Status: ${statusText}
Issued: ${latest.not_before || 'Unknown'}
Expires: ${latest.not_after || 'Unknown'}
Issuer: ${this.escapeHtml(latest.issuer_name || 'Unknown')}
${latest.certificate_id ? ` ` : ''}
`; return html; } /** * Generate certificate list using existing collapsible structure */ generateCertificateList(certificateDetails) { if (!certificateDetails || certificateDetails.length === 0) { return ''; } // Limit display to prevent overwhelming the UI const maxDisplay = 8; const certificates = certificateDetails.slice(0, maxDisplay); const remaining = certificateDetails.length - maxDisplay; let html = `
📋 Certificate Details (${certificates.length}${remaining > 0 ? ` of ${certificateDetails.length}` : ''})
`; certificates.forEach((cert, index) => { const isValid = cert.is_currently_valid; let statusText = isValid ? '✅ Valid' : '❌ Invalid/Expired'; let statusColor = isValid ? '#00ff41' : '#ff6b6b'; if (cert.expires_soon && isValid) { statusText = 'âš ī¸ Valid (Expiring Soon)'; statusColor = '#ff9900'; } html += `
#${index + 1} ${statusText} ${cert.certificate_id ? ` crt.sh ↗ ` : ''}
Common Name: ${this.escapeHtml(cert.common_name || 'N/A')}
Issuer: ${this.escapeHtml(cert.issuer_name || 'Unknown')}
Valid From: ${cert.not_before || 'Unknown'}
Valid Until: ${cert.not_after || 'Unknown'}
${cert.validity_period_days ? `
Period: ${cert.validity_period_days} days
` : ''}
`; }); if (remaining > 0) { html += `
📋 ${remaining} additional certificate${remaining > 1 ? 's' : ''} not shown.
Use the export function to see all certificates.
`; } html += '
'; return html; } /** * Generate certificate summary using minimal new CSS */ generateCertificateSummary(certificates) { const total = certificates.total_certificates || 0; const valid = certificates.valid_certificates || 0; const expired = certificates.expired_certificates || 0; const expiringSoon = certificates.expires_soon_count || 0; const issuers = certificates.unique_issuers || []; let html = `
${total}
Total
${valid}
Valid
${expired}
Expired
${expiringSoon}
Expiring Soon
`; // Certificate authorities using existing array display if (issuers.length > 0) { html += `
Certificate Authorities:
`; issuers.forEach(issuer => { html += `
${this.escapeHtml(issuer)}
`; }); html += '
'; } return html; } generateLargeEntityDetails(node) { const attributes = node.attributes || {}; const nodes = attributes.nodes || []; const nodeType = attributes.node_type || 'nodes'; let html = ` '; return html; } generateCorrelationDetails(node) { const metadata = node.metadata || {}; const values = metadata.values || []; const sources = metadata.sources || []; const mergeCount = metadata.merge_count || 1; let html = ''; // Correlation values section with meaningful labels - reuses existing modal structure html += ` '; // Correlated nodes section - reuses existing relationship display const correlatedNodes = metadata.correlated_nodes || []; if (correlatedNodes.length > 0) { html += ` '; } return html; } /** * Create a mapping of values to their source attribute information */ createValueSourceMap(values, sources) { const valueSourceMap = {}; // Group sources by their meaningful attributes const attrGroups = {}; sources.forEach(source => { const meaningfulAttr = source.meaningful_attr || source.parent_attr || 'correlation'; if (!attrGroups[meaningfulAttr]) { attrGroups[meaningfulAttr] = { nodeIds: [], paths: [] }; } attrGroups[meaningfulAttr].nodeIds.push(source.node_id); attrGroups[meaningfulAttr].paths.push(source.path || ''); }); // Map values to their best attribute names values.forEach((value, index) => { // Find the most meaningful attribute name const attrNames = Object.keys(attrGroups); const bestAttr = attrNames.find(attr => attr !== 'correlation' && attr !== 'unknown') || attrNames[0] || 'correlation'; if (attrGroups[bestAttr]) { valueSourceMap[index] = { meaningfulName: bestAttr, details: `Found in: ${[...new Set(attrGroups[bestAttr].nodeIds)].join(', ')}` }; } }); return valueSourceMap; } generateCorrelationObjectLayout(node) { const metadata = node.metadata || {}; const values = metadata.values || []; const mergeCount = metadata.merge_count || 1; let html = '
'; if (mergeCount > 1) { html += `

🔗Merged Correlations

${mergeCount} values
`; values.forEach((value, index) => { const displayValue = typeof value === 'string' && value.length > 50 ? value.substring(0, 47) + '...' : value; html += `
${displayValue}
`; }); html += '
'; } else { const singleValue = values.length > 0 ? values[0] : (metadata.value || 'Unknown'); html += `

🔗Correlation Value

${singleValue}
`; } // Show correlated nodes const correlatedNodes = metadata.correlated_nodes || []; if (correlatedNodes.length > 0) { html += `

🌐Correlated Nodes

${correlatedNodes.length}
`; correlatedNodes.forEach(nodeId => { html += ` `; }); html += '
'; } html += '
'; return html; } generateLargeEntityLayout(node) { const attributes = node.attributes || {}; const nodes = attributes.nodes || []; const nodeType = attributes.node_type || 'nodes'; let html = `

đŸ“ĻLarge Entity Container

${attributes.count} ${nodeType}s
Source Provider: ${attributes.source_provider || 'Unknown'}
Discovery Depth: ${attributes.discovery_depth || 'Unknown'}

📋Contained ${nodeType}s

`; nodes.forEach((innerNodeId, index) => { const innerNode = this.graphManager.nodes.get(innerNodeId); html += `
● ${innerNodeId} â–ŧ
${innerNode ? this.generateStandardNodeLayout(innerNode) : '
No details available
'}
`; }); html += '
'; return html; } generateStandardNodeLayout(node) { let html = '
'; // Relationships section html += this.generateRelationshipsSection(node); // Attributes section with smart categorization html += this.generateAttributesSection(node); // Description section html += this.generateDescriptionSection(node); // Metadata section (collapsed by default) html += this.generateMetadataSection(node); html += '
'; return html; } generateRelationshipsSection(node) { let html = ''; if (node.incoming_edges && node.incoming_edges.length > 0) { html += ` '; } if (node.outgoing_edges && node.outgoing_edges.length > 0) { html += ` '; } return html; } generateAttributesSection(attributes) { const categorized = this.categorizeAttributes(attributes); let html = ''; Object.entries(categorized).forEach(([category, attrs]) => { if (Object.keys(attrs).length === 0) return; html += ` '; }); return html; } formatCertificateData(certData) { if (!certData || typeof certData !== 'object') { return '

No certificate data available

'; } let html = '
'; // Handle certificate summary if (certData.total_certificates) { html += `
Total Certificates: ${certData.total_certificates} ${certData.has_valid_cert ? 'Valid' : 'Invalid'}
`; } // Handle unique issuers if (certData.unique_issuers && Array.isArray(certData.unique_issuers)) { html += `
Issuers:
`; certData.unique_issuers.forEach(issuer => { html += `
${this.escapeHtml(String(issuer))}
`; }); html += '
'; } html += '
'; return html; } formatAttributeValue(value) { if (value === null || value === undefined) { return 'None'; } if (Array.isArray(value)) { if (value.length === 0) return 'None'; if (value.length === 1) return this.escapeHtml(String(value[0])); let html = '
'; value.forEach((item, index) => { html += `
${this.escapeHtml(String(item))}
`; }); html += '
'; return html; } if (typeof value === 'object' && value !== null) { return `
${this.formatObjectCompact(value)}
`; } return this.escapeHtml(String(value)); } categorizeAttributes(attributes) { const categories = { 'DNS Records': {}, 'Certificates': {}, 'Network Info': {}, 'Provider Data': {}, 'Other': {} }; for (const [key, value] of Object.entries(attributes)) { const lowerKey = key.toLowerCase(); if (lowerKey.includes('dns') || lowerKey.includes('record') || key.endsWith('_record')) { categories['DNS Records'][key] = value; } else if (lowerKey.includes('cert') || lowerKey.includes('ssl') || lowerKey.includes('tls')) { categories['Certificates'][key] = value; } else if (lowerKey.includes('ip') || lowerKey.includes('asn') || lowerKey.includes('network')) { categories['Network Info'][key] = value; } else if (lowerKey.includes('shodan') || lowerKey.includes('crtsh') || lowerKey.includes('provider')) { categories['Provider Data'][key] = value; } else { categories['Other'][key] = value; } } return categories; } formatObjectCompact(obj) { if (!obj || typeof obj !== 'object') return ''; let html = ''; const entries = Object.entries(obj); entries.forEach(([key, value]) => { html += `
${key}: `; if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { html += `[${value.length} items]`; } else { html += `{${Object.keys(value).length} properties}`; } } else { html += this.escapeHtml(String(value)); } html += '
'; }); return html; } generateDescriptionSection(node) { if (!node.description) return ''; return `

📝Description

${this.escapeHtml(node.description)}
`; } generateMetadataSection(node) { if (!node.metadata || Object.keys(node.metadata).length === 0) return ''; return ` `; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Recursively formats a JavaScript object into an HTML unordered list with collapsible sections. * @param {Object} obj - The object to format. * @returns {string} - An HTML string representing the object. */ formatObjectToHtml(obj) { if (!obj || Object.keys(obj).length === 0) { return '

No data available.

'; } let html = ''; return html; } /** * Enhanced showNodeModal with better event handling */ showNodeModal(node) { if (!this.elements.nodeModal || !node) return; if (this.elements.modalTitle) { this.elements.modalTitle.innerHTML = ` ${this.getNodeTypeIcon(node.type)} ${node.id} `; } const detailsHtml = this.generateNodeDetailsHtml(node); if (this.elements.modalDetails) { this.elements.modalDetails.innerHTML = detailsHtml; // Add enhanced event handlers this.addModalEventHandlers(); } this.elements.nodeModal.style.display = 'block'; } /** * Add event handlers for enhanced modal interactions */ addModalEventHandlers() { // Handle node navigation links - FIXED to work properly this.elements.modalDetails.querySelectorAll('.node-link-compact').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id'); console.log('Node link clicked:', nodeId); if (nodeId && this.graphManager && this.graphManager.nodes) { const nextNode = this.graphManager.nodes.get(nodeId); if (nextNode) { console.log('Navigating to node:', nextNode); // Don't hide modal, just update content this.showNodeModal(nextNode); } else { console.warn('Node not found:', nodeId); } } }); }); // Handle legacy node links this.elements.modalDetails.querySelectorAll('.node-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id'); if (nodeId && this.graphManager && this.graphManager.nodes) { const nextNode = this.graphManager.nodes.get(nodeId); if (nextNode) { this.showNodeModal(nextNode); } } }); }); } initializeModalFunctionality() { // Make sure the graph manager has node access console.log('Initializing modal functionality...'); // Set up event delegation for dynamic content document.addEventListener('click', (e) => { if (e.target.classList.contains('node-link-compact') || e.target.classList.contains('node-link')) { e.preventDefault(); e.stopPropagation(); const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id'); if (nodeId && this.graphManager && this.graphManager.nodes) { const nextNode = this.graphManager.nodes.get(nodeId); if (nextNode) { this.showNodeModal(nextNode); } } } }); } /** * Get icon for node type */ getNodeTypeIcon(nodeType) { const icons = { 'domain': '🌐', 'ip': '📍', 'asn': 'đŸĸ', 'large_entity': 'đŸ“Ļ', 'correlation_object': '🔗' }; return icons[nodeType] || '●'; } /** * Enhanced hideModal with animation */ hideModal() { if (this.elements.nodeModal) { this.elements.nodeModal.classList.add('modal-closing'); setTimeout(() => { this.elements.nodeModal.style.display = 'none'; this.elements.nodeModal.classList.remove('modal-closing'); }, 200); } } /** * Copy text to clipboard with user feedback */ async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.showMessage('Copied to clipboard', 'success'); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); this.showMessage('Copied to clipboard', 'success'); } catch (fallbackErr) { this.showMessage('Failed to copy text', 'error'); } document.body.removeChild(textArea); } } /** * Toggle all entity nodes in large entity view */ toggleAllEntities() { const entityCards = this.elements.modalDetails.querySelectorAll('.entity-node-card'); const allExpanded = Array.from(entityCards).every(card => card.classList.contains('expanded')); entityCards.forEach(card => { if (allExpanded) { card.classList.remove('expanded'); } else { card.classList.add('expanded'); } }); // Update button text const toggleBtn = this.elements.modalDetails.querySelector('.toggle-all-btn'); if (toggleBtn) { toggleBtn.textContent = allExpanded ? 'Expand All' : 'Collapse All'; } } /** * Enhanced keyboard navigation for modals */ setupModalKeyboardNavigation() { document.addEventListener('keydown', (e) => { if (this.elements.nodeModal && this.elements.nodeModal.style.display === 'block') { switch (e.key) { case 'Escape': this.hideModal(); break; case 'Tab': this.handleModalTabNavigation(e); break; case 'Enter': if (e.target.classList.contains('node-link') || e.target.classList.contains('node-link-item')) { e.target.click(); } break; } } }); } /** * Handle tab navigation within modal */ handleModalTabNavigation(e) { const focusableElements = this.elements.nodeModal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement) { lastElement.focus(); e.preventDefault(); } } else { if (document.activeElement === lastElement) { firstElement.focus(); e.preventDefault(); } } } /** * Initialize enhanced modal functionality */ initializeEnhancedModals() { this.setupModalKeyboardNavigation(); // Add CSS classes for animations const style = document.createElement('style'); style.textContent = ` .modal-opening { animation: modalFadeIn 0.3s ease-out; } .modal-closing { animation: modalFadeOut 0.2s ease-in; } @keyframes modalFadeIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } @keyframes modalFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } } .array-value.expanded .array-items { max-height: none; } .modal-title-icon { margin-right: 0.5rem; font-size: 1.2rem; } .modal-title-text { font-family: 'Special Elite', monospace; } `; document.head.appendChild(style); } /** * Show Settings modal */ showSettingsModal() { if (this.elements.settingsModal) { this.elements.settingsModal.style.display = 'block'; } } /** * Hide Settings modal */ hideSettingsModal() { if (this.elements.settingsModal) { this.elements.settingsModal.style.display = 'none'; } } /** * Save API Keys */ async saveApiKeys() { const inputs = this.elements.apiKeyInputs.querySelectorAll('input'); const keys = {}; inputs.forEach(input => { const provider = input.dataset.provider; const value = input.value.trim(); if (provider && value) { keys[provider] = value; } }); if (Object.keys(keys).length === 0) { this.showWarning('No API keys were entered.'); return; } try { const response = await this.apiCall('/api/config/api-keys', 'POST', keys); if (response.success) { this.showSuccess(response.message); this.hideSettingsModal(); this.loadProviders(); // Refresh provider status } else { throw new Error(response.error || 'Failed to save API keys'); } } catch (error) { this.showError(`Error saving API keys: ${error.message}`); } } /** * Reset API Key fields */ resetApiKeys() { const inputs = this.elements.apiKeyInputs.querySelectorAll('input'); inputs.forEach(input => { input.value = ''; }); } /** * Check if graph data has changed * @param {Object} graphData - New graph data * @returns {boolean} True if data has changed */ hasGraphChanged(graphData) { // 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; // 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}`); return changed; } /** * Make API call to server * @param {string} endpoint - API endpoint * @param {string} method - HTTP method * @param {Object} data - Request data * @returns {Promise} Response data */ async apiCall(endpoint, method = 'GET', data = null) { console.log(`Making API call: ${method} ${endpoint}`, data ? data : '(no data)'); try { const options = { method: method, headers: { 'Content-Type': 'application/json' } }; if (data && method !== 'GET') { options.body = JSON.stringify(data); console.log('Request body:', options.body); } console.log('Fetch options:', options); const response = await fetch(endpoint, options); console.log('Response status:', response.status, response.statusText); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); console.log('Response data:', result); return result; } catch (error) { console.error(`API call failed for ${method} ${endpoint}:`, error); throw error; } } /** * Validate target (domain or IP) * @param {string} target - Target to validate * @returns {boolean} True if valid */ isValidTarget(target) { return this.isValidDomain(target) || this.isValidIp(target); } /** * Validate domain name * @param {string} domain - Domain to validate * @returns {boolean} True if valid */ isValidDomain(domain) { console.log(`Validating domain: "${domain}"`); if (!domain || typeof domain !== 'string' || domain.length > 253 || /^\d{1,3}(\.\d{1,3}){3}$/.test(domain)) { return false; } const parts = domain.split('.'); if (parts.length < 2 || parts.some(part => !/^[a-zA-Z0-9-]{1,63}$/.test(part) || part.startsWith('-') || part.endsWith('-'))) { return false; } return true; } /** * Validate IP address * @param {string} ip - IP to validate * @returns {boolean} True if valid */ isValidIp(ip) { console.log(`Validating IP: "${ip}"`); const parts = ip.split('.'); if (parts.length !== 4) { return false; } return parts.every(part => { const num = parseInt(part, 10); return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part; }); } /** * Format status text for display * @param {string} status - Raw status * @returns {string} Formatted status */ formatStatus(status) { const statusMap = { 'idle': 'Idle', 'running': 'Running', 'completed': 'Completed', 'failed': 'Failed', 'stopped': 'Stopped' }; return statusMap[status] || status; } /** * Format label for display * @param {string} label - Raw label * @returns {string} Formatted label */ formatLabel(label) { return label.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); } /** * Format value for display * @param {*} value - Raw value * @returns {string} Formatted value */ formatValue(value) { if (typeof value === 'object' && value !== null) { // Use
 for nicely formatted JSON
            return `
${JSON.stringify(value, null, 2)}
`; } else { // Escape HTML to prevent XSS issues with string values const strValue = String(value); return strValue.replace(//g, ">"); } } /** * Show success message * @param {string} message - Success message */ showSuccess(message) { this.showMessage(message, 'success'); } /** * Show info message * @param {string} message - Info message */ showInfo(message) { this.showMessage(message, 'info'); } showWarning(message) { this.showMessage(message, 'warning'); } /** * Show error message * @param {string} message - Error message */ showError(message) { this.showMessage(message, 'error'); } /** * Show connection error */ showConnectionError() { this.updateConnectionStatus('error'); } /** * Show message with visual feedback * @param {string} message - Message text * @param {string} type - Message type (success, error, warning, info) */ showMessage(message, type = 'info') { console.log(`${type.toUpperCase()}: ${message}`); // Create message element const messageElement = document.createElement('div'); messageElement.className = `message-toast message-${type}`; messageElement.innerHTML = `
${message}
`; // Add to container const container = document.getElementById('message-container'); if (container) { container.appendChild(messageElement); // Auto-remove after delay setTimeout(() => { if (messageElement.parentNode) { messageElement.style.animation = 'slideOutRight 0.3s ease-out'; setTimeout(() => { if (messageElement.parentNode) { messageElement.remove(); } }, 300); } }, type === 'error' ? 8000 : 5000); // Errors stay longer } // Update connection status to show activity if (type === 'success' && this.consecutiveErrors === 0) { this.updateConnectionStatus(this.isScanning ? 'active' : 'idle'); } } /** * Get message background color based on type * @param {string} type - Message type * @returns {string} CSS color */ getMessageColor(type) { const colors = { 'success': '#2c5c34', 'error': '#5c2c2c', 'warning': '#5c4c2c', 'info': '#2c3e5c' }; return colors[type] || colors.info; } /** * Get message border color based on type * @param {string} type - Message type * @returns {string} CSS color */ getMessageBorderColor(type) { const colors = { 'success': '#00ff41', 'error': '#ff6b6b', 'warning': '#ff9900', 'info': '#00aaff' }; return colors[type] || colors.info; } /** * Build the API key modal dynamically * @param {Object} providers - Provider information */ buildApiKeyModal(providers) { if (!this.elements.apiKeyInputs) return; this.elements.apiKeyInputs.innerHTML = ''; // Clear existing inputs let hasApiKeyProviders = false; for (const [name, info] of Object.entries(providers)) { if (info.requires_api_key) { hasApiKeyProviders = true; const inputGroup = document.createElement('div'); inputGroup.className = 'apikey-section'; if (info.enabled) { // If the API key is set and the provider is enabled inputGroup.innerHTML = `
API Key is set

Provides infrastructure context and service information.

`; } else { // If the API key is not set inputGroup.innerHTML = `

Provides infrastructure context and service information.

`; } this.elements.apiKeyInputs.appendChild(inputGroup); } } // Add event listeners for the new clear buttons this.elements.apiKeyInputs.querySelectorAll('.clear-api-key-btn').forEach(button => { button.addEventListener('click', (e) => { const provider = e.target.dataset.provider; this.clearApiKey(provider); }); }); if (!hasApiKeyProviders) { this.elements.apiKeyInputs.innerHTML = '

No providers require API keys.

'; } } /** * Clear an API key for a specific provider * @param {string} provider The name of the provider to clear the API key for */ async clearApiKey(provider) { try { const response = await this.apiCall('/api/config/api-keys', 'POST', { [provider]: '' }); if (response.success) { this.showSuccess(`API key for ${provider} has been cleared.`); this.loadProviders(); // This will rebuild the modal with the updated state } else { throw new Error(response.error || 'Failed to clear API key'); } } catch (error) { this.showError(`Error clearing API key: ${error.message}`); } } } // Add CSS animations for message toasts const style = document.createElement('style'); style.textContent = ` @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; } } .message-container { pointer-events: auto; } .message-toast { pointer-events: auto; } `; document.head.appendChild(style); // Initialize application when page loads console.log('Creating DNSReconApp instance...'); const app = new DNSReconApp();