from django.db import models from netscan.fields import EncryptedCharField class NginxSettings(models.Model): """ Singleton (pk=1) — the one-off cluster connection config. Use NginxSettings.get() everywhere instead of pk lookups. """ namespace = models.CharField(max_length=200, default='ingress-nginx') pod_label = models.CharField( max_length=200, default='app.kubernetes.io/name=ingress-nginx', help_text='kubectl -l selector for the ingress-nginx pods', ) container = models.CharField(max_length=100, default='controller') fetch_interval_seconds = models.IntegerField( default=30, help_text='How often to pull new logs (seconds)', ) # Local dev / testing: read from a file instead of kubectl log_file_path = models.CharField( max_length=500, blank=True, help_text=( 'Absolute path to a local nginx log file for testing. ' 'Leave empty to use kubectl in production.' ), ) enabled = models.BooleanField(default=True) last_fetch_at = models.DateTimeField(null=True, blank=True) class Meta: verbose_name = 'Nginx Settings' verbose_name_plural = 'Nginx Settings' def __str__(self): return f'Nginx Settings ({self.namespace})' @classmethod def get(cls): obj, _ = cls.objects.get_or_create(pk=1) return obj @property def source_display(self): if self.log_file_path: return f'File: {self.log_file_path}' return f'kubectl -n {self.namespace} -l {self.pod_label}' class NginxAlertProfile(models.Model): """ One or more alert profiles — each with its own thresholds and Telegram config. """ name = models.CharField(max_length=200) alert_window_seconds = models.IntegerField( default=60, help_text='Rolling window in seconds for rate analysis', ) alert_max_requests = models.IntegerField( default=200, help_text='Max requests per IP per window before triggering a rate alert', ) alert_max_error_rate = models.FloatField( default=0.6, help_text='Error rate threshold (0–1) to flag an IP', ) alert_min_requests = models.IntegerField( default=15, help_text='Minimum requests before running error-rate check', ) telegram_bot_token = EncryptedCharField(blank=True) telegram_chat_id = EncryptedCharField(blank=True) enabled = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name class NginxAccessLog(models.Model): """Stores parsed nginx access log entries (cluster-global, no profile FK).""" timestamp = models.DateTimeField(db_index=True) remote_addr = models.GenericIPAddressField(db_index=True) method = models.CharField(max_length=20) request_uri = models.TextField() protocol = models.CharField(max_length=20) status = models.IntegerField(db_index=True) body_bytes_sent = models.IntegerField() http_referer = models.TextField(blank=True) http_user_agent = models.TextField(blank=True) request_length = models.IntegerField(default=0) request_time = models.FloatField() service = models.CharField(max_length=200, blank=True, db_index=True) upstream_addr = models.CharField(max_length=200, blank=True) upstream_response_time = models.FloatField(null=True, blank=True) upstream_status = models.IntegerField(null=True, blank=True) request_id = models.CharField(max_length=100, blank=True, db_index=True) # Geo (populated after insert) country = models.CharField(max_length=100, blank=True) country_code = models.CharField(max_length=10, blank=True) region = models.CharField(max_length=100, blank=True) city = models.CharField(max_length=100, blank=True) class Meta: ordering = ['-timestamp'] indexes = [ models.Index(fields=['remote_addr', 'timestamp']), models.Index(fields=['timestamp', 'status']), # covers chart aggregation queries ] @property def geo_display(self): parts = [p for p in [self.city, self.country] if p] return ', '.join(parts) if parts else '—' class IPGeoCache(models.Model): ip = models.GenericIPAddressField(unique=True, db_index=True) country = models.CharField(max_length=100, blank=True) country_code = models.CharField(max_length=10, blank=True) region = models.CharField(max_length=100, blank=True) city = models.CharField(max_length=100, blank=True) lat = models.FloatField(null=True, blank=True) lon = models.FloatField(null=True, blank=True) isp = models.CharField(max_length=300, blank=True) is_private = models.BooleanField(default=False) fetched_at = models.DateTimeField(auto_now=True) def __str__(self): if self.is_private: return f'{self.ip} (private)' parts = [p for p in [self.city, self.country] if p] return f'{self.ip} — {", ".join(parts)}' class ThreatAlert(models.Model): ALERT_TYPES = [ ('rate_limit', 'High Request Rate'), ('error_rate', 'High Error Rate'), ] profile = models.ForeignKey( NginxAlertProfile, on_delete=models.CASCADE, related_name='alerts' ) alert_type = models.CharField(max_length=50, choices=ALERT_TYPES) remote_addr = models.GenericIPAddressField(db_index=True) detected_at = models.DateTimeField(auto_now_add=True) window_start = models.DateTimeField() window_end = models.DateTimeField() request_count = models.IntegerField() error_count = models.IntegerField(default=0) detail = models.TextField() notified = models.BooleanField(default=False) dismissed = models.BooleanField(default=False) country = models.CharField(max_length=100, blank=True) city = models.CharField(max_length=100, blank=True) class Meta: ordering = ['-detected_at'] def __str__(self): return f'[{self.get_alert_type_display()}] {self.remote_addr} @ {self.detected_at:%Y-%m-%d %H:%M}' @property def error_rate(self): return self.error_count / self.request_count if self.request_count else 0