""" Router pour l'onglet Ports & Services Scan de services réseau sur les IPs connues — parallélisé par batch """ import asyncio from datetime import datetime from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from pydantic import BaseModel from typing import List, Dict, Any from backend.app.core.database import get_db from backend.app.core.config import config_manager from backend.app.models.ip import IP from backend.app.services.network import NetworkScanner from backend.app.services.websocket import ws_manager router = APIRouter(prefix="/api/services", tags=["Services"]) # Verrou pour empêcher les scans simultanés _scan_lock = asyncio.Lock() # Nombre d'IPs scannées en parallèle PARALLEL_IPS = 20 class ServiceScanRequest(BaseModel): """Requête de scan de services""" ports: List[int] @router.get("/list") async def list_services(): """Retourne la liste des services configurés dans config.yaml""" config = config_manager.config return { "services": [s.model_dump() for s in config.services] } async def _scan_single_ip( scanner: NetworkScanner, ip_info: Dict[str, str], selected_ports: List[int], port_to_service: Dict[int, Dict[str, Any]], idx: int, total_ips: int, semaphore: asyncio.Semaphore ) -> list: """Scanne une IP avec sémaphore de concurrence""" async with semaphore: ip_addr = ip_info["ip"] try: open_ports = await scanner.scan_ports(ip_addr, selected_ports) except Exception as e: print(f"[Services] Erreur scan {ip_addr}: {e}") await ws_manager.broadcast({ "type": "service_scan_log", "message": f"{ip_addr} — Erreur: {e}" }) return [] # Log du résultat if open_ports: port_names = [] for p in open_ports: svc = port_to_service.get(p, {"name": str(p)}) port_names.append(f"{svc['name']}({p})") await ws_manager.broadcast({ "type": "service_scan_log", "message": f"{ip_addr} — {', '.join(port_names)}" }) else: await ws_manager.broadcast({ "type": "service_scan_log", "message": f"{ip_addr} — aucun service" }) # Construire les résultats et les envoyer en temps réel ip_results = [] for port in open_ports: svc_info = port_to_service.get(port, {"name": f"Port {port}", "protocol": None}) protocol = svc_info["protocol"] url = None if protocol in ("http", "https"): if (protocol == "http" and port == 80) or (protocol == "https" and port == 443): url = f"{protocol}://{ip_addr}" else: url = f"{protocol}://{ip_addr}:{port}" result_entry = { "ip": ip_addr, "name": ip_info["name"], "hostname": ip_info["hostname"], "port": port, "service_name": svc_info["name"], "protocol": protocol or "", "url": url } ip_results.append(result_entry) # Envoyer le résultat en temps réel await ws_manager.broadcast({ "type": "service_scan_result", "result": result_entry }) return ip_results async def _run_service_scan(selected_ports: List[int], ip_list: list): """Tâche de fond : scanne les IPs en parallèle et envoie les résultats via WebSocket""" config = config_manager.config # Mapping port → service port_to_service = {} for svc in config.services: port_to_service[svc.port] = { "name": svc.name, "protocol": svc.protocol } total_ips = len(ip_list) scanner = NetworkScanner( cidr=config.network.cidr, timeout=config.scan.timeout, ping_count=config.scan.ping_count ) await ws_manager.broadcast({ "type": "service_scan_start", "message": f"Scan démarré ({len(selected_ports)} ports, {total_ips} IPs, {PARALLEL_IPS} en parallèle)" }) # Sémaphore pour limiter la concurrence semaphore = asyncio.Semaphore(PARALLEL_IPS) completed = 0 async def scan_with_progress(ip_info, idx): nonlocal completed result = await _scan_single_ip( scanner, ip_info, selected_ports, port_to_service, idx, total_ips, semaphore ) completed += 1 # Progression await ws_manager.broadcast({ "type": "service_scan_progress", "current": completed, "total": total_ips, "ip": ip_info["ip"] }) return result # Lancer toutes les IPs en parallèle (limité par sémaphore) tasks = [ scan_with_progress(ip_info, idx) for idx, ip_info in enumerate(ip_list) ] all_results = await asyncio.gather(*tasks) # Aplatir les résultats results = [] for ip_results in all_results: results.extend(ip_results) # Trier par IP puis par port results.sort(key=lambda r: ( tuple(int(p) for p in r["ip"].split(".")), r["port"] )) stats = { "total_ips": total_ips, "total_found": len(results), "scan_time": datetime.now().isoformat() } await ws_manager.broadcast({ "type": "service_scan_complete", "message": f"Scan terminé : {len(results)} services détectés sur {total_ips} IPs", "stats": stats, "results": results }) @router.post("/scan") async def scan_services(request: ServiceScanRequest, db: Session = Depends(get_db)): """ Lance un scan de services en tâche de fond (parallélisé). Les résultats sont envoyés via WebSocket. """ selected_ports = request.ports if not selected_ports: return {"status": "error", "message": "Aucun port sélectionné"} if _scan_lock.locked(): return {"status": "error", "message": "Un scan de services est déjà en cours"} # Récupérer la liste des IPs all_ips = db.query(IP).filter(IP.last_seen.isnot(None)).all() ip_list = [ {"ip": ip.ip, "name": ip.name or "", "hostname": ip.hostname or ""} for ip in all_ips ] if not ip_list: return {"status": "error", "message": "Aucune IP connue en base"} async def locked_scan(): async with _scan_lock: await _run_service_scan(selected_ports, ip_list) asyncio.create_task(locked_scan()) return { "status": "started", "message": f"Scan lancé pour {len(selected_ports)} ports sur {len(ip_list)} IPs ({PARALLEL_IPS} en parallèle)", "total_ips": len(ip_list) }