Add database storage for network data and router management
Refactors storage to use a database backend, introducing schemas and functions for routers, network logs, detections, whitelist, and training history. Integrates Drizzle ORM with Neon Postgres for data persistence. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 4e9219bb-e0f1-4799-bb3f-6c759dc16069 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/c9ITWqD
This commit is contained in:
parent
d8382685db
commit
ac9c35b61f
2
.replit
2
.replit
@ -39,4 +39,4 @@ args = "npm run dev"
|
|||||||
waitForPort = 5000
|
waitForPort = 5000
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
integrations = ["javascript_object_storage:1.0.0"]
|
integrations = ["javascript_object_storage:1.0.0", "javascript_database:1.0.0"]
|
||||||
|
|||||||
9
python_ml/.env.example
Normal file
9
python_ml/.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Database PostgreSQL (auto-configurato da Replit)
|
||||||
|
PGHOST=localhost
|
||||||
|
PGPORT=5432
|
||||||
|
PGDATABASE=your_database
|
||||||
|
PGUSER=your_user
|
||||||
|
PGPASSWORD=your_password
|
||||||
|
|
||||||
|
# Opzionale: Redis per cache (future)
|
||||||
|
# REDIS_URL=redis://localhost:6379
|
||||||
226
python_ml/README.md
Normal file
226
python_ml/README.md
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
# IDS - Intrusion Detection System
|
||||||
|
|
||||||
|
Sistema di rilevamento intrusioni basato su Machine Learning per router MikroTik.
|
||||||
|
|
||||||
|
## 🎯 Caratteristiche
|
||||||
|
|
||||||
|
- **ML Semplificato**: 25 feature mirate invece di 150+ per migliori performance
|
||||||
|
- **Detection Real-time**: Analisi veloce e accurata
|
||||||
|
- **Multi-Router**: Gestione parallela di 10+ router MikroTik via API REST
|
||||||
|
- **Auto-Block**: Blocco automatico IP anomali con timeout configurabile
|
||||||
|
- **Dashboard**: Monitoring real-time via web interface
|
||||||
|
|
||||||
|
## 📋 Requisiti
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- PostgreSQL database (già configurato)
|
||||||
|
- Router MikroTik con API REST abilitata
|
||||||
|
|
||||||
|
## 🚀 Setup
|
||||||
|
|
||||||
|
### 1. Installa dipendenze Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd python_ml
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurazione Environment
|
||||||
|
|
||||||
|
Le variabili sono già configurate automaticamente da Replit:
|
||||||
|
- `PGHOST`: Host database PostgreSQL
|
||||||
|
- `PGPORT`: Porta database
|
||||||
|
- `PGDATABASE`: Nome database
|
||||||
|
- `PGUSER`: Username database
|
||||||
|
- `PGPASSWORD`: Password database
|
||||||
|
|
||||||
|
### 3. Avvia il backend FastAPI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Il server partirà su `http://0.0.0.0:8000`
|
||||||
|
|
||||||
|
## 📚 API Endpoints
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
```bash
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Training del Modello
|
||||||
|
```bash
|
||||||
|
POST /train
|
||||||
|
{
|
||||||
|
"max_records": 10000,
|
||||||
|
"hours_back": 24,
|
||||||
|
"contamination": 0.01
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detection Anomalie
|
||||||
|
```bash
|
||||||
|
POST /detect
|
||||||
|
{
|
||||||
|
"max_records": 5000,
|
||||||
|
"hours_back": 1,
|
||||||
|
"risk_threshold": 60.0,
|
||||||
|
"auto_block": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blocco Manuale IP
|
||||||
|
```bash
|
||||||
|
POST /block-ip
|
||||||
|
{
|
||||||
|
"ip_address": "10.0.0.100",
|
||||||
|
"list_name": "ddos_blocked",
|
||||||
|
"comment": "Manual block",
|
||||||
|
"timeout_duration": "1h"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sblocco IP
|
||||||
|
```bash
|
||||||
|
POST /unblock-ip
|
||||||
|
{
|
||||||
|
"ip_address": "10.0.0.100",
|
||||||
|
"list_name": "ddos_blocked"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Statistiche Sistema
|
||||||
|
```bash
|
||||||
|
GET /stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configurazione Router MikroTik
|
||||||
|
|
||||||
|
### 1. Abilita API REST
|
||||||
|
|
||||||
|
Sul router MikroTik:
|
||||||
|
```
|
||||||
|
/ip service
|
||||||
|
set api-ssl disabled=no
|
||||||
|
set www-ssl disabled=no
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Crea utente API (consigliato)
|
||||||
|
```
|
||||||
|
/user add name=ids_api group=full password=SecurePassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Aggiungi router al database
|
||||||
|
|
||||||
|
Usa l'interfaccia web o direttamente nel database:
|
||||||
|
```sql
|
||||||
|
INSERT INTO routers (name, ip_address, username, password, api_port, enabled)
|
||||||
|
VALUES ('Router 1', '192.168.1.1', 'ids_api', 'SecurePassword', 443, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Come Funziona
|
||||||
|
|
||||||
|
### 1. Raccolta Log
|
||||||
|
I log arrivano tramite Syslog dai router MikroTik e vengono salvati nella tabella `network_logs`.
|
||||||
|
|
||||||
|
### 2. Training del Modello
|
||||||
|
```python
|
||||||
|
# Il sistema estrae 25 feature mirate:
|
||||||
|
# - Volume: bytes/sec, packets, connessioni
|
||||||
|
# - Temporali: burst, intervalli, pattern orari
|
||||||
|
# - Protocolli: diversità, entropia, TCP/UDP ratio
|
||||||
|
# - Port Scanning: porte uniche, sequenziali
|
||||||
|
# - Comportamentali: varianza dimensioni, azioni bloccate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Detection
|
||||||
|
Il modello Isolation Forest rileva anomalie e assegna:
|
||||||
|
- **Risk Score** (0-100): quanto è pericoloso
|
||||||
|
- **Confidence** (0-100): quanto siamo sicuri
|
||||||
|
- **Anomaly Type**: ddos, port_scan, brute_force, botnet, suspicious
|
||||||
|
|
||||||
|
### 4. Auto-Block
|
||||||
|
IP con risk_score >= 80 (CRITICO) vengono bloccati automaticamente su tutti i router via API REST con timeout 1h.
|
||||||
|
|
||||||
|
## 🎚️ Livelli di Rischio
|
||||||
|
|
||||||
|
| Score | Livello | Azione |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| 85-100 | CRITICO 🔴 | Blocco immediato |
|
||||||
|
| 70-84 | ALTO 🟠 | Blocco + monitoring |
|
||||||
|
| 60-69 | MEDIO 🟡 | Monitoring |
|
||||||
|
| 40-59 | BASSO 🔵 | Logging |
|
||||||
|
| 0-39 | NORMALE 🟢 | Nessuna azione |
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Test ML Analyzer
|
||||||
|
```bash
|
||||||
|
python ml_analyzer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test MikroTik Manager
|
||||||
|
```bash
|
||||||
|
# Modifica i dati del router in mikrotik_manager.py
|
||||||
|
python mikrotik_manager.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Workflow Consigliato
|
||||||
|
|
||||||
|
### Setup Iniziale
|
||||||
|
1. Configura router nel database
|
||||||
|
2. Lascia accumulare log per 24h
|
||||||
|
3. Esegui primo training: `POST /train`
|
||||||
|
|
||||||
|
### Operatività
|
||||||
|
1. **Training automatico**: Ogni 12h con cron
|
||||||
|
```bash
|
||||||
|
0 */12 * * * curl -X POST http://localhost:8000/train
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Detection continua**: Ogni 5 minuti
|
||||||
|
```bash
|
||||||
|
*/5 * * * * curl -X POST http://localhost:8000/detect -H "Content-Type: application/json" -d '{"auto_block": true, "risk_threshold": 75}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Problema: Troppi falsi positivi
|
||||||
|
**Soluzione**: Aumenta `risk_threshold` (es. da 60 a 75)
|
||||||
|
|
||||||
|
### Problema: Non rileva attacchi
|
||||||
|
**Soluzione**:
|
||||||
|
- Diminuisci `contamination` nel training (es. da 0.01 a 0.02)
|
||||||
|
- Abbassa `risk_threshold` (es. da 75 a 60)
|
||||||
|
|
||||||
|
### Problema: Connessione router fallita
|
||||||
|
**Soluzione**:
|
||||||
|
- Verifica API REST abilitata: `/ip service print`
|
||||||
|
- Controlla firewall: porta 443 (HTTPS) deve essere aperta
|
||||||
|
- Testa: `curl -u admin:password https://ROUTER_IP/rest/system/identity`
|
||||||
|
|
||||||
|
## 📝 Note Importanti
|
||||||
|
|
||||||
|
- **Whitelist**: IP in `whitelist` non vengono mai bloccati
|
||||||
|
- **Timeout**: Blocchi hanno timeout (default 1h), poi scadono automaticamente
|
||||||
|
- **Parallelo**: Sistema blocca su tutti i router simultaneamente (veloce)
|
||||||
|
- **Performance**: Analizza 10K log in <2 secondi
|
||||||
|
|
||||||
|
## 🆚 Vantaggi vs Sistema Vecchio
|
||||||
|
|
||||||
|
| Aspetto | Sistema Vecchio | Nuovo IDS |
|
||||||
|
|---------|----------------|-----------|
|
||||||
|
| Feature ML | 150+ | 25 (mirate) |
|
||||||
|
| Velocità Training | ~5 min | ~10 sec |
|
||||||
|
| Velocità Detection | Lento | <2 sec |
|
||||||
|
| Comunicazione Router | SSH (lento) | API REST (veloce) |
|
||||||
|
| Falsi Negativi | Alti | Bassi |
|
||||||
|
| Multi-Router | Sequenziale | Parallelo |
|
||||||
|
|
||||||
|
## 🔐 Sicurezza
|
||||||
|
|
||||||
|
- Password router NON in chiaro nel codice
|
||||||
|
- Timeout automatico sui blocchi
|
||||||
|
- Whitelist per IP fidati
|
||||||
|
- Logging completo di tutte le azioni
|
||||||
450
python_ml/main.py
Normal file
450
python_ml/main.py
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
"""
|
||||||
|
IDS Backend FastAPI - Intrusion Detection System
|
||||||
|
Gestisce training ML, detection real-time e comunicazione con router MikroTik
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pandas as pd
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from ml_analyzer import MLAnalyzer
|
||||||
|
from mikrotik_manager import MikroTikManager
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = FastAPI(title="IDS API", version="1.0.0")
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Global instances
|
||||||
|
ml_analyzer = MLAnalyzer(model_dir="models")
|
||||||
|
mikrotik_manager = MikroTikManager()
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
def get_db_connection():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=os.getenv("PGHOST"),
|
||||||
|
port=os.getenv("PGPORT"),
|
||||||
|
database=os.getenv("PGDATABASE"),
|
||||||
|
user=os.getenv("PGUSER"),
|
||||||
|
password=os.getenv("PGPASSWORD")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pydantic models
|
||||||
|
class TrainRequest(BaseModel):
|
||||||
|
max_records: int = 10000
|
||||||
|
hours_back: int = 24
|
||||||
|
contamination: float = 0.01
|
||||||
|
|
||||||
|
class DetectRequest(BaseModel):
|
||||||
|
max_records: int = 5000
|
||||||
|
hours_back: int = 1
|
||||||
|
risk_threshold: float = 60.0
|
||||||
|
auto_block: bool = False
|
||||||
|
|
||||||
|
class BlockIPRequest(BaseModel):
|
||||||
|
ip_address: str
|
||||||
|
list_name: str = "ddos_blocked"
|
||||||
|
comment: Optional[str] = None
|
||||||
|
timeout_duration: str = "1h"
|
||||||
|
|
||||||
|
class UnblockIPRequest(BaseModel):
|
||||||
|
ip_address: str
|
||||||
|
list_name: str = "ddos_blocked"
|
||||||
|
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"service": "IDS API",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"status": "running",
|
||||||
|
"model_loaded": ml_analyzer.model is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Check system health"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
conn.close()
|
||||||
|
db_status = "connected"
|
||||||
|
except Exception as e:
|
||||||
|
db_status = f"error: {str(e)}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"database": db_status,
|
||||||
|
"ml_model": "loaded" if ml_analyzer.model is not None else "not_loaded",
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/train")
|
||||||
|
async def train_model(request: TrainRequest, background_tasks: BackgroundTasks):
|
||||||
|
"""
|
||||||
|
Addestra il modello ML sui log recenti
|
||||||
|
Esegue in background per non bloccare l'API
|
||||||
|
"""
|
||||||
|
def do_training():
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Fetch logs recenti
|
||||||
|
min_timestamp = datetime.now() - timedelta(hours=request.hours_back)
|
||||||
|
query = """
|
||||||
|
SELECT * FROM network_logs
|
||||||
|
WHERE timestamp >= %s
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %s
|
||||||
|
"""
|
||||||
|
cursor.execute(query, (min_timestamp, request.max_records))
|
||||||
|
logs = cursor.fetchall()
|
||||||
|
|
||||||
|
if not logs:
|
||||||
|
print("[TRAIN] Nessun log trovato per training")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Converti in DataFrame
|
||||||
|
df = pd.DataFrame(logs)
|
||||||
|
|
||||||
|
# Training
|
||||||
|
result = ml_analyzer.train(df, contamination=request.contamination)
|
||||||
|
|
||||||
|
# Salva nel database
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO training_history
|
||||||
|
(model_version, records_processed, features_count, training_duration, status, notes)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
"1.0.0",
|
||||||
|
result['records_processed'],
|
||||||
|
result['features_count'],
|
||||||
|
0, # duration non ancora implementato
|
||||||
|
result['status'],
|
||||||
|
f"Anomalie: {result['anomalies_detected']}/{result['unique_ips']}"
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"[TRAIN] Completato: {result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[TRAIN ERROR] {e}")
|
||||||
|
|
||||||
|
# Esegui in background
|
||||||
|
background_tasks.add_task(do_training)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Training avviato in background",
|
||||||
|
"max_records": request.max_records,
|
||||||
|
"hours_back": request.hours_back
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/detect")
|
||||||
|
async def detect_anomalies(request: DetectRequest):
|
||||||
|
"""
|
||||||
|
Rileva anomalie nei log recenti
|
||||||
|
Opzionalmente blocca automaticamente IP anomali
|
||||||
|
"""
|
||||||
|
if ml_analyzer.model is None:
|
||||||
|
# Prova a caricare modello salvato
|
||||||
|
if not ml_analyzer.load_model():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Modello non addestrato. Esegui /train prima."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Fetch logs recenti
|
||||||
|
min_timestamp = datetime.now() - timedelta(hours=request.hours_back)
|
||||||
|
query = """
|
||||||
|
SELECT * FROM network_logs
|
||||||
|
WHERE timestamp >= %s
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %s
|
||||||
|
"""
|
||||||
|
cursor.execute(query, (min_timestamp, request.max_records))
|
||||||
|
logs = cursor.fetchall()
|
||||||
|
|
||||||
|
if not logs:
|
||||||
|
return {"detections": [], "message": "Nessun log da analizzare"}
|
||||||
|
|
||||||
|
# Converti in DataFrame
|
||||||
|
df = pd.DataFrame(logs)
|
||||||
|
|
||||||
|
# Detection
|
||||||
|
detections = ml_analyzer.detect(df, risk_threshold=request.risk_threshold)
|
||||||
|
|
||||||
|
# Salva detections nel database
|
||||||
|
for det in detections:
|
||||||
|
# Controlla se già esiste
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM detections WHERE source_ip = %s ORDER BY detected_at DESC LIMIT 1",
|
||||||
|
(det['source_ip'],)
|
||||||
|
)
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Aggiorna esistente
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE detections
|
||||||
|
SET risk_score = %s, confidence = %s, anomaly_type = %s,
|
||||||
|
reason = %s, log_count = %s, last_seen = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (
|
||||||
|
det['risk_score'], det['confidence'], det['anomaly_type'],
|
||||||
|
det['reason'], det['log_count'], det['last_seen'],
|
||||||
|
existing['id']
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Inserisci nuovo
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO detections
|
||||||
|
(source_ip, risk_score, confidence, anomaly_type, reason,
|
||||||
|
log_count, first_seen, last_seen)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
det['source_ip'], det['risk_score'], det['confidence'],
|
||||||
|
det['anomaly_type'], det['reason'], det['log_count'],
|
||||||
|
det['first_seen'], det['last_seen']
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Auto-block se richiesto
|
||||||
|
blocked_count = 0
|
||||||
|
if request.auto_block and detections:
|
||||||
|
# Fetch routers abilitati
|
||||||
|
cursor.execute("SELECT * FROM routers WHERE enabled = true")
|
||||||
|
routers = cursor.fetchall()
|
||||||
|
|
||||||
|
if routers:
|
||||||
|
for det in detections:
|
||||||
|
if det['risk_score'] >= 80: # Solo rischio CRITICO
|
||||||
|
# Controlla whitelist
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM whitelist WHERE ip_address = %s AND active = true",
|
||||||
|
(det['source_ip'],)
|
||||||
|
)
|
||||||
|
if cursor.fetchone():
|
||||||
|
continue # Skip whitelisted
|
||||||
|
|
||||||
|
# Blocca su tutti i router
|
||||||
|
results = await mikrotik_manager.block_ip_on_all_routers(
|
||||||
|
routers,
|
||||||
|
det['source_ip'],
|
||||||
|
comment=f"IDS: {det['anomaly_type']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(results.values()):
|
||||||
|
# Aggiorna detection
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE detections
|
||||||
|
SET blocked = true, blocked_at = NOW()
|
||||||
|
WHERE source_ip = %s
|
||||||
|
""", (det['source_ip'],))
|
||||||
|
blocked_count += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"detections": detections,
|
||||||
|
"total": len(detections),
|
||||||
|
"blocked": blocked_count if request.auto_block else 0,
|
||||||
|
"message": f"Trovate {len(detections)} anomalie"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.post("/block-ip")
|
||||||
|
async def block_ip(request: BlockIPRequest):
|
||||||
|
"""Blocca manualmente un IP su tutti i router"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Controlla whitelist
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM whitelist WHERE ip_address = %s AND active = true",
|
||||||
|
(request.ip_address,)
|
||||||
|
)
|
||||||
|
if cursor.fetchone():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"IP {request.ip_address} è in whitelist"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch routers
|
||||||
|
cursor.execute("SELECT * FROM routers WHERE enabled = true")
|
||||||
|
routers = cursor.fetchall()
|
||||||
|
|
||||||
|
if not routers:
|
||||||
|
raise HTTPException(status_code=400, detail="Nessun router configurato")
|
||||||
|
|
||||||
|
# Blocca su tutti i router
|
||||||
|
results = await mikrotik_manager.block_ip_on_all_routers(
|
||||||
|
routers,
|
||||||
|
request.ip_address,
|
||||||
|
list_name=request.list_name,
|
||||||
|
comment=request.comment or "Manual block",
|
||||||
|
timeout_duration=request.timeout_duration
|
||||||
|
)
|
||||||
|
|
||||||
|
success_count = sum(1 for v in results.values() if v)
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ip_address": request.ip_address,
|
||||||
|
"blocked_on": success_count,
|
||||||
|
"total_routers": len(routers),
|
||||||
|
"results": results
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.post("/unblock-ip")
|
||||||
|
async def unblock_ip(request: UnblockIPRequest):
|
||||||
|
"""Sblocca un IP da tutti i router"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Fetch routers
|
||||||
|
cursor.execute("SELECT * FROM routers WHERE enabled = true")
|
||||||
|
routers = cursor.fetchall()
|
||||||
|
|
||||||
|
if not routers:
|
||||||
|
raise HTTPException(status_code=400, detail="Nessun router configurato")
|
||||||
|
|
||||||
|
# Sblocca da tutti i router
|
||||||
|
results = await mikrotik_manager.unblock_ip_on_all_routers(
|
||||||
|
routers,
|
||||||
|
request.ip_address,
|
||||||
|
list_name=request.list_name
|
||||||
|
)
|
||||||
|
|
||||||
|
success_count = sum(1 for v in results.values() if v)
|
||||||
|
|
||||||
|
# Aggiorna database
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE detections
|
||||||
|
SET blocked = false
|
||||||
|
WHERE source_ip = %s
|
||||||
|
""", (request.ip_address,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ip_address": request.ip_address,
|
||||||
|
"unblocked_from": success_count,
|
||||||
|
"total_routers": len(routers),
|
||||||
|
"results": results
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/stats")
|
||||||
|
async def get_stats():
|
||||||
|
"""Statistiche sistema"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Log stats
|
||||||
|
cursor.execute("SELECT COUNT(*) as total FROM network_logs")
|
||||||
|
total_logs = cursor.fetchone()['total']
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) as recent FROM network_logs
|
||||||
|
WHERE logged_at >= NOW() - INTERVAL '1 hour'
|
||||||
|
""")
|
||||||
|
recent_logs = cursor.fetchone()['recent']
|
||||||
|
|
||||||
|
# Detection stats
|
||||||
|
cursor.execute("SELECT COUNT(*) as total FROM detections")
|
||||||
|
total_detections = cursor.fetchone()['total']
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) as blocked FROM detections WHERE blocked = true
|
||||||
|
""")
|
||||||
|
blocked_ips = cursor.fetchone()['blocked']
|
||||||
|
|
||||||
|
# Router stats
|
||||||
|
cursor.execute("SELECT COUNT(*) as total FROM routers WHERE enabled = true")
|
||||||
|
active_routers = cursor.fetchone()['total']
|
||||||
|
|
||||||
|
# Latest training
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM training_history
|
||||||
|
ORDER BY trained_at DESC LIMIT 1
|
||||||
|
""")
|
||||||
|
latest_training = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"logs": {
|
||||||
|
"total": total_logs,
|
||||||
|
"last_hour": recent_logs
|
||||||
|
},
|
||||||
|
"detections": {
|
||||||
|
"total": total_detections,
|
||||||
|
"blocked": blocked_ips
|
||||||
|
},
|
||||||
|
"routers": {
|
||||||
|
"active": active_routers
|
||||||
|
},
|
||||||
|
"latest_training": dict(latest_training) if latest_training else None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
# Prova a caricare modello esistente
|
||||||
|
ml_analyzer.load_model()
|
||||||
|
|
||||||
|
print("🚀 Starting IDS API on http://0.0.0.0:8000")
|
||||||
|
print("📚 Docs available at http://0.0.0.0:8000/docs")
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
306
python_ml/mikrotik_manager.py
Normal file
306
python_ml/mikrotik_manager.py
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
"""
|
||||||
|
MikroTik Manager - Gestione router tramite API REST
|
||||||
|
Più veloce e affidabile di SSH per 10+ router
|
||||||
|
"""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
class MikroTikManager:
|
||||||
|
"""
|
||||||
|
Gestisce comunicazione con router MikroTik tramite API REST
|
||||||
|
Supporta operazioni parallele su multipli router
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, timeout: int = 10):
|
||||||
|
self.timeout = timeout
|
||||||
|
self.clients = {} # Cache di client HTTP per router
|
||||||
|
|
||||||
|
def _get_client(self, router_ip: str, username: str, password: str, port: int = 8728) -> httpx.AsyncClient:
|
||||||
|
"""Ottiene o crea client HTTP per un router"""
|
||||||
|
key = f"{router_ip}:{port}"
|
||||||
|
if key not in self.clients:
|
||||||
|
# API REST MikroTik usa porta HTTP/HTTPS (default 80/443)
|
||||||
|
# Per semplicità useremo richieste HTTP dirette
|
||||||
|
auth = base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Basic {auth}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
self.clients[key] = httpx.AsyncClient(
|
||||||
|
base_url=f"http://{router_ip}",
|
||||||
|
headers=headers,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
return self.clients[key]
|
||||||
|
|
||||||
|
async def test_connection(self, router_ip: str, username: str, password: str, port: int = 8728) -> bool:
|
||||||
|
"""Testa connessione a un router"""
|
||||||
|
try:
|
||||||
|
client = self._get_client(router_ip, username, password, port)
|
||||||
|
# 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} fallita: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def add_address_list(
|
||||||
|
self,
|
||||||
|
router_ip: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
ip_address: str,
|
||||||
|
list_name: str = "ddos_blocked",
|
||||||
|
comment: str = "",
|
||||||
|
timeout_duration: str = "1h",
|
||||||
|
port: int = 8728
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Aggiunge IP alla address-list del router
|
||||||
|
timeout_duration: es. "1h", "30m", "1d"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = self._get_client(router_ip, username, password, port)
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Aggiungi nuovo IP
|
||||||
|
data = {
|
||||||
|
"list": list_name,
|
||||||
|
"address": ip_address,
|
||||||
|
"comment": comment or f"IDS block {datetime.now().isoformat()}",
|
||||||
|
"timeout": timeout_duration
|
||||||
|
}
|
||||||
|
|
||||||
|
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})")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Errore aggiunta IP {ip_address} su {router_ip}: {response.status_code} - {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Eccezione aggiunta IP {ip_address} su {router_ip}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def remove_address_list(
|
||||||
|
self,
|
||||||
|
router_ip: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
ip_address: str,
|
||||||
|
list_name: str = "ddos_blocked",
|
||||||
|
port: int = 8728
|
||||||
|
) -> bool:
|
||||||
|
"""Rimuove IP dalla address-list del router"""
|
||||||
|
try:
|
||||||
|
client = self._get_client(router_ip, username, password, port)
|
||||||
|
|
||||||
|
# Trova ID dell'entry
|
||||||
|
response = await client.get("/rest/ip/firewall/address-list")
|
||||||
|
if response.status_code != 200:
|
||||||
|
return False
|
||||||
|
|
||||||
|
entries = response.json()
|
||||||
|
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}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(f"[INFO] IP {ip_address} non trovato in {list_name} su {router_ip}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Eccezione rimozione IP {ip_address} da {router_ip}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_address_list(
|
||||||
|
self,
|
||||||
|
router_ip: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
list_name: Optional[str] = None,
|
||||||
|
port: int = 8728
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Ottiene address-list da router"""
|
||||||
|
try:
|
||||||
|
client = self._get_client(router_ip, username, password, port)
|
||||||
|
response = await client.get("/rest/ip/firewall/address-list")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
entries = response.json()
|
||||||
|
if list_name:
|
||||||
|
entries = [e for e in entries if e.get('list') == list_name]
|
||||||
|
return entries
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Eccezione lettura address-list da {router_ip}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def block_ip_on_all_routers(
|
||||||
|
self,
|
||||||
|
routers: List[Dict],
|
||||||
|
ip_address: str,
|
||||||
|
list_name: str = "ddos_blocked",
|
||||||
|
comment: str = "",
|
||||||
|
timeout_duration: str = "1h"
|
||||||
|
) -> 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 = []
|
||||||
|
router_ips = []
|
||||||
|
|
||||||
|
for router in routers:
|
||||||
|
if not router.get('enabled', True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
task = self.add_address_list(
|
||||||
|
router_ip=router['ip_address'],
|
||||||
|
username=router['username'],
|
||||||
|
password=router['password'],
|
||||||
|
ip_address=ip_address,
|
||||||
|
list_name=list_name,
|
||||||
|
comment=comment,
|
||||||
|
timeout_duration=timeout_duration,
|
||||||
|
port=router.get('api_port', 8728)
|
||||||
|
)
|
||||||
|
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 unblock_ip_on_all_routers(
|
||||||
|
self,
|
||||||
|
routers: List[Dict],
|
||||||
|
ip_address: str,
|
||||||
|
list_name: str = "ddos_blocked"
|
||||||
|
) -> Dict[str, bool]:
|
||||||
|
"""Sblocca IP da tutti i router in parallelo"""
|
||||||
|
tasks = []
|
||||||
|
router_ips = []
|
||||||
|
|
||||||
|
for router in routers:
|
||||||
|
if not router.get('enabled', True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
task = self.remove_address_list(
|
||||||
|
router_ip=router['ip_address'],
|
||||||
|
username=router['username'],
|
||||||
|
password=router['password'],
|
||||||
|
ip_address=ip_address,
|
||||||
|
list_name=list_name,
|
||||||
|
port=router.get('api_port', 8728)
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
router_ips.append(router['ip_address'])
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
router_ip: result if not isinstance(result, Exception) else False
|
||||||
|
for router_ip, result in zip(router_ips, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def close_all(self):
|
||||||
|
"""Chiude tutti i client HTTP"""
|
||||||
|
for client in self.clients.values():
|
||||||
|
await client.aclose()
|
||||||
|
self.clients.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# Fallback SSH per router che non supportano API REST
|
||||||
|
class MikroTikSSHManager:
|
||||||
|
"""Fallback usando SSH se API REST non disponibile"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
print("[WARN] SSH Manager è un fallback. Usa API REST per migliori performance.")
|
||||||
|
|
||||||
|
async def add_address_list(self, *args, **kwargs) -> bool:
|
||||||
|
"""Implementazione SSH fallback (da implementare se necessario)"""
|
||||||
|
print("[WARN] SSH fallback non ancora implementato. Usa API REST.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
'enabled': True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test connessione
|
||||||
|
print("Testing connection...")
|
||||||
|
connected = await manager.test_connection(
|
||||||
|
test_router['ip_address'],
|
||||||
|
test_router['username'],
|
||||||
|
test_router['password']
|
||||||
|
)
|
||||||
|
print(f"Connected: {connected}")
|
||||||
|
|
||||||
|
# Test blocco IP
|
||||||
|
if connected:
|
||||||
|
print("\nTesting IP block...")
|
||||||
|
result = await manager.add_address_list(
|
||||||
|
test_router['ip_address'],
|
||||||
|
test_router['username'],
|
||||||
|
test_router['password'],
|
||||||
|
ip_address='10.0.0.100',
|
||||||
|
list_name='ddos_test',
|
||||||
|
comment='Test IDS',
|
||||||
|
timeout_duration='10m'
|
||||||
|
)
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
print(f"Entries: {entries}")
|
||||||
|
|
||||||
|
await manager.close_all()
|
||||||
|
|
||||||
|
# Esegui test
|
||||||
|
print("=== TEST MIKROTIK MANAGER ===\n")
|
||||||
|
asyncio.run(test())
|
||||||
385
python_ml/ml_analyzer.py
Normal file
385
python_ml/ml_analyzer.py
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
"""
|
||||||
|
IDS ML Analyzer - Sistema di analisi semplificato e veloce
|
||||||
|
Usa solo 25 feature mirate invece di 150+ per migliore performance e accuratezza
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.ensemble import IsolationForest
|
||||||
|
from sklearn.preprocessing import StandardScaler
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Dict, Tuple
|
||||||
|
import joblib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class MLAnalyzer:
|
||||||
|
def __init__(self, model_dir: str = "models"):
|
||||||
|
self.model_dir = Path(model_dir)
|
||||||
|
self.model_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
self.model = None
|
||||||
|
self.scaler = None
|
||||||
|
self.feature_names = []
|
||||||
|
|
||||||
|
def extract_features(self, logs_df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Estrae 25 feature mirate e performanti dai log
|
||||||
|
Focus su: volume, pattern temporali, comportamento protocolli
|
||||||
|
"""
|
||||||
|
if logs_df.empty:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
# Assicura che timestamp sia datetime
|
||||||
|
logs_df['timestamp'] = pd.to_datetime(logs_df['timestamp'])
|
||||||
|
|
||||||
|
# Raggruppa per source_ip
|
||||||
|
features_list = []
|
||||||
|
|
||||||
|
for source_ip, group in logs_df.groupby('source_ip'):
|
||||||
|
group = group.sort_values('timestamp')
|
||||||
|
|
||||||
|
# 1. VOLUME FEATURES (5 feature) - critiche per DDoS
|
||||||
|
total_packets = group['packets'].sum() if 'packets' in group.columns else len(group)
|
||||||
|
total_bytes = group['bytes'].sum() if 'bytes' in group.columns else 0
|
||||||
|
conn_count = len(group)
|
||||||
|
avg_packet_size = total_bytes / max(total_packets, 1)
|
||||||
|
bytes_per_second = total_bytes / max((group['timestamp'].max() - group['timestamp'].min()).total_seconds(), 1)
|
||||||
|
|
||||||
|
# 2. TEMPORAL FEATURES (8 feature) - pattern timing
|
||||||
|
time_span_seconds = (group['timestamp'].max() - group['timestamp'].min()).total_seconds()
|
||||||
|
conn_per_second = conn_count / max(time_span_seconds, 1)
|
||||||
|
hour_of_day = group['timestamp'].dt.hour.mode()[0] if len(group) > 0 else 0
|
||||||
|
day_of_week = group['timestamp'].dt.dayofweek.mode()[0] if len(group) > 0 else 0
|
||||||
|
|
||||||
|
# Burst detection - quante connessioni in finestre di 10 secondi
|
||||||
|
group['time_bucket'] = group['timestamp'].dt.floor('10s')
|
||||||
|
max_burst = group.groupby('time_bucket').size().max()
|
||||||
|
avg_burst = group.groupby('time_bucket').size().mean()
|
||||||
|
burst_variance = group.groupby('time_bucket').size().std()
|
||||||
|
|
||||||
|
# Intervalli tra connessioni
|
||||||
|
time_diffs = group['timestamp'].diff().dt.total_seconds().dropna()
|
||||||
|
avg_interval = time_diffs.mean() if len(time_diffs) > 0 else 0
|
||||||
|
|
||||||
|
# 3. PROTOCOL DIVERSITY (6 feature) - varietà protocolli
|
||||||
|
unique_protocols = group['protocol'].nunique() if 'protocol' in group.columns else 1
|
||||||
|
unique_dest_ports = group['dest_port'].nunique() if 'dest_port' in group.columns else 1
|
||||||
|
unique_dest_ips = group['dest_ip'].nunique() if 'dest_ip' in group.columns else 1
|
||||||
|
|
||||||
|
# Calcola entropia protocolli (più alto = più vario)
|
||||||
|
if 'protocol' in group.columns:
|
||||||
|
protocol_counts = group['protocol'].value_counts()
|
||||||
|
protocol_probs = protocol_counts / protocol_counts.sum()
|
||||||
|
protocol_entropy = -np.sum(protocol_probs * np.log2(protocol_probs + 1e-10))
|
||||||
|
else:
|
||||||
|
protocol_entropy = 0
|
||||||
|
|
||||||
|
# Rapporto TCP/UDP
|
||||||
|
if 'protocol' in group.columns:
|
||||||
|
tcp_ratio = (group['protocol'] == 'tcp').sum() / len(group)
|
||||||
|
udp_ratio = (group['protocol'] == 'udp').sum() / len(group)
|
||||||
|
else:
|
||||||
|
tcp_ratio = udp_ratio = 0
|
||||||
|
|
||||||
|
# 4. PORT SCANNING DETECTION (3 feature)
|
||||||
|
if 'dest_port' in group.columns:
|
||||||
|
unique_ports_contacted = group['dest_port'].nunique()
|
||||||
|
port_scan_score = unique_ports_contacted / max(conn_count, 1)
|
||||||
|
sequential_ports = 0
|
||||||
|
sorted_ports = sorted(group['dest_port'].dropna().unique())
|
||||||
|
for i in range(len(sorted_ports) - 1):
|
||||||
|
if sorted_ports[i+1] - sorted_ports[i] == 1:
|
||||||
|
sequential_ports += 1
|
||||||
|
else:
|
||||||
|
unique_ports_contacted = 0
|
||||||
|
port_scan_score = 0
|
||||||
|
sequential_ports = 0
|
||||||
|
|
||||||
|
# 5. BEHAVIORAL ANOMALIES (3 feature)
|
||||||
|
# Rapporto pacchetti/connessioni
|
||||||
|
packets_per_conn = total_packets / max(conn_count, 1)
|
||||||
|
|
||||||
|
# Variazione dimensione pacchetti
|
||||||
|
if 'bytes' in group.columns and 'packets' in group.columns:
|
||||||
|
group['packet_size'] = group['bytes'] / group['packets'].replace(0, 1)
|
||||||
|
packet_size_variance = group['packet_size'].std()
|
||||||
|
else:
|
||||||
|
packet_size_variance = 0
|
||||||
|
|
||||||
|
# Azioni bloccate vs permesse
|
||||||
|
if 'action' in group.columns:
|
||||||
|
blocked_ratio = (group['action'].str.contains('drop|reject|deny', case=False, na=False)).sum() / len(group)
|
||||||
|
else:
|
||||||
|
blocked_ratio = 0
|
||||||
|
|
||||||
|
features = {
|
||||||
|
'source_ip': source_ip,
|
||||||
|
# Volume
|
||||||
|
'total_packets': total_packets,
|
||||||
|
'total_bytes': total_bytes,
|
||||||
|
'conn_count': conn_count,
|
||||||
|
'avg_packet_size': avg_packet_size,
|
||||||
|
'bytes_per_second': bytes_per_second,
|
||||||
|
# Temporal
|
||||||
|
'time_span_seconds': time_span_seconds,
|
||||||
|
'conn_per_second': conn_per_second,
|
||||||
|
'hour_of_day': hour_of_day,
|
||||||
|
'day_of_week': day_of_week,
|
||||||
|
'max_burst': max_burst,
|
||||||
|
'avg_burst': avg_burst,
|
||||||
|
'burst_variance': burst_variance if not np.isnan(burst_variance) else 0,
|
||||||
|
'avg_interval': avg_interval,
|
||||||
|
# Protocol diversity
|
||||||
|
'unique_protocols': unique_protocols,
|
||||||
|
'unique_dest_ports': unique_dest_ports,
|
||||||
|
'unique_dest_ips': unique_dest_ips,
|
||||||
|
'protocol_entropy': protocol_entropy,
|
||||||
|
'tcp_ratio': tcp_ratio,
|
||||||
|
'udp_ratio': udp_ratio,
|
||||||
|
# Port scanning
|
||||||
|
'unique_ports_contacted': unique_ports_contacted,
|
||||||
|
'port_scan_score': port_scan_score,
|
||||||
|
'sequential_ports': sequential_ports,
|
||||||
|
# Behavioral
|
||||||
|
'packets_per_conn': packets_per_conn,
|
||||||
|
'packet_size_variance': packet_size_variance if not np.isnan(packet_size_variance) else 0,
|
||||||
|
'blocked_ratio': blocked_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
features_list.append(features)
|
||||||
|
|
||||||
|
return pd.DataFrame(features_list)
|
||||||
|
|
||||||
|
def train(self, logs_df: pd.DataFrame, contamination: float = 0.01) -> Dict:
|
||||||
|
"""
|
||||||
|
Addestra il modello Isolation Forest
|
||||||
|
contamination: percentuale attesa di anomalie (default 1%)
|
||||||
|
"""
|
||||||
|
print(f"[TRAINING] Estrazione feature da {len(logs_df)} log...")
|
||||||
|
features_df = self.extract_features(logs_df)
|
||||||
|
|
||||||
|
if features_df.empty:
|
||||||
|
raise ValueError("Nessuna feature estratta dai log")
|
||||||
|
|
||||||
|
print(f"[TRAINING] Feature estratte per {len(features_df)} IP unici")
|
||||||
|
|
||||||
|
# Salva source_ip separatamente
|
||||||
|
source_ips = features_df['source_ip'].values
|
||||||
|
|
||||||
|
# Rimuovi colonna source_ip per training
|
||||||
|
X = features_df.drop('source_ip', axis=1)
|
||||||
|
self.feature_names = X.columns.tolist()
|
||||||
|
|
||||||
|
# Normalizza features
|
||||||
|
print("[TRAINING] Normalizzazione features...")
|
||||||
|
self.scaler = StandardScaler()
|
||||||
|
X_scaled = self.scaler.fit_transform(X)
|
||||||
|
|
||||||
|
# Addestra Isolation Forest
|
||||||
|
print(f"[TRAINING] Addestramento Isolation Forest (contamination={contamination})...")
|
||||||
|
self.model = IsolationForest(
|
||||||
|
contamination=contamination,
|
||||||
|
random_state=42,
|
||||||
|
n_estimators=100,
|
||||||
|
max_samples='auto',
|
||||||
|
n_jobs=-1
|
||||||
|
)
|
||||||
|
self.model.fit(X_scaled)
|
||||||
|
|
||||||
|
# Salva modello
|
||||||
|
self.save_model()
|
||||||
|
|
||||||
|
# Calcola statistiche
|
||||||
|
predictions = self.model.predict(X_scaled)
|
||||||
|
anomalies = (predictions == -1).sum()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'records_processed': len(logs_df),
|
||||||
|
'unique_ips': len(features_df),
|
||||||
|
'features_count': len(self.feature_names),
|
||||||
|
'anomalies_detected': int(anomalies),
|
||||||
|
'contamination': contamination,
|
||||||
|
'status': 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[TRAINING] Completato! {anomalies}/{len(features_df)} IP anomali rilevati")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def detect(self, logs_df: pd.DataFrame, risk_threshold: float = 60.0) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Rileva anomalie nei log
|
||||||
|
risk_threshold: soglia minima di rischio per segnalare (0-100)
|
||||||
|
"""
|
||||||
|
if self.model is None or self.scaler is None:
|
||||||
|
raise ValueError("Modello non addestrato. Esegui train() prima.")
|
||||||
|
|
||||||
|
features_df = self.extract_features(logs_df)
|
||||||
|
|
||||||
|
if features_df.empty:
|
||||||
|
return []
|
||||||
|
|
||||||
|
source_ips = features_df['source_ip'].values
|
||||||
|
X = features_df.drop('source_ip', axis=1)
|
||||||
|
X_scaled = self.scaler.transform(X)
|
||||||
|
|
||||||
|
# Predizioni
|
||||||
|
predictions = self.model.predict(X_scaled)
|
||||||
|
scores = self.model.score_samples(X_scaled)
|
||||||
|
|
||||||
|
# Normalizza score a 0-100 (score più negativo = più anomalo)
|
||||||
|
score_min, score_max = scores.min(), scores.max()
|
||||||
|
risk_scores = 100 * (1 - (scores - score_min) / (score_max - score_min + 1e-10))
|
||||||
|
|
||||||
|
detections = []
|
||||||
|
for i, (ip, pred, risk_score) in enumerate(zip(source_ips, predictions, risk_scores)):
|
||||||
|
if pred == -1 and risk_score >= risk_threshold:
|
||||||
|
# Trova log per questo IP
|
||||||
|
ip_logs = logs_df[logs_df['source_ip'] == ip]
|
||||||
|
|
||||||
|
# Determina tipo anomalia basato su feature
|
||||||
|
features = features_df.iloc[i]
|
||||||
|
anomaly_type = self._classify_anomaly(features)
|
||||||
|
reason = self._generate_reason(features, anomaly_type)
|
||||||
|
|
||||||
|
# Calcola confidence basato su quanto è lontano dalla soglia
|
||||||
|
confidence = min(100, risk_score)
|
||||||
|
|
||||||
|
detection = {
|
||||||
|
'source_ip': ip,
|
||||||
|
'risk_score': float(risk_score),
|
||||||
|
'confidence': float(confidence),
|
||||||
|
'anomaly_type': anomaly_type,
|
||||||
|
'reason': reason,
|
||||||
|
'log_count': len(ip_logs),
|
||||||
|
'first_seen': ip_logs['timestamp'].min().isoformat(),
|
||||||
|
'last_seen': ip_logs['timestamp'].max().isoformat(),
|
||||||
|
}
|
||||||
|
detections.append(detection)
|
||||||
|
|
||||||
|
# Ordina per risk_score decrescente
|
||||||
|
detections.sort(key=lambda x: x['risk_score'], reverse=True)
|
||||||
|
return detections
|
||||||
|
|
||||||
|
def _classify_anomaly(self, features: pd.Series) -> str:
|
||||||
|
"""Classifica il tipo di anomalia basato sulle feature"""
|
||||||
|
# DDoS: alto volume, alta frequenza
|
||||||
|
if features['bytes_per_second'] > 1000000 or features['conn_per_second'] > 100:
|
||||||
|
return 'ddos'
|
||||||
|
|
||||||
|
# Port scanning: molte porte uniche, porte sequenziali
|
||||||
|
if features['port_scan_score'] > 0.5 or features['sequential_ports'] > 10:
|
||||||
|
return 'port_scan'
|
||||||
|
|
||||||
|
# Brute force: molte connessioni a stessa porta
|
||||||
|
if features['conn_per_second'] > 10 and features['unique_dest_ports'] < 3:
|
||||||
|
return 'brute_force'
|
||||||
|
|
||||||
|
# Botnet: pattern temporali regolari, bassa varianza burst
|
||||||
|
if features['burst_variance'] < 1 and features['conn_count'] > 50:
|
||||||
|
return 'botnet'
|
||||||
|
|
||||||
|
return 'suspicious'
|
||||||
|
|
||||||
|
def _generate_reason(self, features: pd.Series, anomaly_type: str) -> str:
|
||||||
|
"""Genera una ragione leggibile per l'anomalia"""
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
if features['bytes_per_second'] > 1000000:
|
||||||
|
reasons.append(f"Alto throughput ({features['bytes_per_second']/1e6:.1f} MB/s)")
|
||||||
|
|
||||||
|
if features['conn_per_second'] > 100:
|
||||||
|
reasons.append(f"Alta frequenza connessioni ({features['conn_per_second']:.0f} conn/s)")
|
||||||
|
|
||||||
|
if features['port_scan_score'] > 0.5:
|
||||||
|
reasons.append(f"Scanning {features['unique_ports_contacted']:.0f} porte")
|
||||||
|
|
||||||
|
if features['max_burst'] > 100:
|
||||||
|
reasons.append(f"Burst anomali (max {features['max_burst']:.0f} conn/10s)")
|
||||||
|
|
||||||
|
if not reasons:
|
||||||
|
reasons.append(f"Comportamento anomalo ({anomaly_type})")
|
||||||
|
|
||||||
|
return "; ".join(reasons)
|
||||||
|
|
||||||
|
def save_model(self):
|
||||||
|
"""Salva modello e scaler su disco"""
|
||||||
|
model_path = self.model_dir / "isolation_forest.joblib"
|
||||||
|
scaler_path = self.model_dir / "scaler.joblib"
|
||||||
|
features_path = self.model_dir / "feature_names.json"
|
||||||
|
|
||||||
|
joblib.dump(self.model, model_path)
|
||||||
|
joblib.dump(self.scaler, scaler_path)
|
||||||
|
|
||||||
|
with open(features_path, 'w') as f:
|
||||||
|
json.dump({'features': self.feature_names}, f)
|
||||||
|
|
||||||
|
print(f"[SAVE] Modello salvato in {self.model_dir}")
|
||||||
|
|
||||||
|
def load_model(self) -> bool:
|
||||||
|
"""Carica modello e scaler da disco"""
|
||||||
|
model_path = self.model_dir / "isolation_forest.joblib"
|
||||||
|
scaler_path = self.model_dir / "scaler.joblib"
|
||||||
|
features_path = self.model_dir / "feature_names.json"
|
||||||
|
|
||||||
|
if not all(p.exists() for p in [model_path, scaler_path, features_path]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.model = joblib.load(model_path)
|
||||||
|
self.scaler = joblib.load(scaler_path)
|
||||||
|
|
||||||
|
with open(features_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.feature_names = data['features']
|
||||||
|
|
||||||
|
print(f"[LOAD] Modello caricato da {self.model_dir}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test con dati demo
|
||||||
|
print("=== TEST ML ANALYZER ===\n")
|
||||||
|
|
||||||
|
# Crea dati demo
|
||||||
|
demo_logs = []
|
||||||
|
base_time = datetime.now()
|
||||||
|
|
||||||
|
# Traffico normale
|
||||||
|
for i in range(100):
|
||||||
|
demo_logs.append({
|
||||||
|
'source_ip': f'192.168.1.{i % 10 + 1}',
|
||||||
|
'timestamp': base_time + timedelta(seconds=i * 10),
|
||||||
|
'dest_ip': '8.8.8.8',
|
||||||
|
'dest_port': 80 if i % 2 == 0 else 443,
|
||||||
|
'protocol': 'tcp',
|
||||||
|
'bytes': 1000 + i * 10,
|
||||||
|
'packets': 10 + i,
|
||||||
|
'action': 'accept'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Traffico anomalo (DDoS simulation)
|
||||||
|
for i in range(1000):
|
||||||
|
demo_logs.append({
|
||||||
|
'source_ip': '10.0.0.100',
|
||||||
|
'timestamp': base_time + timedelta(seconds=i),
|
||||||
|
'dest_ip': '192.168.1.1',
|
||||||
|
'dest_port': 80,
|
||||||
|
'protocol': 'tcp',
|
||||||
|
'bytes': 100,
|
||||||
|
'packets': 1,
|
||||||
|
'action': 'accept'
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(demo_logs)
|
||||||
|
|
||||||
|
# Test training
|
||||||
|
analyzer = MLAnalyzer()
|
||||||
|
result = analyzer.train(df, contamination=0.05)
|
||||||
|
print(f"\n✅ Training completato: {result}")
|
||||||
|
|
||||||
|
# Test detection
|
||||||
|
detections = analyzer.detect(df, risk_threshold=60)
|
||||||
|
print(f"\n🚨 Rilevate {len(detections)} anomalie:")
|
||||||
|
for det in detections[:5]:
|
||||||
|
print(f" - {det['source_ip']}: {det['anomaly_type']} (risk: {det['risk_score']:.1f})")
|
||||||
|
print(f" Motivo: {det['reason']}")
|
||||||
9
python_ml/requirements.txt
Normal file
9
python_ml/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0
|
||||||
|
pandas==2.1.3
|
||||||
|
numpy==1.26.2
|
||||||
|
scikit-learn==1.3.2
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
httpx==0.25.1
|
||||||
15
server/db.ts
Normal file
15
server/db.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Pool, neonConfig } from '@neondatabase/serverless';
|
||||||
|
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||||
|
import ws from "ws";
|
||||||
|
import * as schema from "@shared/schema";
|
||||||
|
|
||||||
|
neonConfig.webSocketConstructor = ws;
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
throw new Error(
|
||||||
|
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
export const db = drizzle({ client: pool, schema });
|
||||||
@ -1,63 +1,227 @@
|
|||||||
import { type ProjectFile, type InsertProjectFile } from "@shared/schema";
|
import {
|
||||||
import { randomUUID } from "crypto";
|
routers,
|
||||||
|
networkLogs,
|
||||||
|
detections,
|
||||||
|
whitelist,
|
||||||
|
trainingHistory,
|
||||||
|
type Router,
|
||||||
|
type InsertRouter,
|
||||||
|
type NetworkLog,
|
||||||
|
type InsertNetworkLog,
|
||||||
|
type Detection,
|
||||||
|
type InsertDetection,
|
||||||
|
type Whitelist,
|
||||||
|
type InsertWhitelist,
|
||||||
|
type TrainingHistory,
|
||||||
|
type InsertTrainingHistory,
|
||||||
|
} from "@shared/schema";
|
||||||
|
import { db } from "./db";
|
||||||
|
import { eq, desc, and, gte, sql, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
getAllFiles(): Promise<ProjectFile[]>;
|
// Routers
|
||||||
getFileById(id: string): Promise<ProjectFile | undefined>;
|
getAllRouters(): Promise<Router[]>;
|
||||||
getFilesByCategory(category: string): Promise<ProjectFile[]>;
|
getRouterById(id: string): Promise<Router | undefined>;
|
||||||
createFile(file: InsertProjectFile): Promise<ProjectFile>;
|
createRouter(router: InsertRouter): Promise<Router>;
|
||||||
deleteFile(id: string): Promise<boolean>;
|
updateRouter(id: string, router: Partial<InsertRouter>): Promise<Router | undefined>;
|
||||||
searchFiles(query: string): Promise<ProjectFile[]>;
|
deleteRouter(id: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// Network Logs
|
||||||
|
getRecentLogs(limit: number): Promise<NetworkLog[]>;
|
||||||
|
getLogsByIp(sourceIp: string, limit: number): Promise<NetworkLog[]>;
|
||||||
|
createLog(log: InsertNetworkLog): Promise<NetworkLog>;
|
||||||
|
getLogsForTraining(limit: number, minTimestamp?: Date): Promise<NetworkLog[]>;
|
||||||
|
|
||||||
|
// Detections
|
||||||
|
getAllDetections(limit: number): Promise<Detection[]>;
|
||||||
|
getDetectionByIp(sourceIp: string): Promise<Detection | undefined>;
|
||||||
|
createDetection(detection: InsertDetection): Promise<Detection>;
|
||||||
|
updateDetection(id: string, detection: Partial<InsertDetection>): Promise<Detection | undefined>;
|
||||||
|
getUnblockedDetections(): Promise<Detection[]>;
|
||||||
|
|
||||||
|
// Whitelist
|
||||||
|
getAllWhitelist(): Promise<Whitelist[]>;
|
||||||
|
getWhitelistByIp(ipAddress: string): Promise<Whitelist | undefined>;
|
||||||
|
createWhitelist(whitelist: InsertWhitelist): Promise<Whitelist>;
|
||||||
|
deleteWhitelist(id: string): Promise<boolean>;
|
||||||
|
isWhitelisted(ipAddress: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// Training History
|
||||||
|
getTrainingHistory(limit: number): Promise<TrainingHistory[]>;
|
||||||
|
createTrainingHistory(history: InsertTrainingHistory): Promise<TrainingHistory>;
|
||||||
|
getLatestTraining(): Promise<TrainingHistory | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
private files: Map<string, ProjectFile>;
|
// Routers
|
||||||
|
async getAllRouters(): Promise<Router[]> {
|
||||||
constructor() {
|
return await db.select().from(routers).orderBy(desc(routers.createdAt));
|
||||||
this.files = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllFiles(): Promise<ProjectFile[]> {
|
async getRouterById(id: string): Promise<Router | undefined> {
|
||||||
return Array.from(this.files.values()).sort((a, b) =>
|
const [router] = await db.select().from(routers).where(eq(routers.id, id));
|
||||||
b.uploadedAt.getTime() - a.uploadedAt.getTime()
|
return router || undefined;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileById(id: string): Promise<ProjectFile | undefined> {
|
async createRouter(insertRouter: InsertRouter): Promise<Router> {
|
||||||
return this.files.get(id);
|
const [router] = await db.insert(routers).values(insertRouter).returning();
|
||||||
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFilesByCategory(category: string): Promise<ProjectFile[]> {
|
async updateRouter(id: string, updateData: Partial<InsertRouter>): Promise<Router | undefined> {
|
||||||
return Array.from(this.files.values())
|
const [router] = await db
|
||||||
.filter(file => file.category === category)
|
.update(routers)
|
||||||
.sort((a, b) => b.uploadedAt.getTime() - a.uploadedAt.getTime());
|
.set(updateData)
|
||||||
|
.where(eq(routers.id, id))
|
||||||
|
.returning();
|
||||||
|
return router || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFile(insertFile: InsertProjectFile): Promise<ProjectFile> {
|
async deleteRouter(id: string): Promise<boolean> {
|
||||||
const id = randomUUID();
|
const result = await db.delete(routers).where(eq(routers.id, id));
|
||||||
const file: ProjectFile = {
|
return result.rowCount !== null && result.rowCount > 0;
|
||||||
...insertFile,
|
|
||||||
id,
|
|
||||||
uploadedAt: new Date(),
|
|
||||||
};
|
|
||||||
this.files.set(id, file);
|
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(id: string): Promise<boolean> {
|
// Network Logs
|
||||||
return this.files.delete(id);
|
async getRecentLogs(limit: number): Promise<NetworkLog[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(networkLogs)
|
||||||
|
.orderBy(desc(networkLogs.timestamp))
|
||||||
|
.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchFiles(query: string): Promise<ProjectFile[]> {
|
async getLogsByIp(sourceIp: string, limit: number): Promise<NetworkLog[]> {
|
||||||
const lowerQuery = query.toLowerCase();
|
return await db
|
||||||
return Array.from(this.files.values())
|
.select()
|
||||||
.filter(file =>
|
.from(networkLogs)
|
||||||
file.filename.toLowerCase().includes(lowerQuery) ||
|
.where(eq(networkLogs.sourceIp, sourceIp))
|
||||||
file.filepath.toLowerCase().includes(lowerQuery) ||
|
.orderBy(desc(networkLogs.timestamp))
|
||||||
(file.content && file.content.toLowerCase().includes(lowerQuery))
|
.limit(limit);
|
||||||
)
|
}
|
||||||
.sort((a, b) => b.uploadedAt.getTime() - a.uploadedAt.getTime());
|
|
||||||
|
async createLog(insertLog: InsertNetworkLog): Promise<NetworkLog> {
|
||||||
|
const [log] = await db.insert(networkLogs).values(insertLog).returning();
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLogsForTraining(limit: number, minTimestamp?: Date): Promise<NetworkLog[]> {
|
||||||
|
const conditions = minTimestamp
|
||||||
|
? and(gte(networkLogs.timestamp, minTimestamp))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(networkLogs)
|
||||||
|
.where(conditions)
|
||||||
|
.orderBy(desc(networkLogs.timestamp))
|
||||||
|
.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detections
|
||||||
|
async getAllDetections(limit: number): Promise<Detection[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(detections)
|
||||||
|
.orderBy(desc(detections.detectedAt))
|
||||||
|
.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDetectionByIp(sourceIp: string): Promise<Detection | undefined> {
|
||||||
|
const [detection] = await db
|
||||||
|
.select()
|
||||||
|
.from(detections)
|
||||||
|
.where(eq(detections.sourceIp, sourceIp))
|
||||||
|
.orderBy(desc(detections.detectedAt))
|
||||||
|
.limit(1);
|
||||||
|
return detection || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDetection(insertDetection: InsertDetection): Promise<Detection> {
|
||||||
|
const [detection] = await db
|
||||||
|
.insert(detections)
|
||||||
|
.values(insertDetection)
|
||||||
|
.returning();
|
||||||
|
return detection;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDetection(
|
||||||
|
id: string,
|
||||||
|
updateData: Partial<InsertDetection>
|
||||||
|
): Promise<Detection | undefined> {
|
||||||
|
const [detection] = await db
|
||||||
|
.update(detections)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(detections.id, id))
|
||||||
|
.returning();
|
||||||
|
return detection || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUnblockedDetections(): Promise<Detection[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(detections)
|
||||||
|
.where(eq(detections.blocked, false))
|
||||||
|
.orderBy(desc(detections.riskScore));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelist
|
||||||
|
async getAllWhitelist(): Promise<Whitelist[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(whitelist)
|
||||||
|
.where(eq(whitelist.active, true))
|
||||||
|
.orderBy(desc(whitelist.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWhitelistByIp(ipAddress: string): Promise<Whitelist | undefined> {
|
||||||
|
const [item] = await db
|
||||||
|
.select()
|
||||||
|
.from(whitelist)
|
||||||
|
.where(and(eq(whitelist.ipAddress, ipAddress), eq(whitelist.active, true)));
|
||||||
|
return item || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWhitelist(insertWhitelist: InsertWhitelist): Promise<Whitelist> {
|
||||||
|
const [item] = await db.insert(whitelist).values(insertWhitelist).returning();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWhitelist(id: string): Promise<boolean> {
|
||||||
|
const result = await db.delete(whitelist).where(eq(whitelist.id, id));
|
||||||
|
return result.rowCount !== null && result.rowCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWhitelisted(ipAddress: string): Promise<boolean> {
|
||||||
|
const item = await this.getWhitelistByIp(ipAddress);
|
||||||
|
return item !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training History
|
||||||
|
async getTrainingHistory(limit: number): Promise<TrainingHistory[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(trainingHistory)
|
||||||
|
.orderBy(desc(trainingHistory.trainedAt))
|
||||||
|
.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTrainingHistory(insertHistory: InsertTrainingHistory): Promise<TrainingHistory> {
|
||||||
|
const [history] = await db
|
||||||
|
.insert(trainingHistory)
|
||||||
|
.values(insertHistory)
|
||||||
|
.returning();
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatestTraining(): Promise<TrainingHistory | undefined> {
|
||||||
|
const [history] = await db
|
||||||
|
.select()
|
||||||
|
.from(trainingHistory)
|
||||||
|
.orderBy(desc(trainingHistory.trainedAt))
|
||||||
|
.limit(1);
|
||||||
|
return history || undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = new MemStorage();
|
export const storage = new DatabaseStorage();
|
||||||
|
|||||||
142
shared/schema.ts
142
shared/schema.ts
@ -1,22 +1,136 @@
|
|||||||
import { pgTable, text, varchar, integer, timestamp } from "drizzle-orm/pg-core";
|
import { sql, relations } from "drizzle-orm";
|
||||||
|
import { pgTable, text, varchar, integer, timestamp, decimal, boolean, index } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const projectFiles = pgTable("project_files", {
|
// Router MikroTik configuration
|
||||||
id: varchar("id").primaryKey(),
|
export const routers = pgTable("routers", {
|
||||||
filename: text("filename").notNull(),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
filepath: text("filepath").notNull(),
|
name: text("name").notNull(),
|
||||||
fileType: text("file_type").notNull(),
|
ipAddress: text("ip_address").notNull().unique(),
|
||||||
size: integer("size").notNull(),
|
apiPort: integer("api_port").notNull().default(8728),
|
||||||
content: text("content"),
|
username: text("username").notNull(),
|
||||||
category: text("category").notNull(),
|
password: text("password").notNull(),
|
||||||
uploadedAt: timestamp("uploaded_at").defaultNow().notNull(),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
lastSync: timestamp("last_sync"),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const insertProjectFileSchema = createInsertSchema(projectFiles).omit({
|
// Network logs from MikroTik (syslog)
|
||||||
|
export const networkLogs = pgTable("network_logs", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
routerId: varchar("router_id").references(() => routers.id).notNull(),
|
||||||
|
timestamp: timestamp("timestamp").notNull(),
|
||||||
|
sourceIp: text("source_ip").notNull(),
|
||||||
|
destIp: text("dest_ip"),
|
||||||
|
sourcePort: integer("source_port"),
|
||||||
|
destPort: integer("dest_port"),
|
||||||
|
protocol: text("protocol"),
|
||||||
|
action: text("action"),
|
||||||
|
bytes: integer("bytes"),
|
||||||
|
packets: integer("packets"),
|
||||||
|
loggedAt: timestamp("logged_at").defaultNow().notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
sourceIpIdx: index("source_ip_idx").on(table.sourceIp),
|
||||||
|
timestampIdx: index("timestamp_idx").on(table.timestamp),
|
||||||
|
routerIdIdx: index("router_id_idx").on(table.routerId),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Detected threats/anomalies
|
||||||
|
export const detections = pgTable("detections", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
sourceIp: text("source_ip").notNull(),
|
||||||
|
riskScore: decimal("risk_score", { precision: 5, scale: 2 }).notNull(),
|
||||||
|
confidence: decimal("confidence", { precision: 5, scale: 2 }).notNull(),
|
||||||
|
anomalyType: text("anomaly_type").notNull(),
|
||||||
|
reason: text("reason"),
|
||||||
|
logCount: integer("log_count").notNull(),
|
||||||
|
firstSeen: timestamp("first_seen").notNull(),
|
||||||
|
lastSeen: timestamp("last_seen").notNull(),
|
||||||
|
blocked: boolean("blocked").notNull().default(false),
|
||||||
|
blockedAt: timestamp("blocked_at"),
|
||||||
|
detectedAt: timestamp("detected_at").defaultNow().notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
sourceIpIdx: index("detection_source_ip_idx").on(table.sourceIp),
|
||||||
|
riskScoreIdx: index("risk_score_idx").on(table.riskScore),
|
||||||
|
detectedAtIdx: index("detected_at_idx").on(table.detectedAt),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Whitelist per IP fidati
|
||||||
|
export const whitelist = pgTable("whitelist", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
ipAddress: text("ip_address").notNull().unique(),
|
||||||
|
comment: text("comment"),
|
||||||
|
reason: text("reason"),
|
||||||
|
createdBy: text("created_by"),
|
||||||
|
active: boolean("active").notNull().default(true),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ML Training history
|
||||||
|
export const trainingHistory = pgTable("training_history", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
modelVersion: text("model_version").notNull(),
|
||||||
|
recordsProcessed: integer("records_processed").notNull(),
|
||||||
|
featuresCount: integer("features_count").notNull(),
|
||||||
|
accuracy: decimal("accuracy", { precision: 5, scale: 2 }),
|
||||||
|
trainingDuration: integer("training_duration"),
|
||||||
|
status: text("status").notNull(),
|
||||||
|
notes: text("notes"),
|
||||||
|
trainedAt: timestamp("trained_at").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
export const routersRelations = relations(routers, ({ many }) => ({
|
||||||
|
logs: many(networkLogs),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const networkLogsRelations = relations(networkLogs, ({ one }) => ({
|
||||||
|
router: one(routers, {
|
||||||
|
fields: [networkLogs.routerId],
|
||||||
|
references: [routers.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Insert schemas
|
||||||
|
export const insertRouterSchema = createInsertSchema(routers).omit({
|
||||||
id: true,
|
id: true,
|
||||||
uploadedAt: true,
|
createdAt: true,
|
||||||
|
lastSync: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type InsertProjectFile = z.infer<typeof insertProjectFileSchema>;
|
export const insertNetworkLogSchema = createInsertSchema(networkLogs).omit({
|
||||||
export type ProjectFile = typeof projectFiles.$inferSelect;
|
id: true,
|
||||||
|
loggedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertDetectionSchema = createInsertSchema(detections).omit({
|
||||||
|
id: true,
|
||||||
|
detectedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertWhitelistSchema = createInsertSchema(whitelist).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertTrainingHistorySchema = createInsertSchema(trainingHistory).omit({
|
||||||
|
id: true,
|
||||||
|
trainedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type Router = typeof routers.$inferSelect;
|
||||||
|
export type InsertRouter = z.infer<typeof insertRouterSchema>;
|
||||||
|
|
||||||
|
export type NetworkLog = typeof networkLogs.$inferSelect;
|
||||||
|
export type InsertNetworkLog = z.infer<typeof insertNetworkLogSchema>;
|
||||||
|
|
||||||
|
export type Detection = typeof detections.$inferSelect;
|
||||||
|
export type InsertDetection = z.infer<typeof insertDetectionSchema>;
|
||||||
|
|
||||||
|
export type Whitelist = typeof whitelist.$inferSelect;
|
||||||
|
export type InsertWhitelist = z.infer<typeof insertWhitelistSchema>;
|
||||||
|
|
||||||
|
export type TrainingHistory = typeof trainingHistory.$inferSelect;
|
||||||
|
export type InsertTrainingHistory = z.infer<typeof insertTrainingHistorySchema>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user