/** * Main application logic for DNSRecon web interface * Handles UI interactions, API communication, and data flow * DEBUG VERSION WITH EXTRA LOGGING */ 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(); 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 targetDomain: document.getElementById('target-domain'), 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'), configureApiKeys: document.getElementById('configure-api-keys'), // Status elements scanStatus: document.getElementById('scan-status'), targetDisplay: document.getElementById('target-display'), depthDisplay: document.getElementById('depth-display'), progressDisplay: document.getElementById('progress-display'), indicatorsDisplay: document.getElementById('indicators-display'), relationshipsDisplay: document.getElementById('relationships-display'), 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'), // API Key Modal elements apiKeyModal: document.getElementById('api-key-modal'), apiKeyModalClose: document.getElementById('api-key-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'), // Filter elements nodeTypeFilter: document.getElementById('node-type-filter'), confidenceFilter: document.getElementById('confidence-filter'), confidenceValue: document.getElementById('confidence-value') }; // Verify critical elements exist const requiredElements = ['targetDomain', '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.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.configureApiKeys.addEventListener('click', () => this.showApiKeyModal()); // Enter key support for target domain input this.elements.targetDomain.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(); }); } // API Key Modal interactions if (this.elements.apiKeyModalClose) { this.elements.apiKeyModalClose.addEventListener('click', () => this.hideApiKeyModal()); } if (this.elements.apiKeyModal) { this.elements.apiKeyModal.addEventListener('click', (e) => { if (e.target === this.elements.apiKeyModal) this.hideApiKeyModal(); }); } 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); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.hideModal(); this.hideApiKeyModal(); } }); // Window events window.addEventListener('beforeunload', () => { if (this.isScanning) { return 'A scan is currently in progress. Are you sure you want to leave?'; } }); // Filter events this.elements.nodeTypeFilter.addEventListener('change', () => this.applyFilters()); this.elements.confidenceFilter.addEventListener('input', () => { this.elements.confidenceValue.textContent = this.elements.confidenceFilter.value; this.applyFilters(); }); 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) { console.log('=== STARTING SCAN ==='); try { const targetDomain = this.elements.targetDomain.value.trim(); const maxDepth = parseInt(this.elements.maxDepth.value); console.log(`Target domain: "${targetDomain}", Max depth: ${maxDepth}`); // Validation if (!targetDomain) { console.log('Validation failed: empty domain'); this.showError('Please enter a target domain'); this.elements.targetDomain.focus(); return; } if (!this.isValidDomain(targetDomain)) { console.log(`Validation failed: invalid domain format for "${targetDomain}"`); this.showError('Please enter a valid domain name (e.g., example.com)'); this.elements.targetDomain.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_domain: targetDomain, max_depth: maxDepth, clear_graph: clearGraph }; 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 ${targetDomain} 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...'; } // 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 = '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}`; } if (this.elements.progressDisplay) { this.elements.progressDisplay.textContent = `${status.progress_percentage.toFixed(1)}%`; } if (this.elements.indicatorsDisplay) { this.elements.indicatorsDisplay.textContent = status.indicators_processed || 0; } // 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 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.elements.startScan) { this.elements.startScan.disabled = true; this.elements.startScan.classList.add('loading'); this.elements.startScan.innerHTML = '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 = 'Terminate Scan'; } 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': case 'completed': case 'failed': case 'stopped': this.isScanning = false; if (this.elements.startScan) { this.elements.startScan.disabled = !isQueueEmpty; this.elements.startScan.classList.remove('loading'); this.elements.startScan.innerHTML = '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 = 'Terminate Scan'; } 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; } } /** * 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 = `
Merged Correlations: ${mergeCount} values
`; detailsHtml += '${node.description || 'No description available.'}
`; detailsHtml += 'No data available.
'; } let html = '