Files
ipwatch/backend/app/routers/services.py
2026-02-07 18:53:18 +01:00

225 lines
6.8 KiB
Python

"""
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)
}