ipwatch
This commit is contained in:
362
backend/app/routers/scan.py
Executable file
362
backend/app/routers/scan.py
Executable file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user