3680 lines
136 KiB
JavaScript
3680 lines
136 KiB
JavaScript
|
|
/**
|
||
|
|
* 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 = `
|
||
|
|
<div class="heatmap-grid">
|
||
|
|
<div class="heatmap-content">
|
||
|
|
<div class="heatmap-timeline">
|
||
|
|
<div class="timeline-grid"></div>
|
||
|
|
<div class="timeline-labels"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="heatmap-legend">
|
||
|
|
<span class="legend-label">Low</span>
|
||
|
|
<div class="legend-gradient"></div>
|
||
|
|
<span class="legend-label">High</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
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;
|