mirror of
https://github.com/deunix-educ/Fail2banMqttActionBanishment.git
synced 2026-08-24 03:11:58 +02:00
80 lines
3.4 KiB
Python
80 lines
3.4 KiB
Python
"""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,
|
|
)
|