""" Endpoints API pour le contrôle des scans réseau """ from fastapi import APIRouter, Depends, BackgroundTasks from sqlalchemy.orm import Session from datetime import datetime, timedelta from typing import Dict, Any, Optional, List from pydantic import BaseModel from backend.app.core.database import get_db from backend.app.core.config import config_manager from backend.app.models.ip import IP, IPHistory from backend.app.models.scan_log import ScanLog from backend.app.services.network import NetworkScanner, OuiLookup from backend.app.services.websocket import ws_manager router = APIRouter(prefix="/api/scan", tags=["Scan"]) class ScanLogResponse(BaseModel): """Schéma de réponse logs scan""" id: int ip: Optional[str] status: Optional[str] message: str created_at: datetime class Config: from_attributes = True async def perform_scan(db: Session): """ Effectue un scan complet du réseau Fonction asynchrone pour background task Args: db: Session de base de données """ try: async def scan_log(message: str): print(message) try: await ws_manager.broadcast_scan_log(message) except Exception: pass await scan_log(f"[{datetime.now()}] Début du scan réseau...") # Notifier début du scan try: await ws_manager.broadcast_scan_start() except Exception as e: print(f"Erreur broadcast start (ignorée): {e}") # Récupérer la config config = config_manager.config await scan_log(f"[{datetime.now()}] Config chargée: {config.network.cidr}") # Initialiser le scanner scanner = NetworkScanner( cidr=config.network.cidr, timeout=config.scan.timeout, ping_count=config.scan.ping_count ) # Convertir les ports en liste d'entiers port_list = [] for port_range in config.ports.ranges: if '-' in port_range: start, end = map(int, port_range.split('-')) port_list.extend(range(start, end + 1)) else: port_list.append(int(port_range)) await scan_log(f"[{datetime.now()}] Ports à scanner: {len(port_list)}") # Récupérer les IPs connues known_ips = config.ip_classes await scan_log(f"[{datetime.now()}] IPs connues: {len(known_ips)}") # Callback de progression pour WebSocket async def progress_callback(current: int, total: int, current_ip: str, status: str, ping_ok: bool): try: ping_label = "ok" if ping_ok else "fail" await ws_manager.broadcast_scan_progress({ "current": current, "total": total, "ip": current_ip }) await ws_manager.broadcast_scan_log( f"[{current}/{total}] {current_ip} -> ping:{ping_label} ({status})" ) except Exception: # Ignorer les erreurs WebSocket pour ne pas bloquer le scan pass # Lancer le scan await scan_log(f"[{datetime.now()}] Lancement du scan (parallélisme: {config.scan.parallel_pings})...") scan_results = await scanner.full_scan( known_ips=known_ips, port_list=port_list, max_concurrent=config.scan.parallel_pings, progress_callback=progress_callback ) await scan_log(f"[{datetime.now()}] Scan terminé: {len(scan_results)} IPs trouvées") # Mettre à jour la base de données stats = { "total": 0, "online": 0, "offline": 0, "new": 0, "updated": 0 } for ip_address, ip_data in scan_results.items(): stats["total"] += 1 if ip_data["last_status"] == "online": stats["online"] += 1 else: stats["offline"] += 1 # Log par IP (historique scan) ping_label = "ok" if ip_data["last_status"] == "online" else "fail" log_message = f"Scan {ip_address} -> ping:{ping_label} ({ip_data['last_status']})" db.add(ScanLog( ip=ip_address, status=ip_data["last_status"], message=log_message )) # Vérifier si l'IP existe déjà existing_ip = db.query(IP).filter(IP.ip == ip_address).first() if existing_ip: # Mettre à jour l'IP existante old_status = existing_ip.last_status # Si l'IP passe de offline à online ET qu'elle était inconnue, c'est une "nouvelle détection" # On réinitialise first_seen pour qu'elle apparaisse dans "Nouvelles Détections" if (old_status == "offline" and ip_data["last_status"] == "online" and not existing_ip.known): existing_ip.first_seen = datetime.now() # Détecter changement de MAC address new_mac = ip_data.get("mac") if new_mac and existing_ip.mac and new_mac != existing_ip.mac: # MAC a changé ! Marquer comme changée existing_ip.mac_changed = True print(f"[ALERTE] MAC changée pour {ip_address}: {existing_ip.mac} -> {new_mac}") else: # Pas de changement ou pas de MAC précédente existing_ip.mac_changed = False existing_ip.last_status = ip_data["last_status"] if ip_data["last_seen"]: existing_ip.last_seen = ip_data["last_seen"] existing_ip.mac = ip_data.get("mac") or existing_ip.mac vendor = ip_data.get("vendor") if (not vendor or vendor == "Unknown") and existing_ip.mac: vendor = OuiLookup.lookup(existing_ip.mac) or vendor if config.scan.force_vendor_update: if vendor and vendor != "Unknown": existing_ip.vendor = vendor else: if (not existing_ip.vendor or existing_ip.vendor == "Unknown") and vendor and vendor != "Unknown": existing_ip.vendor = vendor existing_ip.hostname = ip_data.get("hostname") or existing_ip.hostname existing_ip.open_ports = ip_data.get("open_ports", []) # Mettre à jour host seulement si présent dans ip_data (config) if "host" in ip_data: existing_ip.host = ip_data["host"] # Mettre à jour le flag network_device (basé sur host="Network") # Utiliser le host existant si ip_data n'en a pas current_host = ip_data.get("host") or existing_ip.host existing_ip.network_device = (current_host == "Network") # Si l'état a changé, notifier via WebSocket if old_status != ip_data["last_status"]: await ws_manager.broadcast_ip_update({ "ip": ip_address, "old_status": old_status, "new_status": ip_data["last_status"] }) stats["updated"] += 1 else: # Créer une nouvelle IP vendor = ip_data.get("vendor") if (not vendor or vendor == "Unknown") and ip_data.get("mac"): vendor = OuiLookup.lookup(ip_data.get("mac")) or vendor new_ip = IP( ip=ip_address, name=ip_data.get("name"), known=ip_data.get("known", False), network_device=ip_data.get("host") == "Network", location=ip_data.get("location"), host=ip_data.get("host"), first_seen=datetime.now(), last_seen=ip_data.get("last_seen") or datetime.now(), last_status=ip_data["last_status"], mac=ip_data.get("mac"), vendor=vendor, hostname=ip_data.get("hostname"), open_ports=ip_data.get("open_ports", []) ) db.add(new_ip) # Notifier nouvelle IP await ws_manager.broadcast_new_ip({ "ip": ip_address, "status": ip_data["last_status"], "known": ip_data.get("known", False) }) stats["new"] += 1 # Ajouter à l'historique history_entry = IPHistory( ip=ip_address, timestamp=datetime.now(), status=ip_data["last_status"], open_ports=ip_data.get("open_ports", []) ) db.add(history_entry) # Commit les changements db.commit() # Notifier fin du scan avec stats await ws_manager.broadcast_scan_complete(stats) print(f"[{datetime.now()}] Scan terminé: {stats}") except Exception as e: print(f"Erreur lors du scan: {e}") db.rollback() @router.post("/start") async def start_scan(background_tasks: BackgroundTasks, db: Session = Depends(get_db)): """ Déclenche un scan réseau immédiat Returns: Message de confirmation """ # Lancer le scan en arrière-plan background_tasks.add_task(perform_scan, db) return { "message": "Scan réseau démarré", "timestamp": datetime.now() } @router.get("/logs", response_model=List[ScanLogResponse]) async def get_scan_logs(limit: int = 200, db: Session = Depends(get_db)): """ Retourne les derniers logs de scan """ logs = db.query(ScanLog).order_by(ScanLog.created_at.desc()).limit(limit).all() return list(reversed(logs)) @router.post("/ports/{ip_address}") async def scan_ip_ports(ip_address: str, db: Session = Depends(get_db)): """ Scanne les ports d'une IP spécifique Args: ip_address: Adresse IP à scanner db: Session de base de données Returns: Liste des ports ouverts """ try: # Récupérer la config config = config_manager.config # Convertir les ports en liste d'entiers port_list = [] for port_range in config.ports.ranges: if '-' in port_range: start, end = map(int, port_range.split('-')) port_list.extend(range(start, end + 1)) else: port_list.append(int(port_range)) # Initialiser le scanner scanner = NetworkScanner( cidr=config.network.cidr, timeout=config.scan.timeout, ping_count=config.scan.ping_count ) # Scanner les ports de cette IP print(f"[{datetime.now()}] Scan ports pour {ip_address}...") open_ports = await scanner.scan_ports(ip_address, port_list) print(f"[{datetime.now()}] Ports ouverts pour {ip_address}: {open_ports}") # Mettre à jour la base de données ip_record = db.query(IP).filter(IP.ip == ip_address).first() if ip_record: ip_record.open_ports = open_ports ip_record.last_seen = datetime.now() db.commit() # Notifier via WebSocket await ws_manager.broadcast_ip_update({ "ip": ip_address, "open_ports": open_ports }) return { "message": "Scan de ports terminé", "ip": ip_address, "open_ports": open_ports, "timestamp": datetime.now() } except Exception as e: print(f"Erreur scan ports {ip_address}: {e}") return { "message": f"Erreur: {str(e)}", "ip": ip_address, "open_ports": [], "timestamp": datetime.now() } @router.post("/cleanup-history") async def cleanup_history(hours: int = 24, db: Session = Depends(get_db)): """ Nettoie l'historique plus ancien que X heures Args: hours: Nombre d'heures à conserver (défaut: 24h) db: Session de base de données Returns: Nombre d'entrées supprimées """ cutoff_date = datetime.now() - timedelta(hours=hours) deleted = db.query(IPHistory).filter( IPHistory.timestamp < cutoff_date ).delete() db.commit() return { "message": f"Historique nettoyé", "deleted_entries": deleted, "older_than_hours": hours }