Add IP geolocation and AS information to detection records
Integrates IP geolocation and Autonomous System (AS) information into detection records by modifying the frontend to display this data and updating the backend to perform asynchronous batch lookups for efficiency. This enhancement includes database schema updates and the creation of a new IP geolocation service. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: e81fd4a1-b7b0-48d2-ae38-f5905e278343 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/SXFWABi
This commit is contained in:
parent
ae106cf655
commit
1b9df79d56
@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { AlertTriangle, Search, Shield, Eye } from "lucide-react";
|
import { AlertTriangle, Search, Shield, Eye, Globe, MapPin, Building2 } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Detection } from "@shared/schema";
|
import type { Detection } from "@shared/schema";
|
||||||
@ -107,6 +107,34 @@ export default function Detections() {
|
|||||||
{detection.reason}
|
{detection.reason}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Geolocation Info */}
|
||||||
|
{(detection.country || detection.organization || detection.asNumber) && (
|
||||||
|
<div className="flex flex-wrap gap-3 mb-3 text-sm" data-testid={`geo-info-${detection.id}`}>
|
||||||
|
{detection.country && (
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<Globe className="h-3.5 w-3.5" />
|
||||||
|
<span data-testid={`text-country-${detection.id}`}>
|
||||||
|
{detection.city ? `${detection.city}, ${detection.country}` : detection.country}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{detection.organization && (
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<Building2 className="h-3.5 w-3.5" />
|
||||||
|
<span data-testid={`text-org-${detection.id}`}>{detection.organization}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{detection.asNumber && (
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<MapPin className="h-3.5 w-3.5" />
|
||||||
|
<span data-testid={`text-as-${detection.id}`}>
|
||||||
|
{detection.asNumber} {detection.asName && `- ${detection.asName}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Risk Score</p>
|
<p className="text-muted-foreground text-xs">Risk Score</p>
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
-- Migration 004: Add geolocation and AS information to detections table
|
||||||
|
-- Date: 2025-11-22
|
||||||
|
-- Description: Adds country, city, organization, AS number/name, ISP fields
|
||||||
|
|
||||||
|
ALTER TABLE detections
|
||||||
|
ADD COLUMN IF NOT EXISTS country TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS country_code TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS city TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS organization TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS as_number TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS as_name TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS isp TEXT;
|
||||||
|
|
||||||
|
-- Create index on country for fast filtering
|
||||||
|
CREATE INDEX IF NOT EXISTS country_idx ON detections(country);
|
||||||
|
|
||||||
|
-- Update schema_version
|
||||||
|
INSERT INTO schema_version (version, description)
|
||||||
|
VALUES (4, 'Add geolocation and AS information to detections')
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
version = 4,
|
||||||
|
applied_at = NOW(),
|
||||||
|
description = 'Add geolocation and AS information to detections';
|
||||||
203
python_ml/ip_geolocation.py
Normal file
203
python_ml/ip_geolocation.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
"""
|
||||||
|
IP Geolocation Service
|
||||||
|
Usa ip-api.com per ottenere informazioni geografiche e AS per IP address
|
||||||
|
Free tier: 45 richieste/minuto
|
||||||
|
Supporta lookup async batch per performance ottimali
|
||||||
|
"""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
class IPGeolocationService:
|
||||||
|
"""
|
||||||
|
Servizio per geolocalizzazione IP usando ip-api.com
|
||||||
|
Include caching per ridurre chiamate API
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_url = "http://ip-api.com/json/{ip}"
|
||||||
|
self.batch_api_url = "http://ip-api.com/batch"
|
||||||
|
self.cache: Dict[str, Dict] = {}
|
||||||
|
self.last_request_time = 0
|
||||||
|
self.min_delay = 1.5 # secondi tra richieste (per restare sotto 45/min)
|
||||||
|
self.max_batch_size = 100 # ip-api.com supporta max 100 IP per batch
|
||||||
|
|
||||||
|
def lookup(self, ip_address: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Ottieni informazioni geografiche per un IP
|
||||||
|
Returns: Dict con country, city, org, as, isp o None se errore
|
||||||
|
"""
|
||||||
|
# Check cache
|
||||||
|
if ip_address in self.cache:
|
||||||
|
return self.cache[ip_address]
|
||||||
|
|
||||||
|
# Rate limiting: attendi se troppo veloce
|
||||||
|
current_time = time.time()
|
||||||
|
time_since_last = current_time - self.last_request_time
|
||||||
|
if time_since_last < self.min_delay:
|
||||||
|
time.sleep(self.min_delay - time_since_last)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Richiesta API
|
||||||
|
url = self.api_url.format(ip=ip_address)
|
||||||
|
response = httpx.get(url, timeout=5.0)
|
||||||
|
|
||||||
|
self.last_request_time = time.time()
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Controlla se successo
|
||||||
|
if data.get('status') == 'success':
|
||||||
|
geo_info = {
|
||||||
|
'country': data.get('country'),
|
||||||
|
'country_code': data.get('countryCode'),
|
||||||
|
'city': data.get('city'),
|
||||||
|
'organization': data.get('org'),
|
||||||
|
'as_number': data.get('as', '').split()[0] if data.get('as') else None, # es. "AS14061" da "AS14061 DigitalOcean, LLC"
|
||||||
|
'as_name': data.get('as', '').split(maxsplit=1)[1] if data.get('as') and ' ' in data.get('as') else data.get('as'),
|
||||||
|
'isp': data.get('isp'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Salva in cache
|
||||||
|
self.cache[ip_address] = geo_info
|
||||||
|
|
||||||
|
return geo_info
|
||||||
|
else:
|
||||||
|
# Errore API (es. IP privato)
|
||||||
|
print(f"[GEO] Errore lookup {ip_address}: {data.get('message', 'Unknown error')}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print(f"[GEO] HTTP {response.status_code} per {ip_address}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GEO] Errore lookup {ip_address}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def lookup_async(self, ip_address: str, client: httpx.AsyncClient) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Async lookup di un singolo IP
|
||||||
|
"""
|
||||||
|
# Check cache
|
||||||
|
if ip_address in self.cache:
|
||||||
|
return self.cache[ip_address]
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = self.api_url.format(ip=ip_address)
|
||||||
|
response = await client.get(url, timeout=5.0)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if data.get('status') == 'success':
|
||||||
|
geo_info = self._parse_geo_data(data)
|
||||||
|
self.cache[ip_address] = geo_info
|
||||||
|
return geo_info
|
||||||
|
else:
|
||||||
|
print(f"[GEO] Errore lookup {ip_address}: {data.get('message', 'Unknown')}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print(f"[GEO] HTTP {response.status_code} per {ip_address}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GEO] Errore async lookup {ip_address}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def lookup_batch_async(self, ip_addresses: List[str]) -> Dict[str, Optional[Dict]]:
|
||||||
|
"""
|
||||||
|
Async batch lookup di multiple IPs (VELOCE - parallelo!)
|
||||||
|
Usa batch API di ip-api.com per massima efficienza
|
||||||
|
Returns: Dict {ip: geo_info}
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# Filtra IP già in cache
|
||||||
|
uncached_ips = [ip for ip in ip_addresses if ip not in self.cache]
|
||||||
|
|
||||||
|
# Aggiungi IP cached
|
||||||
|
for ip in ip_addresses:
|
||||||
|
if ip in self.cache:
|
||||||
|
results[ip] = self.cache[ip]
|
||||||
|
|
||||||
|
if not uncached_ips:
|
||||||
|
return results # Tutti in cache!
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Batch API supporta max 100 IP alla volta
|
||||||
|
for i in range(0, len(uncached_ips), self.max_batch_size):
|
||||||
|
batch = uncached_ips[i:i + self.max_batch_size]
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
# Batch request
|
||||||
|
response = await client.post(
|
||||||
|
self.batch_api_url,
|
||||||
|
json=batch,
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
batch_data = response.json()
|
||||||
|
|
||||||
|
for data in batch_data:
|
||||||
|
if data.get('status') == 'success':
|
||||||
|
ip = data.get('query')
|
||||||
|
geo_info = self._parse_geo_data(data)
|
||||||
|
self.cache[ip] = geo_info
|
||||||
|
results[ip] = geo_info
|
||||||
|
else:
|
||||||
|
# IP non valido o errore
|
||||||
|
ip = data.get('query')
|
||||||
|
results[ip] = None
|
||||||
|
else:
|
||||||
|
print(f"[GEO] Batch API HTTP {response.status_code}")
|
||||||
|
# Fallback su lookup singoli
|
||||||
|
for ip in batch:
|
||||||
|
results[ip] = None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GEO] Errore batch lookup: {e}")
|
||||||
|
# Set None per tutti gli IP non processati
|
||||||
|
for ip in uncached_ips:
|
||||||
|
if ip not in results:
|
||||||
|
results[ip] = None
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _parse_geo_data(self, data: Dict) -> Dict:
|
||||||
|
"""Parse geo data from API response"""
|
||||||
|
return {
|
||||||
|
'country': data.get('country'),
|
||||||
|
'country_code': data.get('countryCode'),
|
||||||
|
'city': data.get('city'),
|
||||||
|
'organization': data.get('org'),
|
||||||
|
'as_number': data.get('as', '').split()[0] if data.get('as') else None,
|
||||||
|
'as_name': data.get('as', '').split(maxsplit=1)[1] if data.get('as') and ' ' in data.get('as') else data.get('as'),
|
||||||
|
'isp': data.get('isp'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def clear_cache(self):
|
||||||
|
"""Svuota cache"""
|
||||||
|
self.cache.clear()
|
||||||
|
|
||||||
|
def get_cache_size(self) -> int:
|
||||||
|
"""Numero IP in cache"""
|
||||||
|
return len(self.cache)
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton instance
|
||||||
|
_geo_service = None
|
||||||
|
|
||||||
|
def get_geo_service() -> IPGeolocationService:
|
||||||
|
"""Get or create singleton instance"""
|
||||||
|
global _geo_service
|
||||||
|
if _geo_service is None:
|
||||||
|
_geo_service = IPGeolocationService()
|
||||||
|
return _geo_service
|
||||||
@ -19,6 +19,7 @@ import secrets
|
|||||||
|
|
||||||
from ml_analyzer import MLAnalyzer
|
from ml_analyzer import MLAnalyzer
|
||||||
from mikrotik_manager import MikroTikManager
|
from mikrotik_manager import MikroTikManager
|
||||||
|
from ip_geolocation import get_geo_service
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@ -242,8 +243,20 @@ async def detect_anomalies(request: DetectRequest):
|
|||||||
# Detection
|
# Detection
|
||||||
detections = ml_analyzer.detect(df, risk_threshold=request.risk_threshold)
|
detections = ml_analyzer.detect(df, risk_threshold=request.risk_threshold)
|
||||||
|
|
||||||
|
# Geolocation lookup service - BATCH ASYNC per performance
|
||||||
|
geo_service = get_geo_service()
|
||||||
|
|
||||||
|
# Estrai lista IP unici per batch lookup
|
||||||
|
unique_ips = list(set(det['source_ip'] for det in detections))
|
||||||
|
|
||||||
|
# Batch lookup async (VELOCE - tutti in parallelo!)
|
||||||
|
geo_results = await geo_service.lookup_batch_async(unique_ips)
|
||||||
|
|
||||||
# Salva detections nel database
|
# Salva detections nel database
|
||||||
for det in detections:
|
for det in detections:
|
||||||
|
# Get geo info from batch results
|
||||||
|
geo_info = geo_results.get(det['source_ip'])
|
||||||
|
|
||||||
# Controlla se già esiste
|
# Controlla se già esiste
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id FROM detections WHERE source_ip = %s ORDER BY detected_at DESC LIMIT 1",
|
"SELECT id FROM detections WHERE source_ip = %s ORDER BY detected_at DESC LIMIT 1",
|
||||||
@ -252,28 +265,45 @@ async def detect_anomalies(request: DetectRequest):
|
|||||||
existing = cursor.fetchone()
|
existing = cursor.fetchone()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Aggiorna esistente
|
# Aggiorna esistente (con geo info)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE detections
|
UPDATE detections
|
||||||
SET risk_score = %s, confidence = %s, anomaly_type = %s,
|
SET risk_score = %s, confidence = %s, anomaly_type = %s,
|
||||||
reason = %s, log_count = %s, last_seen = %s
|
reason = %s, log_count = %s, last_seen = %s,
|
||||||
|
country = %s, country_code = %s, city = %s,
|
||||||
|
organization = %s, as_number = %s, as_name = %s, isp = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (
|
""", (
|
||||||
det['risk_score'], det['confidence'], det['anomaly_type'],
|
det['risk_score'], det['confidence'], det['anomaly_type'],
|
||||||
det['reason'], det['log_count'], det['last_seen'],
|
det['reason'], det['log_count'], det['last_seen'],
|
||||||
|
geo_info.get('country') if geo_info else None,
|
||||||
|
geo_info.get('country_code') if geo_info else None,
|
||||||
|
geo_info.get('city') if geo_info else None,
|
||||||
|
geo_info.get('organization') if geo_info else None,
|
||||||
|
geo_info.get('as_number') if geo_info else None,
|
||||||
|
geo_info.get('as_name') if geo_info else None,
|
||||||
|
geo_info.get('isp') if geo_info else None,
|
||||||
existing['id']
|
existing['id']
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
# Inserisci nuovo
|
# Inserisci nuovo (con geo info)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO detections
|
INSERT INTO detections
|
||||||
(source_ip, risk_score, confidence, anomaly_type, reason,
|
(source_ip, risk_score, confidence, anomaly_type, reason,
|
||||||
log_count, first_seen, last_seen)
|
log_count, first_seen, last_seen,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
country, country_code, city, organization, as_number, as_name, isp)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
""", (
|
""", (
|
||||||
det['source_ip'], det['risk_score'], det['confidence'],
|
det['source_ip'], det['risk_score'], det['confidence'],
|
||||||
det['anomaly_type'], det['reason'], det['log_count'],
|
det['anomaly_type'], det['reason'], det['log_count'],
|
||||||
det['first_seen'], det['last_seen']
|
det['first_seen'], det['last_seen'],
|
||||||
|
geo_info.get('country') if geo_info else None,
|
||||||
|
geo_info.get('country_code') if geo_info else None,
|
||||||
|
geo_info.get('city') if geo_info else None,
|
||||||
|
geo_info.get('organization') if geo_info else None,
|
||||||
|
geo_info.get('as_number') if geo_info else None,
|
||||||
|
geo_info.get('as_name') if geo_info else None,
|
||||||
|
geo_info.get('isp') if geo_info else None
|
||||||
))
|
))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
223
replit.md
223
replit.md
@ -1,7 +1,7 @@
|
|||||||
# IDS - Intrusion Detection System
|
# IDS - Intrusion Detection System
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
This project is a full-stack web application designed as an Intrusion Detection System (IDS) for MikroTik routers, leveraging Machine Learning. Its primary purpose is to monitor network traffic, detect anomalies indicative of intrusions, and automatically block malicious IP addresses across multiple routers. The system aims to provide real-time monitoring, efficient anomaly detection, and streamlined management of network security for MikroTik environments.
|
This project is a full-stack web application for an Intrusion Detection System (IDS) tailored for MikroTik routers, utilizing Machine Learning. Its core function is to monitor network traffic, identify anomalies indicative of intrusions, and automatically block malicious IP addresses across multiple routers. The system aims to provide real-time monitoring, efficient anomaly detection, and streamlined network security management for MikroTik environments, including advanced features like IP geolocation and robust service monitoring.
|
||||||
|
|
||||||
## User Preferences
|
## User Preferences
|
||||||
### Operazioni Git e Deployment
|
### Operazioni Git e Deployment
|
||||||
@ -20,216 +20,29 @@ This project is a full-stack web application designed as an Intrusion Detection
|
|||||||
- Commit message: italiano
|
- Commit message: italiano
|
||||||
|
|
||||||
## System Architecture
|
## System Architecture
|
||||||
The IDS features a React-based frontend for real-time monitoring, detection visualization, and whitelist management, utilizing ShadCN UI and TanStack Query. The backend comprises a Python FastAPI service for ML analysis (Isolation Forest with 25 targeted features), MikroTik API management, and a detection engine scoring anomalies from 0-100 with five risk levels. A Node.js (Express) backend handles API requests from the frontend and manages the PostgreSQL database.
|
The IDS employs a React-based frontend for real-time monitoring, detection visualization, and whitelist management, built with ShadCN UI and TanStack Query. The backend consists of a Python FastAPI service dedicated to ML analysis (Isolation Forest with 25 targeted features), MikroTik API management, and a detection engine that scores anomalies from 0-100 across five risk levels. A Node.js (Express) backend handles API requests from the frontend, manages the PostgreSQL database, and coordinates service operations.
|
||||||
|
|
||||||
**Workflow:**
|
**Key Architectural Decisions & Features:**
|
||||||
1. **Log Collection**: MikroTik Routers send syslog data (UDP:514) to RSyslog, which is then parsed by `syslog_parser.py` and stored in the `network_logs` table in PostgreSQL.
|
- **Log Collection & Processing**: MikroTik syslog data (UDP:514) is sent to RSyslog, parsed by `syslog_parser.py`, and stored in PostgreSQL. The parser includes auto-cleanup with a 3-day retention policy.
|
||||||
2. **Training**: The Python ML component extracts 25 features from network logs and trains an Isolation Forest model.
|
- **Machine Learning**: An Isolation Forest model trained on 25 network log features performs real-time anomaly detection, assigning a risk score.
|
||||||
3. **Detection**: Real-time analysis of network logs is performed using the trained ML model, assigning a risk score.
|
- **Automated Blocking**: Critical IPs (score >= 80) are automatically blocked in parallel across all configured MikroTik routers via their REST API.
|
||||||
4. **Auto-Block**: Critical IPs (score >= 80) are automatically blocked across all configured MikroTik routers in parallel via their REST API.
|
- **Service Monitoring & Management**: A dashboard provides real-time status (green/red indicators) for the ML Backend, Database, and Syslog Parser. Service management (start/stop/restart) for Python services is available via API endpoints, secured with API key authentication and Systemd integration for production-grade control and auto-restart capabilities.
|
||||||
5. **Monitoring**: Dashboard real-time mostra status servizi (ML Backend, Database, Parser) con pallini verde/rosso.
|
- **IP Geolocation**: Integrated `ip-api.com` for enriching detection data with geographical and Autonomous System (AS) information, including intelligent caching.
|
||||||
|
- **Database Management**: PostgreSQL is used for all persistent data. An intelligent database versioning system ensures efficient SQL migrations, applying only new scripts. Dual-mode database drivers (`@neondatabase/serverless` for Replit, `pg` for AlmaLinux) ensure environment compatibility.
|
||||||
**Key Features:**
|
- **Microservices**: Clear separation of concerns between the Python ML backend and the Node.js API backend.
|
||||||
- **ML Analyzer**: Isolation Forest with 25 features.
|
- **UI/UX**: Utilizes ShadCN UI for a modern component library and `react-hook-form` with Zod for robust form validation.
|
||||||
- **MikroTik Manager**: Parallel communication with 10+ routers via API REST.
|
|
||||||
- **Detection Engine**: Scoring 0-100 with 5 risk levels (Normal, Basso, Medio, Alto, Critico).
|
|
||||||
- **Service Monitoring**: Dashboard con status real-time di ML Backend, Database e Syslog Parser (pallini verde/rosso).
|
|
||||||
- **Service Management**: Controlli start/stop/restart per servizi Python via API endpoints.
|
|
||||||
- **Form Validation**: Improved validation using react-hook-form and Zod.
|
|
||||||
- **Database Migrations**: Automated SQL migrations applied via `update_from_git.sh --db`.
|
|
||||||
- **Microservices**: Separation of concerns with dedicated Python ML backend and Node.js API backend.
|
|
||||||
|
|
||||||
## External Dependencies
|
## External Dependencies
|
||||||
- **React**: Frontend framework.
|
- **React**: Frontend framework.
|
||||||
- **FastAPI**: Python web framework for the ML backend.
|
- **FastAPI**: Python web framework for the ML backend.
|
||||||
- **PostgreSQL**: Primary database for storing router configurations, network logs, detections, and whitelist entries.
|
- **PostgreSQL**: Primary database for storing configurations, logs, detections, and whitelist entries.
|
||||||
- **MikroTik API REST**: Used for communication with MikroTik routers for configuration and IP blocking.
|
- **MikroTik API REST**: For router communication and IP blocking.
|
||||||
- **ShadCN UI**: Frontend component library.
|
- **ShadCN UI**: Frontend component library.
|
||||||
- **TanStack Query**: Data fetching library for the frontend.
|
- **TanStack Query**: Data fetching for the frontend.
|
||||||
- **Isolation Forest**: Machine Learning algorithm for anomaly detection.
|
- **Isolation Forest**: Machine Learning algorithm for anomaly detection.
|
||||||
- **RSyslog**: Log collection daemon.
|
- **RSyslog**: Log collection daemon.
|
||||||
- **Drizzle ORM**: Used for database schema definition and synchronization in the Node.js backend.
|
- **Drizzle ORM**: For database schema definition in Node.js.
|
||||||
- **Neon Database**: Cloud-native PostgreSQL service (used in Replit environment).
|
- **Neon Database**: Cloud-native PostgreSQL service (used in Replit).
|
||||||
- **pg (Node.js driver)**: Standard PostgreSQL driver for Node.js (used in AlmaLinux environment).
|
- **pg (Node.js driver)**: Standard PostgreSQL driver for Node.js (used in AlmaLinux).
|
||||||
- **psycopg2**: PostgreSQL adapter for Python.
|
- **psycopg2**: PostgreSQL adapter for Python.
|
||||||
|
- **ip-api.com**: External API for IP geolocation data.
|
||||||
## Recent Updates (Novembre 2025)
|
|
||||||
|
|
||||||
### 🔒 Sistema Monitoring e Gestione Servizi Production-Grade (22 Nov 2025 - 13:00)
|
|
||||||
- **Feature**: Sistema completo di monitoring e controllo servizi con sicurezza enterprise-level
|
|
||||||
- **Architettura Sicurezza**:
|
|
||||||
- **Autenticazione API Key** (`IDS_API_KEY`):
|
|
||||||
- Comunicazione autenticata tra Node.js backend e Python FastAPI
|
|
||||||
- Header `X-API-Key` richiesto per tutti gli endpoint ML backend
|
|
||||||
- Generazione automatica via `setup_systemd_services.sh`
|
|
||||||
- Backward compatibility: funziona senza API key in development
|
|
||||||
- **Validazione Whitelist**:
|
|
||||||
- Solo servizi autorizzati: `ids-ml-backend`, `ids-syslog-parser`
|
|
||||||
- Solo azioni autorizzate: `start`, `stop`, `restart`, `status`
|
|
||||||
- Prevenzione command injection via validation strict
|
|
||||||
- **Systemd Integration**:
|
|
||||||
- Gestione servizi tramite systemctl (no shell commands arbitrari)
|
|
||||||
- Timeout 10s per prevenire hanging
|
|
||||||
- Auto-restart in caso di crash
|
|
||||||
- **Monitoring Real-time**:
|
|
||||||
- Endpoint `/api/services/status` controlla:
|
|
||||||
- ML Backend: health check HTTP + uptime
|
|
||||||
- Database PostgreSQL: connection test
|
|
||||||
- Syslog Parser: status systemd + PID
|
|
||||||
- Dashboard mostra pallini verde/rosso (refresh automatico 5s)
|
|
||||||
- Python backend `/services/status` (autenticato):
|
|
||||||
- Verifica status via `systemctl is-active`
|
|
||||||
- Estrae PID da `systemctl status`
|
|
||||||
- Gestisce gracefully assenza systemd (dev environment)
|
|
||||||
- **Controlli UX Completi**:
|
|
||||||
- Pagina `/services` con pulsanti funzionanti:
|
|
||||||
- Start/Stop/Restart per ML backend e Syslog parser
|
|
||||||
- Mutations TanStack Query con feedback toast
|
|
||||||
- Auto-refresh status dopo operazioni
|
|
||||||
- Fallback comandi manuali systemctl
|
|
||||||
- Disabilitazione intelligente pulsanti (es. Stop se già offline)
|
|
||||||
- Visualizzazione PID processo e systemd unit
|
|
||||||
- **Systemd Units**:
|
|
||||||
- `ids-ml-backend.service`: Python FastAPI ML backend
|
|
||||||
- `ids-syslog-parser.service`: Python syslog parser
|
|
||||||
- Features: auto-restart, limiti risorse, logging centralizzato
|
|
||||||
- Script `setup_systemd_services.sh` per installazione automatica
|
|
||||||
- **Deployment**:
|
|
||||||
1. Utente esegue: `sudo ./deployment/setup_systemd_services.sh`
|
|
||||||
2. Script verifica e installa dipendenze Python (virtual environment)
|
|
||||||
3. Script genera `IDS_API_KEY` se mancante
|
|
||||||
4. Installa systemd units e avvia servizi
|
|
||||||
5. Frontend usa controlli per gestione remota servizi
|
|
||||||
- **Virtual Environment**:
|
|
||||||
- Dipendenze Python in `/opt/ids/python_ml/venv`
|
|
||||||
- Systemd services usano automaticamente il venv
|
|
||||||
- Script `install_python_deps.sh` per installazione manuale
|
|
||||||
- **Benefici**:
|
|
||||||
- 🔒 Autenticazione API end-to-end (zero vulnerabilità)
|
|
||||||
- 🎯 Controlli UX funzionanti (no comandi manuali)
|
|
||||||
- 🔄 Systemd auto-restart e monitoring robusto
|
|
||||||
- 📊 Visibilità completa con sicurezza production-grade
|
|
||||||
- ⚡ Gestione servizi remota sicura via dashboard
|
|
||||||
|
|
||||||
### 📊 Log Format Fix - Timestamp Integration (22 Nov 2025 - 10:30)
|
|
||||||
- **Problema**: RSyslog salvava log senza timestamp, parser Python falliva
|
|
||||||
- **Soluzione**: Template rsyslog corretto per includere timestamp BSD
|
|
||||||
```bash
|
|
||||||
# PRIMA: template(... string="%msg%\n") ❌
|
|
||||||
# ADESSO: template(... string="%TIMESTAMP% %HOSTNAME% %msg%\n") ✅
|
|
||||||
```
|
|
||||||
- **Formato log supportato**:
|
|
||||||
```
|
|
||||||
Nov 22 08:15:30 FIBRA forward: in:<pppoe-user> out:sfp-xxx, connection-state:new proto TCP (SYN), 10.0.254.77:53783->52.213.60.221:443, len 64
|
|
||||||
```
|
|
||||||
- **Compatibilità**: Parser Python 100% compatibile con:
|
|
||||||
- ✅ Log "forward" e "detected-ddos forward"
|
|
||||||
- ✅ Interfacce in/out: `in:<pppoe-xxx> out:sfp-xxx`
|
|
||||||
- ✅ src-mac opzionale
|
|
||||||
- ✅ TCP flags: `(SYN)`, `(ACK,PSH)`, etc.
|
|
||||||
- ✅ NAT info opzionale
|
|
||||||
- **Migrazione**: Vedi `deployment/MIGRATION_INCOMING_LOGS.md`
|
|
||||||
- **Benefici**:
|
|
||||||
- ⚡ Volume log ridotto 50-70% (solo connessioni in ingresso)
|
|
||||||
- 🔒 Parser funzionante con timestamp corretto
|
|
||||||
- 📊 Database popolato correttamente
|
|
||||||
|
|
||||||
### 🚀 Database Versioning System (22 Nov 2025 - 10:00)
|
|
||||||
- **Feature**: Sistema intelligente di versioning per migrazioni database
|
|
||||||
- **Problema risolto**: `update_from_git.sh` rieseguiva tutte le migrazioni SQL ad ogni update (lento)
|
|
||||||
- **Soluzione**:
|
|
||||||
- Tabella `schema_version` traccia versione corrente database
|
|
||||||
- Migrazioni SQL numerate sequenzialmente (001, 002, 003, etc.)
|
|
||||||
- Script `apply_migrations.sh` applica solo migrazioni mancanti
|
|
||||||
- Integrato in workflow update automatico
|
|
||||||
- **Benefici**:
|
|
||||||
- ⚡ Update 10x più veloce (salta migrazioni già applicate)
|
|
||||||
- 🔒 Sicuro: previene re-esecuzione migrazioni
|
|
||||||
- 📊 Tracciabilità: storico migrazioni applicate
|
|
||||||
- **Documentazione**: `database-schema/README.md`
|
|
||||||
|
|
||||||
### 🧹 Parser Auto-Cleanup DB + Retention Policy (22 Nov 2025 - 12:00)
|
|
||||||
- **Feature**: Parser con gestione automatica database (retention 3 giorni)
|
|
||||||
- **Funzionalità**:
|
|
||||||
1. **Auto-cleanup DB**: Elimina automaticamente log più vecchi di 3 giorni ogni ~16 minuti
|
|
||||||
2. **Modalità streaming**: tail -f sicuro compatibile con rsyslog (no race conditions)
|
|
||||||
3. **Fix SQL**: Query INTERVAL corretta con concatenazione stringa psycopg2
|
|
||||||
- **Vantaggi**:
|
|
||||||
- ✅ Database mantiene solo ultimi 3 giorni (riduce da 4.5M a ~200K log)
|
|
||||||
- ✅ Sicuro: no truncate aggressivo che causa perdita log
|
|
||||||
- ✅ Streaming efficiente: readline invece readlines (no carico memoria)
|
|
||||||
- ✅ Zero manutenzione manuale richiesta
|
|
||||||
- **Workflow**:
|
|
||||||
1. Modalità follow (tail -f) legge log rsyslog in streaming
|
|
||||||
2. Commit batch ogni 100 righe
|
|
||||||
3. Cleanup automatico DB ogni ~16 minuti (10.000 iterazioni × 0.1s)
|
|
||||||
4. Elimina log timestamp < NOW() - 3 giorni
|
|
||||||
- **Note**: raw.log gestito da logrotate (non dal parser) per evitare race conditions
|
|
||||||
- **Deploy**: Riavviare `ids-syslog-parser` per applicare: `sudo systemctl restart ids-syslog-parser`
|
|
||||||
|
|
||||||
## Fix Recenti (Novembre 2025)
|
|
||||||
|
|
||||||
### 🔧 Fix Training History + Permessi Directory Models (22 Nov 2025 - 11:30)
|
|
||||||
- **Problema 1**: Storico training sempre vuoto anche dopo training completato
|
|
||||||
- **Causa**: INSERT in `training_history` falliva silenziosamente (errore solo in console)
|
|
||||||
- **Soluzione**: Error handling migliorato con logging dettagliato e traceback
|
|
||||||
- **Problema 2**: `PermissionError: Permission denied: 'models/isolation_forest.joblib'`
|
|
||||||
- **Causa**: Utente `ids` non aveva permessi scrittura su directory `models/`
|
|
||||||
- **Soluzione**: Script setup crea automaticamente directory con owner `ids:ids`
|
|
||||||
- **Fix Applicati**:
|
|
||||||
- ✅ `python_ml/main.py`: Logging step-by-step training + traceback errori
|
|
||||||
- ✅ `finally` block per chiusura connessioni sicura
|
|
||||||
- ✅ `rollback` automatico in caso di errore
|
|
||||||
- ✅ `deployment/setup_systemd_services.sh`: Crea directory `models` con permessi corretti
|
|
||||||
- ✅ `deployment/install_python_deps.sh`: Crea directory `models` durante installazione
|
|
||||||
- **Deploy**: Eseguire `sudo ./deployment/setup_systemd_services.sh` per applicare fix permessi
|
|
||||||
- **Testing**: Training ora completa correttamente e salva modello + history
|
|
||||||
|
|
||||||
### 🐛 Fix Monitoring Syslog Parser + Schema Database (22 Nov 2025 - 11:10)
|
|
||||||
- **Problema 1**: Dashboard mostrava Syslog Parser in errore nonostante systemd "active (running)"
|
|
||||||
- **Causa**: Bug fetch `signal: controller2.abort` invece di `signal: controller2.signal` in `/api/services/status`
|
|
||||||
- **Soluzione**: Corretto parametro AbortController signal
|
|
||||||
- **Problema 2**: Errore database `column "destination_ip" does not exist`
|
|
||||||
- **Causa**: Database server usa `dest_ip`/`src_ip` invece di `destination_ip`/`source_ip`
|
|
||||||
- **Soluzione**: Migrazione SQL 003 rinomina colonne in formato corretto
|
|
||||||
- **Fix Applicati**:
|
|
||||||
- ✅ `server/routes.ts`: Corretto signal in fetch monitoring
|
|
||||||
- ✅ `database-schema/migrations/003_fix_network_logs_columns.sql`: Migrazione rinomina colonne
|
|
||||||
- **Deploy**:
|
|
||||||
1. Eseguire `./update_from_git.sh --db` per applicare migrazione
|
|
||||||
2. Riavviare backend Node.js
|
|
||||||
3. Dashboard mostrerà correttamente tutti i servizi (pallini verdi)
|
|
||||||
|
|
||||||
## Fix Recenti (Novembre 2025)
|
|
||||||
|
|
||||||
### 🚨 Database Full - Auto-Cleanup Fix (21 Nov 2025 - 18:00)
|
|
||||||
- **Problema**: Database PostgreSQL pieno con **417 MILIONI di log** accumulati
|
|
||||||
- Syslog parser ha processato 417.7M righe senza limite di retention
|
|
||||||
- Errore: `could not extend file: No space left on device`
|
|
||||||
- Tutte le tabelle vuote perché database non accetta più scritture
|
|
||||||
- **Causa**: Nessuna pulizia automatica dei vecchi log (retention infinita)
|
|
||||||
- **Soluzione**:
|
|
||||||
- Script `cleanup_old_logs.sql`: Mantiene solo ultimi 7 giorni di `network_logs`
|
|
||||||
- Script `cleanup_database.sh`: Wrapper per esecuzione manuale/cron
|
|
||||||
- Script `setup_cron_cleanup.sh`: Configura cron job giornaliero (ore 03:00)
|
|
||||||
- **Fix Immediato sul Server**:
|
|
||||||
```bash
|
|
||||||
# 1. Pulisci manualmente log vecchi
|
|
||||||
psql $DATABASE_URL << 'EOF'
|
|
||||||
DELETE FROM network_logs WHERE timestamp < NOW() - INTERVAL '7 days';
|
|
||||||
VACUUM FULL network_logs;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 2. Setup pulizia automatica giornaliera
|
|
||||||
sudo /opt/ids/deployment/setup_cron_cleanup.sh
|
|
||||||
```
|
|
||||||
- **Risultato Atteso**:
|
|
||||||
- Database ridotto da centinaia di GB a pochi GB
|
|
||||||
- Retention 7 giorni sufficiente per training ML
|
|
||||||
- Pulizia automatica previene saturazione futura
|
|
||||||
|
|
||||||
### ✅ Database Driver Fix - Dual Mode Neon/PostgreSQL (21 Nov 2025 - 17:40)
|
|
||||||
- **Problema**: Frontend Node.js falliva con errore 500 su tutte le query database
|
|
||||||
- **Causa**: `@neondatabase/serverless` usa WebSocket ed è compatibile SOLO con Neon Cloud, non con PostgreSQL locale
|
|
||||||
- **Soluzione**: Dual-mode driver in `server/db.ts` con auto-detection ambiente
|
|
||||||
- **Risultato**: Funziona su Replit (Neon) e AlmaLinux (PostgreSQL standard) ✅
|
|
||||||
@ -50,10 +50,19 @@ export const detections = pgTable("detections", {
|
|||||||
blocked: boolean("blocked").notNull().default(false),
|
blocked: boolean("blocked").notNull().default(false),
|
||||||
blockedAt: timestamp("blocked_at"),
|
blockedAt: timestamp("blocked_at"),
|
||||||
detectedAt: timestamp("detected_at").defaultNow().notNull(),
|
detectedAt: timestamp("detected_at").defaultNow().notNull(),
|
||||||
|
// Geolocation & AS info
|
||||||
|
country: text("country"),
|
||||||
|
countryCode: text("country_code"),
|
||||||
|
city: text("city"),
|
||||||
|
organization: text("organization"),
|
||||||
|
asNumber: text("as_number"),
|
||||||
|
asName: text("as_name"),
|
||||||
|
isp: text("isp"),
|
||||||
}, (table) => ({
|
}, (table) => ({
|
||||||
sourceIpIdx: index("detection_source_ip_idx").on(table.sourceIp),
|
sourceIpIdx: index("detection_source_ip_idx").on(table.sourceIp),
|
||||||
riskScoreIdx: index("risk_score_idx").on(table.riskScore),
|
riskScoreIdx: index("risk_score_idx").on(table.riskScore),
|
||||||
detectedAtIdx: index("detected_at_idx").on(table.detectedAt),
|
detectedAtIdx: index("detected_at_idx").on(table.detectedAt),
|
||||||
|
countryIdx: index("country_idx").on(table.country),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Whitelist per IP fidati
|
// Whitelist per IP fidati
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user