225 lines
6.8 KiB
Python
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)
|
|
}
|