mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
87 lines
2.7 KiB
Python
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)
|