scan port

This commit is contained in:
2026-02-07 18:53:18 +01:00
parent dff1b03e42
commit 4eb3defa59
11 changed files with 1037 additions and 3 deletions

View File

@@ -82,6 +82,14 @@ class OPNsenseConfig(BaseModel):
protocol: str = "http" # "http" ou "https"
class ServiceDefinition(BaseModel):
"""Définition d'un service réseau (pour scan de services)"""
name: str
port: int
protocol: Optional[str] = None # http, https, ssh, rdp, smb...
description: str = ""
class DatabaseConfig(BaseModel):
"""Configuration base de données"""
path: str = "./data/db.sqlite"
@@ -123,6 +131,7 @@ class IPWatchConfig(BaseModel):
colors: ColorsConfig = Field(default_factory=ColorsConfig)
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
opnsense: OPNsenseConfig = Field(default_factory=OPNsenseConfig)
services: List[ServiceDefinition] = Field(default_factory=list)
class ConfigManager:

View File

@@ -17,6 +17,7 @@ from backend.app.routers import config as config_router
from backend.app.routers import system as system_router
from backend.app.routers import tracking as tracking_router
from backend.app.routers import opnsense as opnsense_router
from backend.app.routers import services as services_router
from backend.app.services.scheduler import scan_scheduler
from backend.app.routers.scan import perform_scan
@@ -129,6 +130,7 @@ app.include_router(system_router.router)
app.include_router(tracking_router.router)
app.include_router(architecture_router.router)
app.include_router(opnsense_router.router)
app.include_router(services_router.router)
# Servir les ressources d'architecture
architecture_dir = Path("./architecture")

View File

@@ -0,0 +1,224 @@
"""
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)
}