Files
ipwatch/backend/app/routers/scan.py
2026-02-07 16:57:37 +01:00

363 lines
12 KiB
Python
Executable File

"""
Endpoints API pour le contrôle des scans réseau
"""
from fastapi import APIRouter, Depends, BackgroundTasks
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List
from pydantic import BaseModel
from backend.app.core.database import get_db
from backend.app.core.config import config_manager
from backend.app.models.ip import IP, IPHistory
from backend.app.models.scan_log import ScanLog
from backend.app.services.network import NetworkScanner, OuiLookup
from backend.app.services.websocket import ws_manager
router = APIRouter(prefix="/api/scan", tags=["Scan"])
class ScanLogResponse(BaseModel):
"""Schéma de réponse logs scan"""
id: int
ip: Optional[str]
status: Optional[str]
message: str
created_at: datetime
class Config:
from_attributes = True
async def perform_scan(db: Session):
"""
Effectue un scan complet du réseau
Fonction asynchrone pour background task
Args:
db: Session de base de données
"""
try:
async def scan_log(message: str):
print(message)
try:
await ws_manager.broadcast_scan_log(message)
except Exception:
pass
await scan_log(f"[{datetime.now()}] Début du scan réseau...")
# Notifier début du scan
try:
await ws_manager.broadcast_scan_start()
except Exception as e:
print(f"Erreur broadcast start (ignorée): {e}")
# Récupérer la config
config = config_manager.config
await scan_log(f"[{datetime.now()}] Config chargée: {config.network.cidr}")
# Initialiser le scanner
scanner = NetworkScanner(
cidr=config.network.cidr,
timeout=config.scan.timeout,
ping_count=config.scan.ping_count
)
# Convertir les ports en liste d'entiers
port_list = []
for port_range in config.ports.ranges:
if '-' in port_range:
start, end = map(int, port_range.split('-'))
port_list.extend(range(start, end + 1))
else:
port_list.append(int(port_range))
await scan_log(f"[{datetime.now()}] Ports à scanner: {len(port_list)}")
# Récupérer les IPs connues
known_ips = config.ip_classes
await scan_log(f"[{datetime.now()}] IPs connues: {len(known_ips)}")
# Callback de progression pour WebSocket
async def progress_callback(current: int, total: int, current_ip: str, status: str, ping_ok: bool):
try:
ping_label = "ok" if ping_ok else "fail"
await ws_manager.broadcast_scan_progress({
"current": current,
"total": total,
"ip": current_ip
})
await ws_manager.broadcast_scan_log(
f"[{current}/{total}] {current_ip} -> ping:{ping_label} ({status})"
)
except Exception:
# Ignorer les erreurs WebSocket pour ne pas bloquer le scan
pass
# Lancer le scan
await scan_log(f"[{datetime.now()}] Lancement du scan (parallélisme: {config.scan.parallel_pings})...")
scan_results = await scanner.full_scan(
known_ips=known_ips,
port_list=port_list,
max_concurrent=config.scan.parallel_pings,
progress_callback=progress_callback
)
await scan_log(f"[{datetime.now()}] Scan terminé: {len(scan_results)} IPs trouvées")
# Mettre à jour la base de données
stats = {
"total": 0,
"online": 0,
"offline": 0,
"new": 0,
"updated": 0
}
for ip_address, ip_data in scan_results.items():
stats["total"] += 1
if ip_data["last_status"] == "online":
stats["online"] += 1
else:
stats["offline"] += 1
# Log par IP (historique scan)
ping_label = "ok" if ip_data["last_status"] == "online" else "fail"
log_message = f"Scan {ip_address} -> ping:{ping_label} ({ip_data['last_status']})"
db.add(ScanLog(
ip=ip_address,
status=ip_data["last_status"],
message=log_message
))
# Vérifier si l'IP existe déjà
existing_ip = db.query(IP).filter(IP.ip == ip_address).first()
if existing_ip:
# Mettre à jour l'IP existante
old_status = existing_ip.last_status
# Si l'IP passe de offline à online ET qu'elle était inconnue, c'est une "nouvelle détection"
# On réinitialise first_seen pour qu'elle apparaisse dans "Nouvelles Détections"
if (old_status == "offline" and ip_data["last_status"] == "online" and not existing_ip.known):
existing_ip.first_seen = datetime.now()
# Détecter changement de MAC address
new_mac = ip_data.get("mac")
if new_mac and existing_ip.mac and new_mac != existing_ip.mac:
# MAC a changé ! Marquer comme changée
existing_ip.mac_changed = True
print(f"[ALERTE] MAC changée pour {ip_address}: {existing_ip.mac} -> {new_mac}")
else:
# Pas de changement ou pas de MAC précédente
existing_ip.mac_changed = False
existing_ip.last_status = ip_data["last_status"]
if ip_data["last_seen"]:
existing_ip.last_seen = ip_data["last_seen"]
existing_ip.mac = ip_data.get("mac") or existing_ip.mac
vendor = ip_data.get("vendor")
if (not vendor or vendor == "Unknown") and existing_ip.mac:
vendor = OuiLookup.lookup(existing_ip.mac) or vendor
if config.scan.force_vendor_update:
if vendor and vendor != "Unknown":
existing_ip.vendor = vendor
else:
if (not existing_ip.vendor or existing_ip.vendor == "Unknown") and vendor and vendor != "Unknown":
existing_ip.vendor = vendor
existing_ip.hostname = ip_data.get("hostname") or existing_ip.hostname
existing_ip.open_ports = ip_data.get("open_ports", [])
# Mettre à jour host seulement si présent dans ip_data (config)
if "host" in ip_data:
existing_ip.host = ip_data["host"]
# Mettre à jour le flag network_device (basé sur host="Network")
# Utiliser le host existant si ip_data n'en a pas
current_host = ip_data.get("host") or existing_ip.host
existing_ip.network_device = (current_host == "Network")
# Si l'état a changé, notifier via WebSocket
if old_status != ip_data["last_status"]:
await ws_manager.broadcast_ip_update({
"ip": ip_address,
"old_status": old_status,
"new_status": ip_data["last_status"]
})
stats["updated"] += 1
else:
# Créer une nouvelle IP
vendor = ip_data.get("vendor")
if (not vendor or vendor == "Unknown") and ip_data.get("mac"):
vendor = OuiLookup.lookup(ip_data.get("mac")) or vendor
new_ip = IP(
ip=ip_address,
name=ip_data.get("name"),
known=ip_data.get("known", False),
network_device=ip_data.get("host") == "Network",
location=ip_data.get("location"),
host=ip_data.get("host"),
first_seen=datetime.now(),
last_seen=ip_data.get("last_seen") or datetime.now(),
last_status=ip_data["last_status"],
mac=ip_data.get("mac"),
vendor=vendor,
hostname=ip_data.get("hostname"),
open_ports=ip_data.get("open_ports", [])
)
db.add(new_ip)
# Notifier nouvelle IP
await ws_manager.broadcast_new_ip({
"ip": ip_address,
"status": ip_data["last_status"],
"known": ip_data.get("known", False)
})
stats["new"] += 1
# Ajouter à l'historique
history_entry = IPHistory(
ip=ip_address,
timestamp=datetime.now(),
status=ip_data["last_status"],
open_ports=ip_data.get("open_ports", [])
)
db.add(history_entry)
# Commit les changements
db.commit()
# Notifier fin du scan avec stats
await ws_manager.broadcast_scan_complete(stats)
print(f"[{datetime.now()}] Scan terminé: {stats}")
except Exception as e:
print(f"Erreur lors du scan: {e}")
db.rollback()
@router.post("/start")
async def start_scan(background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
"""
Déclenche un scan réseau immédiat
Returns:
Message de confirmation
"""
# Lancer le scan en arrière-plan
background_tasks.add_task(perform_scan, db)
return {
"message": "Scan réseau démarré",
"timestamp": datetime.now()
}
@router.get("/logs", response_model=List[ScanLogResponse])
async def get_scan_logs(limit: int = 200, db: Session = Depends(get_db)):
"""
Retourne les derniers logs de scan
"""
logs = db.query(ScanLog).order_by(ScanLog.created_at.desc()).limit(limit).all()
return list(reversed(logs))
@router.post("/ports/{ip_address}")
async def scan_ip_ports(ip_address: str, db: Session = Depends(get_db)):
"""
Scanne les ports d'une IP spécifique
Args:
ip_address: Adresse IP à scanner
db: Session de base de données
Returns:
Liste des ports ouverts
"""
try:
# Récupérer la config
config = config_manager.config
# Convertir les ports en liste d'entiers
port_list = []
for port_range in config.ports.ranges:
if '-' in port_range:
start, end = map(int, port_range.split('-'))
port_list.extend(range(start, end + 1))
else:
port_list.append(int(port_range))
# Initialiser le scanner
scanner = NetworkScanner(
cidr=config.network.cidr,
timeout=config.scan.timeout,
ping_count=config.scan.ping_count
)
# Scanner les ports de cette IP
print(f"[{datetime.now()}] Scan ports pour {ip_address}...")
open_ports = await scanner.scan_ports(ip_address, port_list)
print(f"[{datetime.now()}] Ports ouverts pour {ip_address}: {open_ports}")
# Mettre à jour la base de données
ip_record = db.query(IP).filter(IP.ip == ip_address).first()
if ip_record:
ip_record.open_ports = open_ports
ip_record.last_seen = datetime.now()
db.commit()
# Notifier via WebSocket
await ws_manager.broadcast_ip_update({
"ip": ip_address,
"open_ports": open_ports
})
return {
"message": "Scan de ports terminé",
"ip": ip_address,
"open_ports": open_ports,
"timestamp": datetime.now()
}
except Exception as e:
print(f"Erreur scan ports {ip_address}: {e}")
return {
"message": f"Erreur: {str(e)}",
"ip": ip_address,
"open_ports": [],
"timestamp": datetime.now()
}
@router.post("/cleanup-history")
async def cleanup_history(hours: int = 24, db: Session = Depends(get_db)):
"""
Nettoie l'historique plus ancien que X heures
Args:
hours: Nombre d'heures à conserver (défaut: 24h)
db: Session de base de données
Returns:
Nombre d'entrées supprimées
"""
cutoff_date = datetime.now() - timedelta(hours=hours)
deleted = db.query(IPHistory).filter(
IPHistory.timestamp < cutoff_date
).delete()
db.commit()
return {
"message": f"Historique nettoyé",
"deleted_entries": deleted,
"older_than_hours": hours
}