scan port
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
224
backend/app/routers/services.py
Normal file
224
backend/app/routers/services.py
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user