This commit is contained in:
2025-12-06 12:28:55 +01:00
parent 56d633e296
commit 13b3c58ec8
56 changed files with 4328 additions and 1 deletions

View File

@@ -0,0 +1,8 @@
"""
Routers API pour IPWatch
"""
from .ips import router as ips_router
from .scan import router as scan_router
from .websocket import router as websocket_router
__all__ = ["ips_router", "scan_router", "websocket_router"]

216
backend/app/routers/ips.py Normal file
View File

@@ -0,0 +1,216 @@
"""
Endpoints API pour la gestion des IPs
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import List, Optional
from datetime import datetime, timedelta
from backend.app.core.database import get_db
from backend.app.models.ip import IP, IPHistory
from pydantic import BaseModel
router = APIRouter(prefix="/api/ips", tags=["IPs"])
# Schémas Pydantic pour validation
class IPUpdate(BaseModel):
"""Schéma pour mise à jour d'IP"""
name: Optional[str] = None
known: Optional[bool] = None
location: Optional[str] = None
host: Optional[str] = None
class IPResponse(BaseModel):
"""Schéma de réponse IP"""
ip: str
name: Optional[str]
known: bool
location: Optional[str]
host: Optional[str]
first_seen: Optional[datetime]
last_seen: Optional[datetime]
last_status: Optional[str]
mac: Optional[str]
vendor: Optional[str]
hostname: Optional[str]
open_ports: List[int]
class Config:
from_attributes = True
class IPHistoryResponse(BaseModel):
"""Schéma de réponse historique"""
id: int
ip: str
timestamp: datetime
status: str
open_ports: List[int]
class Config:
from_attributes = True
@router.get("/", response_model=List[IPResponse])
async def get_all_ips(
status: Optional[str] = None,
known: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""
Récupère toutes les IPs avec filtres optionnels
Args:
status: Filtrer par statut (online/offline)
known: Filtrer par IPs connues/inconnues
db: Session de base de données
Returns:
Liste des IPs
"""
query = db.query(IP)
if status:
query = query.filter(IP.last_status == status)
if known is not None:
query = query.filter(IP.known == known)
ips = query.all()
return ips
@router.get("/{ip_address}", response_model=IPResponse)
async def get_ip(ip_address: str, db: Session = Depends(get_db)):
"""
Récupère les détails d'une IP spécifique
Args:
ip_address: Adresse IP
db: Session de base de données
Returns:
Détails de l'IP
"""
ip = db.query(IP).filter(IP.ip == ip_address).first()
if not ip:
raise HTTPException(status_code=404, detail="IP non trouvée")
return ip
@router.put("/{ip_address}", response_model=IPResponse)
async def update_ip(
ip_address: str,
ip_update: IPUpdate,
db: Session = Depends(get_db)
):
"""
Met à jour les informations d'une IP
Args:
ip_address: Adresse IP
ip_update: Données à mettre à jour
db: Session de base de données
Returns:
IP mise à jour
"""
ip = db.query(IP).filter(IP.ip == ip_address).first()
if not ip:
raise HTTPException(status_code=404, detail="IP non trouvée")
# Mettre à jour les champs fournis
update_data = ip_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(ip, field, value)
db.commit()
db.refresh(ip)
return ip
@router.delete("/{ip_address}")
async def delete_ip(ip_address: str, db: Session = Depends(get_db)):
"""
Supprime une IP (et son historique)
Args:
ip_address: Adresse IP
db: Session de base de données
Returns:
Message de confirmation
"""
ip = db.query(IP).filter(IP.ip == ip_address).first()
if not ip:
raise HTTPException(status_code=404, detail="IP non trouvée")
db.delete(ip)
db.commit()
return {"message": f"IP {ip_address} supprimée"}
@router.get("/{ip_address}/history", response_model=List[IPHistoryResponse])
async def get_ip_history(
ip_address: str,
hours: int = 24,
db: Session = Depends(get_db)
):
"""
Récupère l'historique d'une IP
Args:
ip_address: Adresse IP
hours: Nombre d'heures d'historique (défaut: 24h)
db: Session de base de données
Returns:
Liste des événements historiques
"""
# Vérifier que l'IP existe
ip = db.query(IP).filter(IP.ip == ip_address).first()
if not ip:
raise HTTPException(status_code=404, detail="IP non trouvée")
# Calculer la date limite
since = datetime.utcnow() - timedelta(hours=hours)
# Récupérer l'historique
history = db.query(IPHistory).filter(
IPHistory.ip == ip_address,
IPHistory.timestamp >= since
).order_by(desc(IPHistory.timestamp)).all()
return history
@router.get("/stats/summary")
async def get_stats(db: Session = Depends(get_db)):
"""
Récupère les statistiques globales du réseau
Returns:
Statistiques (total, online, offline, known, unknown)
"""
total = db.query(IP).count()
online = db.query(IP).filter(IP.last_status == "online").count()
offline = db.query(IP).filter(IP.last_status == "offline").count()
known = db.query(IP).filter(IP.known == True).count()
unknown = db.query(IP).filter(IP.known == False).count()
return {
"total": total,
"online": online,
"offline": offline,
"known": known,
"unknown": unknown
}

201
backend/app/routers/scan.py Normal file
View File

@@ -0,0 +1,201 @@
"""
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
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.services.network import NetworkScanner
from backend.app.services.websocket import ws_manager
router = APIRouter(prefix="/api/scan", tags=["Scan"])
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:
print(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
print(f"[{datetime.now()}] Config chargée: {config.network.cidr}")
# Initialiser le scanner
scanner = NetworkScanner(
cidr=config.network.cidr,
timeout=config.scan.timeout
)
# 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))
print(f"[{datetime.now()}] Ports à scanner: {port_list}")
# Récupérer les IPs connues
known_ips = config.ip_classes
print(f"[{datetime.now()}] IPs connues: {len(known_ips)}")
# Lancer le scan
print(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
)
print(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
# 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
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
existing_ip.vendor = ip_data.get("vendor") or existing_ip.vendor
existing_ip.hostname = ip_data.get("hostname") or existing_ip.hostname
existing_ip.open_ports = ip_data.get("open_ports", [])
# 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
new_ip = IP(
ip=ip_address,
name=ip_data.get("name"),
known=ip_data.get("known", False),
location=ip_data.get("location"),
host=ip_data.get("host"),
first_seen=datetime.utcnow(),
last_seen=ip_data.get("last_seen") or datetime.utcnow(),
last_status=ip_data["last_status"],
mac=ip_data.get("mac"),
vendor=ip_data.get("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.utcnow(),
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.utcnow()
}
@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.utcnow() - 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
}

View File

@@ -0,0 +1,35 @@
"""
Endpoint WebSocket pour notifications temps réel
"""
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from backend.app.services.websocket import ws_manager
router = APIRouter(tags=["WebSocket"])
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
Endpoint WebSocket pour notifications temps réel
Args:
websocket: Connexion WebSocket
"""
await ws_manager.connect(websocket)
try:
# Boucle de réception (keep-alive)
while True:
# Recevoir des messages du client (heartbeat)
data = await websocket.receive_text()
# On peut gérer des commandes du client ici si nécessaire
# Pour l'instant, on fait juste un echo pour keep-alive
if data == "ping":
await ws_manager.send_personal_message("pong", websocket)
except WebSocketDisconnect:
ws_manager.disconnect(websocket)
except Exception as e:
print(f"Erreur WebSocket: {e}")
ws_manager.disconnect(websocket)