/**
* Main application logic for DNSRecon web interface
* Handles UI interactions, API communication, and data flow
* UPDATED: Now compatible with a strictly flat, unified data model for attributes.
*/
class DNSReconApp {
constructor() {
console.log('DNSReconApp constructor called');
this.graphManager = null;
this.scanStatus = 'idle';
this.pollInterval = null;
this.currentSessionId = null;
this.elements = {};
this.isScanning = false;
this.lastGraphUpdate = null;
this.init();
}
/**
* Initialize the application
*/
init() {
console.log('DNSReconApp init called');
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing application...');
try {
this.initializeElements();
this.setupEventHandlers();
this.initializeGraph();
this.updateStatus();
this.loadProviders();
this.initializeEnhancedModals();
this.addCheckboxStyling();
this.updateGraph();
console.log('DNSRecon application initialized successfully');
} catch (error) {
console.error('Failed to initialize DNSRecon application:', error);
this.showError(`Initialization failed: ${error.message}`);
}
});
}
/**
* Initialize DOM element references
*/
initializeElements() {
console.log('Initializing DOM elements...');
this.elements = {
// Form elements
targetInput: document.getElementById('target-input'),
maxDepth: document.getElementById('max-depth'),
startScan: document.getElementById('start-scan'),
addToGraph: document.getElementById('add-to-graph'),
stopScan: document.getElementById('stop-scan'),
exportOptions: document.getElementById('export-options'),
exportModal: document.getElementById('export-modal'),
exportModalClose: document.getElementById('export-modal-close'),
exportGraphJson: document.getElementById('export-graph-json'),
exportTargetsTxt: document.getElementById('export-targets-txt'),
exportExecutiveSummary: document.getElementById('export-executive-summary'),
configureSettings: document.getElementById('configure-settings'),
// Status elements
scanStatus: document.getElementById('scan-status'),
targetDisplay: document.getElementById('target-display'),
depthDisplay: document.getElementById('depth-display'),
relationshipsDisplay: document.getElementById('relationships-display'),
progressCompact: document.getElementById('progress-compact'),
progressFill: document.getElementById('progress-fill'),
// Provider elements
providerList: document.getElementById('provider-list'),
// Node Modal elements
nodeModal: document.getElementById('node-modal'),
modalTitle: document.getElementById('modal-title'),
modalDetails: document.getElementById('modal-details'),
modalClose: document.getElementById('modal-close'),
// Settings Modal elements
settingsModal: document.getElementById('settings-modal'),
settingsModalClose: document.getElementById('settings-modal-close'),
// Other elements
sessionId: document.getElementById('session-id'),
connectionStatus: document.getElementById('connection-status'),
};
// Verify critical elements exist
const requiredElements = ['targetInput', 'startScan', 'scanStatus'];
for (const elementName of requiredElements) {
if (!this.elements[elementName]) {
throw new Error(`Required element '${elementName}' not found in DOM`);
}
}
console.log('DOM elements initialized successfully');
this.createMessageContainer();
}
/**
* Create a message container for showing user feedback
*/
createMessageContainer() {
// Check if message container already exists
let messageContainer = document.getElementById('message-container');
if (!messageContainer) {
messageContainer = document.createElement('div');
messageContainer.id = 'message-container';
messageContainer.className = 'message-container';
messageContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
max-width: 400px;
`;
document.body.appendChild(messageContainer);
}
}
/**
* Setup event handlers
*/
setupEventHandlers() {
console.log('Setting up event handlers...');
try {
// Form interactions
this.initializeModalFunctionality();
this.elements.startScan.addEventListener('click', (e) => {
e.preventDefault();
this.startScan();
});
this.elements.addToGraph.addEventListener('click', (e) => {
e.preventDefault();
this.startScan(false);
});
this.elements.stopScan.addEventListener('click', (e) => {
e.preventDefault();
this.stopScan();
});
this.elements.exportOptions.addEventListener('click', (e) => {
e.preventDefault();
this.showExportModal();
});
if (this.elements.exportModalClose) {
this.elements.exportModalClose.addEventListener('click', () => this.hideExportModal());
}
if (this.elements.exportModal) {
this.elements.exportModal.addEventListener('click', (e) => {
if (e.target === this.elements.exportModal) this.hideExportModal();
});
}
if (this.elements.exportGraphJson) {
this.elements.exportGraphJson.addEventListener('click', () => this.exportGraphJson());
}
if (this.elements.exportTargetsTxt) {
this.elements.exportTargetsTxt.addEventListener('click', () => this.exportTargetsTxt());
}
if (this.elements.exportExecutiveSummary) {
this.elements.exportExecutiveSummary.addEventListener('click', () => this.exportExecutiveSummary());
}
this.elements.configureSettings.addEventListener('click', () => this.showSettingsModal());
// Enter key support for target domain input
this.elements.targetInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !this.isScanning) {
this.startScan();
}
});
// Node Modal interactions
if (this.elements.modalClose) {
this.elements.modalClose.addEventListener('click', () => this.hideModal());
}
if (this.elements.nodeModal) {
this.elements.nodeModal.addEventListener('click', (e) => {
if (e.target === this.elements.nodeModal) this.hideModal();
});
}
// Settings Modal interactions
if (this.elements.settingsModalClose) {
this.elements.settingsModalClose.addEventListener('click', () => this.hideSettingsModal());
}
if (this.elements.settingsModal) {
this.elements.settingsModal.addEventListener('click', (e) => {
if (e.target === this.elements.settingsModal) this.hideSettingsModal();
});
}
if (this.elements.saveApiKeys) {
this.elements.saveApiKeys.removeEventListener('click', this.saveApiKeys);
}
if (this.elements.resetApiKeys) {
this.elements.resetApiKeys.removeEventListener('click', this.resetApiKeys);
}
// Setup new handlers
const saveSettingsBtn = document.getElementById('save-settings');
const resetSettingsBtn = document.getElementById('reset-settings');
if (saveSettingsBtn) {
saveSettingsBtn.addEventListener('click', () => this.saveSettings());
}
if (resetSettingsBtn) {
resetSettingsBtn.addEventListener('click', () => this.resetSettings());
}
// Listen for the custom event from the graph
document.addEventListener('nodeSelected', (e) => {
this.showNodeModal(e.detail.node);
});
// Listen for the new iterateScan event from the graph context menu
document.addEventListener('iterateScan', (e) => {
if (this.isScanning) {
this.showWarning('A scan is already in progress.');
return;
}
const { nodeId } = e.detail;
console.log(`Received iterateScan event for node: ${nodeId}`);
this.elements.targetInput.value = nodeId;
this.startScan(false, nodeId); // Pass nodeId to force rescan
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.hideModal();
this.hideSettingsModal();
this.hideExportModal(); // Add this line
}
});
// Window events
window.addEventListener('beforeunload', () => {
if (this.isScanning) {
return 'A scan is currently in progress. Are you sure you want to leave?';
}
});
console.log('Event handlers set up successfully');
} catch (error) {
console.error('Failed to setup event handlers:', error);
throw error;
}
}
/**
* Initialize graph visualization
*/
initializeGraph() {
try {
console.log('Initializing graph manager...');
this.graphManager = new GraphManager('network-graph');
console.log('Graph manager initialized successfully');
} catch (error) {
console.error('Failed to initialize graph manager:', error);
this.showError('Failed to initialize graph visualization');
}
}
/**
* Start scan with error handling
*/
async startScan(clearGraph = true, forceRescanTarget = null) {
console.log('=== STARTING SCAN ===');
try {
const target = this.elements.targetInput.value.trim();
const maxDepth = parseInt(this.elements.maxDepth.value);
console.log(`Target: "${target}", Max depth: ${maxDepth}`);
// Validation
if (!target) {
console.log('Validation failed: empty target');
this.showError('Please enter a target domain or IP');
this.elements.targetInput.focus();
return;
}
if (!this.isValidTarget(target)) {
console.log(`Validation failed: invalid target format for "${target}"`);
this.showError('Please enter a valid domain name (e.g., example.com) or IP address (e.g., 8.8.8.8)');
this.elements.targetInput.focus();
return;
}
console.log('Validation passed, setting UI state to scanning...');
this.setUIState('scanning');
this.showInfo('Starting reconnaissance scan...');
console.log('Making API call to start scan...');
const requestData = {
target: target,
max_depth: maxDepth,
clear_graph: clearGraph,
force_rescan_target: forceRescanTarget
};
const response = await this.apiCall('/api/scan/start', 'POST', requestData);
if (response.success) {
this.currentSessionId = response.scan_id;
this.showSuccess('Reconnaissance scan started successfully');
if (clearGraph) {
this.graphManager.clear();
}
console.log(`Scan started for ${target} with depth ${maxDepth}`);
// Start polling immediately with faster interval for responsiveness
this.startPolling(1000);
// Force an immediate status update
console.log('Forcing immediate status update...');
setTimeout(() => {
this.updateStatus();
this.updateGraph();
}, 100);
} else {
throw new Error(response.error || 'Failed to start scan');
}
} catch (error) {
console.error('Failed to start scan:', error);
this.showError(`Failed to start scan: ${error.message}`);
this.setUIState('idle');
}
}
/**
* Scan stop with immediate UI feedback
*/
async stopScan() {
try {
console.log('Stopping scan...');
// Immediately disable stop button and show stopping state
if (this.elements.stopScan) {
this.elements.stopScan.disabled = true;
this.elements.stopScan.innerHTML = '[STOPPING] Stopping... ';
}
// Show immediate feedback
this.showInfo('Stopping scan...');
const response = await this.apiCall('/api/scan/stop', 'POST');
if (response.success) {
this.showSuccess('Scan stop requested');
// Force immediate status update
setTimeout(() => {
this.updateStatus();
}, 100);
// Continue polling for a bit to catch the status change
this.startPolling(500); // Fast polling to catch status change
// Stop fast polling after 10 seconds
setTimeout(() => {
if (this.scanStatus === 'stopped' || this.scanStatus === 'idle') {
this.stopPolling();
}
}, 10000);
} else {
throw new Error(response.error || 'Failed to stop scan');
}
} catch (error) {
console.error('Failed to stop scan:', error);
this.showError(`Failed to stop scan: ${error.message}`);
// Re-enable stop button on error
if (this.elements.stopScan) {
this.elements.stopScan.disabled = false;
this.elements.stopScan.innerHTML = '[STOP] Terminate Scan ';
}
}
}
/**
* Show Export modal
*/
showExportModal() {
if (this.elements.exportModal) {
this.elements.exportModal.style.display = 'block';
}
}
/**
* Hide Export modal
*/
hideExportModal() {
if (this.elements.exportModal) {
this.elements.exportModal.style.display = 'none';
}
}
/**
* Export graph data as JSON with proper error handling
*/
async exportGraphJson() {
try {
console.log('Exporting graph data as JSON...');
// Show loading state
if (this.elements.exportGraphJson) {
const originalContent = this.elements.exportGraphJson.innerHTML;
this.elements.exportGraphJson.innerHTML = '[...] Exporting... ';
this.elements.exportGraphJson.disabled = true;
// Store original content for restoration
this.elements.exportGraphJson._originalContent = originalContent;
}
// Make API call to get export data
const response = await fetch('/api/export', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
// Check if response is JSON or file download
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json') && !response.headers.get('content-disposition')) {
// This is an error response in JSON format
const errorData = await response.json();
throw new Error(errorData.error || 'Export failed');
}
// Get the filename from headers or create one
const contentDisposition = response.headers.get('content-disposition');
let filename = 'dnsrecon_export.json';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch) {
filename = filenameMatch[1].replace(/['"]/g, '');
}
}
// Create blob and download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.showSuccess('Graph data exported successfully');
this.hideExportModal();
} catch (error) {
console.error('Failed to export graph data:', error);
this.showError(`Export failed: ${error.message}`);
} finally {
// Restore button state
if (this.elements.exportGraphJson) {
const originalContent = this.elements.exportGraphJson._originalContent ||
'[JSON] Export Graph Data ';
this.elements.exportGraphJson.innerHTML = originalContent;
this.elements.exportGraphJson.disabled = false;
}
}
}
async exportTargetsTxt() {
await this.exportFile('/api/export/targets', this.elements.exportTargetsTxt, 'Exporting Targets...');
}
async exportExecutiveSummary() {
await this.exportFile('/api/export/summary', this.elements.exportExecutiveSummary, 'Generating Summary...');
}
async exportFile(endpoint, buttonElement, loadingMessage) {
try {
console.log(`Exporting from ${endpoint}...`);
const originalContent = buttonElement.innerHTML;
buttonElement.innerHTML = `[...] ${loadingMessage} `;
buttonElement.disabled = true;
const response = await fetch(endpoint, { method: 'GET' });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
const contentDisposition = response.headers.get('content-disposition');
let filename = 'export.txt';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch) {
filename = filenameMatch[1].replace(/['"]/g, '');
}
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.showSuccess('File exported successfully');
this.hideExportModal();
} catch (error) {
console.error(`Failed to export from ${endpoint}:`, error);
this.showError(`Export failed: ${error.message}`);
} finally {
const originalContent = buttonElement._originalContent || buttonElement.innerHTML;
buttonElement.innerHTML = originalContent;
buttonElement.disabled = false;
}
}
/**
* 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(() => {
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 {
const response = await this.apiCall('/api/scan/status');
if (response.success && response.status) {
const status = response.status;
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');
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);
// FIXED: Always update graph, even if empty - let GraphManager handle placeholder
if (this.graphManager) {
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.error('Graph update failed:', response);
// FIXED: Show placeholder when graph update fails
if (this.graphManager && this.graphManager.container) {
const placeholder = this.graphManager.container.querySelector('.graph-placeholder');
if (placeholder) {
placeholder.style.display = 'flex';
}
}
}
} catch (error) {
console.error('Failed to update graph:', error);
// FIXED: Show placeholder on error
if (this.graphManager && this.graphManager.container) {
const placeholder = this.graphManager.container.querySelector('.graph-placeholder');
if (placeholder) {
placeholder.style.display = 'flex';
}
}
}
}
/**
* Update status display elements
* @param {Object} status - Status object from server
*/
updateStatusDisplay(status) {
try {
// Update status text with animation
if (this.elements.scanStatus) {
const formattedStatus = this.formatStatus(status.status);
if (this.elements.scanStatus.textContent !== formattedStatus) {
this.elements.scanStatus.textContent = formattedStatus;
this.elements.scanStatus.classList.add('fade-in');
setTimeout(() => this.elements.scanStatus.classList.remove('fade-in'), 300);
}
// Add status-specific classes for styling
this.elements.scanStatus.className = `status-value status-${status.status}`;
}
if (this.elements.targetDisplay) {
this.elements.targetDisplay.textContent = status.target_domain || 'None';
}
if (this.elements.depthDisplay) {
this.elements.depthDisplay.textContent = `${status.current_depth}/${status.max_depth}`;
}
// Update progress bar and compact display
if (this.elements.progressFill) {
const completed = status.indicators_completed || 0;
const totalTasks = status.total_tasks_ever_enqueued || 0;
const progressPercentage = status.progress_percentage || 0;
this.elements.progressFill.style.width = `${progressPercentage}%`;
if (this.elements.progressCompact) {
this.elements.progressCompact.textContent = `${completed}/${totalTasks} - ${Math.round(progressPercentage)}%`;
}
// Add pulsing animation for active scans
if (status.status === 'running') {
this.elements.progressFill.parentElement.classList.add('scanning');
} else {
this.elements.progressFill.parentElement.classList.remove('scanning');
}
}
// Update session ID display with user session info
if (this.elements.sessionId) {
const scanSessionId = this.currentSessionId;
const userSessionId = status.user_session_id;
if (scanSessionId && userSessionId) {
this.elements.sessionId.textContent = `Session: ${userSessionId.substring(0, 8)}... | Scan: ${scanSessionId}`;
} else if (userSessionId) {
this.elements.sessionId.textContent = `User Session: ${userSessionId.substring(0, 8)}...`;
} else {
this.elements.sessionId.textContent = 'Session: Loading...';
}
}
this.setUIState(status.status, status.task_queue_size);
} 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) {
const isQueueEmpty = task_queue_size === 0;
switch (state) {
case 'scanning':
this.isScanning = true;
if (this.graphManager) {
this.graphManager.isScanning = true;
}
if (this.elements.startScan) {
this.elements.startScan.disabled = true;
this.elements.startScan.classList.add('loading');
this.elements.startScan.innerHTML = '[SCANNING] Scanning... ';
}
if (this.elements.addToGraph) {
this.elements.addToGraph.disabled = true;
this.elements.addToGraph.classList.add('loading');
}
if (this.elements.stopScan) {
this.elements.stopScan.disabled = false;
this.elements.stopScan.classList.remove('loading');
this.elements.stopScan.innerHTML = '[STOP] Terminate Scan ';
}
if (this.elements.targetInput) this.elements.targetInput.disabled = true;
if (this.elements.maxDepth) this.elements.maxDepth.disabled = true;
if (this.elements.configureSettings) this.elements.configureSettings.disabled = true;
break;
case 'idle':
case 'completed':
case 'failed':
case 'stopped':
this.isScanning = false;
if (this.graphManager) {
this.graphManager.isScanning = false;
}
if (this.elements.startScan) {
this.elements.startScan.disabled = !isQueueEmpty;
this.elements.startScan.classList.remove('loading');
this.elements.startScan.innerHTML = '[RUN] Start Reconnaissance ';
}
if (this.elements.addToGraph) {
this.elements.addToGraph.disabled = !isQueueEmpty;
this.elements.addToGraph.classList.remove('loading');
}
if (this.elements.stopScan) {
this.elements.stopScan.disabled = true;
this.elements.stopScan.innerHTML = '[STOP] Terminate Scan ';
}
if (this.elements.targetInput) this.elements.targetInput.disabled = false;
if (this.elements.maxDepth) this.elements.maxDepth.disabled = false;
if (this.elements.configureSettings) this.elements.configureSettings.disabled = false;
break;
}
}
/**
* Load provider information
*/
async loadProviders() {
try {
const response = await this.apiCall('/api/providers');
if (response.success) {
this.updateProviderDisplay(response.providers);
this.buildSettingsModal(response.providers); // Updated to use new function
console.log('Providers loaded successfully');
}
} catch (error) {
console.error('Failed to load providers:', error);
}
}
/**
* Build the enhanced settings modal with provider configuration and API keys
* @param {Object} providers - Provider information from backend
*/
buildSettingsModal(providers) {
this.buildProviderConfigSection(providers);
this.buildApiKeySection(providers);
this.updateSettingsCounts(providers);
}
/**
* Build the provider configuration section with enable/disable checkboxes
* @param {Object} providers - Provider information
*/
buildProviderConfigSection(providers) {
const providerConfigList = document.getElementById('provider-config-list');
if (!providerConfigList) return;
providerConfigList.innerHTML = '';
for (const [name, info] of Object.entries(providers)) {
const providerConfig = document.createElement('div');
providerConfig.className = 'provider-item';
const statusClass = info.enabled ? 'enabled' : 'disabled';
const statusIcon = info.enabled ? '✓' : '✗';
providerConfig.innerHTML = `
`;
providerConfigList.appendChild(providerConfig);
}
// Add checkbox styling and event handlers
this.setupProviderCheckboxes();
}
/**
* Setup provider checkbox styling and event handlers
*/
setupProviderCheckboxes() {
const checkboxes = document.querySelectorAll('.provider-toggle');
checkboxes.forEach(checkbox => {
// Apply existing checkbox styling
checkbox.style.cssText = `
appearance: none;
width: 16px;
height: 16px;
border: 2px solid #555;
background: #1a1a1a;
cursor: pointer;
position: relative;
border-radius: 3px;
transition: all 0.3s ease;
`;
// Update visual state
this.updateCheckboxAppearance(checkbox);
// Add change event handler
checkbox.addEventListener('change', (e) => {
this.updateCheckboxAppearance(e.target);
});
});
}
/**
* Add CSS for checkbox styling since we're using existing styles
*/
addCheckboxStyling() {
// Add CSS for the checkboxes to work with existing styles
const style = document.createElement('style');
style.textContent = `
.provider-toggle[data-checked="true"]::after {
content: '✓';
position: absolute;
top: -2px;
left: 2px;
color: #1a1a1a;
font-size: 12px;
font-weight: bold;
}
.provider-toggle:hover {
border-color: #00ff41;
}
.api-key-status-row {
transition: all 0.3s ease;
}
.provider-item {
margin-bottom: 1rem;
}
.provider-item:last-child {
margin-bottom: 0;
}
`;
document.head.appendChild(style);
}
/**
* Update checkbox appearance based on checked state
*/
updateCheckboxAppearance(checkbox) {
if (checkbox.checked) {
checkbox.style.background = '#00ff41';
checkbox.style.borderColor = '#00ff41';
checkbox.style.setProperty('content', '"✓"', 'important');
// Add checkmark via pseudo-element simulation
checkbox.setAttribute('data-checked', 'true');
} else {
checkbox.style.background = '#1a1a1a';
checkbox.style.borderColor = '#555';
checkbox.removeAttribute('data-checked');
}
}
/**
* Enhanced API key section builder - FIXED to always allow API key input
* @param {Object} providers - Provider information
*/
buildApiKeySection(providers) {
const apiKeyInputs = document.getElementById('api-key-inputs');
if (!apiKeyInputs) return;
apiKeyInputs.innerHTML = '';
let hasApiKeyProviders = false;
for (const [name, info] of Object.entries(providers)) {
if (info.requires_api_key) {
hasApiKeyProviders = true;
const inputGroup = document.createElement('div');
inputGroup.className = 'provider-item';
// Check if API key is set via backend (not clearable) or frontend (clearable)
const isBackendConfigured = info.api_key_source === 'backend';
if (info.api_key_configured && isBackendConfigured) {
// API key is configured via backend - show status only
inputGroup.innerHTML = `
API Key Active
Configured via environment variable
`;
} else if (info.api_key_configured && !isBackendConfigured) {
// API key is configured via frontend - show status with clear option
inputGroup.innerHTML = `
API Key Active
Set via web interface (session-only)
[×]
Clear
`;
} else {
// API key not configured - ALWAYS show input field
const statusClass = info.enabled ? 'enabled' : 'api-key-required';
const statusText = info.enabled ? '○ Ready for API Key' : '⚠️ API Key Required';
inputGroup.innerHTML = `
`;
}
apiKeyInputs.appendChild(inputGroup);
}
}
if (!hasApiKeyProviders) {
apiKeyInputs.innerHTML = `
No providers require API keys
All Active
`;
}
// Setup clear button event handlers
this.setupApiKeyClearHandlers();
}
/**
* Setup API key clear button handlers
*/
setupApiKeyClearHandlers() {
document.querySelectorAll('.clear-api-key-btn').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const provider = e.currentTarget.dataset.provider;
this.clearSingleApiKey(provider, e.currentTarget);
});
});
}
/**
* Clear a single API key with immediate feedback
*/
async clearSingleApiKey(provider, buttonElement) {
try {
// Show immediate feedback
const originalContent = buttonElement.innerHTML;
buttonElement.innerHTML = '[...] Clearing... ';
buttonElement.disabled = true;
const response = await this.apiCall('/api/config/api-keys', 'POST', { [provider]: '' });
if (response.success) {
// Find the parent container and update it
const providerContainer = buttonElement.closest('.provider-item');
const statusRow = providerContainer.querySelector('.api-key-status-row');
// Animate out the current status
statusRow.style.transition = 'all 0.3s ease';
statusRow.style.opacity = '0';
statusRow.style.transform = 'translateX(-10px)';
setTimeout(() => {
// Replace with input field
const providerName = buttonElement.dataset.provider;
const apiKeySection = this.elements.apiKeyInputs;
// Rebuild the API key section to reflect changes
this.loadProviders();
this.showSuccess(`API key for ${provider} has been cleared.`);
}, 300);
} else {
throw new Error(response.error || 'Failed to clear API key');
}
} catch (error) {
// Restore button on error
buttonElement.innerHTML = originalContent;
buttonElement.disabled = false;
this.showError(`Error clearing API key: ${error.message}`);
}
}
/**
* Update settings modal counts
*/
updateSettingsCounts(providers) {
const providerCount = Object.keys(providers).length;
const apiKeyCount = Object.values(providers).filter(p => p.requires_api_key).length;
const providerCountElement = document.getElementById('provider-count');
const apiKeyCountElement = document.getElementById('api-key-count');
if (providerCountElement) providerCountElement.textContent = providerCount;
if (apiKeyCountElement) apiKeyCountElement.textContent = apiKeyCount;
}
/**
* Enhanced save settings function
*/
async saveSettings() {
try {
const settings = {
apiKeys: {},
providerSettings: {}
};
// Collect API key inputs
const apiKeyInputs = document.querySelectorAll('#api-key-inputs input[type="password"]');
apiKeyInputs.forEach(input => {
const provider = input.dataset.provider;
const value = input.value.trim();
if (provider && value) {
settings.apiKeys[provider] = value;
}
});
// Collect provider enable/disable settings
const providerCheckboxes = document.querySelectorAll('.provider-toggle');
providerCheckboxes.forEach(checkbox => {
const provider = checkbox.dataset.provider;
if (provider) {
settings.providerSettings[provider] = {
enabled: checkbox.checked
};
}
});
// Save API keys if any
if (Object.keys(settings.apiKeys).length > 0) {
const apiKeyResponse = await this.apiCall('/api/config/api-keys', 'POST', settings.apiKeys);
if (!apiKeyResponse.success) {
throw new Error(apiKeyResponse.error || 'Failed to save API keys');
}
}
// Save provider settings if any
if (Object.keys(settings.providerSettings).length > 0) {
const providerResponse = await this.apiCall('/api/config/providers', 'POST', settings.providerSettings);
if (!providerResponse.success) {
throw new Error(providerResponse.error || 'Failed to save provider settings');
}
}
this.showSuccess('Settings saved successfully');
this.hideSettingsModal();
// Reload providers to reflect changes
this.loadProviders();
} catch (error) {
this.showError(`Error saving settings: ${error.message}`);
}
}
/**
* Reset settings to defaults
*/
async resetSettings() {
try {
// Clear all API key inputs
const apiKeyInputs = document.querySelectorAll('#api-key-inputs input[type="password"]');
apiKeyInputs.forEach(input => {
input.value = '';
});
// Reset all provider checkboxes to enabled (default)
const providerCheckboxes = document.querySelectorAll('.provider-toggle');
providerCheckboxes.forEach(checkbox => {
checkbox.checked = true;
this.updateCheckboxAppearance(checkbox);
});
// Reset recursion depth to default
const depthSelect = document.getElementById('max-depth');
if (depthSelect) {
depthSelect.value = '2';
}
this.showInfo('Settings reset to defaults');
} catch (error) {
this.showError(`Error resetting settings: ${error.message}`);
}
}
/**
* 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 = `
Requests:
${info.statistics.total_requests || 0}
Success Rate:
${(info.statistics.success_rate || 0).toFixed(1)}%
Relationships:
${info.statistics.relationships_found || 0}
Rate Limit:
${info.rate_limit}/min
`;
this.elements.providerList.appendChild(providerItem);
}
}
/**
* UPDATED: Enhanced node details HTML generation for unified data model
* Now properly groups attributes by provider/type with organized sections
*/
generateNodeDetailsHtml(node) {
if (!node) return 'Details not available.
';
let detailsHtml = '';
// Node Header - compact
detailsHtml += `
`;
// Quick Stats Bar - compact
const incomingCount = node.incoming_edges ? node.incoming_edges.length : 0;
const outgoingCount = node.outgoing_edges ? node.outgoing_edges.length : 0;
detailsHtml += `
←
${incomingCount}
In
→
${outgoingCount}
Out
`;
// Handle different node types
if (node.type === 'correlation_object') {
detailsHtml += this.generateCorrelationDetails(node);
} else if (node.type === 'large_entity') {
detailsHtml += this.generateLargeEntityDetails(node);
} else {
detailsHtml += this.generateStandardNodeDetails(node);
}
detailsHtml += '
';
return detailsHtml;
}
/**
* UPDATED: Generate details for standard nodes with organized attribute grouping and data warnings
*/
generateStandardNodeDetails(node) {
let html = '';
// Check for and display a crt.sh data warning if it exists
const crtshWarningAttr = this.findAttributeByName(node.attributes, 'crtsh_data_warning');
if (crtshWarningAttr) {
html += `
⚠️ Data Integrity Warning
${this.escapeHtml(crtshWarningAttr.value)}
This can occur for very large domains (e.g., google.com) where crt.sh may return a limited subset of all available certificates. As a result, the certificate status may not be fully representative.
`;
}
// Relationships sections
html += this.generateRelationshipsSection(node);
// UPDATED: Enhanced attributes section with intelligent grouping (no formatting)
if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) {
html += this.generateOrganizedAttributesSection(node.attributes, node.type);
}
// Description section
html += this.generateDescriptionSection(node);
// Metadata section (collapsed by default)
html += this.generateMetadataSection(node);
return html;
}
/**
* Helper method to find an attribute by name in the standardized attributes list
* @param {Array} attributes - List of StandardAttribute objects
* @param {string} name - Attribute name to find
* @returns {Object|null} The attribute object if found, null otherwise
*/
findAttributeByName(attributes, name) {
if (!Array.isArray(attributes)) {
return null;
}
return attributes.find(attr => attr.name === name) || null;
}
generateOrganizedAttributesSection(attributes, nodeType) {
if (!Array.isArray(attributes) || attributes.length === 0) {
return '';
}
const groups = this.groupAttributesByProviderAndType(attributes, nodeType);
let html = '';
const sortedGroups = Object.entries(groups).sort((a, b) => {
const priorityOrder = { 'high': 0, 'medium': 1, 'low': 2 };
return priorityOrder[a[1].priority] - priorityOrder[b[1].priority];
});
for (const [groupName, groupData] of sortedGroups) {
if (groupData.attributes.length === 0) continue;
const isOpen = groupData.priority === 'high';
html += `
${groupData.icon} ${groupName}
${groupData.attributes.length}
`;
groupData.attributes.forEach(attr => {
html += `
${this.escapeHtml(attr.name || 'Unknown')}
${this.formatAttributeValue(attr)}
`;
});
html += '
';
}
return html;
}
formatAttributeValue(attr) {
const value = attr.value;
const name = attr.name || '';
if (value === null || value === undefined) {
return 'N/A';
}
if (Array.isArray(value)) {
if (value.length === 0) {
return 'Empty Array';
}
// ENHANCED: Special handling for specific DNS record types
if (name.endsWith('_records') || name.includes('record')) {
const recordType = name.replace('_records', '').toUpperCase();
// Format nicely for DNS records
if (value.length <= 5) {
const formattedRecords = value.map(record => {
// Add record type prefix if not already present
if (recordType !== 'DNS' && !record.includes(':')) {
return `${recordType}: ${record}`;
}
return record;
});
return this.escapeHtml(formattedRecords.join('\n'));
} else {
const preview = value.slice(0, 3).map(record => {
if (recordType !== 'DNS' && !record.includes(':')) {
return `${recordType}: ${record}`;
}
return record;
}).join('\n');
return this.escapeHtml(`${preview}\n... (+${value.length - 3} more ${recordType} records)`);
}
}
// For other arrays (existing logic)
if (value.length <= 3) {
return this.escapeHtml(value.join(', '));
} else {
const preview = value.slice(0, 2).join(', ');
return this.escapeHtml(`${preview} ... (${value.length} total)`);
}
}
if (typeof value === 'object') {
return 'Object';
}
return this.escapeHtml(String(value));
}
groupAttributesByProviderAndType(attributes, nodeType) {
if (!Array.isArray(attributes) || attributes.length === 0) {
return {};
}
const groups = {
'DNS Records': { icon: '📋', priority: 'high', attributes: [] },
'Certificate Information': { icon: '🔒', priority: 'high', attributes: [] },
'Network Information': { icon: '🌐', priority: 'high', attributes: [] },
'Provider Data': { icon: '📊', priority: 'medium', attributes: [] },
'Technical Details': { icon: '⚙️', priority: 'low', attributes: [] }
};
for (const attr of attributes) {
const provider = (attr.provider || '').toLowerCase();
const name = (attr.name || '').toLowerCase();
const type = (attr.type || '').toLowerCase();
let assigned = false;
// ENHANCED: Better DNS record detection for specific record types
if (provider === 'dns' ||
name.endsWith('_records') || // Catches a_records, mx_records, txt_records, etc.
name.includes('record') ||
['ptr', 'mx', 'cname', 'ns', 'txt', 'soa', 'srv', 'caa', 'a_records', 'aaaa_records'].some(keyword => name.includes(keyword))) {
groups['DNS Records'].attributes.push(attr);
assigned = true;
}
// Certificate-related attributes
else if (provider === 'crtsh' || name.startsWith('cert_') ||
['certificate', 'ssl', 'tls', 'issuer', 'validity', 'san'].some(keyword => name.includes(keyword))) {
groups['Certificate Information'].attributes.push(attr);
assigned = true;
}
// Network/Shodan attributes
else if (provider === 'shodan' ||
['port', 'service', 'banner', 'asn', 'organization', 'country', 'city', 'network'].some(keyword => name.includes(keyword))) {
groups['Network Information'].attributes.push(attr);
assigned = true;
}
// Provider-specific data
else if (provider && ['shodan_', 'crtsh_', 'dns_'].some(prefix => name.startsWith(prefix))) {
groups['Provider Data'].attributes.push(attr);
assigned = true;
}
// If not assigned to any specific group, put in technical details
if (!assigned) {
groups['Technical Details'].attributes.push(attr);
}
}
// Remove empty groups
Object.keys(groups).forEach(groupName => {
if (groups[groupName].attributes.length === 0) {
delete groups[groupName];
}
});
return groups;
}
formatEdgeLabel(relationshipType, confidence) {
if (!relationshipType) return '';
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
return `${relationshipType} ${confidenceText}`;
}
createEdgeTooltip(edge) {
let tooltip = ``;
tooltip += `
${edge.label || 'Relationship'}
`;
tooltip += `
Confidence: ${(edge.confidence_score * 100).toFixed(1)}%
`;
// UPDATED: Use raw provider name (no formatting)
if (edge.source_provider) {
tooltip += `
Provider: ${edge.source_provider}
`;
}
if (edge.discovery_timestamp) {
const date = new Date(edge.discovery_timestamp);
tooltip += `
Discovered: ${date.toLocaleString()}
`;
}
tooltip += `
`;
return tooltip;
}
/**
* UPDATED: Enhanced correlation details showing the correlated attribute clearly (no formatting)
*/
generateCorrelationDetails(node) {
const attributes = node.attributes || [];
const correlationValueAttr = attributes.find(attr => attr.name === 'correlation_value');
const value = correlationValueAttr ? correlationValueAttr.value : 'Unknown';
const metadataAttr = attributes.find(attr => attr.name === 'correlation_value');
const metadata = metadataAttr ? metadataAttr.metadata : {};
const correlatedNodes = metadata.correlated_nodes || [];
const sources = metadata.sources || [];
let html = '';
// Show what attribute is being correlated (raw names)
const primarySource = sources.length > 0 ? sources[0].attribute : 'unknown';
html += `
🔗 Correlation: ${primarySource}
${correlatedNodes.length}
Shared Value
${this.escapeHtml(String(value))}
Attribute Source
${primarySource}
Correlated Nodes
${correlatedNodes.length} nodes
`;
// Show the correlated nodes
if (correlatedNodes.length > 0) {
html += `
🌐 Correlated Nodes (${correlatedNodes.length})
`;
correlatedNodes.forEach(nodeId => {
html += `
${nodeId}
`;
});
html += '
';
}
return html;
}
/**
* UPDATED: Generate large entity details using unified data model
*/
generateLargeEntityDetails(node) {
// Look for attributes in the unified model structure
const attributes = node.attributes || [];
const nodesAttribute = attributes.find(attr => attr.name === 'nodes');
const countAttribute = attributes.find(attr => attr.name === 'count');
const nodeTypeAttribute = attributes.find(attr => attr.name === 'node_type');
const sourceProviderAttribute = attributes.find(attr => attr.name === 'source_provider');
const discoveryDepthAttribute = attributes.find(attr => attr.name === 'discovery_depth');
const nodes = nodesAttribute ? nodesAttribute.value : [];
const count = countAttribute ? countAttribute.value : 0;
const nodeType = nodeTypeAttribute ? nodeTypeAttribute.value : 'nodes';
const sourceProvider = sourceProviderAttribute ? sourceProviderAttribute.value : 'Unknown';
const discoveryDepth = discoveryDepthAttribute ? discoveryDepthAttribute.value : 'Unknown';
let html = `
📦 Entity Summary
Contains
${count} ${nodeType}s
Provider
${sourceProvider}
Depth
${discoveryDepth}
📋 Contained ${nodeType}s (${Array.isArray(nodes) ? nodes.length : 0})
`;
const largeEntityId = node.id;
if (Array.isArray(nodes)) {
nodes.forEach(innerNodeId => {
html += `
${innerNodeId}
`;
});
}
html += '
';
return html;
}
generateRelationshipsSection(node) {
let html = '';
if (node.incoming_edges && node.incoming_edges.length > 0) {
html += `
⬅️ Source Relationships (${node.incoming_edges.length})
`;
node.incoming_edges.forEach(edge => {
const confidence = edge.data.confidence_score || 0;
const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
html += `
${edge.from}
${edge.data.relationship_type}
${'●'.repeat(Math.ceil(confidence * 3))}
`;
});
html += '
';
}
if (node.outgoing_edges && node.outgoing_edges.length > 0) {
html += `
➡️ Target Relationships (${node.outgoing_edges.length})
`;
node.outgoing_edges.forEach(edge => {
const confidence = edge.data.confidence_score || 0;
const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
html += `
${edge.to}
${edge.data.relationship_type}
${'●'.repeat(Math.ceil(confidence * 3))}
`;
});
html += '
';
}
return html;
}
formatObjectCompact(obj) {
if (!obj || typeof obj !== 'object') return '';
const entries = Object.entries(obj);
if (entries.length <= 2) {
let html = '';
entries.forEach(([key, value]) => {
html += `${key}: ${this.escapeHtml(String(value))}
`;
});
return html;
}
// For complex objects, show first entry with expansion
return `
${entries[0][0]}: ${this.escapeHtml(String(entries[0][1]))}
+${entries.length - 1} more properties...
${entries.slice(1).map(([key, value]) =>
`
${key}: ${this.escapeHtml(String(value))}
`
).join('')}
`;
}
generateDescriptionSection(node) {
if (!node.description) return '';
return `
${this.escapeHtml(node.description)}
`;
}
generateMetadataSection(node) {
if (!node.metadata || Object.keys(node.metadata).length === 0) return '';
return `
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Recursively formats a JavaScript object into an HTML unordered list with collapsible sections.
* @param {Object} obj - The object to format.
* @returns {string} - An HTML string representing the object.
*/
formatObjectToHtml(obj) {
if (!obj || Object.keys(obj).length === 0) {
return 'No data available.
';
}
let html = '';
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const value = obj[key];
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
if (typeof value === 'object' && value !== null) {
html += `${formattedKey} `;
html += this.formatObjectToHtml(value);
html += ` `;
} else {
html += `${formattedKey}: ${this.formatValue(value)} `;
}
}
}
html += ' ';
return html;
}
/**
* Enhanced showNodeModal with better event handling
*/
showNodeModal(node) {
if (!this.elements.nodeModal || !node) return;
if (this.elements.modalTitle) {
this.elements.modalTitle.innerHTML = `
${this.getNodeTypeIcon(node.type)}
${node.id}
`;
}
const detailsHtml = this.generateNodeDetailsHtml(node);
if (this.elements.modalDetails) {
this.elements.modalDetails.innerHTML = detailsHtml;
// Add enhanced event handlers
this.addModalEventHandlers();
}
this.elements.nodeModal.style.display = 'block';
}
/**
* Add event handlers for enhanced modal interactions
*/
addModalEventHandlers() {
// Handle node navigation links - FIXED to work properly
this.elements.modalDetails.querySelectorAll('.node-link-compact').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id');
if (nodeId && this.graphManager && this.graphManager.nodes) {
const nextNode = this.graphManager.nodes.get(nodeId);
if (nextNode) {
console.log('Navigating to node:', nextNode);
// Don't hide modal, just update content
this.showNodeModal(nextNode);
} else {
console.warn('Node not found:', nodeId);
}
}
});
});
// Handle the new extract button
this.elements.modalDetails.querySelectorAll('.extract-node-btn').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const largeEntityId = e.target.dataset.largeEntityId;
const nodeId = e.target.dataset.nodeId;
console.log(`Extract button clicked for node ${nodeId} from entity ${largeEntityId}`);
this.extractNode(largeEntityId, nodeId);
});
});
// Handle legacy node links
this.elements.modalDetails.querySelectorAll('.node-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id');
if (nodeId && this.graphManager && this.graphManager.nodes) {
const nextNode = this.graphManager.nodes.get(nodeId);
if (nextNode) {
this.showNodeModal(nextNode);
}
}
});
});
}
async extractNode(largeEntityId, nodeId) {
try {
const response = await this.apiCall('/api/graph/large-entity/extract', 'POST', {
large_entity_id: largeEntityId,
node_id: nodeId,
});
if (response.success) {
this.showSuccess(response.message);
// If the scanner was idle, it's now running. Start polling to see the new node appear.
if (this.scanStatus === 'idle') {
this.startPolling(1000);
} else {
// If already scanning, force a quick graph update to see the change sooner.
setTimeout(() => this.updateGraph(), 500);
}
} else {
throw new Error(response.error || 'Extraction failed on the server.');
}
} catch (error) {
console.error('Failed to extract node:', error);
this.showError(`Extraction failed: ${error.message}`);
}
}
initializeModalFunctionality() {
// Make sure the graph manager has node access
console.log('Initializing modal functionality...');
// Set up event delegation for dynamic content
document.addEventListener('click', (e) => {
const target = e.target.closest('.node-link-compact, .node-link');
if (target) {
e.preventDefault();
e.stopPropagation();
const nodeId = target.dataset.nodeId || target.getAttribute('data-node-id');
if (nodeId && this.graphManager && this.graphManager.nodes) {
const nextNode = this.graphManager.nodes.get(nodeId);
if (nextNode) {
this.showNodeModal(nextNode);
}
}
}
});
}
/**
* Get icon for node type
*/
getNodeTypeIcon(nodeType) {
const icons = {
'domain': '🌍',
'ip': '📍',
'asn': '🏢',
'large_entity': '📦',
'correlation_object': '🔗'
};
return icons[nodeType] || '●';
}
/**
* Enhanced hideModal with animation
*/
hideModal() {
if (this.elements.nodeModal) {
this.elements.nodeModal.classList.add('modal-closing');
setTimeout(() => {
this.elements.nodeModal.style.display = 'none';
this.elements.nodeModal.classList.remove('modal-closing');
}, 200);
}
}
/**
* Enhanced keyboard navigation for modals
*/
setupModalKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (this.elements.nodeModal && this.elements.nodeModal.style.display === 'block') {
switch (e.key) {
case 'Escape':
this.hideModal();
break;
case 'Tab':
this.handleModalTabNavigation(e);
break;
case 'Enter':
if (e.target.classList.contains('node-link') || e.target.classList.contains('node-link-item')) {
e.target.click();
}
break;
}
}
});
}
/**
* Handle tab navigation within modal
*/
handleModalTabNavigation(e) {
const focusableElements = this.elements.nodeModal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
/**
* Initialize enhanced modal functionality
*/
initializeEnhancedModals() {
this.setupModalKeyboardNavigation();
// Add CSS classes for animations
const style = document.createElement('style');
style.textContent = `
.modal-opening {
animation: modalFadeIn 0.3s ease-out;
}
.modal-closing {
animation: modalFadeOut 0.2s ease-in;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes modalFadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
.array-value.expanded .array-items {
max-height: none;
}
.modal-title-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.modal-title-text {
font-family: 'Special Elite', monospace;
}
`;
document.head.appendChild(style);
}
/**
* Show Settings modal
*/
showSettingsModal() {
if (this.elements.settingsModal) {
this.elements.settingsModal.style.display = 'block';
}
}
/**
* Hide Settings modal
*/
hideSettingsModal() {
if (this.elements.settingsModal) {
this.elements.settingsModal.style.display = 'none';
}
}
/**
* Save API Keys
*/
async saveApiKeys() {
const inputs = this.elements.apiKeyInputs.querySelectorAll('input');
const keys = {};
inputs.forEach(input => {
const provider = input.dataset.provider;
const value = input.value.trim();
if (provider && value) {
keys[provider] = value;
}
});
if (Object.keys(keys).length === 0) {
this.showWarning('No API keys were entered.');
return;
}
try {
const response = await this.apiCall('/api/config/api-keys', 'POST', keys);
if (response.success) {
this.showSuccess(response.message);
this.hideSettingsModal();
this.loadProviders(); // Refresh provider status
} else {
throw new Error(response.error || 'Failed to save API keys');
}
} catch (error) {
this.showError(`Error saving API keys: ${error.message}`);
}
}
/**
* Reset API Key fields
*/
resetApiKeys() {
const inputs = this.elements.apiKeyInputs.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
});
}
/**
* Make API call to server
* @param {string} endpoint - API endpoint
* @param {string} method - HTTP method
* @param {Object} data - Request data
* @returns {Promise} Response data
*/
async apiCall(endpoint, method = 'GET', data = null) {
try {
const options = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if (data && method !== 'GET') {
options.body = JSON.stringify(data);
console.log('Request body:', options.body);
}
const response = await fetch(endpoint, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error(`API call failed for ${method} ${endpoint}:`, error);
throw error;
}
}
/**
* Validate domain name
* @param {string} domain - Domain to validate
* @returns {boolean} True if valid
*/
isValidDomain(domain) {
console.log(`Validating domain: "${domain}"`);
if (!domain || typeof domain !== 'string' || domain.length > 253 || /^\d{1,3}(\.\d{1,3}){3}$/.test(domain)) {
return false;
}
const parts = domain.split('.');
if (parts.length < 2 || parts.some(part => !/^[a-zA-Z0-9-]{1,63}$/.test(part) || part.startsWith('-') || part.endsWith('-'))) {
return false;
}
return true;
}
/**
* Validate target (domain or IP) - UPDATED for IPv6 support
* @param {string} target - Target to validate
* @returns {boolean} True if valid
*/
isValidTarget(target) {
return this.isValidDomain(target) || this.isValidIp(target);
}
/**
* Validate IP address (IPv4 or IPv6)
* @param {string} ip - IP to validate
* @returns {boolean} True if valid
*/
isValidIp(ip) {
console.log(`Validating IP: "${ip}"`);
if (!ip || typeof ip !== 'string') {
return false;
}
ip = ip.trim();
// IPv4 validation
if (this.isValidIPv4(ip)) {
return true;
}
// IPv6 validation
if (this.isValidIPv6(ip)) {
return true;
}
return false;
}
/**
* Validate IPv4 address
* @param {string} ip - IP to validate
* @returns {boolean} True if valid IPv4
*/
isValidIPv4(ip) {
const ipv4Pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const match = ip.match(ipv4Pattern);
if (!match) {
return false;
}
// Check each octet is between 0-255
for (let i = 1; i <= 4; i++) {
const octet = parseInt(match[i], 10);
if (octet < 0 || octet > 255) {
return false;
}
// Check for leading zeros (except for '0' itself)
if (match[i].length > 1 && match[i][0] === '0') {
return false;
}
}
return true;
}
/**
* Validate IPv6 address
* @param {string} ip - IP to validate
* @returns {boolean} True if valid IPv6
*/
isValidIPv6(ip) {
// Handle IPv6 with embedded IPv4 (e.g., ::ffff:192.168.1.1)
if (ip.includes('.')) {
const lastColon = ip.lastIndexOf(':');
if (lastColon !== -1) {
const ipv6Part = ip.substring(0, lastColon + 1);
const ipv4Part = ip.substring(lastColon + 1);
if (this.isValidIPv4(ipv4Part)) {
// Validate the IPv6 part (should end with ::)
return this.isValidIPv6Pure(ipv6Part + '0:0');
}
}
}
return this.isValidIPv6Pure(ip);
}
/**
* Validate pure IPv6 address (no embedded IPv4)
* @param {string} ip - IPv6 address to validate
* @returns {boolean} True if valid IPv6
*/
isValidIPv6Pure(ip) {
// Basic format check
if (!ip || ip.length < 2 || ip.length > 39) {
return false;
}
// Check for invalid characters
if (!/^[0-9a-fA-F:]+$/.test(ip)) {
return false;
}
// Handle double colon (::) for zero compression
const doubleColonCount = (ip.match(/::/g) || []).length;
if (doubleColonCount > 1) {
return false; // Only one :: allowed
}
let parts;
if (doubleColonCount === 1) {
// Expand the :: notation
const [before, after] = ip.split('::');
const beforeParts = before ? before.split(':') : [];
const afterParts = after ? after.split(':') : [];
// Calculate how many zero groups the :: represents
const totalParts = beforeParts.length + afterParts.length;
const zeroGroups = 8 - totalParts;
if (zeroGroups < 1) {
return false; // :: must represent at least one zero group
}
// Build the full address
parts = beforeParts.concat(Array(zeroGroups).fill('0')).concat(afterParts);
} else {
// No :: notation, split normally
parts = ip.split(':');
}
// IPv6 should have exactly 8 groups
if (parts.length !== 8) {
return false;
}
// Validate each group (1-4 hex digits)
for (const part of parts) {
if (!part || part.length > 4 || !/^[0-9a-fA-F]+$/.test(part)) {
return false;
}
}
return true;
}
/**
* Format status text for display
* @param {string} status - Raw status
* @returns {string} Formatted status
*/
formatStatus(status) {
const statusMap = {
'idle': 'Idle',
'running': 'Running',
'completed': 'Completed',
'failed': 'Failed',
'stopped': 'Stopped'
};
return statusMap[status] || status;
}
/**
* Format value for display
* @param {*} value - Raw value
* @returns {string} Formatted value
*/
formatValue(value) {
if (typeof value === 'object' && value !== null) {
// Use for nicely formatted JSON
return `${JSON.stringify(value, null, 2)} `;
} else {
// Escape HTML to prevent XSS issues with string values
const strValue = String(value);
return strValue.replace(//g, ">");
}
}
/**
* Show success message
* @param {string} message - Success message
*/
showSuccess(message) {
this.showMessage(message, 'success');
}
/**
* Show info message
* @param {string} message - Info message
*/
showInfo(message) {
this.showMessage(message, 'info');
}
showWarning(message) {
this.showMessage(message, 'warning');
}
/**
* Show error message
* @param {string} message - Error message
*/
showError(message) {
this.showMessage(message, 'error');
}
/**
* Show connection error
*/
showConnectionError() {
this.updateConnectionStatus('error');
}
/**
* Show message with visual feedback
* @param {string} message - Message text
* @param {string} type - Message type (success, error, warning, info)
*/
showMessage(message, type = 'info') {
// Create message element
const messageElement = document.createElement('div');
messageElement.className = `message-toast message-${type}`;
messageElement.innerHTML = `
${message}
×
`;
// Add to container
const container = document.getElementById('message-container');
if (container) {
container.appendChild(messageElement);
// Auto-remove after delay
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.remove();
}
}, 300);
}
}, type === 'error' ? 8000 : 5000); // Errors stay longer
}
// Update connection status to show activity
if (type === 'success' && this.consecutiveErrors === 0) {
this.updateConnectionStatus(this.isScanning ? 'active' : 'idle');
}
}
/**
* Get message background color based on type
* @param {string} type - Message type
* @returns {string} CSS color
*/
getMessageColor(type) {
const colors = {
'success': '#2c5c34',
'error': '#5c2c2c',
'warning': '#5c4c2c',
'info': '#2c3e5c'
};
return colors[type] || colors.info;
}
/**
* Get message border color based on type
* @param {string} type - Message type
* @returns {string} CSS color
*/
getMessageBorderColor(type) {
const colors = {
'success': '#00ff41',
'error': '#ff6b6b',
'warning': '#ff9900',
'info': '#00aaff'
};
return colors[type] || colors.info;
}
}
// Add CSS animations for message toasts
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.message-container {
pointer-events: auto;
}
.message-toast {
pointer-events: auto;
}
`;
document.head.appendChild(style);
// Initialize application when page loads
console.log('Creating DNSReconApp instance...');
const app = new DNSReconApp();