mirror of
https://github.com/deunix-educ/Fail2banMqttActionBanishment.git
synced 2026-08-24 03:11:58 +02:00
First commit
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""Déclenche à la main une commande sans émission automatique (Phase 4) :
|
||||
contrairement à SYNC_BAN/ESCALATE (toujours publiées par master_listen.py
|
||||
via self.correlation_rules), WHITELIST/BAN_ALLPORTS/NOTIFY_ONLY/RATE_LIMIT
|
||||
correspondent à une décision humaine ("je sais que cette IP est
|
||||
particulière"), pas à une règle automatique — pas de logique de
|
||||
corrélation à construire pour ça. Diffusées à tous les noeuds abonnés via
|
||||
fail2ban/broadcast/action, exécutées par master_client.py::execute_<nom>.
|
||||
|
||||
REPORT_ABUSE fait exception : PAS de diffusion MQTT. Diffuser à chaque
|
||||
noeud produirait un rapport en double par noeud connecté pour le même
|
||||
incident (vu comme du bruit par AbuseIPDB, et demanderait une clé API par
|
||||
noeud). Ce process tourne déjà sur le master, avec accès à l'historique
|
||||
BanEvent complet (tous noeuds) pour construire un commentaire pertinent —
|
||||
il soumet donc le rapport lui-même (banevents/abuseipdb.py), sans passer
|
||||
par MQTT ni par master_client.py.
|
||||
|
||||
Usage (à lancer sur le master, où vit le certificat master-internal, seule
|
||||
identité autorisée en écriture sur fail2ban/broadcast/action) :
|
||||
manage.py publish_command WHITELIST 203.0.113.5
|
||||
manage.py publish_command BAN_ALLPORTS 203.0.113.5
|
||||
manage.py publish_command NOTIFY_ONLY 203.0.113.5
|
||||
manage.py publish_command RATE_LIMIT 203.0.113.5
|
||||
manage.py publish_command REPORT_ABUSE 203.0.113.5 --comment "texte"
|
||||
"""
|
||||
import json
|
||||
import ssl
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from banevents.abuseipdb import submit_report
|
||||
from banevents.models import BanEvent
|
||||
|
||||
BROADCAST_ACTION_TOPIC = 'fail2ban/broadcast/action'
|
||||
# Commandes diffusées en MQTT à tous les noeuds — cf. master_client.py
|
||||
# command_handlers pour la liste des commandes qu'un noeud sait exécuter.
|
||||
MANUAL_COMMANDS = ('WHITELIST', 'BAN_ALLPORTS', 'NOTIFY_ONLY', 'RATE_LIMIT')
|
||||
DEFAULT_ABUSEIPDB_CATEGORIES = '18' # Brute-Force
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Publie une commande manuelle (WHITELIST/BAN_ALLPORTS/NOTIFY_ONLY/RATE_LIMIT/REPORT_ABUSE).'
|
||||
|
||||
def add_arguments(self, parser: Any) -> None:
|
||||
parser.add_argument('cmd', choices=(*MANUAL_COMMANDS, 'REPORT_ABUSE'))
|
||||
parser.add_argument('ip')
|
||||
parser.add_argument('--categories', default=DEFAULT_ABUSEIPDB_CATEGORIES, help='REPORT_ABUSE uniquement')
|
||||
parser.add_argument('--comment', default='', help='REPORT_ABUSE uniquement')
|
||||
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
cmd = options['cmd']
|
||||
ip = options['ip']
|
||||
|
||||
if cmd == 'REPORT_ABUSE':
|
||||
self.handle_report_abuse(ip, options['categories'], options['comment'])
|
||||
return
|
||||
|
||||
client = mqtt.Client(
|
||||
mqtt.CallbackAPIVersion.VERSION2, client_id=f'publish-command-{uuid.uuid4().hex[:8]}'
|
||||
)
|
||||
# Même identité que master_listen.py (master-internal) — seule
|
||||
# scopée en écriture sur fail2ban/broadcast/action, cf.
|
||||
# master-acl.conf.
|
||||
client.tls_set(
|
||||
certfile=settings.MQTT_MASTER_LISTENER_CERT,
|
||||
keyfile=settings.MQTT_MASTER_LISTENER_KEY,
|
||||
tls_version=ssl.PROTOCOL_TLSv1_2,
|
||||
)
|
||||
client.connect(settings.MQTT_MASTER_HOST, settings.MQTT_MASTER_PORT, keepalive=10)
|
||||
client.loop_start()
|
||||
|
||||
payload = {'cmd': cmd, 'ip': ip, 'origin_node': 'manual', 'reason': 'manual'}
|
||||
info = client.publish(BROADCAST_ACTION_TOPIC, json.dumps(payload), qos=1)
|
||||
info.wait_for_publish(timeout=10)
|
||||
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
|
||||
if not info.is_published():
|
||||
raise CommandError(f'Échec de publication de {cmd} pour {ip} (PUBACK non reçu).')
|
||||
self.stdout.write(self.style.SUCCESS(f'{cmd} publié pour {ip} sur {BROADCAST_ACTION_TOPIC}'))
|
||||
# Laisse le temps au disconnect propre de partir avant que le
|
||||
# process ne se termine (paho ferme le socket en tâche de fond).
|
||||
time.sleep(0.2)
|
||||
|
||||
def handle_report_abuse(self, ip: str, categories: str, comment: str) -> None:
|
||||
comment = comment or self._default_comment(ip)
|
||||
|
||||
if not settings.ABUSEIPDB_ENABLED or not settings.ABUSEIPDB_API_KEY:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f'[dry-run] AbuseIPDB désactivé (ABUSEIPDB_ENABLED/ABUSEIPDB_API_KEY, voir .env) — '
|
||||
f'rien envoyé. Aurait signalé {ip} (catégories {categories}) : {comment}'
|
||||
))
|
||||
return
|
||||
|
||||
ok, detail = submit_report(ip, categories, comment)
|
||||
if ok:
|
||||
self.stdout.write(self.style.SUCCESS(f'{ip} signalé à AbuseIPDB (catégories {categories}).'))
|
||||
else:
|
||||
raise CommandError(f'Échec du signalement AbuseIPDB pour {ip} : {detail}')
|
||||
|
||||
def _default_comment(self, ip: str) -> str:
|
||||
"""Commentaire par défaut construit depuis l'historique BanEvent
|
||||
de cette IP — le master voit tous les noeuds, contrairement à un
|
||||
rapport qui partirait d'un seul d'entre eux."""
|
||||
events = BanEvent.objects.filter(ip_address=ip, action='BAN').order_by('-received_at')[:5]
|
||||
jails = sorted({e.jail_name for e in events if e.jail_name})
|
||||
nodes = sorted({e.node for e in events if e.node})
|
||||
if not jails:
|
||||
return f'Fail2Ban: banned IP {ip}.'
|
||||
return f'Fail2Ban: banned for {", ".join(jails)} on {len(nodes)} node(s).'
|
||||
Reference in New Issue
Block a user