// 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 = `
π Live Discoveries
Hostnames:
0
IP Addresses:
0
Discovery Edges:
0
Operations:
0
DNS Records:
0
Certificates:
0
Shodan Results:
0
VirusTotal:
0
`;
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();
});