Coverage for api\honeypot.py: 92.7%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
1"""
2Señuelo (honeypot) en la ruta /admin/.
4El panel de administracion real se ha movido a la ruta que indique ADMIN_URL,
5asi que en /admin/ ya no queda nada legitimo, todo lo que llegue aqui es
6alguien buscando el panel, por lo que se debe registrar el intento
7"""
8import logging
10from django.conf import settings
11from django.core.cache import cache
12from django.core.mail import send_mail
13from django.shortcuts import render
14from django.utils import timezone
15from django.views.decorators.csrf import csrf_exempt
17from api.client_ip import get_client_ip
19logger = logging.getLogger(__name__)
21# Clave del limite por IP y del tope global por hora
22_IP_KEY = 'honeypot:avisado:{ip}'
23_HOUR_KEY = 'honeypot:enviados:{hour}'
26@csrf_exempt
27def admin_honeypot(request):
28 """
29 Vista señuelo servida en /admin/. Ver el modulo para el porque de cada cosa.
30 """
31 ip = get_client_ip(request) or 'desconocida'
32 is_attempt = request.method == 'POST'
34 # Solo el usuario. La contraseña no se lee ni para descartarla
35 user_account = request.POST.get('username', '').strip()[:150] if is_attempt else ''
37 logger.warning(
38 "HONEYPOT %s %s desde %s | usuario probado: %s | agente: %s",
39 request.method,
40 request.get_full_path(),
41 ip,
42 user_account or '-',
43 request.META.get('HTTP_USER_AGENT', '-'),
44 )
46 if is_attempt or settings.HONEYPOT_NOTIFY_ON_GET:
47 _notify(request, ip, user_account)
49 # Siempre 200 y siempre el mismo mensaje. El error solo se enseña cuando ha
50 # habido un envio, para que la primera visita parezca un acceso normal.
51 return render(request, 'honeypot/login.html', {
52 'hay_error': is_attempt,
53 'usuario': user_account,
54 })
57def _notify(request, ip, user_account):
58 """
59 Manda el correo si toca. Silencioso por diseño: cualquier problema se queda en
60 el registro, porque un fallo visible en /admin/ delataria el señuelo.
61 """
62 recipient = settings.HONEYPOT_NOTIFY_EMAIL
63 if not recipient:
64 return
66 # Uno por IP y hora. cache.add solo escribe si la clave no estaba y es
67 # atomico, asi que dos procesos que atiendan el mismo escaneo a la vez no mandan dos correos.
68 if not cache.add(_IP_KEY.format(ip=ip), True, settings.HONEYPOT_EMAIL_COOLDOWN):
69 return
71 if not _quota_left(ip):
72 return
74 # Mensaje que se envia por correo
75 body = (
76 "Se ha intentado acceder al panel de administracion señuelo.\n"
77 "En esa ruta no hay ningun panel real, asi que este acceso no es trafico propio.\n\n"
78 f"Fecha: {timezone.now():%Y-%m-%d %H:%M:%S %Z}\n"
79 f"IP: {ip}\n"
80 f"Metodo: {request.method}\n"
81 f"Ruta: {request.get_full_path()}\n"
82 f"Usuario probado: {user_account or '(ninguno)'}\n"
83 f"Agente: {request.META.get('HTTP_USER_AGENT', '-')}\n"
84 f"Host: {request.get_host()}\n\n"
85 "La contraseña introducida no se registra a proposito.\n\n"
86 f"No se enviaran mas avisos de esta IP durante "
87 f"{settings.HONEYPOT_EMAIL_COOLDOWN // 60} minutos. "
88 "El registro completo esta en el fichero de log."
89 )
91 try:
92 send_mail(
93 subject=f"[BlaBlaUCM] Intento de acceso al panel señuelo desde {ip}",
94 message=body,
95 from_email=settings.EMAIL_HOST_USER,
96 recipient_list=[recipient],
97 )
98 logger.info("HONEYPOT aviso enviado por la IP %s", ip)
99 except Exception as e:
100 logger.error("HONEYPOT no se pudo enviar el aviso de la IP %s: %s", ip, e)
103def _quota_left(ip):
104 """
105 Tope global de avisos por hora
106 """
107 key = _HOUR_KEY.format(hour=timezone.now().strftime('%Y%m%d%H'))
109 # 3700s y no 3600 ya que el contador debe sobrevivir a la hora que representa
110 cache.add(key, 0, 3700)
111 try:
112 sent = cache.incr(key)
113 except ValueError:
114 # La clave ha caducado entre el add y el incr, es el primero de la hora
115 sent = 1
117 if sent > settings.HONEYPOT_MAX_EMAILS_HOUR:
118 logger.warning(
119 "HONEYPOT tope horario de avisos alcanzado (%s); no se avisa de la IP %s. "
120 "El acceso si queda registrado.",
121 settings.HONEYPOT_MAX_EMAILS_HOUR, ip,
122 )
123 return False
125 return True