Files
2026-04-01 15:51:20 +11:00

87 lines
2.7 KiB
Python

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