First commit

This commit is contained in:
2026-07-20 11:05:07 +02:00
commit b592ab669f
159 changed files with 10294 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
"""Logique d'ingestion partagée entre mqtt_listen (broker local, un seul
noeud) et master_listen (broker master, tous les noeuds) : les deux
persistent dans le même modèle BanEvent, donnant une vue multi-noeuds
unifiée via le filtre par noeud déjà présent sur le tableau de bord."""
import datetime
from typing import Any
from django.utils import timezone
from .models import BanEvent, EnrollmentRequest, NodeRegistry
def clean_tag(value: Any) -> str:
"""Normalise un tag fail2ban optionnel : "-" (défaut Init de l'action,
pour les jails sans port/protocole propre, ex. recidive) devient vide."""
value = str(value or '').strip()
return '' if value == '-' else value
def save_ban_event(payload: dict[str, Any], alias_hint: str = '') -> BanEvent:
event_time = None
raw_time = payload.get('time')
if raw_time:
try:
event_time = timezone.make_aware(
datetime.datetime.fromtimestamp(float(raw_time)), timezone.get_default_timezone()
)
except (TypeError, ValueError, OSError):
event_time = None
bantime = payload.get('bantime')
try:
bantime = int(float(bantime)) if bantime is not None else None
except (TypeError, ValueError):
bantime = None
node = str(payload.get('node', ''))
# filter().update() plutôt que update_or_create() : une seule requête
# sur le chemin chaud (noeud déjà connu, la quasi-totalité du trafic),
# là où update_or_create() fait toujours un SELECT ... FOR UPDATE (no-op
# silencieux sur SQLite, qui ne supporte pas ce verrou) + un UPDATE.
# Contourne .save()/les signaux Django — sans conséquence aujourd'hui
# (NodeRegistry n'a aucun signal), à revoir si un jour l'un lui en ajoute.
if not NodeRegistry.objects.filter(node_id=node).update(last_seen=timezone.now()):
# alias_hint (CN du certificat client, cf. master_listen.py::
# handle_ban) : uniquement à la création, jamais pour écraser un
# alias déjà édité à la main dans /admin/ — get_or_create() ne
# touche 'defaults' que si la ligne est effectivement créée.
defaults = {'alias': alias_hint}
# Requête EnrollmentRequest.country volontairement isolée dans
# cette branche (jamais exécutée sur le chemin chaud, cf.
# commentaire ci-dessus sur filter().update()) : un noeud rejoint
# via /communaute/ a laissé le pays de son serveur dans sa
# demande approuvée — .node_name y correspond au CN présenté ici
# (alias_hint), pas de lien direct en base entre les deux tables.
if alias_hint:
enrollment = (
EnrollmentRequest.objects.filter(
node_name=alias_hint, status=EnrollmentRequest.STATUS_APPROVED, country__gt='',
)
.order_by('-processed_at')
.first()
)
if enrollment:
defaults['country'] = enrollment.country
NodeRegistry.objects.get_or_create(node_id=node, defaults=defaults)
return BanEvent.objects.create(
node=node,
action=str(payload.get('action', '')),
jail_name=str(payload.get('name', '')),
ip_address=payload['ip'],
port=clean_tag(payload.get('port')),
protocol=clean_tag(payload.get('protocol')),
bantime=bantime,
reason=str(payload.get('reason', '')),
event_time=event_time,
raw_payload=payload,
)