From 26d7445eb7ddc03c89e3b0d0ce0bf234544e6f5e Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Mon, 16 Feb 2026 10:51:01 +0000 Subject: [PATCH] Improve IP blocking and log analysis performance Optimize critical IP blocking using bulk operations and refine log query for better accuracy and performance. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 545f1e17-399b-4078-b609-9458832db9c4 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/B8f0CIv --- attached_assets/image_1771238643212.png | Bin 0 -> 62043 bytes client/src/pages/Dashboard.tsx | 2 +- python_ml/main.py | 78 +++++--- python_ml/mikrotik_manager.py | 242 ++++++++++++++++++------ server/routes.ts | 16 +- 5 files changed, 244 insertions(+), 94 deletions(-) create mode 100644 attached_assets/image_1771238643212.png diff --git a/attached_assets/image_1771238643212.png b/attached_assets/image_1771238643212.png new file mode 100644 index 0000000000000000000000000000000000000000..9f41773f8e3f1a5e618ea68f7d8572616e47ab78 GIT binary patch literal 62043 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 Mt0I$s)x`Jz0_Ox>+W-In literal 0 HcmV?d00001 diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index 717e905..d69f92a 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -206,7 +206,7 @@ export default function Dashboard() { {stats?.logs.recent || 0}

- Ultimi 1000 log analizzati + Ultime 24 ore

diff --git a/python_ml/main.py b/python_ml/main.py index 6201f44..c44828a 100644 --- a/python_ml/main.py +++ b/python_ml/main.py @@ -505,7 +505,7 @@ async def block_ip(request: BlockIPRequest): @app.post("/block-all-critical") async def block_all_critical(request: BlockAllCriticalRequest): - """Blocca tutti gli IP critici non ancora bloccati sui router""" + """Blocca tutti gli IP critici non ancora bloccati sui router - versione ottimizzata con bulk blocking""" try: conn = get_db_connection() cursor = conn.cursor(cursor_factory=RealDictCursor) @@ -514,6 +514,8 @@ async def block_all_critical(request: BlockAllCriticalRequest): routers = cursor.fetchall() if not routers: + cursor.close() + conn.close() raise HTTPException(status_code=400, detail="Nessun router configurato") cursor.execute(""" @@ -541,49 +543,63 @@ async def block_all_critical(request: BlockAllCriticalRequest): "skipped_whitelisted": 0 } + ip_data = {row['source_ip']: row for row in unblocked_ips} + ip_list = [row['source_ip'] for row in unblocked_ips] + + print(f"[BLOCK-ALL] Avvio blocco massivo: {len(ip_list)} IP con score >= {request.min_score} su {len(routers)} router") + + bulk_results = await mikrotik_manager.bulk_block_ips_on_all_routers( + routers=routers, + ip_list=ip_list, + list_name=request.list_name, + comment_prefix=f"IDS bulk-block (score>={request.min_score})", + timeout_duration="1h", + concurrency=10 + ) + blocked_count = 0 failed_count = 0 results_detail = [] - for ip_row in unblocked_ips: - ip = ip_row['source_ip'] - score = ip_row['max_score'] - anomaly = ip_row['anomaly_type'] - - try: - block_results = await mikrotik_manager.block_ip_on_all_routers( - routers, - ip, - list_name=request.list_name, - comment=f"IDS bulk-block: {anomaly} (score: {score:.0f})" - ) - - if any(block_results.values()): - cursor.execute(""" - UPDATE detections - SET blocked = true, blocked_at = NOW() - WHERE source_ip = %s AND blocked = false - """, (ip,)) - blocked_count += 1 - results_detail.append({"ip": ip, "score": score, "status": "blocked"}) - else: - failed_count += 1 - results_detail.append({"ip": ip, "score": score, "status": "failed"}) - - except Exception as e: - failed_count += 1 - results_detail.append({"ip": ip, "score": score, "status": f"error: {str(e)}"}) + blocked_source_ips = [] + + for ip in ip_list: + router_results = bulk_results.get(ip, {}) + score = ip_data[ip]['max_score'] + anomaly = ip_data[ip]['anomaly_type'] + + if any(router_results.values()): + blocked_count += 1 + blocked_source_ips.append(ip) + results_detail.append({"ip": ip, "score": float(score), "status": "blocked"}) + else: + failed_count += 1 + results_detail.append({"ip": ip, "score": float(score), "status": "failed"}) + + if blocked_source_ips: + batch_size = 100 + for i in range(0, len(blocked_source_ips), batch_size): + batch = blocked_source_ips[i:i+batch_size] + placeholders = ','.join(['%s'] * len(batch)) + cursor.execute(f""" + UPDATE detections + SET blocked = true, blocked_at = NOW() + WHERE source_ip IN ({placeholders}) AND blocked = false + """, batch) + conn.commit() + print(f"[BLOCK-ALL] Database aggiornato: {len(blocked_source_ips)} IP marcati come bloccati") - conn.commit() cursor.close() conn.close() + print(f"[BLOCK-ALL] Completato: {blocked_count} bloccati, {failed_count} falliti su {len(ip_list)} totali") + return { "message": f"Blocco massivo completato: {blocked_count} IP bloccati, {failed_count} falliti", "blocked": blocked_count, "failed": failed_count, "total_critical": len(unblocked_ips), - "details": results_detail[:50] + "details": results_detail[:100] } except HTTPException: diff --git a/python_ml/mikrotik_manager.py b/python_ml/mikrotik_manager.py index 74a3f00..bf2694c 100644 --- a/python_ml/mikrotik_manager.py +++ b/python_ml/mikrotik_manager.py @@ -1,14 +1,14 @@ """ MikroTik Manager - Gestione router tramite API REST Più veloce e affidabile di SSH per 10+ router +Porte REST API: 80 (HTTP) o 443 (HTTPS) """ import httpx import asyncio import ssl -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Set from datetime import datetime -import hashlib import base64 @@ -16,39 +16,33 @@ class MikroTikManager: """ Gestisce comunicazione con router MikroTik tramite API REST Supporta operazioni parallele su multipli router + Porte default: 80 (HTTP REST) o 443 (HTTPS REST) """ - def __init__(self, timeout: int = 10): + def __init__(self, timeout: int = 15): self.timeout = timeout - self.clients = {} # Cache di client HTTP per router + self.clients = {} - def _get_client(self, router_ip: str, username: str, password: str, port: int = 8728, use_ssl: bool = False) -> httpx.AsyncClient: + def _get_client(self, router_ip: str, username: str, password: str, port: int = 80, use_ssl: bool = False) -> httpx.AsyncClient: """Ottiene o crea client HTTP per un router""" key = f"{router_ip}:{port}:{use_ssl}" if key not in self.clients: - # API REST MikroTik: - # - Porta 8728: HTTP (default) - # - Porta 8729: HTTPS (SSL) - protocol = "https" if use_ssl or port == 8729 else "http" + protocol = "https" if use_ssl or port == 443 else "http" auth = base64.b64encode(f"{username}:{password}".encode()).decode() headers = { "Authorization": f"Basic {auth}", "Content-Type": "application/json" } - # SSL context per MikroTik (supporta protocolli TLS legacy) ssl_context = None if protocol == "https": ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE - # Abilita protocolli TLS legacy per MikroTik (TLS 1.0+) try: ssl_context.minimum_version = ssl.TLSVersion.TLSv1 except AttributeError: - # Python < 3.7 fallback pass - # Abilita cipher suite legacy per compatibilità ssl_context.set_ciphers('DEFAULT@SECLEVEL=1') self.clients[key] = httpx.AsyncClient( @@ -59,20 +53,41 @@ class MikroTikManager: ) return self.clients[key] - async def test_connection(self, router_ip: str, username: str, password: str, port: int = 8728, use_ssl: bool = False) -> bool: + async def test_connection(self, router_ip: str, username: str, password: str, port: int = 80, use_ssl: bool = False) -> bool: """Testa connessione a un router""" try: - # Auto-detect SSL: porta 8729 = SSL - if port == 8729: + if port == 443: use_ssl = True client = self._get_client(router_ip, username, password, port, use_ssl) - # Prova a leggere system identity response = await client.get("/rest/system/identity") return response.status_code == 200 except Exception as e: print(f"[ERROR] Connessione a {router_ip}:{port} fallita: {e}") return False + async def _get_existing_ips_set( + self, + router_ip: str, + username: str, + password: str, + list_name: str, + port: int = 80, + use_ssl: bool = False + ) -> Set[str]: + """Scarica la address-list UNA VOLTA e ritorna un set di IP già presenti""" + try: + if port == 443: + use_ssl = True + client = self._get_client(router_ip, username, password, port, use_ssl) + response = await client.get(f"/rest/ip/firewall/address-list", params={"list": list_name}) + if response.status_code == 200: + entries = response.json() + return {entry.get('address', '') for entry in entries if entry.get('list') == list_name} + return set() + except Exception as e: + print(f"[ERROR] Lettura address-list da {router_ip}: {e}") + return set() + async def add_address_list( self, router_ip: str, @@ -82,29 +97,32 @@ class MikroTikManager: list_name: str = "ddos_blocked", comment: str = "", timeout_duration: str = "1h", - port: int = 8728, - use_ssl: bool = False + port: int = 80, + use_ssl: bool = False, + skip_check: bool = False, + existing_ips: Optional[Set[str]] = None ) -> bool: """ Aggiunge IP alla address-list del router - timeout_duration: es. "1h", "30m", "1d" + skip_check: se True, non verifica se l'IP esiste già (per bulk operations) + existing_ips: set di IP già nella lista (per evitare GET per ogni IP) """ try: - # Auto-detect SSL: porta 8729 = SSL - if port == 8729: + if port == 443: use_ssl = True client = self._get_client(router_ip, username, password, port, use_ssl) - # Controlla se IP già esiste - response = await client.get("/rest/ip/firewall/address-list") - if response.status_code == 200: - existing = response.json() - for entry in existing: - if entry.get('address') == ip_address and entry.get('list') == list_name: - print(f"[INFO] IP {ip_address} già in lista {list_name} su {router_ip}") + if not skip_check: + if existing_ips is not None: + if ip_address in existing_ips: return True + else: + response = await client.get("/rest/ip/firewall/address-list") + if response.status_code == 200: + for entry in response.json(): + if entry.get('address') == ip_address and entry.get('list') == list_name: + return True - # Aggiungi nuovo IP data = { "list": list_name, "address": ip_address, @@ -114,11 +132,25 @@ class MikroTikManager: response = await client.post("/rest/ip/firewall/address-list/add", json=data) - if response.status_code == 201 or response.status_code == 200: - print(f"[SUCCESS] IP {ip_address} aggiunto a {list_name} su {router_ip} (timeout: {timeout_duration})") + if response.status_code in (200, 201): + print(f"[SUCCESS] IP {ip_address} aggiunto a {list_name} su {router_ip}") return True + elif response.status_code in (400, 409): + resp_text = response.text.lower() + if "already" in resp_text or "exists" in resp_text or "duplicate" in resp_text or "failure: already" in resp_text: + return True + try: + verify_resp = await client.get("/rest/ip/firewall/address-list", params={"address": ip_address}) + if verify_resp.status_code == 200: + for entry in verify_resp.json(): + if entry.get('address') == ip_address and entry.get('list') == list_name: + return True + except Exception: + pass + print(f"[ERROR] IP {ip_address} su {router_ip}: {response.status_code} - {response.text}") + return False else: - print(f"[ERROR] Errore aggiunta IP {ip_address} su {router_ip}: {response.status_code} - {response.text}") + print(f"[ERROR] Aggiunta IP {ip_address} su {router_ip}: {response.status_code} - {response.text}") return False except Exception as e: @@ -132,17 +164,15 @@ class MikroTikManager: password: str, ip_address: str, list_name: str = "ddos_blocked", - port: int = 8728, + port: int = 80, use_ssl: bool = False ) -> bool: """Rimuove IP dalla address-list del router""" try: - # Auto-detect SSL: porta 8729 = SSL - if port == 8729: + if port == 443: use_ssl = True client = self._get_client(router_ip, username, password, port, use_ssl) - # Trova ID dell'entry response = await client.get("/rest/ip/firewall/address-list") if response.status_code != 200: return False @@ -151,7 +181,6 @@ class MikroTikManager: for entry in entries: if entry.get('address') == ip_address and entry.get('list') == list_name: entry_id = entry.get('.id') - # Rimuovi entry response = await client.delete(f"/rest/ip/firewall/address-list/{entry_id}") if response.status_code == 200: print(f"[SUCCESS] IP {ip_address} rimosso da {list_name} su {router_ip}") @@ -170,13 +199,12 @@ class MikroTikManager: username: str, password: str, list_name: Optional[str] = None, - port: int = 8728, + port: int = 80, use_ssl: bool = False ) -> List[Dict]: """Ottiene address-list da router""" try: - # Auto-detect SSL: porta 8729 = SSL - if port == 8729: + if port == 443: use_ssl = True client = self._get_client(router_ip, username, password, port, use_ssl) response = await client.get("/rest/ip/firewall/address-list") @@ -203,7 +231,6 @@ class MikroTikManager: ) -> Dict[str, bool]: """ Blocca IP su tutti i router in parallelo - routers: lista di dict con {ip_address, username, password, api_port} Returns: dict con {router_ip: success_bool} """ tasks = [] @@ -221,20 +248,125 @@ class MikroTikManager: list_name=list_name, comment=comment, timeout_duration=timeout_duration, - port=router.get('api_port', 8728) + port=router.get('api_port', 80) ) tasks.append(task) router_ips.append(router['ip_address']) - # Esegui in parallelo results = await asyncio.gather(*tasks, return_exceptions=True) - # Combina risultati return { router_ip: result if not isinstance(result, Exception) else False for router_ip, result in zip(router_ips, results) } + async def bulk_block_ips_on_all_routers( + self, + routers: List[Dict], + ip_list: List[str], + list_name: str = "ddos_blocked", + comment_prefix: str = "IDS bulk-block", + timeout_duration: str = "1h", + concurrency: int = 10, + progress_callback=None + ) -> Dict[str, Dict[str, bool]]: + """ + Blocco massivo ottimizzato: scarica address-list UNA VOLTA per router, + poi aggiunge solo IP non presenti con concurrency limitata. + + Returns: {ip: {router_ip: success_bool}} + """ + enabled_routers = [r for r in routers if r.get('enabled', True)] + if not enabled_routers: + return {} + + print(f"[BULK] Inizio blocco massivo: {len(ip_list)} IP su {len(enabled_routers)} router") + + existing_cache = {} + for router in enabled_routers: + router_ip = router['ip_address'] + port = router.get('api_port', 80) + use_ssl = port == 443 + existing_ips = await self._get_existing_ips_set( + router_ip, router['username'], router['password'], + list_name, port, use_ssl + ) + existing_cache[router_ip] = existing_ips + print(f"[BULK] Router {router_ip}: {len(existing_ips)} IP già in lista") + + new_ips = [] + for ip in ip_list: + is_new_on_any = False + for router in enabled_routers: + if ip not in existing_cache.get(router['ip_address'], set()): + is_new_on_any = True + break + if is_new_on_any: + new_ips.append(ip) + + already_blocked = len(ip_list) - len(new_ips) + print(f"[BULK] {already_blocked} IP già bloccati, {len(new_ips)} nuovi da bloccare") + + results = {} + semaphore = asyncio.Semaphore(concurrency) + blocked_count = 0 + + async def block_single_ip(ip: str) -> Dict[str, bool]: + nonlocal blocked_count + async with semaphore: + router_results = {} + tasks = [] + r_ips = [] + + for router in enabled_routers: + r_ip = router['ip_address'] + if ip in existing_cache.get(r_ip, set()): + router_results[r_ip] = True + continue + + task = self.add_address_list( + router_ip=r_ip, + username=router['username'], + password=router['password'], + ip_address=ip, + list_name=list_name, + comment=f"{comment_prefix} {ip}", + timeout_duration=timeout_duration, + port=router.get('api_port', 80), + skip_check=True + ) + tasks.append(task) + r_ips.append(r_ip) + + if tasks: + task_results = await asyncio.gather(*tasks, return_exceptions=True) + for r_ip, result in zip(r_ips, task_results): + router_results[r_ip] = result if not isinstance(result, Exception) else False + + blocked_count += 1 + if progress_callback and blocked_count % 50 == 0: + await progress_callback(blocked_count, len(new_ips)) + + return router_results + + batch_tasks = [block_single_ip(ip) for ip in new_ips] + batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True) + + for ip, result in zip(new_ips, batch_results): + if isinstance(result, Exception): + results[ip] = {r['ip_address']: False for r in enabled_routers} + else: + results[ip] = result + + for ip in ip_list: + if ip not in results: + results[ip] = {r['ip_address']: True for r in enabled_routers} + + total_success = sum(1 for ip_results in results.values() if any(ip_results.values())) + print(f"[BULK] Completato: {total_success}/{len(ip_list)} IP bloccati con successo") + + return results + async def unblock_ip_on_all_routers( self, routers: List[Dict], @@ -255,7 +387,7 @@ class MikroTikManager: password=router['password'], ip_address=ip_address, list_name=list_name, - port=router.get('api_port', 8728) + port=router.get('api_port', 80) ) tasks.append(task) router_ips.append(router['ip_address']) @@ -274,7 +406,6 @@ class MikroTikManager: self.clients.clear() -# Fallback SSH per router che non supportano API REST class MikroTikSSHManager: """Fallback usando SSH se API REST non disponibile""" @@ -288,29 +419,26 @@ class MikroTikSSHManager: if __name__ == "__main__": - # Test MikroTik Manager async def test(): manager = MikroTikManager() - # Test router demo (sostituire con dati reali) test_router = { 'ip_address': '192.168.1.1', 'username': 'admin', 'password': 'password', - 'api_port': 8728, + 'api_port': 80, 'enabled': True } - # Test connessione print("Testing connection...") connected = await manager.test_connection( test_router['ip_address'], test_router['username'], - test_router['password'] + test_router['password'], + port=test_router['api_port'] ) print(f"Connected: {connected}") - # Test blocco IP if connected: print("\nTesting IP block...") result = await manager.add_address_list( @@ -320,22 +448,22 @@ if __name__ == "__main__": ip_address='10.0.0.100', list_name='ddos_test', comment='Test IDS', - timeout_duration='10m' + timeout_duration='10m', + port=test_router['api_port'] ) print(f"Block result: {result}") - # Leggi lista print("\nReading address list...") entries = await manager.get_address_list( test_router['ip_address'], test_router['username'], test_router['password'], - list_name='ddos_test' + list_name='ddos_test', + port=test_router['api_port'] ) print(f"Entries: {entries}") await manager.close_all() - # Esegui test print("=== TEST MIKROTIK MANAGER ===\n") asyncio.run(test()) diff --git a/server/routes.ts b/server/routes.ts index e226486..9e885e8 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -492,10 +492,16 @@ export async function registerRoutes(app: Express): Promise { high: sql`count(*) filter (where ${detections.riskScore}::numeric >= 70 and ${detections.riskScore}::numeric < 85)::int`, }).from(detections); - const logStats = await db.select({ - count: sql`count(*)::int`, - }).from(networkLogs) - .where(gte(networkLogs.timestamp, new Date(Date.now() - 24 * 60 * 60 * 1000))); + let logCount = 0; + try { + const logStats = await db.execute( + sql`SELECT count(*)::int as count FROM network_logs WHERE timestamp >= NOW() - INTERVAL '24 hours'` + ); + logCount = (logStats as any).rows?.[0]?.count ?? (logStats as any)[0]?.count ?? 0; + } catch (logError) { + console.error('[DB WARN] Log count query failed:', logError); + logCount = 0; + } res.json({ routers: { @@ -509,7 +515,7 @@ export async function registerRoutes(app: Express): Promise { high: detectionStats[0]?.high || 0 }, logs: { - recent: logStats[0]?.count || 0 + recent: logCount }, whitelist: { total: whitelistResult.total