/** * T-Pot Attack Map Dashboard * Enhanced UI and data visualization functionality */ /** * Attack Cache Manager * Handles persistent storage of attack events using IndexedDB with LocalStorage fallback */ class AttackCache { constructor() { this.dbName = 'TPotAttackCache'; this.storeName = 'attacks'; this.version = 1; this.retentionPeriod = 24 * 60 * 60 * 1000; // 24 hours this.maxEvents = 10000; // Performance/memory limit this.cleanupInterval = 5 * 60 * 1000; // Cleanup every 5 minutes this.storageType = null; this.db = null; this.localStorageKey = 'tpot_attack_cache'; } async init() { console.log('[CACHE] Initializing attack cache...'); try { await this.initIndexedDB(); this.storageType = 'indexeddb'; console.log('[CACHE] Using IndexedDB for storage'); } catch (error) { console.warn('[CACHE] IndexedDB failed, falling back to LocalStorage:', error); this.initLocalStorage(); this.storageType = 'localstorage'; console.log('[CACHE] Using LocalStorage for storage'); } // Start periodic cleanup setInterval(() => { this.cleanup(); }, this.cleanupInterval); console.log(`[CACHE] Cache initialized with ${this.storageType}, retention: 24h`); } async initIndexedDB() { return new Promise((resolve, reject) => { if (!window.indexedDB) { reject(new Error('IndexedDB not supported')); return; } const request = indexedDB.open(this.dbName, this.version); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create attacks object store if it doesn't exist if (!db.objectStoreNames.contains(this.storeName)) { const store = db.createObjectStore(this.storeName, { keyPath: 'id', autoIncrement: true }); // Create index on timestamp for efficient cleanup queries store.createIndex('timestamp', 'timestamp', { unique: false }); store.createIndex('source_ip', 'source_ip', { unique: false }); store.createIndex('honeypot', 'honeypot', { unique: false }); } }; }); } initLocalStorage() { // LocalStorage is synchronous, just verify it's available if (!window.localStorage) { throw new Error('LocalStorage not supported'); } // Initialize empty cache if doesn't exist if (!localStorage.getItem(this.localStorageKey)) { const emptyCache = { events: [], lastCleanup: Date.now(), version: 1 }; localStorage.setItem(this.localStorageKey, JSON.stringify(emptyCache)); } } async storeEvent(event) { // Add timestamp and unique ID if not present const eventToStore = { ...event, timestamp: event.timestamp || Date.now(), cached_at: Date.now() }; try { if (this.storageType === 'indexeddb') { await this.storeEventIndexedDB(eventToStore); } else { this.storeEventLocalStorage(eventToStore); } } catch (error) { console.warn('[CACHE] Failed to store event:', error); } } async storeEventIndexedDB(event) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); const request = store.add(event); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } storeEventLocalStorage(event) { const cache = JSON.parse(localStorage.getItem(this.localStorageKey) || '{"events":[]}'); cache.events.push(event); // Keep within limits if (cache.events.length > this.maxEvents) { cache.events = cache.events.slice(-this.maxEvents); } localStorage.setItem(this.localStorageKey, JSON.stringify(cache)); } async getStoredEvents() { try { if (this.storageType === 'indexeddb') { return await this.getStoredEventsIndexedDB(); } else { return this.getStoredEventsLocalStorage(); } } catch (error) { console.warn('[CACHE] Failed to retrieve stored events:', error); return []; } } async getStoredEventsIndexedDB() { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); const index = store.index('timestamp'); // Get events from last 24 hours const cutoff = Date.now() - this.retentionPeriod; const range = IDBKeyRange.lowerBound(cutoff); const request = index.getAll(range); request.onsuccess = () => { const events = request.result || []; console.log(`[CACHE] Retrieved ${events.length} events from IndexedDB`); resolve(events); }; request.onerror = () => reject(request.error); }); } getStoredEventsLocalStorage() { const cache = JSON.parse(localStorage.getItem(this.localStorageKey) || '{"events":[]}'); const cutoff = Date.now() - this.retentionPeriod; // Filter events within retention period const validEvents = cache.events.filter(event => event.timestamp && event.timestamp > cutoff ); console.log(`[CACHE] Retrieved ${validEvents.length} events from LocalStorage`); return validEvents; } async cleanup() { try { if (this.storageType === 'indexeddb') { await this.cleanupIndexedDB(); } else { this.cleanupLocalStorage(); } } catch (error) { console.warn('[CACHE] Cleanup failed:', error); } } async cleanupIndexedDB() { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); const index = store.index('timestamp'); // Delete events older than retention period const cutoff = Date.now() - this.retentionPeriod; const range = IDBKeyRange.upperBound(cutoff); const request = index.openCursor(range); let deletedCount = 0; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { cursor.delete(); deletedCount++; cursor.continue(); } else { if (deletedCount > 0) { console.log(`[CACHE] Cleaned up ${deletedCount} old events from IndexedDB`); } resolve(); } }; request.onerror = () => reject(request.error); }); } cleanupLocalStorage() { const cache = JSON.parse(localStorage.getItem(this.localStorageKey) || '{"events":[]}'); const cutoff = Date.now() - this.retentionPeriod; const originalCount = cache.events.length; // Keep only events within retention period cache.events = cache.events.filter(event => event.timestamp && event.timestamp > cutoff ); // Limit total events for performance if (cache.events.length > this.maxEvents) { cache.events = cache.events.slice(-this.maxEvents); } cache.lastCleanup = Date.now(); localStorage.setItem(this.localStorageKey, JSON.stringify(cache)); const deletedCount = originalCount - cache.events.length; if (deletedCount > 0) { console.log(`[CACHE] Cleaned up ${deletedCount} old events from LocalStorage`); } } async getStatistics() { const events = await this.getStoredEvents(); return { totalEvents: events.length, storageType: this.storageType, oldestEvent: events.length > 0 ? Math.min(...events.map(e => e.timestamp)) : null, newestEvent: events.length > 0 ? Math.max(...events.map(e => e.timestamp)) : null, retentionPeriod: this.retentionPeriod }; } async clearCache() { console.log('[CACHE] Clearing all cached data...'); try { if (this.storageType === 'indexeddb') { await this.clearCacheIndexedDB(); } else { this.clearCacheLocalStorage(); } console.log('[CACHE] Cache cleared successfully'); } catch (error) { console.error('[CACHE] Failed to clear cache:', error); throw error; } } async clearCacheIndexedDB() { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); const request = store.clear(); request.onsuccess = () => { console.log('[CACHE] IndexedDB cache cleared'); resolve(); }; request.onerror = () => reject(request.error); }); } clearCacheLocalStorage() { const emptyCache = { events: [], lastCleanup: Date.now(), version: 1 }; localStorage.setItem(this.localStorageKey, JSON.stringify(emptyCache)); console.log('[CACHE] LocalStorage cache cleared'); } } class AttackMapDashboard { constructor() { this.charts = {}; this.theme = localStorage.getItem('theme') || 'dark'; this.panelCollapsed = localStorage.getItem('sidePanelCollapsed') === 'true' || false; this.bottomPanelHeight = parseInt(localStorage.getItem('bottomPanelHeight')) || 350; this.activeTab = 'live-feed'; this.searchFilters = {}; this.settings = this.loadSettings(); this.attackHistory = []; this.protocolStats = {}; this.countryStats = {}; this.timelineInterval = '24h'; // Default to 24 hours this.timelineData = this.initializeTimelineData(); // Add timeline data structure this.lastTimelineUpdate = Date.now(); // Track last update time this.connectionStatus = 'connecting'; // Track current connection status // Honeypot Performance Tracking this.honeypotStats = { data: new Map(), // Map to store honeypot stats retention: 15 * 60 * 1000, // 15 minutes in milliseconds updateInterval: 60000, // Update chart every minute lastCleanup: Date.now() }; // Initialize attack cache system this.attackCache = new AttackCache(); this.cacheInitialized = false; this.restoringFromCache = false; this.init(); } async init() { this.initTheme(); this.initEventListeners(); this.initCharts(); this.initThreatHeatmap(); this.initPanels(); this.initTabs(); this.initSearch(); this.initSettings(); this.initSoundSystem(); this.initHoneypotTracking(); // Initialize cache system await this.initializeCache(); this.hideLoadingScreen(); // Start background tasks this.startPerformanceMonitoring(); this.startDataAggregation(); this.startConnectionStatusMonitoring(); } async initializeCache() { try { await this.attackCache.init(); this.cacheInitialized = true; // Update cache status UI this.updateCacheStatus(); // Try to restore data from cache await this.restoreFromCache(); console.log('[DASHBOARD] Cache system initialized successfully'); } catch (error) { console.error('[DASHBOARD] Failed to initialize cache:', error); this.cacheInitialized = false; this.updateCacheStatus('error'); } } updateCacheStatus(status = null) { const cacheStatus = document.getElementById('cache-status'); const cacheIndicator = document.getElementById('cache-indicator'); const cacheText = document.getElementById('cache-text'); if (!cacheStatus || !cacheIndicator || !cacheText) return; if (!this.cacheInitialized || status === 'error') { cacheStatus.style.display = 'none'; return; } // Show cache status cacheStatus.style.display = 'flex'; // Set status based on cache state if (status === 'restoring') { cacheIndicator.className = 'status-indicator connecting'; cacheText.textContent = 'Restoring...'; cacheStatus.title = 'Restoring data from cache'; } else { cacheIndicator.className = 'status-indicator cached'; cacheText.textContent = 'Cached'; // Update tooltip with cache info this.attackCache.getStatistics().then(stats => { const storageType = stats.storageType === 'indexeddb' ? 'IndexedDB' : 'LocalStorage'; cacheStatus.title = `${stats.totalEvents} events cached (${storageType})`; }).catch(() => { cacheStatus.title = 'Data cache active'; }); } // Add click handler to show cache details cacheStatus.onclick = () => this.showCacheDetails(); } async showCacheDetails() { try { const stats = await this.attackCache.getStatistics(); const storageType = stats.storageType === 'indexeddb' ? 'IndexedDB' : 'LocalStorage'; const oldestDate = stats.oldestEvent ? new Date(stats.oldestEvent).toLocaleString() : 'None'; const newestDate = stats.newestEvent ? new Date(stats.newestEvent).toLocaleString() : 'None'; const message = ` Cache Statistics: • Storage: ${storageType} • Events: ${stats.totalEvents} • Oldest: ${oldestDate} • Newest: ${newestDate} • Retention: 24 hours `; this.showNotification(message.trim(), 'info', 'cache'); } catch (error) { console.error('[CACHE] Failed to get statistics:', error); this.showNotification('Failed to get cache statistics', 'error', 'cache'); } } async restoreFromCache() { console.log('[DASHBOARD] Attempting to restore data from cache...'); this.restoringFromCache = true; this.updateCacheStatus('restoring'); try { const cachedEvents = await this.attackCache.getStoredEvents(); const stats = await this.attackCache.getStatistics(); console.log(`[DASHBOARD] Found ${cachedEvents.length} cached events (${stats.storageType})`); if (cachedEvents.length > 0) { // Sort events by timestamp (oldest first) cachedEvents.sort((a, b) => a.timestamp - b.timestamp); // Restore all cached events for complete statistics this.attackHistory = cachedEvents.map(event => ({ ...event, restored: true // Mark as restored for debugging })); // Process events in chunks to prevent UI blocking const BATCH_SIZE = 500; let processedCount = 0; const processBatch = () => { const end = Math.min(processedCount + BATCH_SIZE, cachedEvents.length); // Process a batch of events for (let i = processedCount; i < end; i++) { const event = cachedEvents[i]; // Process for timeline this.addAttackToTimeline(event, false); // false = don't trigger updates // Process for heatmap this.addAttackToHeatmap(event, false); // Process for honeypot tracking if (event.honeypot) { this.trackHoneypotAttack(event.honeypot, event.timestamp, false); } // Update tracking data (needed for top IPs and top countries) this.updateIPTracking(event.ip || event.source_ip, event.country, event); this.updateCountryTracking(event.country, event); this.updateProtocolStats(event.protocol); } processedCount = end; if (processedCount < cachedEvents.length) { // Schedule next batch requestAnimationFrame(processBatch); } else { // All batches complete - finalize restoration this.finalizeRestoration(cachedEvents, stats); } }; // Start processing batches processBatch(); } else { console.log('[DASHBOARD] No cached events found'); this.restoringFromCache = false; this.updateCacheStatus(); } } catch (error) { console.error('[DASHBOARD] Failed to restore from cache:', error); this.showNotification('Failed to restore cached data', 'error', 'cache'); this.restoringFromCache = false; this.updateCacheStatus(); } } finalizeRestoration(cachedEvents, stats) { try { // Restore map markers // We need to find the last 200 UNIQUE locations to match the map's visual limit. const mapEvents = []; const uniqueLocations = new Set(); const MAX_MAP_CIRCLES = 200; // Iterate backwards to find the most recent unique locations for (let i = cachedEvents.length - 1; i >= 0; i--) { const event = cachedEvents[i]; // Use coordinates as key if available, otherwise fallback to IP const key = (event.source_lat && event.source_lng) ? `${event.source_lat},${event.source_lng}` : (event.source_ip || event.ip); if (key) { if (uniqueLocations.size < MAX_MAP_CIRCLES || uniqueLocations.has(key)) { mapEvents.push(event); uniqueLocations.add(key); } } } // Reverse to restore chronological order (oldest to newest) mapEvents.reverse(); console.log(`[DASHBOARD] Restoring ${mapEvents.length} events covering ${uniqueLocations.size} unique locations`); for (const event of mapEvents) { if (typeof window.processRestoredAttack === 'function') { window.processRestoredAttack(event); } } // Restore live feed (last 100 events, newest first) const liveFeedEvents = cachedEvents.slice(-100); for (const event of liveFeedEvents) { this.addToAttackTable(event, false); } // Update all visualizations with restored data this.aggregateProtocolStats(); this.aggregateCountryStats(); this.updateTimelineChart(); this.updateHoneypotChartData(); this.updateThreatHeatmap(); this.updateDashboardMetrics(); // Update tables with aggregated data this.updateTopIPsTable(); this.updateTopCountriesTable(); console.log(`[DASHBOARD] Successfully restored ${cachedEvents.length} events from cache`); // Show restoration notification this.showNotification( `${cachedEvents.length} eventos restaurados desde caché (${this.formatTimeAgo(stats.oldestEvent)})`, 'success', 'cache' ); } catch (error) { console.error('[DASHBOARD] Error during final restoration steps:', error); } finally { this.restoringFromCache = false; this.updateCacheStatus(); } } formatTimeAgo(timestamp) { if (!timestamp) return 'unknown'; const now = Date.now(); const diff = now - timestamp; const minutes = Math.floor(diff / (1000 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60)); if (hours > 0) { return `${hours}h ago`; } else if (minutes > 0) { return `${minutes}m ago`; } else { return 'just now'; } } getProtocolColor(protocol, port = null) { const colors = { 'CHARGEN': '#4CAF50', 'FTP-DATA': '#F44336', 'FTP': '#FF5722', 'SSH': '#FF9800', 'TELNET': '#FFC107', 'SMTP': '#8BC34A', 'WINS': '#009688', 'DNS': '#00BCD4', 'DHCP': '#03A9F4', 'TFTP': '#2196F3', 'HTTP': '#3F51B5', 'DICOM': '#9C27B0', 'POP3': '#E91E63', 'NTP': '#795548', 'RPC': '#607D8B', 'IMAP': '#9E9E9E', 'SNMP': '#FF6B35', 'LDAP': '#FF8E53', 'HTTPS': '#0080FF', 'SMB': '#BF00FF', 'SMTPS': '#80FF00', 'EMAIL': '#00FF80', 'IPMI': '#00FFFF', 'IPP': '#8000FF', 'IMAPS': '#FF0080', 'POP3S': '#80FF80', 'NFS': '#FF8080', 'SOCKS': '#8080FF', 'SQL': '#00FF00', 'ORACLE': '#FFFF00', 'PPTP': '#FF00FF', 'MQTT': '#00FF40', 'SSDP': '#40FF00', 'IEC104': '#FF4000', 'HL7': '#4000FF', 'MYSQL': '#00FF00', 'RDP': '#FF0060', 'IPSEC': '#60FF00', 'SIP': '#FFCCFF', 'POSTGRESQL': '#00CCFF', 'ADB': '#FFCCCC', 'VNC': '#0000FF', 'REDIS': '#CC00FF', 'IRC': '#FFCC00', 'JETDIRECT': '#8000FF', 'ELASTICSEARCH': '#FF8000', 'INDUSTRIAL': '#80FF40', 'MEMCACHED': '#40FF80', 'MONGODB': '#FF4080', 'SCADA': '#8040FF', 'OTHER': '#78909C' }; const protocolUpper = protocol?.toUpperCase(); // Return predefined color for known protocols if (colors[protocolUpper]) { return colors[protocolUpper]; } // Fallback for unknown protocols - should use OTHER color for consistency return colors['OTHER']; // Use OTHER color (#78909C) for unknown protocols } // Normalize protocol names to known protocols or "OTHER" normalizeProtocol(protocol) { if (!protocol) return 'OTHER'; // Check if protocol is a numeric string (port number) - convert to OTHER if (/^\d+$/.test(protocol.toString())) { return 'OTHER'; } // List of known protocols to check against const knownProtocols = [ 'CHARGEN', 'FTP-DATA', 'FTP', 'SSH', 'TELNET', 'SMTP', 'WINS', 'DNS', 'DHCP', 'TFTP', 'HTTP', 'DICOM', 'POP3', 'NTP', 'RPC', 'IMAP', 'SNMP', 'LDAP', 'HTTPS', 'SMB', 'SMTPS', 'EMAIL', 'IPMI', 'IPP', 'IMAPS', 'POP3S', 'NFS', 'SOCKS', 'SQL', 'ORACLE', 'PPTP', 'MQTT', 'SSDP', 'IEC104', 'HL7', 'MYSQL', 'RDP', 'IPSEC', 'SIP', 'POSTGRESQL', 'ADB', 'VNC', 'REDIS', 'IRC', 'JETDIRECT', 'ELASTICSEARCH', 'INDUSTRIAL', 'MEMCACHED', 'MONGODB', 'SCADA' ]; const protocolUpper = protocol.toUpperCase(); // If protocol is not in the known list, use "OTHER" if (!knownProtocols.includes(protocolUpper)) { return 'OTHER'; } return protocolUpper; } // Initialize timeline data structure for different intervals initializeTimelineData() { return this.generateTimelineData(this.timelineInterval); } // Generate timeline data based on interval generateTimelineData(interval) { const timeline = []; const now = new Date(); let points, duration, unit; switch (interval) { case '1m': points = 60; // 60 seconds duration = 1000; // 1 second unit = 'second'; break; case '1h': points = 60; // 60 minutes duration = 60 * 1000; // 1 minute unit = 'minute'; break; case '24h': default: points = 24; // 24 hours duration = 60 * 60 * 1000; // 1 hour unit = 'hour'; break; } // Create data points for the specified interval for (let i = points - 1; i >= 0; i--) { const time = new Date(now.getTime() - i * duration); let label, timestamp; switch (interval) { case '1m': label = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); timestamp = Math.floor(time.getTime() / 1000) * 1000; // Round to second break; case '1h': label = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); timestamp = Math.floor(time.getTime() / (60 * 1000)) * (60 * 1000); // Round to minute break; case '24h': default: label = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); timestamp = Math.floor(time.getTime() / (60 * 60 * 1000)) * (60 * 60 * 1000); // Round to hour break; } timeline.push({ timestamp: timestamp, label: label, count: 0, // Initialize with zero, will be populated with real data unit: unit }); } return timeline; } // Populate timeline with real attack data from history populateTimelineFromHistory() { if (!this.attackHistory.length) { console.log(`[DEBUG] No attack history available for timeline population`); return; } console.log(`[DEBUG] Populating timeline (${this.timelineInterval}) from ${this.attackHistory.length} attacks`); // Reset counts this.timelineData.forEach(point => { point.count = 0; }); let attacksInRange = 0; // Count attacks in each time bucket this.attackHistory.forEach(attack => { const attackTime = new Date(attack.timestamp || attack.time || attack.date || Date.now()); // Find the appropriate timeline bucket this.timelineData.forEach(point => { const pointTime = new Date(point.timestamp); const duration = this.getDurationForInterval(); const pointEndTime = new Date(pointTime.getTime() + duration); // Check if attack falls within this time bucket if (attackTime >= pointTime && attackTime < pointEndTime) { point.count++; attacksInRange++; } }); }); console.log(`[DEBUG] Timeline populated: ${attacksInRange} attacks in range, counts:`, this.timelineData.map(p => p.count)); this.updateTimelineChart(); } // Get duration in milliseconds for current interval getDurationForInterval() { switch (this.timelineInterval) { case '1m': return 1000; // 1 second case '1h': return 60 * 1000; // 1 minute case '24h': default: return 60 * 60 * 1000; // 1 hour } } // Add new attack to timeline data addAttackToTimeline(attack, triggerUpdate = true) { const attackTime = new Date(attack.timestamp || attack.time || attack.date || Date.now()); const duration = this.getDurationForInterval(); let attackAdded = false; // Try to find existing time bucket that contains this attack for (let i = 0; i < this.timelineData.length; i++) { const point = this.timelineData[i]; const pointTime = new Date(point.timestamp); const pointEndTime = new Date(pointTime.getTime() + duration); // Check if attack falls within this time bucket if (attackTime >= pointTime && attackTime < pointEndTime) { point.count++; attackAdded = true; console.log(`[DEBUG] Added attack to existing bucket ${i}: ${point.label}, new count: ${point.count}`); break; } } // If attack doesn't fit in any existing bucket, check if we need to add new buckets if (!attackAdded) { const lastPoint = this.timelineData[this.timelineData.length - 1]; const lastPointTime = new Date(lastPoint.timestamp); const lastPointEndTime = new Date(lastPointTime.getTime() + duration); // If attack is after the last bucket, add new bucket(s) if (attackTime >= lastPointEndTime) { let currentTime = lastPointEndTime; // Add buckets until we reach the attack time while (currentTime <= attackTime) { const bucketStartTime = new Date(Math.floor(currentTime.getTime() / duration) * duration); const isAttackBucket = attackTime >= bucketStartTime && attackTime < new Date(bucketStartTime.getTime() + duration); const newPoint = { timestamp: bucketStartTime.getTime(), label: this.formatTimeLabel(bucketStartTime), count: isAttackBucket ? 1 : 0, unit: this.getUnitForInterval() }; // Remove oldest point and add new one this.timelineData.shift(); this.timelineData.push(newPoint); if (isAttackBucket) { attackAdded = true; console.log(`[DEBUG] Added attack to new bucket: ${newPoint.label}, count: 1`); break; } currentTime = new Date(currentTime.getTime() + duration); } } else { console.log(`[DEBUG] Attack is too old to fit in current timeline window`); } } if (attackAdded && triggerUpdate) { this.updateTimelineChart(); } } // Helper to format time labels formatTimeLabel(time) { switch (this.timelineInterval) { case '1m': return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); case '1h': case '24h': default: return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } } // Helper to get unit for interval getUnitForInterval() { switch (this.timelineInterval) { case '1m': return 'second'; case '1h': return 'minute'; case '24h': default: return 'hour'; } } // Update timeline chart with current data updateTimelineChart() { if (!this.charts.timeline) return; const labels = this.timelineData.map(point => point.label); const data = this.timelineData.map(point => point.count); this.charts.timeline.data.labels = labels; this.charts.timeline.data.datasets[0].data = data; this.charts.timeline.update('none'); // Use 'none' mode for better performance } // Convert HSL to RGB hslToRgb(h, s, l) { let r, g, b; if (s === 0) { r = g = b = l; // achromatic } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [r * 255, g * 255, b * 255]; } // Convert hex color to RGB hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } // Theme Management initTheme() { document.documentElement.setAttribute('data-theme', this.theme); this.updateThemeIcon(); } toggleTheme() { this.theme = this.theme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', this.theme); localStorage.setItem('theme', this.theme); this.updateThemeIcon(); this.updateChartsTheme(); // Update map theme if the function exists if (typeof updateMapTheme === 'function') { updateMapTheme(this.theme); } } updateThemeIcon() { const icon = document.querySelector('#theme-toggle i'); if (icon) { icon.className = this.theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; } } // Event Listeners initEventListeners() { // Theme toggle document.getElementById('theme-toggle')?.addEventListener('click', () => { this.toggleTheme(); }); // Fullscreen toggle document.getElementById('fullscreen-toggle')?.addEventListener('click', () => { this.toggleFullscreen(); }); // Settings modal document.getElementById('settings-toggle')?.addEventListener('click', () => { this.openSettings(); }); document.getElementById('settings-close')?.addEventListener('click', () => { this.closeSettings(); }); // Panel controls document.getElementById('panel-toggle')?.addEventListener('click', () => { this.toggleSidePanel(); }); // Panel resizing this.initPanelResize(); // Settings document.getElementById('save-settings')?.addEventListener('click', () => { this.saveSettings(); }); document.getElementById('reset-settings')?.addEventListener('click', () => { this.resetSettings(); }); document.getElementById('clear-cache')?.addEventListener('click', () => { this.clearCache(); }); // Timeline interval selector document.getElementById('timeline-interval')?.addEventListener('change', (e) => { this.changeTimelineInterval(e.target.value); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { this.handleKeyboardShortcuts(e); }); // Window resize window.addEventListener('resize', () => { this.handleWindowResize(); }); } // Panel Management initPanels() { this.updatePanelStates(); this.initPanelResize(); this.updateSidePanelHeight(); // Initial side panel height setup // Apply saved side panel state this.applySidePanelState(); } toggleSidePanel() { this.panelCollapsed = !this.panelCollapsed; // Save the panel state to localStorage localStorage.setItem('sidePanelCollapsed', this.panelCollapsed.toString()); const panel = document.getElementById('side-panel'); if (panel) { panel.classList.toggle('collapsed', this.panelCollapsed); } this.updateMapSize(); } applySidePanelState() { const panel = document.getElementById('side-panel'); if (panel && this.panelCollapsed) { panel.classList.add('collapsed'); } } initPanelResize() { const resizeHandle = document.getElementById('panel-resize'); const bottomPanel = document.getElementById('bottom-panel'); if (!resizeHandle || !bottomPanel) return; let isResizing = false; let startY = 0; let startHeight = 0; resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; startY = e.clientY; startHeight = this.bottomPanelHeight; document.body.style.cursor = 'ns-resize'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const deltaY = startY - e.clientY; const newHeight = Math.max(200, Math.min(600, startHeight + deltaY)); this.bottomPanelHeight = newHeight; localStorage.setItem('bottomPanelHeight', newHeight.toString()); bottomPanel.style.height = `${newHeight}px`; this.updateMapSize(); // This will also update side panel height }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; document.body.style.cursor = ''; } }); } updatePanelStates() { const bottomPanel = document.getElementById('bottom-panel'); if (bottomPanel) { bottomPanel.style.height = `${this.bottomPanelHeight}px`; } this.updateMapSize(); } updateMapSize() { // Update side panel height based on bottom panel position this.updateSidePanelHeight(); // Trigger map resize if needed if (window.map) { setTimeout(() => { window.map.invalidateSize(); }, 300); } } updateSidePanelHeight() { const sidePanel = document.getElementById('side-panel'); const bottomPanel = document.getElementById('bottom-panel'); if (sidePanel && bottomPanel) { // Calculate available height: viewport height - navbar height - bottom panel height const navbarHeight = 70; // Top navbar height const bottomPanelHeight = this.bottomPanelHeight || 350; const availableHeight = window.innerHeight - navbarHeight - bottomPanelHeight; sidePanel.style.height = `${Math.max(200, availableHeight)}px`; } } // Tab Management initTabs() { const tabButtons = document.querySelectorAll('.tab-btn'); tabButtons.forEach(btn => { btn.addEventListener('click', () => { const tabName = btn.getAttribute('data-tab'); this.switchTab(tabName); }); }); // Activate the default tab (live-feed) this.switchTab(this.activeTab); } switchTab(tabName) { // Update active tab this.activeTab = tabName; // Update tab buttons document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName); }); // Hide all tab panes first document.querySelectorAll('.tab-pane').forEach(pane => { pane.classList.remove('active'); pane.style.display = 'none'; }); // Show the selected tab pane const targetTab = document.getElementById(`${tabName}-tab`); if (targetTab) { targetTab.classList.add('active'); targetTab.style.display = 'block'; } // Load tab-specific data this.loadTabData(tabName); // Resize charts if needed setTimeout(() => { this.resizeCharts(); }, 100); } loadTabData(tabName) { switch (tabName) { case 'overview': this.updateOverviewCharts(); break; case 'top-ips': this.updateTopIPsTable(); break; case 'countries': this.updateTopCountriesTable(); break; case 'live-feed': this.updateLiveFeed(); break; } } // Chart Management initCharts() { // Initialize charts with a slight delay to ensure containers are properly sized setTimeout(() => { this.initAttackDistributionChart(); this.initTimelineChart(); this.initProtocolChart(); this.initHoneypotChart(); // Populate timeline and heatmap with any existing attack history setTimeout(() => { this.populateTimelineFromHistory(); this.populateHeatmapFromHistory(); }, 200); // Force a resize after initialization setTimeout(() => { this.resizeCharts(); }, 100); }, 50); } // Handle timeline interval change changeTimelineInterval(newInterval) { console.log(`[DEBUG] Changing timeline interval from ${this.timelineInterval} to ${newInterval}`); this.timelineInterval = newInterval; this.timelineData = this.generateTimelineData(newInterval); // Populate the new timeline with existing attack history this.populateTimelineFromHistory(); // Reinitialize the timeline chart with new data this.initTimelineChart(); // Restart timeline updates with new frequency this.startTimelineUpdates(); console.log(`[DEBUG] Timeline interval changed to ${newInterval}, data points: ${this.timelineData.length}`); } // Update timeline chart title based on interval updateTimelineChartTitle() { const titleMap = { '1m': 'Ataques por Segundo (Último Minuto)', '1h': 'Ataques por Minuto (Última Hora)', '24h': 'Ataques por Hora (Últimas 24 Horas)' }; if (this.charts.timeline && this.charts.timeline.options.plugins.legend) { this.charts.timeline.data.datasets[0].label = titleMap[this.timelineInterval] || 'Ataques'; this.charts.timeline.options.scales.x.title.text = this.getTimelineXAxisTitle(); this.charts.timeline.options.scales.y.title.text = 'Cantidad de Ataques'; this.charts.timeline.update(); } } // Get X-axis title based on interval getTimelineXAxisTitle() { const titleMap = { '1m': 'Hora (Último Minuto)', '1h': 'Hora (Última Hora)', '24h': 'Hora (Últimas 24 Horas)' }; return titleMap[this.timelineInterval] || 'Hora'; } initAttackDistributionChart() { const ctx = document.getElementById('attack-distribution-chart'); if (!ctx) return; const labels = ['SSH', 'HTTP', 'FTP', 'TELNET', 'OTHER']; const colors = labels.map(protocol => { if (protocol === 'OTHER') { return this.getProtocolColor(protocol, 8080); // Default port for initial display } return this.getProtocolColor(protocol); }); this.charts.attackDistribution = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: [30, 25, 15, 10, 20], backgroundColor: colors, borderWidth: 2, borderColor: colors }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }, plugins: { legend: { position: 'bottom', labels: { color: '#b0b0b0', usePointStyle: true, padding: 10, boxWidth: 12, font: { size: 11 } } } }, elements: { arc: { borderWidth: 2 } }, cutout: '60%' } }); // Ensure proper sizing after chart creation setTimeout(() => { if (this.charts.attackDistribution) { this.charts.attackDistribution.resize(); } }, 100); } initTimelineChart() { const ctx = document.getElementById('timeline-chart'); if (!ctx) return; // Destroy existing chart if it exists if (this.charts.timeline) { this.charts.timeline.destroy(); } // Use the structured timeline data const labels = this.timelineData.map(point => point.label); const data = this.timelineData.map(point => point.count); // Get dynamic titles based on current interval const datasetLabel = this.getDatasetLabel(); const xAxisTitle = this.getTimelineXAxisTitle(); this.charts.timeline = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: datasetLabel, data: data, borderColor: '#e20074', backgroundColor: 'rgba(226, 0, 116, 0.1)', fill: true, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { ticks: { color: '#b0b0b0', maxTicksLimit: this.getMaxTicks() }, grid: { color: '#333' }, title: { display: false } }, y: { ticks: { color: '#b0b0b0' }, grid: { color: '#333' }, title: { display: false } } }, plugins: { legend: { position: 'bottom', align: 'center', labels: { color: '#b0b0b0', usePointStyle: true, padding: 15, font: { size: 11 } } }, tooltip: { callbacks: { label: function(context) { return `Ataques: ${context.parsed.y}`; } } } }, animation: { duration: this.timelineInterval === '1m' ? 0 : 750 }, transitions: { active: { animation: { duration: this.timelineInterval === '1m' ? 0 : 400 } } } } }); } // Get dataset label based on current interval getDatasetLabel() { const labelMap = { '1m': 'Ataques por Segundo', '1h': 'Ataques por Minuto', '24h': 'Ataques por Hora' }; return labelMap[this.timelineInterval] || 'Ataques'; } // Get max ticks for X-axis based on interval getMaxTicks() { const tickMap = { '1m': 6, // Show every 10 seconds (fewer ticks for longer HH:MM:SS labels) '1h': 12, // Show every 5 minutes '24h': 12 // Show every 2 hours }; return tickMap[this.timelineInterval] || 12; } initProtocolChart() { const ctx = document.getElementById('protocol-chart'); if (!ctx) return; const labels = ['SSH', 'HTTP', 'FTP', 'TELNET', 'DNS', 'SMTP']; const colors = labels.map(protocol => this.getProtocolColor(protocol)); const initialData = [45, 35, 20, 15, 10, 8]; this.charts.protocol = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Ataques', data: initialData.map(d => Math.sqrt(d)), originalData: initialData, backgroundColor: colors, borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { ticks: { color: '#b0b0b0' }, grid: { display: false } }, y: { ticks: { color: '#b0b0b0', callback: function(value) { return Math.round(Math.pow(value, 2)); } }, grid: { color: '#333' } } }, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { const raw = context.dataset.originalData ? context.dataset.originalData[context.dataIndex] : Math.round(Math.pow(context.raw, 2)); return context.dataset.label + ': ' + raw; } } } } } }); } initHoneypotChart() { const ctx = document.getElementById('honeypot-chart'); if (!ctx) return; // Get theme colors - same as timeline chart const textColor = this.theme === 'dark' ? '#b0b0b0' : '#495057'; const gridColor = this.theme === 'dark' ? '#333' : '#dee2e6'; // Use same transparency as timeline chart const backgroundColor = 'rgba(226, 0, 116, 0.1)'; // Match timeline exactly this.charts.honeypot = new Chart(ctx, { type: 'radar', data: { labels: ['Sin datos'], datasets: [{ label: 'Ataques (Últimos 15m)', data: [0], borderColor: '#e20074', backgroundColor: backgroundColor, // Match timeline transparency pointBackgroundColor: '#e20074', pointBorderColor: '#e20074', pointRadius: 4, borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { r: { beginAtZero: true, ticks: { display: false, // Remove attack count numbers for cleaner look maxTicksLimit: 8 // Limit number of grid rings to prevent performance issues }, grid: { color: gridColor }, pointLabels: { color: textColor, font: { size: 10, // Slightly smaller to reduce crowding weight: '400' // Normal weight for better readability }, padding: 8, // Add padding to prevent overlapping backdropColor: 'transparent' // Remove background }, angleLines: { color: gridColor } } }, plugins: { legend: { position: 'bottom', align: 'center', labels: { color: textColor, // Match timeline chart exactly usePointStyle: true, padding: 15, font: { size: 11 }, generateLabels: function(chart) { const original = Chart.defaults.plugins.legend.labels.generateLabels; const labels = original.call(this, chart); // Customize legend to match timeline chart opacity and stroke labels.forEach(label => { if (label.fillStyle) { // Use the same transparency as timeline chart label.fillStyle = 'rgba(226, 0, 116, 0.1)'; label.strokeStyle = '#e20074'; label.lineWidth = 1; // Match timeline chart default stroke width } }); return labels; } } }, tooltip: { backgroundColor: this.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.95)', titleColor: textColor, bodyColor: textColor, borderColor: gridColor, borderWidth: 1, callbacks: { label: function(context) { const raw = context.dataset.originalData ? context.dataset.originalData[context.dataIndex] : Math.round(Math.pow(context.raw, 2)); return context.dataset.label + ': ' + raw; } } } } } }); // Don't call updateHoneypotChartData here - let it be called when real data arrives } // Honeypot Performance Tracking System initHoneypotTracking() { // Start periodic cleanup of old data setInterval(() => { this.cleanupOldHoneypotData(); }, 5 * 60 * 1000); // Clean up every 5 minutes // Honeypot chart updates will be synchronized with timeline updates // No separate interval needed - updates happen in addAttackEvent and startDataAggregation console.log('[DEBUG] Honeypot performance tracking initialized'); } trackHoneypotAttack(honeypot, timestamp = Date.now(), triggerUpdate = true) { if (!honeypot || typeof honeypot !== 'string') { console.warn('[DEBUG] Invalid honeypot data:', honeypot); return; } // Clean honeypot name (remove any whitespace/special chars) honeypot = honeypot.trim(); // Get or create honeypot entry if (!this.honeypotStats.data.has(honeypot)) { this.honeypotStats.data.set(honeypot, []); console.log(`[DEBUG] New honeypot discovered: ${honeypot}`); } // Add timestamp to honeypot's attack history this.honeypotStats.data.get(honeypot).push(timestamp); // Immediately clean old data for this honeypot this.cleanupHoneypotData(honeypot); const currentStats = this.honeypotStats.data.get(honeypot); if (currentStats) { console.log(`[DEBUG] Tracked attack for honeypot: ${honeypot} (${currentStats.length} total attacks)`); } else { console.log(`[DEBUG] Tracked attack for honeypot: ${honeypot} (expired/cleaned up)`); } } cleanupOldHoneypotData() { const cutoff = Date.now() - this.honeypotStats.retention; for (const [honeypot, timestamps] of this.honeypotStats.data.entries()) { this.cleanupHoneypotData(honeypot, cutoff); } this.honeypotStats.lastCleanup = Date.now(); } cleanupHoneypotData(honeypot, cutoff = null) { if (!cutoff) { cutoff = Date.now() - this.honeypotStats.retention; } const timestamps = this.honeypotStats.data.get(honeypot); if (!timestamps) return; // Filter out old timestamps const filtered = timestamps.filter(ts => ts > cutoff); if (filtered.length === 0) { // Remove honeypot if no recent attacks this.honeypotStats.data.delete(honeypot); } else { this.honeypotStats.data.set(honeypot, filtered); } } getHoneypotStats() { const stats = {}; for (const [honeypot, timestamps] of this.honeypotStats.data.entries()) { stats[honeypot] = timestamps.length; } return stats; } updateHoneypotChartData() { if (!this.charts.honeypot) return; const stats = this.getHoneypotStats(); const honeypots = Object.keys(stats).sort(); const counts = honeypots.map(hp => stats[hp]); // Handle no data case if (honeypots.length === 0) { this.charts.honeypot.data.labels = ['Sin datos']; this.charts.honeypot.data.datasets[0].data = [0]; this.charts.honeypot.data.datasets[0].originalData = [0]; } else { // Update chart data with real honeypot data this.charts.honeypot.data.labels = honeypots; this.charts.honeypot.data.datasets[0].data = counts.map(c => Math.sqrt(c)); this.charts.honeypot.data.datasets[0].originalData = counts; } // Update chart this.charts.honeypot.update('none'); // No animation for performance } // Public method to be called from map.js when processing attacks processAttackForDashboard(attackData) { if (!attackData) return; // Track honeypot attack if honeypot field is present if (attackData.honeypot) { this.trackHoneypotAttack(attackData.honeypot, attackData.timestamp || Date.now()); } // Add to attack history for other dashboard features this.attackHistory.push({ ...attackData, timestamp: attackData.timestamp || Date.now() }); // Keep attack history within reasonable bounds (match cache size) if (this.attackHistory.length > this.attackCache.maxEvents) { this.attackHistory = this.attackHistory.slice(-this.attackCache.maxEvents); } } updateChartsTheme() { const textColor = this.theme === 'dark' ? '#b0b0b0' : '#495057'; const gridColor = this.theme === 'dark' ? '#333' : '#dee2e6'; Object.values(this.charts).forEach(chart => { if (chart && chart.options) { // Update text colors if (chart.options.scales) { Object.values(chart.options.scales).forEach(scale => { if (scale.ticks) { scale.ticks.color = textColor; scale.ticks.backdropColor = 'transparent'; // Ensure transparent backdrop scale.ticks.showLabelBackdrop = false; } if (scale.grid) scale.grid.color = gridColor; if (scale.pointLabels) { scale.pointLabels.color = textColor; scale.pointLabels.backdropColor = 'transparent'; // Ensure transparent backdrop } if (scale.angleLines) scale.angleLines.color = gridColor; }); } // Update legend colors - all charts follow theme now if (chart.options.plugins && chart.options.plugins.legend) { chart.options.plugins.legend.labels.color = textColor; } // Update tooltip colors for theme if (chart.options.plugins && chart.options.plugins.tooltip) { chart.options.plugins.tooltip.backgroundColor = this.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.95)'; chart.options.plugins.tooltip.titleColor = textColor; chart.options.plugins.tooltip.bodyColor = textColor; chart.options.plugins.tooltip.borderColor = gridColor; } // Update honeypot chart dataset colors to match timeline chart exactly if (chart === this.charts.honeypot && chart.data.datasets[0]) { // Use same transparency as timeline chart (0.1) regardless of theme const backgroundColor = 'rgba(226, 0, 116, 0.1)'; chart.data.datasets[0].backgroundColor = backgroundColor; } chart.update(); } }); } resizeCharts() { Object.values(this.charts).forEach(chart => { if (chart && chart.resize) { chart.resize(); } }); } // Search and Filtering initSearch() { const ipSearch = document.getElementById('ip-search'); const countrySearch = document.getElementById('country-search'); if (ipSearch) { ipSearch.addEventListener('input', (e) => { this.filterTable('ip', e.target.value); }); } if (countrySearch) { countrySearch.addEventListener('input', (e) => { this.filterTable('country', e.target.value); }); } } filterTable(type, query) { const tableId = type === 'ip' ? 'ip-tracking' : 'country-tracking'; const tbody = document.getElementById(tableId); if (!tbody) return; const rows = tbody.querySelectorAll('tr'); const searchTerm = query.toLowerCase(); rows.forEach(row => { const cells = row.querySelectorAll('td'); let visible = false; cells.forEach(cell => { if (cell.textContent.toLowerCase().includes(searchTerm)) { visible = true; } }); row.style.display = visible ? '' : 'none'; }); } // Data Management updateOverviewCharts() { // Update charts with current data this.updateAttackDistribution(); // Timeline is updated by its own interval this.updateProtocolBreakdown(); // Update honeypot performance (now part of overview) this.updateHoneypotPerformance(); } updateTopIPs() { // Update top IPs display console.log('[DEBUG] Updating top IPs display'); // This could refresh the IP tracking table } updateCountries() { // Update countries display console.log('[DEBUG] Updating countries display'); // This could refresh the country tracking table } updateLiveFeed() { // Update live feed display console.log('[DEBUG] Updating live feed display'); // This could refresh the live attack feed } updateHoneypotPerformance() { // Force update of honeypot performance chart with latest data this.updateHoneypotChartData(); } updateAttackDistribution() { if (this.charts.attackDistribution && this.protocolStats) { const data = Object.values(this.protocolStats); const labels = Object.keys(this.protocolStats); const colors = labels.map(protocol => { if (protocol?.toUpperCase() === 'OTHER') { // Get the most common port for OTHER protocol const port = this.getMostCommonOtherPort(); return this.getProtocolColor(protocol, port); } return this.getProtocolColor(protocol); }); this.charts.attackDistribution.data.labels = labels; this.charts.attackDistribution.data.datasets[0].data = data; this.charts.attackDistribution.data.datasets[0].backgroundColor = colors; this.charts.attackDistribution.data.datasets[0].borderColor = colors; this.charts.attackDistribution.update(); } } // Get the most common port for OTHER protocol attacks getMostCommonOtherPort() { const recent = this.attackHistory.filter(attack => Date.now() - attack.timestamp < 300000 && attack.protocol?.toUpperCase() === 'OTHER' ); if (recent.length === 0) return null; // Count port frequencies const portCounts = recent.reduce((counts, attack) => { if (attack.dstPort) { counts[attack.dstPort] = (counts[attack.dstPort] || 0) + 1; } return counts; }, {}); // Return the most frequent port const sortedPorts = Object.entries(portCounts) .sort(([,a], [,b]) => b - a); return sortedPorts.length > 0 ? parseInt(sortedPorts[0][0]) : null; } updateTimeline() { if (!this.charts.timeline) return; const now = Date.now(); let currentUnit, lastDataPoint, shouldAddNewPoint = false; // Get the current time unit based on interval switch (this.timelineInterval) { case '1m': currentUnit = Math.floor(now / 1000); // Current second lastDataPoint = this.timelineData[this.timelineData.length - 1]; const lastSecond = Math.floor(lastDataPoint.timestamp / 1000); shouldAddNewPoint = currentUnit > lastSecond; break; case '1h': currentUnit = Math.floor(now / (60 * 1000)); // Current minute lastDataPoint = this.timelineData[this.timelineData.length - 1]; const lastMinute = Math.floor(lastDataPoint.timestamp / (60 * 1000)); shouldAddNewPoint = currentUnit > lastMinute; break; case '24h': default: currentUnit = Math.floor(now / (60 * 60 * 1000)); // Current hour lastDataPoint = this.timelineData[this.timelineData.length - 1]; const lastHour = Math.floor(lastDataPoint.timestamp / (60 * 60 * 1000)); shouldAddNewPoint = currentUnit > lastHour; break; } if (shouldAddNewPoint) { // Add new time unit const newTime = new Date(now); let newTimestamp, newLabel; switch (this.timelineInterval) { case '1m': newTimestamp = Math.floor(now / 1000) * 1000; newLabel = newTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); break; case '1h': newTimestamp = Math.floor(now / (60 * 1000)) * (60 * 1000); newTime.setSeconds(0, 0); newLabel = newTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); break; case '24h': default: newTimestamp = Math.floor(now / (60 * 60 * 1000)) * (60 * 60 * 1000); newTime.setMinutes(0, 0, 0); newLabel = newTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); break; } this.timelineData.push({ timestamp: newTimestamp, label: newLabel, count: this.getAttackCountForInterval(), unit: this.timelineData[0]?.unit || 'hour' }); // Remove oldest data point to maintain window size const maxPoints = this.timelineInterval === '1m' ? 60 : this.timelineInterval === '1h' ? 60 : 24; if (this.timelineData.length > maxPoints) { this.timelineData.shift(); } // Update chart with new data const labels = this.timelineData.map(point => point.label); const data = this.timelineData.map(point => point.count); this.charts.timeline.data.labels = labels; this.charts.timeline.data.datasets[0].data = data; // Disable animation for 1-minute view to improve readability const updateMode = this.timelineInterval === '1m' ? 'none' : 'default'; this.charts.timeline.update(updateMode); console.log(`[DEBUG] Timeline updated for ${this.timelineInterval}, new point added`); } else { // Update current time unit's count in real-time const currentCount = this.getAttackCountForCurrentInterval(); if (currentCount !== lastDataPoint.count) { lastDataPoint.count = currentCount; // Update chart without animation for real-time feel this.charts.timeline.data.datasets[0].data[this.timelineData.length - 1] = currentCount; this.charts.timeline.update('none'); } } } // Get attack count for the last complete interval getAttackCountForInterval() { let intervalMs, lookBackMs; switch (this.timelineInterval) { case '1m': intervalMs = 1000; // 1 second lookBackMs = 2000; // Look back 2 seconds break; case '1h': intervalMs = 60 * 1000; // 1 minute lookBackMs = 2 * 60 * 1000; // Look back 2 minutes break; case '24h': default: intervalMs = 60 * 60 * 1000; // 1 hour lookBackMs = 2 * 60 * 60 * 1000; // Look back 2 hours break; } const now = Date.now(); const intervalStart = now - lookBackMs; const intervalEnd = now - intervalMs; return this.attackHistory.filter(attack => attack.timestamp > intervalStart && attack.timestamp <= intervalEnd ).length; } // Get attack count for the current ongoing interval getAttackCountForCurrentInterval() { let intervalStart; const now = Date.now(); switch (this.timelineInterval) { case '1m': intervalStart = Math.floor(now / 1000) * 1000; // Start of current second break; case '1h': intervalStart = Math.floor(now / (60 * 1000)) * (60 * 1000); // Start of current minute break; case '24h': default: intervalStart = Math.floor(now / (60 * 60 * 1000)) * (60 * 60 * 1000); // Start of current hour break; } return this.attackHistory.filter(attack => attack.timestamp >= intervalStart ).length; } updateProtocolBreakdown() { if (this.charts.protocol && this.protocolStats) { const data = Object.values(this.protocolStats); const labels = Object.keys(this.protocolStats); const colors = labels.map(protocol => this.getProtocolColor(protocol)); this.charts.protocol.data.labels = labels; this.charts.protocol.data.datasets[0].data = data.map(d => Math.sqrt(d)); this.charts.protocol.data.datasets[0].originalData = data; this.charts.protocol.data.datasets[0].backgroundColor = colors; this.charts.protocol.update(); } } getRecentAttackCount() { const now = Date.now(); const oneMinuteAgo = now - 60000; return this.attackHistory.filter(attack => attack.timestamp > oneMinuteAgo ).length; } // Settings Management initSettings() { this.loadSettingsUI(); } loadSettings() { const defaultSettings = { soundAlerts: false, alertSound: 'beep' }; const saved = localStorage.getItem('attack-map-settings'); return saved ? { ...defaultSettings, ...JSON.parse(saved) } : defaultSettings; } loadSettingsUI() { Object.keys(this.settings).forEach(key => { const element = document.getElementById(key.replace(/([A-Z])/g, '-$1').toLowerCase()); if (element) { if (element.type === 'checkbox') { element.checked = this.settings[key]; } else { element.value = this.settings[key]; } } }); // Apply settings after loading them into UI this.applySettings(); } saveSettings() { const settings = {}; // Collect all settings from UI ['sound-alerts', 'alert-sound'].forEach(id => { const element = document.getElementById(id); if (element) { const key = id.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); settings[key] = element.type === 'checkbox' ? element.checked : element.value; } }); this.settings = settings; localStorage.setItem('attack-map-settings', JSON.stringify(settings)); this.applySettings(); this.closeSettings(); this.showNotification('Configuración guardada', 'success', 'settings'); } resetSettings() { localStorage.removeItem('attack-map-settings'); localStorage.removeItem('sidePanelCollapsed'); localStorage.removeItem('bottomPanelHeight'); this.settings = this.loadSettings(); // Reset panel states to defaults this.panelCollapsed = false; this.bottomPanelHeight = 350; // Apply the reset panel states const sidePanel = document.getElementById('side-panel'); const bottomPanel = document.getElementById('bottom-panel'); if (sidePanel) { sidePanel.classList.remove('collapsed'); } if (bottomPanel) { bottomPanel.style.height = '350px'; } this.loadSettingsUI(); this.applySettings(); this.updateMapSize(); // Update layout after reset // Show notification after layout updates to prevent position jumping setTimeout(() => { this.showNotification('Configuración restablecida', 'info', 'settings'); }, 50); } applySettings() { // Apply sound alert toggle visibility - use actual settings value, not just checkbox state const soundOptions = document.getElementById('sound-options'); const soundAlerts = document.getElementById('sound-alerts'); if (soundOptions && soundAlerts) { // Use the stored setting value to determine visibility soundOptions.style.display = this.settings.soundAlerts ? 'block' : 'none'; // Ensure checkbox matches the setting soundAlerts.checked = this.settings.soundAlerts; } } async clearCache() { try { // Show confirmation dialog const confirmed = confirm('Are you sure you want to clear all cached data? This will reset the map markers and live feed. This action cannot be undone.'); if (!confirmed) { return; } // Clear the attack cache if (this.attackCache) { await this.attackCache.clearCache(); } // Clear map markers and data if (window.map) { // Clear Leaflet map layers if (window.circles) window.circles.clearLayers(); if (window.markers) window.markers.clearLayers(); if (window.attackLines) window.attackLines.clearLayers(); // Clear map data objects if (window.circleAttackData) { Object.keys(window.circleAttackData).forEach(key => { delete window.circleAttackData[key]; }); } if (window.markerAttackData) { Object.keys(window.markerAttackData).forEach(key => { delete window.markerAttackData[key]; }); } if (window.circlesObject) { Object.keys(window.circlesObject).forEach(key => { delete window.circlesObject[key]; }); } if (window.markersObject) { Object.keys(window.markersObject).forEach(key => { delete window.markersObject[key]; }); } } // Clear dashboard tables and charts const liveFeedTable = document.getElementById('attack-tracking'); if (liveFeedTable) { liveFeedTable.innerHTML = ''; } // Clear data structures this.ipStats = {}; this.countryStats = {}; this.countryTrackingStats = {}; this.protocolStats = {}; this.attackHistory = []; // Reset Honeypot Stats if (this.honeypotStats && this.honeypotStats.data) { this.honeypotStats.data.clear(); } // Reset Timeline Data this.timelineData = this.initializeTimelineData(); // Update Tables this.updateTopIPsTable(); this.updateTopCountriesTable(); // Reset Charts // 1. Honeypot Chart if (this.charts.honeypot) { this.charts.honeypot.data.labels = ['Sin datos']; this.charts.honeypot.data.datasets[0].data = [0]; if (this.charts.honeypot.data.datasets[0].originalData) { this.charts.honeypot.data.datasets[0].originalData = [0]; } this.charts.honeypot.update(); } // 2. Protocol Chart if (this.charts.protocol) { this.charts.protocol.data.labels = []; this.charts.protocol.data.datasets[0].data = []; if (this.charts.protocol.data.datasets[0].originalData) { this.charts.protocol.data.datasets[0].originalData = []; } this.charts.protocol.update(); } // 3. Timeline Chart if (this.charts.timeline) { const labels = this.timelineData.map(d => d.label); const data = this.timelineData.map(d => d.count); this.charts.timeline.data.labels = labels; this.charts.timeline.data.datasets[0].data = data; this.charts.timeline.update(); } // Reset attack statistics this.stats = { totalAttacks: 0, uniqueAttackers: 0, topCountries: [], topProtocols: [], recentActivity: [] }; // Update cache status indicator this.updateCacheStatus(); this.showNotification('Caché borrada', 'success', 'cache'); } catch (error) { console.error('[ERROR] Failed to clear cache:', error); this.showNotification('Failed to clear cache', 'error', 'cache'); } } initSoundSystem() { // Create audio context for sound alerts this.audioContext = null; this.soundBuffers = {}; this.audioInitialized = false; // Add event listener for sound alerts checkbox const soundAlerts = document.getElementById('sound-alerts'); if (soundAlerts) { soundAlerts.addEventListener('change', () => { // Update the settings object immediately this.settings.soundAlerts = soundAlerts.checked; // Save to localStorage localStorage.setItem('attack-map-settings', JSON.stringify(this.settings)); // Apply visual changes this.applySettings(); // Initialize audio when enabling sound if (soundAlerts.checked && !this.audioInitialized) { this.initializeAudioContext(); } // Show feedback notification this.showNotification( soundAlerts.checked ? 'Sonido activado' : 'Sonido desactivado', 'success', 'sound' ); }); } // Add event listener for alert sound dropdown const alertSound = document.getElementById('alert-sound'); if (alertSound) { alertSound.addEventListener('change', () => { // Update the settings object immediately this.settings.alertSound = alertSound.value; // Save to localStorage localStorage.setItem('attack-map-settings', JSON.stringify(this.settings)); // Show feedback notification this.showNotification( `Alert sound changed to ${alertSound.options[alertSound.selectedIndex].text}`, 'success', 'sound' ); }); } // Initialize audio context on various user interactions const initAudioOnInteraction = (event) => { if (!this.audioInitialized) { this.initializeAudioContext(); console.log('[SOUND] Audio initialized on:', event.type); // Don't forcefully remove audio prompt - let it expire naturally after 5 seconds } }; // Add listeners for user interaction - be more aggressive document.addEventListener('click', initAudioOnInteraction, { once: true }); document.addEventListener('keydown', initAudioOnInteraction, { once: true }); document.addEventListener('touchstart', initAudioOnInteraction, { once: true }); // Also initialize on any interaction with main content areas const contentAreas = ['#map', '#dashboard', '#side-panel', '#bottom-panel']; contentAreas.forEach(selector => { const element = document.querySelector(selector); if (element) { element.addEventListener('click', initAudioOnInteraction, { once: true }); element.addEventListener('mouseover', initAudioOnInteraction, { once: true }); } }); // Show audio prompt if sound is enabled but not initialized this.showAudioPromptIfNeeded(); this.loadSoundEffects(); } showAudioPromptIfNeeded() { // Only show prompt if sound alerts are enabled and audio is not yet initialized if (this.settings.soundAlerts && !this.audioInitialized) { this.showNotification( '🔊 Click anywhere to enable sound alerts', 'info', 'audio' ); } } initializeAudioContext() { try { if (!this.audioContext) { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } // Resume audio context if it's suspended (some browsers suspend it by default) if (this.audioContext.state === 'suspended') { this.audioContext.resume().then(() => { console.log('[SOUND] Audio context resumed'); this.audioInitialized = true; }).catch(error => { console.warn('[SOUND] Could not resume audio context:', error); }); } else { console.log('[SOUND] Audio context initialized, state:', this.audioContext.state); this.audioInitialized = true; } } catch (error) { console.warn('[SOUND] Could not initialize audio context:', error); } } loadSoundEffects() { // Create simple sound effects programmatically this.createSoundEffects(); } createSoundEffects() { // We'll create simple beep sounds using Web Audio API this.soundGenerators = { beep: () => this.generateBeep(800, 0.1), notification: () => this.generateChime([523, 659, 784], 0.15), alert: () => this.generateAlert([400, 800, 400], 0.2), retro_videogame: () => this.generateRetroVideogame() }; } generateBeep(frequency, duration) { if (!this.audioContext) { this.initializeAudioContext(); } if (!this.audioContext) { console.warn('[SOUND] Audio context not available'); return; } const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); oscillator.start(this.audioContext.currentTime); oscillator.stop(this.audioContext.currentTime + duration); } generateChime(frequencies, duration) { if (!this.audioContext) { this.initializeAudioContext(); } if (!this.audioContext) { console.warn('[SOUND] Audio context not available'); return; } frequencies.forEach((freq, index) => { setTimeout(() => { const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.frequency.setValueAtTime(freq, this.audioContext.currentTime); oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.05, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); oscillator.start(this.audioContext.currentTime); oscillator.stop(this.audioContext.currentTime + duration); }, index * 50); }); } generateAlert(frequencies, duration) { if (!this.audioContext) { this.initializeAudioContext(); } if (!this.audioContext) { console.warn('[SOUND] Audio context not available'); return; } frequencies.forEach((freq, index) => { setTimeout(() => { const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.frequency.setValueAtTime(freq, this.audioContext.currentTime); oscillator.type = 'square'; gainNode.gain.setValueAtTime(0.08, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration/3); oscillator.start(this.audioContext.currentTime); oscillator.stop(this.audioContext.currentTime + duration/3); }, index * 100); }); } generateRetroVideogame() { if (!this.audioContext) { this.initializeAudioContext(); } if (!this.audioContext) { console.warn('[SOUND] Audio context not available'); return; } // Classic retro videogame style sound with descending frequency sweep const startFreq = 220; // Starting frequency (A3) const endFreq = 55; // Ending frequency (A1) - two octaves down const duration = 0.3; // Total duration const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); // Use square wave for that classic 8-bit sound oscillator.type = 'square'; // Create the characteristic descending frequency sweep oscillator.frequency.setValueAtTime(startFreq, this.audioContext.currentTime); oscillator.frequency.exponentialRampToValueAtTime(endFreq, this.audioContext.currentTime + duration); // Volume envelope: quick attack, then fade out gainNode.gain.setValueAtTime(0.12, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); oscillator.start(this.audioContext.currentTime); oscillator.stop(this.audioContext.currentTime + duration); // Add a second harmonic for richness (classic arcade sound technique) setTimeout(() => { const oscillator2 = this.audioContext.createOscillator(); const gainNode2 = this.audioContext.createGain(); oscillator2.connect(gainNode2); gainNode2.connect(this.audioContext.destination); oscillator2.type = 'square'; // Second oscillator at a higher frequency for harmonic content oscillator2.frequency.setValueAtTime(startFreq * 1.5, this.audioContext.currentTime); oscillator2.frequency.exponentialRampToValueAtTime(endFreq * 1.5, this.audioContext.currentTime + duration * 0.6); // Lower volume for the harmonic gainNode2.gain.setValueAtTime(0.06, this.audioContext.currentTime); gainNode2.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration * 0.6); oscillator2.start(this.audioContext.currentTime); oscillator2.stop(this.audioContext.currentTime + duration * 0.6); }, 20); // Slight delay for phasing effect } playAlertSound() { if (!this.settings.soundAlerts) { return; } // Try to initialize audio context if not done yet if (!this.audioInitialized) { this.initializeAudioContext(); } if (!this.soundGenerators) { console.warn('[SOUND] Sound generators not available'); return; } const soundType = this.settings.alertSound || 'beep'; const generator = this.soundGenerators[soundType]; if (generator) { try { generator(); console.log('[SOUND] Played sound:', soundType); } catch (error) { console.warn('[SOUND] Could not play sound:', error); // Try to reinitialize audio context on error if (!this.audioInitialized) { this.initializeAudioContext(); } } } else { console.warn('[SOUND] Sound generator not found for:', soundType); } } openSettings() { const modal = document.getElementById('settings-modal'); if (modal) { modal.classList.add('active'); } } closeSettings() { const modal = document.getElementById('settings-modal'); if (modal) { modal.classList.remove('active'); } } // Utility Functions toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(err => { console.log(`Error attempting to enable fullscreen: ${err.message}`); }); } else { document.exitFullscreen(); } } handleKeyboardShortcuts(e) { // Ctrl/Cmd + key combinations if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'f': e.preventDefault(); this.toggleFullscreen(); break; case 't': e.preventDefault(); this.toggleTheme(); break; case ',': e.preventDefault(); this.openSettings(); break; } } // Escape key if (e.key === 'Escape') { this.closeSettings(); } // Tab navigation if (e.key >= '1' && e.key <= '4' && e.altKey) { e.preventDefault(); const tabs = ['live-feed', 'top-ips', 'countries', 'overview']; const index = parseInt(e.key) - 1; if (tabs[index]) { this.switchTab(tabs[index]); } } } handleWindowResize() { this.resizeCharts(); this.updateMapSize(); } startPerformanceMonitoring() { setInterval(() => { // Monitor performance and adjust if needed // performance.memory is a non-standard API (Chrome only) if (window.performance && window.performance.memory) { const memInfo = window.performance.memory; if (memInfo.usedJSHeapSize > 100 * 1024 * 1024) { // 100MB this.optimizeMemoryUsage(); } } }, 30000); // Check every 30 seconds } startConnectionStatusMonitoring() { // Check if WebSocket was already connected before dashboard loaded if (window.webSocketConnected === true) { console.log('[*] Dashboard initialized - WebSocket already connected, setting status to connected'); this.updateConnectionStatus('connected'); } else { console.log('[*] Dashboard initialized - WebSocket not yet connected, setting status to connecting'); // Set initial status to connecting this.updateConnectionStatus('connecting'); } // Periodically check and sync connection status setInterval(() => { this.syncConnectionStatus(); }, 2000); // Check every 2 seconds } syncConnectionStatus() { const now = Date.now(); const IDLE_THRESHOLD = 30000; // 30 seconds // 1. Check WebSocket State if (!window.webSocket) { this.updateConnectionStatus('disconnected'); return; } const state = window.webSocket.readyState; if (state === WebSocket.CONNECTING) { this.updateConnectionStatus('connecting'); return; } if (state === WebSocket.CLOSING || state === WebSocket.CLOSED) { this.updateConnectionStatus('disconnected'); return; } // 2. Socket is OPEN. Check Data Recency. // Use window.lastValidDataTime which tracks actual data events const lastMsgTime = window.lastValidDataTime || 0; const timeSinceLastMsg = now - lastMsgTime; if (timeSinceLastMsg < IDLE_THRESHOLD) { this.updateConnectionStatus('connected'); } else { this.updateConnectionStatus('idle'); } } optimizeMemoryUsage() { // Limit attack history to match cache size if (this.attackHistory.length > this.attackCache.maxEvents) { this.attackHistory = this.attackHistory.slice(-this.attackCache.maxEvents); } // Limit table rows const tables = ['ip-tracking', 'country-tracking', 'attack-tracking']; tables.forEach(tableId => { const tbody = document.getElementById(tableId); if (tbody && tbody.children.length > 100) { while (tbody.children.length > 50) { tbody.removeChild(tbody.lastChild); } } }); } startDataAggregation() { // Update timeline with different frequencies based on interval this.startTimelineUpdates(); // Update other stats every 5 seconds (synchronized updates) setInterval(() => { this.aggregateProtocolStats(); this.aggregateCountryStats(); this.updateDashboardMetrics(); this.updateHoneypotChartData(); // Synchronized with other card updates }, 5000); // Every 5 seconds } // Start timeline updates with appropriate frequency startTimelineUpdates() { if (this.timelineUpdateInterval) { clearInterval(this.timelineUpdateInterval); } let updateFrequency; switch (this.timelineInterval) { case '1m': updateFrequency = 1000; // Update every second break; case '1h': updateFrequency = 5000; // Update every 5 seconds break; case '24h': default: updateFrequency = 30000; // Update every 30 seconds break; } this.timelineUpdateInterval = setInterval(() => { this.updateTimeline(); }, updateFrequency); console.log(`[DEBUG] Timeline updates started with ${updateFrequency}ms interval for ${this.timelineInterval} mode`); } aggregateProtocolStats() { // Use consistent time window for protocol stats const retentionMinutes = 15; const retentionTime = retentionMinutes * 60 * 1000; // 15 minutes const recent = this.attackHistory.filter(attack => Date.now() - attack.timestamp < retentionTime ); this.protocolStats = recent.reduce((stats, attack) => { const normalizedProtocol = this.normalizeProtocol(attack.protocol); stats[normalizedProtocol] = (stats[normalizedProtocol] || 0) + 1; return stats; }, {}); // Update data retention display this.updateDataRetentionInfo('attack-distribution', retentionMinutes); this.updateDataRetentionInfo('protocol-breakdown', retentionMinutes); } aggregateCountryStats() { // Use consistent time window for country stats const retentionMinutes = 15; const retentionTime = retentionMinutes * 60 * 1000; // 15 minutes const recent = this.attackHistory.filter(attack => Date.now() - attack.timestamp < retentionTime ); this.countryStats = recent.reduce((stats, attack) => { stats[attack.country] = (stats[attack.country] || 0) + 1; return stats; }, {}); } // Add method to update data retention information display updateDataRetentionInfo(cardType, retentionMinutes) { const cards = document.querySelectorAll('.dashboard-card'); cards.forEach(card => { const header = card.querySelector('.card-header h4'); if (header) { const text = header.textContent; if ((cardType === 'attack-distribution' && text.includes('Attack Distribution')) || (cardType === 'protocol-breakdown' && text.includes('Protocol Breakdown'))) { // Remove existing retention info let retentionSpan = card.querySelector('.data-retention-info'); if (retentionSpan) { retentionSpan.remove(); } // Add new retention info retentionSpan = document.createElement('span'); retentionSpan.className = 'data-retention-info'; retentionSpan.textContent = ` (Last ${retentionMinutes}m)`; retentionSpan.style.fontSize = '0.8em'; retentionSpan.style.color = 'var(--text-secondary)'; retentionSpan.style.fontWeight = 'normal'; header.appendChild(retentionSpan); } } }); } updateDashboardMetrics() { // Update various dashboard metrics if (this.activeTab === 'overview') { this.updateOverviewCharts(); } // Note: Header stats are updated by WebSocket Stats messages (handleStats in map.js) // which contain correct historical data from Elasticsearch } hideLoadingScreen() { setTimeout(() => { const loadingScreen = document.getElementById('loading-screen'); if (loadingScreen) { loadingScreen.classList.add('hidden'); } }, 1500); } showNotification(message, type = 'info', context = 'general') { // Ensure notification container exists and is properly configured let container = document.getElementById('notification-container'); if (!container) { container = document.createElement('div'); container.id = 'notification-container'; document.body.appendChild(container); } // Create notification element const notification = document.createElement('div'); notification.className = `notification ${type}`; // Add context-specific class if needed if (context !== 'general') { notification.classList.add(`notification-${context}`); } // Create notification structure using DOM API const header = document.createElement('div'); header.className = 'notification-header'; const title = document.createElement('div'); title.className = 'notification-title'; title.textContent = this.getNotificationTitle(type); header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.className = 'notification-close'; closeBtn.innerHTML = '×'; // Safe entity header.appendChild(closeBtn); notification.appendChild(header); const msgDiv = document.createElement('div'); msgDiv.className = 'notification-message'; msgDiv.textContent = message; notification.appendChild(msgDiv); const timeDiv = document.createElement('div'); timeDiv.className = 'notification-timestamp'; timeDiv.textContent = new Date().toLocaleTimeString(); notification.appendChild(timeDiv); // Add close button event listener const closeButton = notification.querySelector('.notification-close'); closeButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (notification.parentNode) { notification.classList.add('fade-out'); setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 300); } }); // Add to container at the top (newest first) container.insertBefore(notification, container.firstChild); // Force reflow to ensure proper positioning notification.offsetHeight; // Auto-remove after 5 seconds setTimeout(() => { if (notification.parentNode) { notification.classList.add('fade-out'); setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 300); } }, 5000); } getNotificationTitle(type) { const titles = { 'success': 'Success', 'info': 'Information', 'warning': 'Warning', 'error': 'Error' }; return titles[type] || 'Notification'; } // Public API for external components addAttackEvent(event) { console.log('[DEBUG] Dashboard received attack event:', event); event.timestamp = Date.now(); this.attackHistory.push(event); // Store in cache if initialized and not currently restoring if (this.cacheInitialized && !this.restoringFromCache) { this.attackCache.storeEvent(event).catch(error => { console.warn('[CACHE] Failed to store event:', error); }); } // Process attack for honeypot tracking if honeypot field is present if (event.honeypot) { this.trackHoneypotAttack(event.honeypot, event.timestamp); } // Try to initialize audio context if not already done (aggressive approach) if (!this.audioInitialized) { console.log('[SOUND] Attempting audio initialization on attack event'); this.initializeAudioContext(); } // Play sound alert for new attack this.playAlertSound(); // Add to timeline data in real-time this.addAttackToTimeline(event); // Add to heatmap data in real-time this.addAttackToHeatmap(event); // Limit history size if (this.attackHistory.length > 1000) { this.attackHistory.shift(); } // Update relevant displays (synchronized updates) this.updateLiveAttackDisplay(event); this.updateHoneypotChartData(); // Update honeypot chart in sync with other cards console.log('[DEBUG] Attack event processed, history length:', this.attackHistory.length); } updateConnectionStatus(status) { // Prevent unnecessary updates if (this.connectionStatus === status) { return; } const indicator = document.getElementById('status-indicator'); const text = document.getElementById('status-text'); console.log(`[DEBUG] Connection status update: ${this.connectionStatus} -> ${status}`); if (indicator && text) { const oldStatus = indicator.className; indicator.className = `status-indicator ${status}`; switch (status) { case 'connected': text.textContent = 'Conectado'; console.log('[*] Status indicator set to Connected'); break; case 'idle': text.textContent = 'Inactivo'; console.log('[*] Status indicator set to Idle'); break; case 'connecting': text.textContent = 'Conectando...'; console.log('[*] Status indicator set to Connecting...'); break; case 'disconnected': default: text.textContent = 'Desconectado'; break; } this.connectionStatus = status; console.log(`[DEBUG] Connection status UI updated from '${oldStatus}' to '${indicator.className}'`); } else { console.warn('[WARNING] Connection status elements not found'); } } // Helper method to update IP tracking data for restored events updateIPTracking(ip, country, event) { if (!ip) return; if (!this.ipStats.has(ip)) { this.ipStats.set(ip, { count: 0, country: country || 'Unknown', firstSeen: event.timestamp }); } const stats = this.ipStats.get(ip); stats.count++; stats.lastSeen = event.timestamp; } // Helper method to update country tracking data for restored events updateCountryTracking(country, event) { if (!country || country === 'Unknown') return; if (!this.countryStats.has(country)) { this.countryStats.set(country, { count: 0, firstSeen: event.timestamp }); } const stats = this.countryStats.get(country); stats.count++; stats.lastSeen = event.timestamp; } // Helper method to update protocol stats for restored events updateProtocolStats(protocol) { if (!protocol) return; this.protocolStats[protocol] = (this.protocolStats[protocol] || 0) + 1; } addToLiveFeed(event) { // Add event to live feed table without highlighting (for cache restoration) this.addToAttackTable(event, false); // false = no highlighting for restored events // Update IP tracking (needed for top IPs table) this.updateIPTracking(event.ip || event.source_ip, event.country, event); // Update country tracking (needed for top countries table) this.updateCountryTracking(event.country, event); // Update protocol statistics (needed for protocol breakdown) this.updateProtocolStats(event.protocol); } updateLiveAttackDisplay(event) { // Update live attack feed table this.addToAttackTable(event); // Update IP tracking this.updateIPTracking(event.ip, event.country, event); // Update country tracking this.updateCountryTracking(event.country, event); // Update protocol statistics this.updateProtocolStats(event.protocol); // Update real-time charts if on overview tab if (this.activeTab === 'overview') { this.updateOverviewCharts(); } } addToAttackTable(event, highlight = true) { const tbody = document.getElementById('attack-tracking'); if (!tbody) { console.warn('[WARNING] attack-tracking table not found'); return; } const row = document.createElement('tr'); // Add the new row highlight class only for real-time events if (highlight) { row.classList.add('new-attack-row'); // Remove the highlight class after animation completes setTimeout(() => { row.classList.remove('new-attack-row'); }, 2000); } // Use the event timestamp if available, otherwise current time const eventTime = event.timestamp ? new Date(event.timestamp) : new Date(); // Format timestamp as YYYY-MM-DD HH:MM:SS const year = eventTime.getFullYear(); const month = String(eventTime.getMonth() + 1).padStart(2, '0'); const day = String(eventTime.getDate()).padStart(2, '0'); const hours = String(eventTime.getHours()).padStart(2, '0'); const minutes = String(eventTime.getMinutes()).padStart(2, '0'); const seconds = String(eventTime.getSeconds()).padStart(2, '0'); const timeDisplay = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // Use ISO code for flag path (should be 2-letter uppercase code) const flagCode = event.iso_code || event.country_code || 'XX'; // Determine protocol - use "OTHER" if not specified or unknown const protocolName = this.normalizeProtocol(event.protocol); const protocol = protocolName.toLowerCase(); const protocolClass = `protocol-${protocol}`; // Create cells using DOM API to prevent XSS const addCell = (className, text) => { const td = document.createElement('td'); td.className = className; td.textContent = text; row.appendChild(td); return td; }; addCell('time-cell', timeDisplay); addCell('ip-cell', event.ip || event.src_ip || 'Unknown'); addCell('ip-rep-cell', event.ip_rep || 'Unknown'); const flagCell = document.createElement('td'); flagCell.className = 'flag-cell'; const flagImg = document.createElement('img'); flagImg.src = `static/flags/${flagCode}.svg`; flagImg.alt = event.country || 'Unknown'; flagImg.className = 'flag-icon'; flagImg.onerror = function() { this.src = 'static/flags/XX.svg'; }; flagCell.appendChild(flagImg); row.appendChild(flagCell); addCell('country-cell', event.country || 'Unknown'); addCell('honeypot-cell', event.honeypot || 'Unknown'); const protocolCell = document.createElement('td'); protocolCell.className = 'protocol-cell'; const badge = document.createElement('span'); badge.className = `protocol-badge ${protocolClass}`; badge.textContent = protocolName; protocolCell.appendChild(badge); row.appendChild(protocolCell); addCell('port-cell', event.port || event.dst_port || 'N/A'); addCell('tpot-hostname-cell', event.tpot_hostname || 'Unknown'); // Add to top of table tbody.insertBefore(row, tbody.firstChild); // Limit table size while (tbody.children.length > 100) { tbody.removeChild(tbody.lastChild); } console.log('[DEBUG] Added row to attack table for IP:', event.ip || event.src_ip); } updateIPTracking(ip, country, event = null) { // Update IP statistics if (!this.ipStats) this.ipStats = {}; // Determine timestamp to use const timestamp = (event && event.timestamp) ? new Date(event.timestamp) : new Date(); if (!this.ipStats[ip]) { this.ipStats[ip] = { hits: 0, country: country, lastSeen: timestamp, ip: ip, reputation: 'Unknown', lastProtocol: 'Unknown', countryCode: 'XX' // Store ISO code }; } this.ipStats[ip].hits++; // Update lastSeen if the event is newer than what we have if (timestamp >= this.ipStats[ip].lastSeen) { this.ipStats[ip].lastSeen = timestamp; } this.ipStats[ip].country = country; // Update country in case it changes // Update reputation, protocol and country code if event data is available if (event) { if (event.ip_rep) { this.ipStats[ip].reputation = event.ip_rep; } if (event.protocol) { this.ipStats[ip].lastProtocol = this.normalizeProtocol(event.protocol); } // Store the actual ISO code from Elasticsearch if (event.iso_code || event.country_code) { this.ipStats[ip].countryCode = event.iso_code || event.country_code; } } // Update IP table if it's the active tab if (this.activeTab === 'top-ips') { this.updateTopIPsTable(); } } updateCountryTracking(country, event = null) { // Update country statistics if (!this.countryTrackingStats) this.countryTrackingStats = {}; // Determine timestamp to use const timestamp = (event && event.timestamp) ? new Date(event.timestamp) : new Date(); if (!this.countryTrackingStats[country]) { this.countryTrackingStats[country] = { hits: 0, country: country, lastSeen: timestamp, topProtocol: 'Unknown', protocolCounts: {}, uniqueIPs: new Set(), lastSeenIP: 'Unknown', countryCode: 'XX' // Store ISO code }; } this.countryTrackingStats[country].hits++; // Update lastSeen if the event is newer than what we have if (timestamp >= this.countryTrackingStats[country].lastSeen) { this.countryTrackingStats[country].lastSeen = timestamp; } // Update additional fields if event data is available if (event) { // Store the actual ISO code from Elasticsearch if (event.iso_code || event.country_code) { this.countryTrackingStats[country].countryCode = event.iso_code || event.country_code; } // Track unique IPs for this country if (event.ip) { this.countryTrackingStats[country].uniqueIPs.add(event.ip); this.countryTrackingStats[country].lastSeenIP = event.ip; } // Track protocol counts to determine top protocol if (event.protocol) { const normalizedProtocol = this.normalizeProtocol(event.protocol); if (!this.countryTrackingStats[country].protocolCounts[normalizedProtocol]) { this.countryTrackingStats[country].protocolCounts[normalizedProtocol] = 0; } this.countryTrackingStats[country].protocolCounts[normalizedProtocol]++; // Update top protocol (most frequent) let maxCount = 0; let topProtocol = 'Unknown'; for (const [protocol, count] of Object.entries(this.countryTrackingStats[country].protocolCounts)) { if (count > maxCount) { maxCount = count; topProtocol = protocol; } } this.countryTrackingStats[country].topProtocol = topProtocol; } } // Update country table if it's the active tab if (this.activeTab === 'countries') { this.updateTopCountriesTable(); } } updateTopIPsTable() { const tbody = document.getElementById('ip-tracking'); if (!tbody || !this.ipStats) return; // Sort IPs by hits (descending) const sortedIPs = Object.values(this.ipStats) .sort((a, b) => b.hits - a.hits) .slice(0, 100); // Top 100 tbody.innerHTML = ''; sortedIPs.forEach((ipData, index) => { const row = document.createElement('tr'); // Format timestamp as YYYY-MM-DD HH:MM:SS const eventTime = ipData.lastSeen; const year = eventTime.getFullYear(); const month = String(eventTime.getMonth() + 1).padStart(2, '0'); const day = String(eventTime.getDate()).padStart(2, '0'); const hours = String(eventTime.getHours()).padStart(2, '0'); const minutes = String(eventTime.getMinutes()).padStart(2, '0'); const seconds = String(eventTime.getSeconds()).padStart(2, '0'); const timeDisplay = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // Use stored ISO code instead of converting country name const flagCode = ipData.countryCode || 'XX'; // Protocol badge styling const protocol = ipData.lastProtocol.toLowerCase(); const protocolClass = `protocol-${protocol}`; // Create cells using DOM API to prevent XSS const addCell = (className, text) => { const td = document.createElement('td'); td.className = className; td.textContent = text; row.appendChild(td); return td; }; addCell('rank-cell', index + 1); addCell('hits-cell', ipData.hits); addCell('ip-cell', ipData.ip); addCell('ip-rep-cell', ipData.reputation); const flagCell = document.createElement('td'); flagCell.className = 'flag-cell'; const flagImg = document.createElement('img'); flagImg.src = `static/flags/${flagCode}.svg`; flagImg.alt = ipData.country || 'Unknown'; flagImg.className = 'flag-icon'; flagImg.onerror = function() { this.src = 'static/flags/XX.svg'; }; flagCell.appendChild(flagImg); row.appendChild(flagCell); addCell('country-cell', ipData.country || 'Unknown'); const protocolCell = document.createElement('td'); protocolCell.className = 'protocol-cell'; const badge = document.createElement('span'); badge.className = `protocol-badge ${protocolClass}`; badge.textContent = ipData.lastProtocol; protocolCell.appendChild(badge); row.appendChild(protocolCell); addCell('time-cell', timeDisplay); tbody.appendChild(row); }); } updateTopCountriesTable() { const tbody = document.getElementById('country-tracking'); if (!tbody || !this.countryTrackingStats) return; // Sort countries by hits (descending) const sortedCountries = Object.values(this.countryTrackingStats) .sort((a, b) => b.hits - a.hits) .slice(0, 100); // Top 100 tbody.innerHTML = ''; sortedCountries.forEach((countryData, index) => { const row = document.createElement('tr'); // Format timestamp as YYYY-MM-DD HH:MM:SS const eventTime = countryData.lastSeen; const year = eventTime.getFullYear(); const month = String(eventTime.getMonth() + 1).padStart(2, '0'); const day = String(eventTime.getDate()).padStart(2, '0'); const hours = String(eventTime.getHours()).padStart(2, '0'); const minutes = String(eventTime.getMinutes()).padStart(2, '0'); const seconds = String(eventTime.getSeconds()).padStart(2, '0'); const timeDisplay = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // Use stored ISO code instead of converting country name const flagCode = countryData.countryCode || 'XX'; // Protocol badge styling const protocol = countryData.topProtocol.toLowerCase(); const protocolClass = `protocol-${protocol}`; // Get unique IP count const uniqueIPCount = countryData.uniqueIPs ? countryData.uniqueIPs.size : 0; // Create cells using DOM API to prevent XSS const addCell = (className, text) => { const td = document.createElement('td'); td.className = className; td.textContent = text; row.appendChild(td); return td; }; addCell('rank-cell', index + 1); addCell('hits-cell', countryData.hits); const flagCell = document.createElement('td'); flagCell.className = 'flag-cell'; const flagImg = document.createElement('img'); flagImg.src = `static/flags/${flagCode}.svg`; flagImg.alt = countryData.country || 'Unknown'; flagImg.className = 'flag-icon'; flagImg.onerror = function() { this.src = 'static/flags/XX.svg'; }; flagCell.appendChild(flagImg); row.appendChild(flagCell); addCell('country-cell', countryData.country || 'Unknown'); const protocolCell = document.createElement('td'); protocolCell.className = 'protocol-cell'; const badge = document.createElement('span'); badge.className = `protocol-badge ${protocolClass}`; badge.textContent = countryData.topProtocol; protocolCell.appendChild(badge); row.appendChild(protocolCell); addCell('unique-ips-cell', uniqueIPCount); addCell('last-ip-cell', countryData.lastSeenIP); addCell('time-cell', timeDisplay); tbody.appendChild(row); }); } updateProtocolStats(protocol) { if (!protocol) return; // Normalize protocol to known protocols or "OTHER" const normalizedProtocol = this.normalizeProtocol(protocol); this.protocolStats[normalizedProtocol] = (this.protocolStats[normalizedProtocol] || 0) + 1; // Update protocol chart if it exists if (this.charts.protocol) { const labels = Object.keys(this.protocolStats); const data = Object.values(this.protocolStats); this.charts.protocol.data.labels = labels; this.charts.protocol.data.datasets[0].data = data.map(d => Math.sqrt(d)); this.charts.protocol.data.datasets[0].originalData = data; this.charts.protocol.update('none'); // Update without animation for performance } } initThreatHeatmap() { const container = document.getElementById('threat-heatmap'); if (!container) return; // Create heatmap grid container.innerHTML = `
Low
High
`; this.setupHeatmapData(); this.updateThreatHeatmap(); } setupHeatmapData() { // Initialize 24 hours of heatmap data with real data this.heatmapData = Array.from({ length: 24 }, (_, hour) => ({ hour: hour, intensity: 0, // Will be calculated from real attack data attacks: 0 // Will be calculated from real attack data })); // Populate with real attack data this.populateHeatmapFromHistory(); } // Populate heatmap with real attack data from history populateHeatmapFromHistory() { if (!this.attackHistory.length) { console.log(`[DEBUG] No attack history available for heatmap population`); return; } console.log(`[DEBUG] Populating heatmap from ${this.attackHistory.length} attacks`); // Reset counts this.heatmapData.forEach(hourData => { hourData.attacks = 0; hourData.intensity = 0; }); const now = Date.now(); const last24Hours = now - (24 * 60 * 60 * 1000); // 24 hours ago // Filter attacks from last 24 hours const recentAttacks = this.attackHistory.filter(attack => attack.timestamp >= last24Hours ); console.log(`[DEBUG] Found ${recentAttacks.length} attacks in last 24 hours for heatmap`); // Count attacks per hour recentAttacks.forEach(attack => { const attackTime = new Date(attack.timestamp); const hour = attackTime.getHours(); if (hour >= 0 && hour <= 23) { this.heatmapData[hour].attacks++; } }); // Calculate intensity based on attack counts const maxAttacks = Math.max(...this.heatmapData.map(h => h.attacks), 1); this.heatmapData.forEach(hourData => { hourData.intensity = maxAttacks > 0 ? (hourData.attacks / maxAttacks) * 100 : 0; }); console.log(`[DEBUG] Heatmap populated - attacks per hour:`, this.heatmapData.map(h => h.attacks)); } // Add attack to heatmap data addAttackToHeatmap(attack, triggerUpdate = true) { const attackTime = new Date(attack.timestamp || Date.now()); const hour = attackTime.getHours(); if (hour >= 0 && hour <= 23) { this.heatmapData[hour].attacks++; // Recalculate intensity const maxAttacks = Math.max(...this.heatmapData.map(h => h.attacks), 1); this.heatmapData.forEach(hourData => { hourData.intensity = maxAttacks > 0 ? (hourData.attacks / maxAttacks) * 100 : 0; }); // Update heatmap display only if not restoring if (triggerUpdate) { this.updateThreatHeatmap(); } } } updateThreatHeatmap() { const timelineGrid = document.querySelector('.timeline-grid'); const timelineLabels = document.querySelector('.timeline-labels'); if (!timelineGrid || !timelineLabels) return; // Create hour labels timelineLabels.innerHTML = ''; for (let i = 0; i < 24; i += 3) { const label = document.createElement('div'); label.className = 'timeline-label'; label.textContent = `${i.toString().padStart(2, '0')}:00`; timelineLabels.appendChild(label); } // Create heatmap cells timelineGrid.innerHTML = ''; this.heatmapData.forEach((data, index) => { const cell = document.createElement('div'); cell.className = 'heatmap-cell'; cell.title = `${data.hour}:00 - ${data.attacks} ataques`; cell.style.backgroundColor = this.getHeatmapColor(data.intensity); cell.addEventListener('click', () => { this.showHeatmapDetails(data); }); timelineGrid.appendChild(cell); }); } getHeatmapColor(intensity) { // Greyish/blue tone color gradient: Light Grey (safe) to Dark Blue (danger) const ratio = intensity / 100; if (ratio <= 0.25) { // Light Grey to Light Blue (0-25%) - Safe/Low threat const localRatio = ratio / 0.25; const r = Math.floor(240 - localRatio * 65); // 240 -> 175 const g = Math.floor(240 - localRatio * 25); // 240 -> 215 const b = Math.floor(240 - localRatio * 15); // 240 -> 225 return `rgba(${r}, ${g}, ${b}, 0.85)`; } else if (ratio <= 0.5) { // Light Blue to Medium Blue (25-50%) - Moderate threat const localRatio = (ratio - 0.25) / 0.25; const r = Math.floor(175 - localRatio * 75); // 175 -> 100 const g = Math.floor(215 - localRatio * 65); // 215 -> 150 const b = Math.floor(225 - localRatio * 25); // 225 -> 200 return `rgba(${r}, ${g}, ${b}, 0.85)`; } else if (ratio <= 0.75) { // Medium Blue to Dark Blue (50-75%) - High threat const localRatio = (ratio - 0.5) / 0.25; const r = Math.floor(100 - localRatio * 50); // 100 -> 50 const g = Math.floor(150 - localRatio * 70); // 150 -> 80 const b = Math.floor(200 - localRatio * 40); // 200 -> 160 return `rgba(${r}, ${g}, ${b}, 0.85)`; } else { // Dark Blue to Navy Blue (75-100%) - Critical threat const localRatio = (ratio - 0.75) / 0.25; const r = Math.floor(50 - localRatio * 30); // 50 -> 20 const g = Math.floor(80 - localRatio * 50); // 80 -> 30 const b = Math.floor(160 - localRatio * 60); // 160 -> 100 return `rgba(${r}, ${g}, ${b}, 0.9)`; } } showHeatmapDetails(data) { // Create and show tooltip near the heatmap const heatmapContainer = document.getElementById('threat-heatmap'); if (!heatmapContainer) return; // Remove existing tooltip const existingTooltip = document.querySelector('.heatmap-tooltip'); if (existingTooltip) { existingTooltip.remove(); } // Create new tooltip const tooltip = document.createElement('div'); tooltip.className = 'heatmap-tooltip'; const strong = document.createElement('strong'); strong.textContent = `Hour ${data.hour}:00`; tooltip.appendChild(strong); tooltip.appendChild(document.createElement('br')); tooltip.appendChild(document.createTextNode(`${data.attacks} ataques`)); tooltip.appendChild(document.createElement('br')); tooltip.appendChild(document.createTextNode(`${Math.round(data.intensity)}% intensidad`)); // Position tooltip near the heatmap const rect = heatmapContainer.getBoundingClientRect(); tooltip.style.cssText = ` position: fixed; left: ${rect.left + 10}px; top: ${rect.bottom - 80}px; background: var(--bg-modal); color: var(--text-primary); padding: var(--spacing-sm); border-radius: var(--radius-md); border: 1px solid var(--border-primary); box-shadow: 0 4px 16px var(--shadow-medium); z-index: 10001; font-size: var(--font-xs); pointer-events: none; `; document.body.appendChild(tooltip); // Auto-remove after 3 seconds setTimeout(() => { if (tooltip && tooltip.parentNode) { tooltip.remove(); } }, 3000); } } // Initialize dashboard when DOM is loaded // Initialize dashboard when DOM and scripts are loaded window.addEventListener('load', () => { // Wait for Chart.js to be available function initWhenReady() { if (typeof Chart !== 'undefined') { console.log('[DEBUG] Chart.js available, initializing Attack Map Dashboard...'); window.attackMapDashboard = new AttackMapDashboard(); console.log('[DEBUG] Dashboard initialized:', window.attackMapDashboard); } else { console.log('[DEBUG] Chart.js not yet available, waiting...'); setTimeout(initWhenReady, 100); } } initWhenReady(); }); // Export for use in other modules window.AttackMapDashboard = AttackMapDashboard;