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

1""" 

2Señuelo (honeypot) en la ruta /admin/. 

3 

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 

9 

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 

16 

17from api.client_ip import get_client_ip 

18 

19logger = logging.getLogger(__name__) 

20 

21# Clave del limite por IP y del tope global por hora 

22_IP_KEY = 'honeypot:avisado:{ip}' 

23_HOUR_KEY = 'honeypot:enviados:{hour}' 

24 

25 

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' 

33 

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 '' 

36 

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 ) 

45 

46 if is_attempt or settings.HONEYPOT_NOTIFY_ON_GET: 

47 _notify(request, ip, user_account) 

48 

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 }) 

55 

56 

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 

65 

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 

70 

71 if not _quota_left(ip): 

72 return 

73 

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 ) 

90 

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) 

101 

102 

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')) 

108 

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 

116 

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 

124 

125 return True