1016 lines
37 KiB
JavaScript
1016 lines
37 KiB
JavaScript
// 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 = `
|
|
<div class="live-discoveries" style="display: none;">
|
|
<h3>🔍 Live Discoveries</h3>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<span class="stat-label">Hostnames:</span>
|
|
<span id="liveHostnames" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">IP Addresses:</span>
|
|
<span id="liveIPs" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Discovery Edges:</span>
|
|
<span id="liveEdges" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Operations:</span>
|
|
<span id="liveOperations" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">DNS Records:</span>
|
|
<span id="liveDNS" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Certificates:</span>
|
|
<span id="liveCerts" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Shodan Results:</span>
|
|
<span id="liveShodan" class="stat-value">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">VirusTotal:</span>
|
|
<span id="liveVT" class="stat-value">0</span>
|
|
</div>
|
|
</div>
|
|
<div class="discoveries-list">
|
|
<h4>📋 Recent Discoveries</h4>
|
|
<div id="recentHostnames" class="discovery-section">
|
|
<strong>Hostnames:</strong>
|
|
<div class="hostname-list"></div>
|
|
</div>
|
|
<div id="recentIPs" class="discovery-section">
|
|
<strong>IP Addresses:</strong>
|
|
<div class="ip-list"></div>
|
|
</div>
|
|
<div id="activityLog" class="discovery-section">
|
|
<strong>Activity Log:</strong>
|
|
<div class="activity-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 =>
|
|
`<span class="discovery-item">${hostname}</span>`
|
|
).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 =>
|
|
`<span class="discovery-item">${ip}</span>`
|
|
).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 `<div class="activity-item">[${time}] ${activity.message}</div>`;
|
|
}).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 =>
|
|
`<div class="activity-item">[${now}] ${activity}</div>`
|
|
).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}<br>From: ${d.source}<br>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 = `
|
|
<strong>${node.label}</strong><br>
|
|
Depth: ${node.depth}<br>
|
|
DNS Records: ${node.dns_records}<br>
|
|
Certificates: ${node.certificates}<br>
|
|
IPs: ${node.ip_addresses.length}<br>
|
|
Discovery: ${node.discovery_methods.join(', ')}<br>
|
|
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 = `
|
|
<div class="detail-item">
|
|
<span class="detail-label">Hostname:</span>
|
|
<span class="detail-value">${node.label}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Discovery Depth:</span>
|
|
<span class="detail-value">${node.depth}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">DNS Records:</span>
|
|
<span class="detail-value">${node.dns_records}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Certificates:</span>
|
|
<span class="detail-value">${node.certificates}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">IP Addresses:</span>
|
|
<span class="detail-value">${node.ip_addresses.join(', ') || 'None'}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Discovery Methods:</span>
|
|
<span class="detail-value">${node.discovery_methods.join(', ')}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">First Seen:</span>
|
|
<span class="detail-value">${node.first_seen ? new Date(node.first_seen).toLocaleString() : 'Unknown'}</span>
|
|
</div>
|
|
`;
|
|
|
|
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();
|
|
}); |