// DNS Reconnaissance Tool - Enhanced Frontend JavaScript with Forensic Data Support class ReconTool { constructor() { this.currentScanId = null; this.pollInterval = null; this.liveDataInterval = null; this.currentReport = null; this.debugMode = true; this.graphVisualization = null; // Add this line this.init(); } debug(message, data = null) { if (this.debugMode) { if (data) { console.log(`πŸ” DEBUG: ${message}`, data); } else { console.log(`πŸ” DEBUG: ${message}`); } } } init() { this.bindEvents(); this.setupRealtimeElements(); // Handle window resize for graph window.addEventListener('resize', () => { if (this.graphVisualization) { this.graphVisualization.handleResize(); } }); } setupRealtimeElements() { // Create live discovery container if it doesn't exist if (!document.getElementById('liveDiscoveries')) { const progressSection = document.getElementById('progressSection'); const liveDiv = document.createElement('div'); liveDiv.id = 'liveDiscoveries'; liveDiv.innerHTML = ` `; progressSection.appendChild(liveDiv); this.debug("Live discoveries container created"); } } bindEvents() { // Start scan button document.getElementById('startScan').addEventListener('click', () => { this.startScan(); }); // New scan button document.getElementById('newScan').addEventListener('click', () => { this.resetToForm(); }); // Report view toggles document.getElementById('showJson').addEventListener('click', () => { this.showReport('json'); }); document.getElementById('showText').addEventListener('click', () => { this.showReport('text'); }); // Download buttons document.getElementById('downloadJson').addEventListener('click', () => { this.downloadReport('json'); }); document.getElementById('downloadText').addEventListener('click', () => { this.downloadReport('text'); }); // Enter key in target field document.getElementById('target').addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.startScan(); } }); document.getElementById('showGraphView').addEventListener('click', () => { this.showGraphView(); }); } showGraphView() { if (this.graphVisualization && this.currentScanId) { document.getElementById('graphSection').style.display = 'block'; // Update button states document.getElementById('showGraphView').classList.add('active'); document.getElementById('showJson').classList.remove('active'); document.getElementById('showText').classList.remove('active'); // Scroll to graph section document.getElementById('graphSection').scrollIntoView({ behavior: 'smooth' }); } else { alert('Graph data not available yet. Please wait for scan completion.'); } } async startScan() { const target = document.getElementById('target').value.trim(); if (!target) { alert('Please enter a target domain or hostname'); return; } const scanData = { target: target, max_depth: parseInt(document.getElementById('maxDepth').value), shodan_key: document.getElementById('shodanKey').value.trim() || null, virustotal_key: document.getElementById('virustotalKey').value.trim() || null }; try { // Show progress section this.showProgressSection(); this.updateProgress(0, 'Starting forensic scan...'); this.debug('Starting scan with data:', scanData); const response = await fetch('/api/scan', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(scanData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); if (result.error) { throw new Error(result.error); } this.currentScanId = result.scan_id; this.debug('Scan started with ID:', this.currentScanId); this.startPolling(); this.startLiveDataPolling(); } catch (error) { console.error('❌ Failed to start scan:', error); this.showError(`Failed to start scan: ${error.message}`); } } startPolling() { this.debug('Starting status polling...'); // Poll every 2 seconds for status updates this.pollInterval = setInterval(() => { this.checkScanStatus(); }, 2000); // Also check immediately this.checkScanStatus(); } startLiveDataPolling() { this.debug('Starting live data polling...'); // Poll every 3 seconds for live data updates this.liveDataInterval = setInterval(() => { this.updateLiveData(); }, 3000); // Show the live discoveries section const liveSection = document.querySelector('.live-discoveries'); if (liveSection) { liveSection.style.display = 'block'; this.debug('Live discoveries section made visible'); } else { this.debug('ERROR: Live discoveries section not found!'); } // Also update immediately this.updateLiveData(); } async checkScanStatus() { if (!this.currentScanId) { return; } try { const response = await fetch(`/api/scan/${this.currentScanId}/status`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const status = await response.json(); if (status.error) { throw new Error(status.error); } // Update progress this.updateProgress(status.progress, status.message); // Update live stats (handle new forensic format) if (status.live_stats) { this.debug('Received live stats:', status.live_stats); this.updateLiveStats(status.live_stats); } // Check if completed if (status.status === 'completed') { this.debug('Scan completed, loading report...'); this.stopPolling(); await this.loadScanReport(); } else if (status.status === 'error') { this.stopPolling(); throw new Error(status.error || 'Scan failed'); } } catch (error) { console.error('❌ Error checking scan status:', error); this.stopPolling(); this.showError(`Error checking scan status: ${error.message}`); } } async updateLiveData() { if (!this.currentScanId) { return; } this.debug(`Fetching live data for scan: ${this.currentScanId}`); try { const response = await fetch(`/api/scan/${this.currentScanId}/live-data`); if (!response.ok) { this.debug(`Live data request failed: HTTP ${response.status}`); return; // Silently fail for live data } const data = await response.json(); if (data.error) { this.debug('Live data error:', data.error); return; // Silently fail for live data } this.debug('Received live data:', data); // Update live discoveries (handle new forensic format) this.updateLiveDiscoveries(data); } catch (error) { // Silently fail for live data updates this.debug('Live data update failed:', error); } } updateLiveStats(stats) { this.debug('Updating live stats:', stats); // Handle both old and new stat formats for compatibility const statMappings = { 'liveHostnames': stats.hostnames || stats.hostnames || 0, 'liveIPs': stats.ip_addresses || stats.ips || 0, 'liveEdges': stats.discovery_edges || 0, // New forensic field 'liveOperations': stats.operations_performed || 0, // New forensic field 'liveDNS': stats.dns_records || 0, 'liveCerts': stats.certificates_total || stats.certificates || 0, 'liveShodan': stats.shodan_results || 0, 'liveVT': stats.virustotal_results || 0 }; Object.entries(statMappings).forEach(([elementId, value]) => { const element = document.getElementById(elementId); if (element) { const currentValue = element.textContent; element.textContent = value; if (currentValue !== value.toString()) { this.debug(`Updated ${elementId}: ${currentValue} -> ${value}`); // Add a brief highlight effect when value changes element.style.backgroundColor = '#ff9900'; setTimeout(() => { element.style.backgroundColor = ''; }, 1000); } } else { this.debug(`ERROR: Element ${elementId} not found!`); } }); } updateLiveDiscoveries(data) { this.debug('Updating live discoveries with data:', data); // Handle new forensic data format let hostnames = []; let ipAddresses = []; let activities = []; // Extract data from forensic format or fallback to old format if (data.hostnames && Array.isArray(data.hostnames)) { hostnames = data.hostnames; } else if (data.stats && data.stats.hostnames) { // If we only have stats, create a placeholder list hostnames = [`${data.stats.hostnames} discovered`]; } if (data.ip_addresses && Array.isArray(data.ip_addresses)) { ipAddresses = data.ip_addresses; } else if (data.stats && data.stats.ip_addresses) { // If we only have stats, create a placeholder list ipAddresses = [`${data.stats.ip_addresses} discovered`]; } // Handle activity log from forensic format if (data.latest_discoveries && Array.isArray(data.latest_discoveries)) { activities = data.latest_discoveries; } // Update hostnames list const hostnameList = document.querySelector('#recentHostnames .hostname-list'); if (hostnameList && hostnames.length > 0) { // Show last 10 hostnames const recentHostnames = hostnames.slice(-10); hostnameList.innerHTML = recentHostnames.map(hostname => `${hostname}` ).join(''); this.debug(`Updated hostname list with ${recentHostnames.length} items`); } else if (hostnameList) { this.debug(`No hostnames to display (${hostnames.length} total)`); } // Update IP addresses list const ipList = document.querySelector('#recentIPs .ip-list'); if (ipList && ipAddresses.length > 0) { // Show last 10 IPs const recentIPs = ipAddresses.slice(-10); ipList.innerHTML = recentIPs.map(ip => `${ip}` ).join(''); this.debug(`Updated IP list with ${recentIPs.length} items`); } else if (ipList) { this.debug(`No IPs to display (${ipAddresses.length} total)`); } // Update activity log const activityList = document.querySelector('#activityLog .activity-list'); if (activityList) { if (activities.length > 0) { const recentActivities = activities.slice(-5); // Last 5 activities activityList.innerHTML = recentActivities.map(activity => { const time = new Date(activity.timestamp * 1000).toLocaleTimeString(); return `
[${time}] ${activity.message}
`; }).join(''); this.debug(`Updated activity log with ${recentActivities.length} items`); } else { // Fallback: show generic activity based on stats const stats = data.stats || {}; const genericActivities = []; if (stats.operations_performed > 0) { genericActivities.push(`${stats.operations_performed} operations performed`); } if (stats.hostnames > 0) { genericActivities.push(`${stats.hostnames} hostnames discovered`); } if (stats.dns_records > 0) { genericActivities.push(`${stats.dns_records} DNS records collected`); } if (genericActivities.length > 0) { const now = new Date().toLocaleTimeString(); activityList.innerHTML = genericActivities.map(activity => `
[${now}] ${activity}
` ).join(''); this.debug(`Updated activity log with ${genericActivities.length} generic items`); } else { this.debug('No activities to display'); } } } } async loadScanReport() { try { this.debug('Loading scan report...'); const response = await fetch(`/api/scan/${this.currentScanId}/report`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const report = await response.json(); if (report.error) { throw new Error(report.error); } this.currentReport = report; this.debug('Report loaded successfully'); this.showResultsSection(); this.showReport('text'); // Default to text view // Load and show graph if (!this.graphVisualization) { this.graphVisualization = new GraphVisualization(); } await this.graphVisualization.loadAndShowGraph(this.currentScanId); } catch (error) { console.error('⚠️ Error loading report:', error); this.showError(`Error loading report: ${error.message}`); } } stopPolling() { this.debug('Stopping polling intervals...'); if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } if (this.liveDataInterval) { clearInterval(this.liveDataInterval); this.liveDataInterval = null; } } showProgressSection() { document.getElementById('scanForm').style.display = 'none'; document.getElementById('progressSection').style.display = 'block'; document.getElementById('resultsSection').style.display = 'none'; this.debug('Showing progress section'); } showResultsSection() { document.getElementById('scanForm').style.display = 'none'; document.getElementById('progressSection').style.display = 'block'; // Keep visible document.getElementById('resultsSection').style.display = 'block'; // Change the title to show it's the final summary const liveSection = document.querySelector('.live-discoveries'); if (liveSection) { const title = liveSection.querySelector('h3'); if (title) { title.textContent = 'πŸ“Š Final Forensic Summary'; } liveSection.style.display = 'block'; } // Hide just the progress bar and scan controls const progressBar = document.querySelector('.progress-bar'); const progressMessage = document.getElementById('progressMessage'); const scanControls = document.querySelector('.scan-controls'); if (progressBar) progressBar.style.display = 'none'; if (progressMessage) progressMessage.style.display = 'none'; if (scanControls) scanControls.style.display = 'none'; this.debug('Showing results section with forensic discoveries'); } resetToForm() { this.stopPolling(); this.currentScanId = null; this.currentReport = null; document.getElementById('scanForm').style.display = 'block'; document.getElementById('progressSection').style.display = 'none'; document.getElementById('resultsSection').style.display = 'none'; // Show progress elements again const progressBar = document.querySelector('.progress-bar'); const progressMessage = document.getElementById('progressMessage'); const scanControls = document.querySelector('.scan-controls'); if (progressBar) progressBar.style.display = 'block'; if (progressMessage) progressMessage.style.display = 'block'; if (scanControls) scanControls.style.display = 'block'; // Hide live discoveries and reset title const liveSection = document.querySelector('.live-discoveries'); if (liveSection) { liveSection.style.display = 'none'; const title = liveSection.querySelector('h3'); if (title) { title.textContent = 'πŸ” Live Discoveries'; } } // Clear form document.getElementById('target').value = ''; document.getElementById('shodanKey').value = ''; document.getElementById('virustotalKey').value = ''; document.getElementById('maxDepth').value = '2'; this.debug('Reset to form view'); } updateProgress(percentage, message) { const progressFill = document.getElementById('progressFill'); const progressMessage = document.getElementById('progressMessage'); progressFill.style.width = `${percentage || 0}%`; progressMessage.textContent = message || 'Processing...'; } showError(message) { // Update progress section to show error this.updateProgress(0, `Error: ${message}`); // Also alert the user alert(`Error: ${message}`); } showReport(type) { if (!this.currentReport) { return; } const reportContent = document.getElementById('reportContent'); const showJsonBtn = document.getElementById('showJson'); const showTextBtn = document.getElementById('showText'); if (type === 'json') { // Show JSON report try { // The json_report should already be a string from the server let jsonData; if (typeof this.currentReport.json_report === 'string') { jsonData = JSON.parse(this.currentReport.json_report); } else { jsonData = this.currentReport.json_report; } reportContent.textContent = JSON.stringify(jsonData, null, 2); } catch (e) { console.error('Error parsing JSON report:', e); reportContent.textContent = this.currentReport.json_report; } showJsonBtn.classList.add('active'); showTextBtn.classList.remove('active'); } else { // Show text report reportContent.textContent = this.currentReport.text_report; showTextBtn.classList.add('active'); showJsonBtn.classList.remove('active'); } } downloadReport(type) { if (!this.currentReport) { return; } let content, filename, mimeType; if (type === 'json') { content = typeof this.currentReport.json_report === 'string' ? this.currentReport.json_report : JSON.stringify(this.currentReport.json_report, null, 2); filename = `forensic-recon-report-${this.currentScanId}.json`; mimeType = 'application/json'; } else { content = this.currentReport.text_report; filename = `forensic-recon-report-${this.currentScanId}.txt`; mimeType = 'text/plain'; } // Create download link const blob = new Blob([content], { type: mimeType }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } } class GraphVisualization { constructor() { this.svg = null; this.simulation = null; this.graphData = null; this.showLabels = false; this.selectedNode = null; this.zoom = null; this.container = null; } async loadAndShowGraph(scanId) { try { console.log('πŸ•ΈοΈ Loading graph data...'); const response = await fetch(`/api/scan/${scanId}/graph`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const graphData = await response.json(); if (graphData.error) { throw new Error(graphData.error); } this.graphData = graphData; this.showGraphSection(); this.initializeGraph(); this.updateGraphStats(graphData.stats); console.log('βœ… Graph loaded successfully', graphData.stats); } catch (error) { console.error('⚠️ Error loading graph:', error); alert(`Failed to load graph: ${error.message}`); } } showGraphSection() { document.getElementById('graphSection').style.display = 'block'; this.bindGraphEvents(); } bindGraphEvents() { document.getElementById('showGraph').addEventListener('click', () => { this.showGraph(); }); document.getElementById('hideGraph').addEventListener('click', () => { this.hideGraph(); }); document.getElementById('resetZoom').addEventListener('click', () => { this.resetZoom(); }); document.getElementById('toggleLabels').addEventListener('click', () => { this.toggleLabels(); }); } initializeGraph() { if (!this.graphData) return; // Clear existing graph d3.select('#discoveryGraph').selectAll('*').remove(); // Set up SVG const container = d3.select('#discoveryGraph'); const containerNode = container.node(); const width = containerNode.clientWidth; const height = containerNode.clientHeight; this.svg = container .attr('width', width) .attr('height', height); // Set up zoom behavior this.zoom = d3.zoom() .scaleExtent([0.1, 3]) .on('zoom', (event) => { this.container.attr('transform', event.transform); }); this.svg.call(this.zoom); // Create container for graph elements this.container = this.svg.append('g'); // Set up force simulation this.simulation = d3.forceSimulation(this.graphData.nodes) .force('link', d3.forceLink(this.graphData.edges) .id(d => d.id) .distance(80) .strength(0.5)) .force('charge', d3.forceManyBody() .strength(-300) .distanceMax(400)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide() .radius(d => d.size + 5)); this.drawGraph(); this.startSimulation(); } drawGraph() { // Draw links const links = this.container.append('g') .selectAll('line') .data(this.graphData.edges) .enter().append('line') .attr('class', 'graph-link') .attr('stroke', d => d.color) .on('mouseover', (event, d) => { this.showTooltip(event, `Discovery: ${d.method}
From: ${d.source}
To: ${d.target}`); }) .on('mouseout', () => { this.hideTooltip(); }); // Draw nodes const nodes = this.container.append('g') .selectAll('circle') .data(this.graphData.nodes) .enter().append('circle') .attr('class', 'graph-node') .attr('r', d => d.size) .attr('fill', d => d.color) .style('opacity', 0.8) .on('mouseover', (event, d) => { this.showNodeTooltip(event, d); this.highlightConnections(d); }) .on('mouseout', (event, d) => { this.hideTooltip(); this.unhighlightConnections(); }) .on('click', (event, d) => { this.selectNode(d); }) .call(d3.drag() .on('start', (event, d) => { if (!event.active) this.simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) .on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; }) .on('end', (event, d) => { if (!event.active) this.simulation.alphaTarget(0); d.fx = null; d.fy = null; })); // Draw labels (initially hidden) const labels = this.container.append('g') .selectAll('text') .data(this.graphData.nodes) .enter().append('text') .attr('class', 'graph-label') .attr('dy', '.35em') .style('opacity', this.showLabels ? 1 : 0) .text(d => d.label); // Store references this.links = links; this.nodes = nodes; this.labels = labels; } startSimulation() { this.simulation.on('tick', () => { this.links .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); this.nodes .attr('cx', d => d.x) .attr('cy', d => d.y); this.labels .attr('x', d => d.x) .attr('y', d => d.y + d.size + 12); }); } showNodeTooltip(event, node) { const tooltip = ` ${node.label}
Depth: ${node.depth}
DNS Records: ${node.dns_records}
Certificates: ${node.certificates}
IPs: ${node.ip_addresses.length}
Discovery: ${node.discovery_methods.join(', ')}
First Seen: ${node.first_seen ? new Date(node.first_seen).toLocaleString() : 'Unknown'} `; this.showTooltip(event, tooltip); } showTooltip(event, content) { const tooltip = document.getElementById('graphTooltip'); tooltip.innerHTML = content; tooltip.className = 'graph-tooltip visible'; const rect = tooltip.getBoundingClientRect(); const containerRect = document.getElementById('discoveryGraph').getBoundingClientRect(); tooltip.style.left = `${event.clientX - containerRect.left + 10}px`; tooltip.style.top = `${event.clientY - containerRect.top - 10}px`; // Adjust position if tooltip goes off screen if (event.clientX + rect.width > window.innerWidth) { tooltip.style.left = `${event.clientX - containerRect.left - rect.width - 10}px`; } if (event.clientY - rect.height < 0) { tooltip.style.top = `${event.clientY - containerRect.top + 20}px`; } } hideTooltip() { const tooltip = document.getElementById('graphTooltip'); tooltip.className = 'graph-tooltip'; } highlightConnections(node) { // Highlight connected links this.links .style('opacity', d => (d.source.id === node.id || d.target.id === node.id) ? 1 : 0.2) .classed('highlighted', d => d.source.id === node.id || d.target.id === node.id); // Highlight connected nodes this.nodes .style('opacity', d => { if (d.id === node.id) return 1; const connected = this.graphData.edges.some(edge => (edge.source.id === node.id && edge.target.id === d.id) || (edge.target.id === node.id && edge.source.id === d.id) ); return connected ? 0.8 : 0.3; }); } unhighlightConnections() { this.links .style('opacity', 0.6) .classed('highlighted', false); this.nodes .style('opacity', 0.8); } selectNode(node) { // Update selected node styling this.nodes .classed('selected', d => d.id === node.id); // Show node details this.showNodeDetails(node); this.selectedNode = node; } showNodeDetails(node) { const detailsContainer = document.getElementById('nodeDetails'); const selectedInfo = document.getElementById('selectedNodeInfo'); const details = `
Hostname: ${node.label}
Discovery Depth: ${node.depth}
DNS Records: ${node.dns_records}
Certificates: ${node.certificates}
IP Addresses: ${node.ip_addresses.join(', ') || 'None'}
Discovery Methods: ${node.discovery_methods.join(', ')}
First Seen: ${node.first_seen ? new Date(node.first_seen).toLocaleString() : 'Unknown'}
`; detailsContainer.innerHTML = details; selectedInfo.style.display = 'block'; } toggleLabels() { this.showLabels = !this.showLabels; if (this.labels) { this.labels.transition() .duration(300) .style('opacity', this.showLabels ? 1 : 0); } const button = document.getElementById('toggleLabels'); button.textContent = this.showLabels ? 'Hide Labels' : 'Show Labels'; } resetZoom() { if (this.svg && this.zoom) { this.svg.transition() .duration(750) .call(this.zoom.transform, d3.zoomIdentity); } } showGraph() { if (this.container) { this.container.style('display', 'block'); } document.getElementById('showGraph').classList.add('active'); document.getElementById('hideGraph').classList.remove('active'); } hideGraph() { if (this.container) { this.container.style('display', 'none'); } document.getElementById('hideGraph').classList.add('active'); document.getElementById('showGraph').classList.remove('active'); } updateGraphStats(stats) { document.getElementById('graphNodes').textContent = stats.node_count || 0; document.getElementById('graphEdges').textContent = stats.edge_count || 0; document.getElementById('graphDepth').textContent = stats.max_depth || 0; } // Handle window resize handleResize() { if (this.svg && this.graphData) { const containerNode = document.getElementById('discoveryGraph'); const width = containerNode.clientWidth; const height = containerNode.clientHeight; this.svg .attr('width', width) .attr('height', height); this.simulation .force('center', d3.forceCenter(width / 2, height / 2)) .restart(); } } } // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { console.log('🌐 Forensic DNS Reconnaissance Tool initialized with debug mode'); new ReconTool(); });