""" Threat detection against NginxAccessLog for each enabled NginxAlertProfile. """ import logging from datetime import timedelta from django.db.models import Count, Q from django.utils import timezone from .models import NginxAlertProfile, NginxAccessLog, ThreatAlert, IPGeoCache from .notifications import send_alert_telegram logger = logging.getLogger(__name__) def detect_threats(profile_id: int): try: profile = NginxAlertProfile.objects.get(pk=profile_id, enabled=True) except NginxAlertProfile.DoesNotExist: return window = timedelta(seconds=profile.alert_window_seconds) now = timezone.now() window_start = now - window qs = ( NginxAccessLog.objects .filter(timestamp__gte=window_start) .values('remote_addr') .annotate( total=Count('id'), errors=Count('id', filter=Q(status__gte=400)), ) ) for row in qs: ip, total, errors = row['remote_addr'], row['total'], row['errors'] existing = ThreatAlert.objects.filter( profile=profile, remote_addr=ip, window_start__gte=window_start, dismissed=False, ) if total >= profile.alert_max_requests: if not existing.filter(alert_type='rate_limit').exists(): _create_alert(profile, 'rate_limit', ip, total, errors, window_start, now) if total >= profile.alert_min_requests: if (errors / total) >= profile.alert_max_error_rate: if not existing.filter(alert_type='error_rate').exists(): _create_alert(profile, 'error_rate', ip, total, errors, window_start, now) def _create_alert(profile, alert_type, ip, total, errors, window_start, window_end): geo = IPGeoCache.objects.filter(ip=ip).first() country = geo.country if geo else '' city = geo.city if geo else '' if alert_type == 'rate_limit': detail = ( f'{total} requests from {ip} in the last {profile.alert_window_seconds}s ' f'(threshold: {profile.alert_max_requests})' ) else: rate_pct = round(errors / total * 100) detail = ( f'{errors}/{total} requests from {ip} failed ' f'({rate_pct}% error rate, threshold: {int(profile.alert_max_error_rate * 100)}%)' ) alert = ThreatAlert.objects.create( profile=profile, alert_type=alert_type, remote_addr=ip, window_start=window_start, window_end=window_end, request_count=total, error_count=errors, detail=detail, country=country, city=city, ) logger.warning('nginxmon alert: %s', detail) send_alert_telegram(profile, alert)