aiwaf 0.1.8.7__py3-none-any.whl → 0.1.8.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiwaf might be problematic. Click here for more details.

@@ -0,0 +1,166 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.conf import settings
3
+ import os
4
+
5
+ class Command(BaseCommand):
6
+ help = 'Manage AI-WAF middleware logging settings and view log status'
7
+
8
+ def add_arguments(self, parser):
9
+ parser.add_argument(
10
+ '--enable',
11
+ action='store_true',
12
+ help='Enable middleware logging (shows settings to add)'
13
+ )
14
+ parser.add_argument(
15
+ '--disable',
16
+ action='store_true',
17
+ help='Disable middleware logging (shows settings to remove)'
18
+ )
19
+ parser.add_argument(
20
+ '--status',
21
+ action='store_true',
22
+ help='Show current middleware logging status'
23
+ )
24
+ parser.add_argument(
25
+ '--clear',
26
+ action='store_true',
27
+ help='Clear/delete middleware log files'
28
+ )
29
+
30
+ def handle(self, *args, **options):
31
+ if options['enable']:
32
+ self._show_enable_instructions()
33
+ elif options['disable']:
34
+ self._show_disable_instructions()
35
+ elif options['clear']:
36
+ self._clear_logs()
37
+ else:
38
+ self._show_status()
39
+
40
+ def _show_status(self):
41
+ """Show current middleware logging configuration"""
42
+ self.stdout.write(self.style.HTTP_INFO("🔍 AI-WAF Middleware Logging Status"))
43
+ self.stdout.write("")
44
+
45
+ # Check settings
46
+ logging_enabled = getattr(settings, 'AIWAF_MIDDLEWARE_LOGGING', False)
47
+ log_file = getattr(settings, 'AIWAF_MIDDLEWARE_LOG', 'aiwaf_requests.log')
48
+ csv_format = getattr(settings, 'AIWAF_MIDDLEWARE_CSV', True)
49
+ csv_file = log_file.replace('.log', '.csv') if csv_format else None
50
+
51
+ # Status
52
+ status_color = self.style.SUCCESS if logging_enabled else self.style.WARNING
53
+ self.stdout.write(f"Status: {status_color('ENABLED' if logging_enabled else 'DISABLED')}")
54
+ self.stdout.write(f"Log File: {log_file}")
55
+ if csv_format:
56
+ self.stdout.write(f"CSV File: {csv_file}")
57
+ self.stdout.write(f"Format: {'CSV' if csv_format else 'Text'}")
58
+ self.stdout.write("")
59
+
60
+ # File existence and sizes
61
+ if logging_enabled:
62
+ self.stdout.write("📁 Log Files:")
63
+
64
+ if csv_format and csv_file:
65
+ if os.path.exists(csv_file):
66
+ size = os.path.getsize(csv_file)
67
+ lines = self._count_csv_lines(csv_file)
68
+ self.stdout.write(f" ✅ {csv_file} ({size:,} bytes, {lines:,} entries)")
69
+ else:
70
+ self.stdout.write(f" ❌ {csv_file} (not found)")
71
+
72
+ if os.path.exists(log_file):
73
+ size = os.path.getsize(log_file)
74
+ self.stdout.write(f" ✅ {log_file} ({size:,} bytes)")
75
+ else:
76
+ self.stdout.write(f" ❌ {log_file} (not found)")
77
+
78
+ # Middleware check
79
+ middleware_list = getattr(settings, 'MIDDLEWARE', [])
80
+ middleware_installed = 'aiwaf.middleware_logger.AIWAFLoggerMiddleware' in middleware_list
81
+
82
+ self.stdout.write("")
83
+ middleware_color = self.style.SUCCESS if middleware_installed else self.style.ERROR
84
+ self.stdout.write(f"Middleware: {middleware_color('INSTALLED' if middleware_installed else 'NOT INSTALLED')}")
85
+
86
+ if logging_enabled and not middleware_installed:
87
+ self.stdout.write(self.style.WARNING("⚠️ Logging is enabled but middleware is not installed!"))
88
+
89
+ def _show_enable_instructions(self):
90
+ """Show instructions for enabling middleware logging"""
91
+ self.stdout.write(self.style.SUCCESS("🚀 Enable AI-WAF Middleware Logging"))
92
+ self.stdout.write("")
93
+ self.stdout.write("Add these settings to your Django settings.py:")
94
+ self.stdout.write("")
95
+ self.stdout.write(self.style.HTTP_INFO("# Enable AI-WAF middleware logging"))
96
+ self.stdout.write(self.style.HTTP_INFO("AIWAF_MIDDLEWARE_LOGGING = True"))
97
+ self.stdout.write(self.style.HTTP_INFO("AIWAF_MIDDLEWARE_LOG = 'aiwaf_requests.log' # Optional"))
98
+ self.stdout.write(self.style.HTTP_INFO("AIWAF_MIDDLEWARE_CSV = True # Optional (default: True)"))
99
+ self.stdout.write("")
100
+ self.stdout.write("Add middleware to MIDDLEWARE list (preferably near the end):")
101
+ self.stdout.write("")
102
+ self.stdout.write(self.style.HTTP_INFO("MIDDLEWARE = ["))
103
+ self.stdout.write(self.style.HTTP_INFO(" # ... your existing middleware ..."))
104
+ self.stdout.write(self.style.HTTP_INFO(" 'aiwaf.middleware_logger.AIWAFLoggerMiddleware',"))
105
+ self.stdout.write(self.style.HTTP_INFO("]"))
106
+ self.stdout.write("")
107
+ self.stdout.write("Benefits:")
108
+ self.stdout.write(" ✅ Fallback when main access logs unavailable")
109
+ self.stdout.write(" ✅ CSV format for easy analysis")
110
+ self.stdout.write(" ✅ Automatic integration with AI-WAF trainer")
111
+ self.stdout.write(" ✅ Captures response times for better detection")
112
+
113
+ def _show_disable_instructions(self):
114
+ """Show instructions for disabling middleware logging"""
115
+ self.stdout.write(self.style.WARNING("⏹️ Disable AI-WAF Middleware Logging"))
116
+ self.stdout.write("")
117
+ self.stdout.write("To disable, update your Django settings.py:")
118
+ self.stdout.write("")
119
+ self.stdout.write(self.style.HTTP_INFO("# Disable AI-WAF middleware logging"))
120
+ self.stdout.write(self.style.HTTP_INFO("AIWAF_MIDDLEWARE_LOGGING = False"))
121
+ self.stdout.write("")
122
+ self.stdout.write("And remove from MIDDLEWARE list:")
123
+ self.stdout.write("")
124
+ self.stdout.write(self.style.HTTP_INFO("MIDDLEWARE = ["))
125
+ self.stdout.write(self.style.HTTP_INFO(" # ... your existing middleware ..."))
126
+ self.stdout.write(self.style.HTTP_INFO(" # 'aiwaf.middleware_logger.AIWAFLoggerMiddleware', # Remove this line"))
127
+ self.stdout.write(self.style.HTTP_INFO("]"))
128
+
129
+ def _clear_logs(self):
130
+ """Clear/delete middleware log files"""
131
+ log_file = getattr(settings, 'AIWAF_MIDDLEWARE_LOG', 'aiwaf_requests.log')
132
+ csv_format = getattr(settings, 'AIWAF_MIDDLEWARE_CSV', True)
133
+ csv_file = log_file.replace('.log', '.csv') if csv_format else None
134
+
135
+ files_deleted = 0
136
+
137
+ # Delete CSV file
138
+ if csv_file and os.path.exists(csv_file):
139
+ try:
140
+ os.remove(csv_file)
141
+ self.stdout.write(self.style.SUCCESS(f"✅ Deleted {csv_file}"))
142
+ files_deleted += 1
143
+ except Exception as e:
144
+ self.stdout.write(self.style.ERROR(f"❌ Failed to delete {csv_file}: {e}"))
145
+
146
+ # Delete text log file
147
+ if os.path.exists(log_file):
148
+ try:
149
+ os.remove(log_file)
150
+ self.stdout.write(self.style.SUCCESS(f"✅ Deleted {log_file}"))
151
+ files_deleted += 1
152
+ except Exception as e:
153
+ self.stdout.write(self.style.ERROR(f"❌ Failed to delete {log_file}: {e}"))
154
+
155
+ if files_deleted == 0:
156
+ self.stdout.write(self.style.WARNING("ℹ️ No log files found to delete"))
157
+ else:
158
+ self.stdout.write(self.style.SUCCESS(f"🗑️ Deleted {files_deleted} log file(s)"))
159
+
160
+ def _count_csv_lines(self, csv_file):
161
+ """Count lines in CSV file (excluding header)"""
162
+ try:
163
+ with open(csv_file, 'r', encoding='utf-8') as f:
164
+ return sum(1 for line in f) - 1 # Subtract header
165
+ except:
166
+ return 0
@@ -0,0 +1,160 @@
1
+ # aiwaf/middleware_logger.py
2
+
3
+ import os
4
+ import csv
5
+ import time
6
+ from datetime import datetime
7
+ from django.conf import settings
8
+ from django.utils.deprecation import MiddlewareMixin
9
+ from .utils import get_ip
10
+
11
+ class AIWAFLoggerMiddleware(MiddlewareMixin):
12
+ """
13
+ Middleware that logs requests to a CSV file for AI-WAF training.
14
+ Acts as a fallback when main access logs are unavailable.
15
+ """
16
+
17
+ def __init__(self, get_response):
18
+ super().__init__(get_response)
19
+ self.log_file = getattr(settings, "AIWAF_MIDDLEWARE_LOG", "aiwaf_requests.log")
20
+ self.csv_format = getattr(settings, "AIWAF_MIDDLEWARE_CSV", True)
21
+ self.log_enabled = getattr(settings, "AIWAF_MIDDLEWARE_LOGGING", False)
22
+
23
+ # CSV file path (if using CSV format)
24
+ if self.csv_format and self.log_enabled:
25
+ self.csv_file = self.log_file.replace('.log', '.csv')
26
+ self._ensure_csv_header()
27
+
28
+ def _ensure_csv_header(self):
29
+ """Ensure CSV file has proper header row"""
30
+ if not os.path.exists(self.csv_file):
31
+ os.makedirs(os.path.dirname(self.csv_file), exist_ok=True) if os.path.dirname(self.csv_file) else None
32
+ with open(self.csv_file, 'w', newline='', encoding='utf-8') as f:
33
+ writer = csv.writer(f)
34
+ writer.writerow([
35
+ 'timestamp', 'ip_address', 'method', 'path', 'status_code',
36
+ 'response_time', 'user_agent', 'referer', 'content_length'
37
+ ])
38
+
39
+ def process_request(self, request):
40
+ """Store request start time"""
41
+ request._aiwaf_start_time = time.time()
42
+ return None
43
+
44
+ def process_response(self, request, response):
45
+ """Log the completed request"""
46
+ if not self.log_enabled:
47
+ return response
48
+
49
+ # Calculate response time
50
+ start_time = getattr(request, '_aiwaf_start_time', time.time())
51
+ response_time = time.time() - start_time
52
+
53
+ # Extract request data
54
+ log_data = {
55
+ 'timestamp': datetime.now().strftime('%d/%b/%Y:%H:%M:%S +0000'),
56
+ 'ip_address': get_ip(request),
57
+ 'method': request.method,
58
+ 'path': request.path,
59
+ 'status_code': response.status_code,
60
+ 'response_time': f"{response_time:.3f}",
61
+ 'user_agent': request.META.get('HTTP_USER_AGENT', '-'),
62
+ 'referer': request.META.get('HTTP_REFERER', '-'),
63
+ 'content_length': response.get('Content-Length', '-')
64
+ }
65
+
66
+ if self.csv_format:
67
+ self._log_to_csv(log_data)
68
+ else:
69
+ self._log_to_text(log_data)
70
+
71
+ return response
72
+
73
+ def _log_to_csv(self, data):
74
+ """Write log entry to CSV file"""
75
+ try:
76
+ with open(self.csv_file, 'a', newline='', encoding='utf-8') as f:
77
+ writer = csv.writer(f)
78
+ writer.writerow([
79
+ data['timestamp'], data['ip_address'], data['method'],
80
+ data['path'], data['status_code'], data['response_time'],
81
+ data['user_agent'], data['referer'], data['content_length']
82
+ ])
83
+ except Exception as e:
84
+ # Fail silently to avoid breaking the application
85
+ pass
86
+
87
+ def _log_to_text(self, data):
88
+ """Write log entry in common log format"""
89
+ try:
90
+ # Common Log Format with response time
91
+ log_line = f'{data["ip_address"]} - - [{data["timestamp"]}] "{data["method"]} {data["path"]} HTTP/1.1" {data["status_code"]} {data["content_length"]} "{data["referer"]}" "{data["user_agent"]}" response-time={data["response_time"]}\n'
92
+
93
+ with open(self.log_file, 'a', encoding='utf-8') as f:
94
+ f.write(log_line)
95
+ except Exception as e:
96
+ # Fail silently to avoid breaking the application
97
+ pass
98
+
99
+
100
+ class AIWAFCSVLogParser:
101
+ """
102
+ Parser for AI-WAF CSV logs that converts them to the format expected by trainer.py
103
+ """
104
+
105
+ @staticmethod
106
+ def parse_csv_log(csv_file_path):
107
+ """
108
+ Parse CSV log file and return records in the format expected by trainer.py
109
+ Returns list of dictionaries with keys: ip, timestamp, path, status, referer, user_agent, response_time
110
+ """
111
+ records = []
112
+
113
+ if not os.path.exists(csv_file_path):
114
+ return records
115
+
116
+ try:
117
+ with open(csv_file_path, 'r', newline='', encoding='utf-8') as f:
118
+ reader = csv.DictReader(f)
119
+ for row in reader:
120
+ try:
121
+ # Convert timestamp to datetime object
122
+ timestamp = datetime.strptime(row['timestamp'], '%d/%b/%Y:%H:%M:%S +0000')
123
+
124
+ record = {
125
+ 'ip': row['ip_address'],
126
+ 'timestamp': timestamp,
127
+ 'path': row['path'],
128
+ 'status': row['status_code'],
129
+ 'referer': row['referer'],
130
+ 'user_agent': row['user_agent'],
131
+ 'response_time': float(row['response_time'])
132
+ }
133
+ records.append(record)
134
+ except (ValueError, KeyError) as e:
135
+ # Skip malformed rows
136
+ continue
137
+ except Exception as e:
138
+ # Return empty list if file can't be read
139
+ pass
140
+
141
+ return records
142
+
143
+ @staticmethod
144
+ def get_log_lines_for_trainer(csv_file_path):
145
+ """
146
+ Convert CSV log to format compatible with trainer.py's _read_all_logs()
147
+ Returns list of log line strings
148
+ """
149
+ records = AIWAFCSVLogParser.parse_csv_log(csv_file_path)
150
+ log_lines = []
151
+
152
+ for record in records:
153
+ # Convert back to common log format that trainer.py expects
154
+ timestamp_str = record['timestamp'].strftime('%d/%b/%Y:%H:%M:%S +0000')
155
+ content_length = '-' # We don't track this in our format
156
+
157
+ log_line = f'{record["ip"]} - - [{timestamp_str}] "GET {record["path"]} HTTP/1.1" {record["status"]} {content_length} "{record["referer"]}" "{record["user_agent"]}" response-time={record["response_time"]:.3f}'
158
+ log_lines.append(log_line)
159
+
160
+ return log_lines
aiwaf/storage.py CHANGED
@@ -3,7 +3,18 @@ import numpy as np
3
3
  import pandas as pd
4
4
  from django.conf import settings
5
5
  from django.utils import timezone
6
- from .models import FeatureSample, BlacklistEntry, IPExemption, DynamicKeyword
6
+
7
+ # Only import models if aiwaf is in INSTALLED_APPS
8
+ try:
9
+ from django.apps import apps
10
+ if apps.is_installed('aiwaf'):
11
+ from .models import FeatureSample, BlacklistEntry, IPExemption, DynamicKeyword
12
+ else:
13
+ # Create dummy classes to avoid import errors
14
+ FeatureSample = BlacklistEntry = IPExemption = DynamicKeyword = None
15
+ except (ImportError, RuntimeError):
16
+ # Handle cases where Django isn't fully initialized yet
17
+ FeatureSample = BlacklistEntry = IPExemption = DynamicKeyword = None
7
18
 
8
19
  # Configuration
9
20
  STORAGE_MODE = getattr(settings, "AIWAF_STORAGE_MODE", "models") # "models" or "csv"
@@ -52,22 +63,25 @@ class CsvFeatureStore:
52
63
  class DbFeatureStore:
53
64
  @staticmethod
54
65
  def persist_rows(rows):
55
- objs = []
56
- for ip,pl,kw,rt,si,bc,t404,label in rows:
57
- objs.append(FeatureSample(
58
- ip=ip, path_len=pl, kw_hits=kw,
59
- resp_time=rt, status_idx=si,
60
- burst_count=bc, total_404=t404,
61
- label=label
62
- ))
63
- FeatureSample.objects.bulk_create(objs, ignore_conflicts=True)
66
+ if FeatureSample is not None:
67
+ objs = []
68
+ for ip,pl,kw,rt,si,bc,t404,label in rows:
69
+ objs.append(FeatureSample(
70
+ ip=ip, path_len=pl, kw_hits=kw,
71
+ resp_time=rt, status_idx=si,
72
+ burst_count=bc, total_404=t404,
73
+ label=label
74
+ ))
75
+ FeatureSample.objects.bulk_create(objs, ignore_conflicts=True)
64
76
 
65
77
  @staticmethod
66
78
  def load_matrix():
67
- qs = FeatureSample.objects.all().values_list(
68
- "path_len","kw_hits","resp_time","status_idx","burst_count","total_404"
69
- )
70
- return np.array(list(qs), dtype=float)
79
+ if FeatureSample is not None:
80
+ qs = FeatureSample.objects.all().values_list(
81
+ "path_len","kw_hits","resp_time","status_idx","burst_count","total_404"
82
+ )
83
+ return np.array(list(qs), dtype=float)
84
+ return np.empty((0,6))
71
85
 
72
86
  def get_store():
73
87
  if getattr(settings, "AIWAF_FEATURE_STORE", "csv") == "db":
@@ -266,8 +280,12 @@ def get_blacklist_store():
266
280
  if STORAGE_MODE == "csv":
267
281
  return CsvBlacklistStore
268
282
  else:
269
- # Return a wrapper for Django models
270
- return ModelBlacklistStore
283
+ # Return a wrapper for Django models (only if models are available)
284
+ if BlacklistEntry is not None:
285
+ return ModelBlacklistStore
286
+ else:
287
+ # Fallback to CSV if models aren't available
288
+ return CsvBlacklistStore
271
289
 
272
290
 
273
291
  def get_exemption_store():
@@ -275,7 +293,10 @@ def get_exemption_store():
275
293
  if STORAGE_MODE == "csv":
276
294
  return CsvExemptionStore
277
295
  else:
278
- return ModelExemptionStore
296
+ if IPExemption is not None:
297
+ return ModelExemptionStore
298
+ else:
299
+ return CsvExemptionStore
279
300
 
280
301
 
281
302
  def get_keyword_store():
@@ -283,7 +304,10 @@ def get_keyword_store():
283
304
  if STORAGE_MODE == "csv":
284
305
  return CsvKeywordStore
285
306
  else:
286
- return ModelKeywordStore
307
+ if DynamicKeyword is not None:
308
+ return ModelKeywordStore
309
+ else:
310
+ return CsvKeywordStore
287
311
 
288
312
 
289
313
  # ============= Django Model Wrappers =============
@@ -293,19 +317,25 @@ class ModelBlacklistStore:
293
317
 
294
318
  @staticmethod
295
319
  def add_ip(ip_address, reason):
296
- BlacklistEntry.objects.get_or_create(ip_address=ip_address, defaults={"reason": reason})
320
+ if BlacklistEntry is not None:
321
+ BlacklistEntry.objects.get_or_create(ip_address=ip_address, defaults={"reason": reason})
297
322
 
298
323
  @staticmethod
299
324
  def is_blocked(ip_address):
300
- return BlacklistEntry.objects.filter(ip_address=ip_address).exists()
325
+ if BlacklistEntry is not None:
326
+ return BlacklistEntry.objects.filter(ip_address=ip_address).exists()
327
+ return False
301
328
 
302
329
  @staticmethod
303
330
  def get_all():
304
- return list(BlacklistEntry.objects.values("ip_address", "reason", "created_at"))
331
+ if BlacklistEntry is not None:
332
+ return list(BlacklistEntry.objects.values("ip_address", "reason", "created_at"))
333
+ return []
305
334
 
306
335
  @staticmethod
307
336
  def remove_ip(ip_address):
308
- BlacklistEntry.objects.filter(ip_address=ip_address).delete()
337
+ if BlacklistEntry is not None:
338
+ BlacklistEntry.objects.filter(ip_address=ip_address).delete()
309
339
 
310
340
 
311
341
  class ModelExemptionStore:
@@ -313,19 +343,25 @@ class ModelExemptionStore:
313
343
 
314
344
  @staticmethod
315
345
  def add_ip(ip_address, reason=""):
316
- IPExemption.objects.get_or_create(ip_address=ip_address, defaults={"reason": reason})
346
+ if IPExemption is not None:
347
+ IPExemption.objects.get_or_create(ip_address=ip_address, defaults={"reason": reason})
317
348
 
318
349
  @staticmethod
319
350
  def is_exempted(ip_address):
320
- return IPExemption.objects.filter(ip_address=ip_address).exists()
351
+ if IPExemption is not None:
352
+ return IPExemption.objects.filter(ip_address=ip_address).exists()
353
+ return False
321
354
 
322
355
  @staticmethod
323
356
  def get_all():
324
- return list(IPExemption.objects.values("ip_address", "reason", "created_at"))
357
+ if IPExemption is not None:
358
+ return list(IPExemption.objects.values("ip_address", "reason", "created_at"))
359
+ return []
325
360
 
326
361
  @staticmethod
327
362
  def remove_ip(ip_address):
328
- IPExemption.objects.filter(ip_address=ip_address).delete()
363
+ if IPExemption is not None:
364
+ IPExemption.objects.filter(ip_address=ip_address).delete()
329
365
 
330
366
 
331
367
  class ModelKeywordStore:
@@ -333,19 +369,24 @@ class ModelKeywordStore:
333
369
 
334
370
  @staticmethod
335
371
  def add_keyword(keyword, count=1):
336
- obj, created = DynamicKeyword.objects.get_or_create(keyword=keyword, defaults={"count": count})
337
- if not created:
338
- obj.count += count
339
- obj.save()
372
+ if DynamicKeyword is not None:
373
+ obj, created = DynamicKeyword.objects.get_or_create(keyword=keyword, defaults={"count": count})
374
+ if not created:
375
+ obj.count += count
376
+ obj.save()
340
377
 
341
378
  @staticmethod
342
379
  def get_top_keywords(limit=10):
343
- return list(DynamicKeyword.objects.order_by("-count").values_list("keyword", flat=True)[:limit])
380
+ if DynamicKeyword is not None:
381
+ return list(DynamicKeyword.objects.order_by("-count").values_list("keyword", flat=True)[:limit])
382
+ return []
344
383
 
345
384
  @staticmethod
346
385
  def remove_keyword(keyword):
347
- DynamicKeyword.objects.filter(keyword=keyword).delete()
386
+ if DynamicKeyword is not None:
387
+ DynamicKeyword.objects.filter(keyword=keyword).delete()
348
388
 
349
389
  @staticmethod
350
390
  def clear_all():
351
- DynamicKeyword.objects.all().delete()
391
+ if DynamicKeyword is not None:
392
+ DynamicKeyword.objects.all().delete()
aiwaf/trainer.py CHANGED
@@ -65,16 +65,31 @@ def remove_exempt_keywords() -> None:
65
65
 
66
66
  def _read_all_logs() -> list[str]:
67
67
  lines = []
68
+
69
+ # First try to read from main access log
68
70
  if LOG_PATH and os.path.exists(LOG_PATH):
69
71
  with open(LOG_PATH, "r", errors="ignore") as f:
70
72
  lines.extend(f.readlines())
71
- for p in sorted(glob.glob(f"{LOG_PATH}.*")):
72
- opener = gzip.open if p.endswith(".gz") else open
73
- try:
74
- with opener(p, "rt", errors="ignore") as f:
75
- lines.extend(f.readlines())
76
- except OSError:
77
- continue
73
+ for p in sorted(glob.glob(f"{LOG_PATH}.*")):
74
+ opener = gzip.open if p.endswith(".gz") else open
75
+ try:
76
+ with opener(p, "rt", errors="ignore") as f:
77
+ lines.extend(f.readlines())
78
+ except OSError:
79
+ continue
80
+
81
+ # If no lines found from main log, try AI-WAF middleware CSV log
82
+ if not lines:
83
+ middleware_csv = getattr(settings, "AIWAF_MIDDLEWARE_LOG", "aiwaf_requests.log").replace('.log', '.csv')
84
+ if os.path.exists(middleware_csv):
85
+ try:
86
+ from .middleware_logger import AIWAFCSVLogParser
87
+ csv_lines = AIWAFCSVLogParser.get_log_lines_for_trainer(middleware_csv)
88
+ lines.extend(csv_lines)
89
+ print(f"📋 Using AI-WAF middleware CSV log: {middleware_csv} ({len(csv_lines)} entries)")
90
+ except Exception as e:
91
+ print(f"⚠️ Failed to read middleware CSV log: {e}")
92
+
78
93
  return lines
79
94
 
80
95
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.8.7
3
+ Version: 0.1.8.9
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -27,6 +27,18 @@ Dynamic: requires-python
27
27
 
28
28
  ---
29
29
 
30
+ ## 🚀 Quick Installation
31
+
32
+ ```bash
33
+ pip install aiwaf
34
+ ```
35
+
36
+ **⚠️ Important:** Add `'aiwaf'` to your Django `INSTALLED_APPS` to avoid setup errors.
37
+
38
+ **📋 Complete Setup Guide:** See [INSTALLATION.md](INSTALLATION.md) for detailed installation instructions and troubleshooting.
39
+
40
+ ---
41
+
30
42
  ## System Requirements
31
43
 
32
44
  No GPU needed—AI-WAF runs entirely on CPU with just Python 3.8+, Django 3.2+, a single vCPU and ~512 MB RAM for small sites; for moderate production traffic you can bump to 2–4 vCPUs and 2–4 GB RAM, offload the daily detect-and-train job to a worker, and rotate logs to keep memory use bounded.
@@ -83,7 +95,28 @@ aiwaf/
83
95
  - Submit forms faster than `AIWAF_MIN_FORM_TIME` seconds (default: 1 second)
84
96
 
85
97
  - **UUID Tampering Protection**
86
- Blocks guessed or invalid UUIDs that dont resolve to real models.
98
+ Blocks guessed or invalid UUIDs that don't resolve to real models.
99
+
100
+ - **Built-in Request Logger**
101
+ Optional middleware logger that captures requests to CSV:
102
+ - **Automatic fallback** when main access logs unavailable
103
+ - **CSV format** for easy analysis and training
104
+ - **Captures response times** for better anomaly detection
105
+ - **Zero configuration** - works out of the box
106
+
107
+
108
+ **Exempt Path & IP Awareness**
109
+
110
+ **Exempt Paths:**
111
+ AI‑WAF automatically exempts common login paths (`/admin/`, `/login/`, `/accounts/login/`, etc.) from all blocking mechanisms. You can add additional exempt paths in your Django `settings.py`:
112
+
113
+ ```python
114
+ AIWAF_EXEMPT_PATHS = [
115
+ "/api/webhooks/",
116
+ "/health/",
117
+ "/special-endpoint/",
118
+ ]
119
+ ```
87
120
 
88
121
 
89
122
  **Exempt Path & IP Awareness**
@@ -213,6 +246,42 @@ AIWAF_CSV_DATA_DIR = "aiwaf_data" # Directory for CSV files
213
246
 
214
247
  ---
215
248
 
249
+ ### Built-in Request Logger (Optional)
250
+
251
+ Enable AI-WAF's built-in request logger as a fallback when main access logs aren't available:
252
+
253
+ ```python
254
+ # Enable middleware logging
255
+ AIWAF_MIDDLEWARE_LOGGING = True # Enable/disable logging
256
+ AIWAF_MIDDLEWARE_LOG = "aiwaf_requests.log" # Log file path
257
+ AIWAF_MIDDLEWARE_CSV = True # Use CSV format (recommended)
258
+ ```
259
+
260
+ **Then add middleware to MIDDLEWARE list:**
261
+
262
+ ```python
263
+ MIDDLEWARE = [
264
+ # ... your existing middleware ...
265
+ 'aiwaf.middleware_logger.AIWAFLoggerMiddleware', # Add near the end
266
+ ]
267
+ ```
268
+
269
+ **Manage middleware logging:**
270
+
271
+ ```bash
272
+ python manage.py aiwaf_logging --status # Check logging status
273
+ python manage.py aiwaf_logging --enable # Show setup instructions
274
+ python manage.py aiwaf_logging --clear # Clear log files
275
+ ```
276
+
277
+ **Benefits:**
278
+ - **Automatic fallback** when `AIWAF_ACCESS_LOG` unavailable
279
+ - **CSV format** with precise timestamps and response times
280
+ - **Zero configuration** - trainer automatically detects and uses CSV logs
281
+ - **Lightweight** - fails silently to avoid breaking your application
282
+
283
+ ---
284
+
216
285
  ### Optional (defaults shown)
217
286
 
218
287
  ```python
@@ -244,14 +313,40 @@ Add in **this** order to your `MIDDLEWARE` list:
244
313
  ```python
245
314
  MIDDLEWARE = [
246
315
  "aiwaf.middleware.IPAndKeywordBlockMiddleware",
247
- "aiwaf.middleware.RateLimitMiddleware",
316
+ "aiwaf.middleware.RateLimitMiddleware",
248
317
  "aiwaf.middleware.AIAnomalyMiddleware",
249
318
  "aiwaf.middleware.HoneypotTimingMiddleware",
250
319
  "aiwaf.middleware.UUIDTamperMiddleware",
251
320
  # ... other middleware ...
321
+ "aiwaf.middleware_logger.AIWAFLoggerMiddleware", # Optional: Add if using built-in logger
252
322
  ]
253
323
  ```
254
324
 
325
+ > **⚠️ Order matters!** AI-WAF protection middleware should come early. The logger middleware should come near the end to capture final response data.
326
+
327
+ ---
328
+
329
+ ## Running Detection & Training
330
+
331
+ ```bash
332
+ python manage.py detect_and_train
333
+ ```
334
+
335
+ ### What happens:
336
+ 1. Read access logs (incl. rotated or gzipped) **OR** AI-WAF middleware CSV logs
337
+ 2. Auto‑block IPs with ≥ 6 total 404s
338
+ 3. Extract features & train IsolationForest
339
+ 4. Save `model.pkl`
340
+ 5. Extract top 10 dynamic keywords from 4xx/5xx
341
+ 6. Remove any keywords associated with newly exempt paths
342
+
343
+ **Note:** If main access log (`AIWAF_ACCESS_LOG`) is unavailable, trainer automatically falls back to AI-WAF middleware CSV logs.
344
+
345
+ ---
346
+
347
+ ## 🧠 How It Works
348
+ ```
349
+
255
350
  ---
256
351
 
257
352
  ## Running Detection & Training
@@ -3,20 +3,22 @@ aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
3
3
  aiwaf/blacklist_manager.py,sha256=92ltIrFfv8WOC4CXwvNVZYfivkRZHGNg3E2QAbHQipQ,550
4
4
  aiwaf/decorators.py,sha256=IUKOdM_gdroffImRZep1g1wT6gNqD10zGwcp28hsJCs,825
5
5
  aiwaf/middleware.py,sha256=1JPrc0npI_a5bnB-thN0ME1ehfTbWBl1j9wTndZwRdQ,9505
6
+ aiwaf/middleware_logger.py,sha256=uTYTvIc4Mv1pjY50aXaqQ5cWAO9qqquijAyVMs1KWlM,6517
6
7
  aiwaf/models.py,sha256=XaG1pd_oZu3y-fw66u4wblGlWcUY9gvsTNKGD0kQk7Y,1672
7
- aiwaf/storage.py,sha256=Z0KWArfLmOHnvUcL5aVx8W_aHMr-qoEW8FVGrM23BvA,11639
8
- aiwaf/trainer.py,sha256=qzQOtMW0_OA5JWWh6Znbc5BG3gcQ6Cb_NNWMOgLf3VQ,8487
8
+ aiwaf/storage.py,sha256=oI9ZuTCrLwLSOE3I3_tDTFs6jiile7Vr4e7jacf2B6Y,13218
9
+ aiwaf/trainer.py,sha256=bgVoBewnNVMJdgxcNchfhsPOnFXxStoBOqNhFYnpsqs,9244
9
10
  aiwaf/utils.py,sha256=BJk5vJCYdGPl_4QQiknjhCbkzv5HZCXgFcBJDMJpHok,3390
10
11
  aiwaf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
12
  aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  aiwaf/management/commands/add_ipexemption.py,sha256=srgdVPDJtF7G9GGIqaZ7L3qTuNheoS_uwlhlRO4W2bc,945
14
+ aiwaf/management/commands/aiwaf_logging.py,sha256=FCIqULn2tii2vD9VxL7vk3PV4k4vr7kaA00KyaCExYY,7692
13
15
  aiwaf/management/commands/aiwaf_reset.py,sha256=0FIBqpZS8xgFFvAKJ-0zAC_-QNQwRkOHpXb8N-OdFr8,3740
14
16
  aiwaf/management/commands/detect_and_train.py,sha256=-o-LZ7QZ5GeJPCekryox1DGXKMmFEkwwrcDsiM166K0,269
15
17
  aiwaf/resources/model.pkl,sha256=5t6h9BX8yoh2xct85MXOO60jdlWyg1APskUOW0jZE1Y,1288265
16
18
  aiwaf/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
19
  aiwaf/templatetags/aiwaf_tags.py,sha256=XXfb7Tl4DjU3Sc40GbqdaqOEtKTUKELBEk58u83wBNw,357
18
- aiwaf-0.1.8.7.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
19
- aiwaf-0.1.8.7.dist-info/METADATA,sha256=qAIF4-sV_Br3BPp6Ivn3fXzY9uWvdtMco1zXBlkKkW0,8664
20
- aiwaf-0.1.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- aiwaf-0.1.8.7.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
22
- aiwaf-0.1.8.7.dist-info/RECORD,,
20
+ aiwaf-0.1.8.9.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
21
+ aiwaf-0.1.8.9.dist-info/METADATA,sha256=2CYLoXxKDrsQtfL1k5lT08tWIeo1GsEDEXZHRXqs_vI,11548
22
+ aiwaf-0.1.8.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ aiwaf-0.1.8.9.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
24
+ aiwaf-0.1.8.9.dist-info/RECORD,,