aiwaf 0.1.9.0.3__tar.gz → 0.1.9.0.5__tar.gz

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.

Files changed (36) hide show
  1. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/PKG-INFO +1 -1
  2. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/__init__.py +1 -1
  3. aiwaf-0.1.9.0.5/aiwaf/blacklist_manager.py +37 -0
  4. aiwaf-0.1.9.0.5/aiwaf/management/commands/debug_csv.py +155 -0
  5. aiwaf-0.1.9.0.5/aiwaf/management/commands/test_exemption.py +120 -0
  6. aiwaf-0.1.9.0.5/aiwaf/management/commands/test_exemption_fix.py +54 -0
  7. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/middleware.py +28 -16
  8. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/middleware_logger.py +10 -1
  9. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/storage.py +43 -11
  10. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/PKG-INFO +1 -1
  11. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/SOURCES.txt +3 -0
  12. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/pyproject.toml +1 -1
  13. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/setup.py +1 -1
  14. aiwaf-0.1.9.0.3/aiwaf/blacklist_manager.py +0 -24
  15. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/LICENSE +0 -0
  16. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/README.md +0 -0
  17. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/apps.py +0 -0
  18. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/decorators.py +0 -0
  19. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/__init__.py +0 -0
  20. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/__init__.py +0 -0
  21. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/add_ipexemption.py +0 -0
  22. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/aiwaf_diagnose.py +0 -0
  23. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/aiwaf_logging.py +0 -0
  24. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/aiwaf_reset.py +0 -0
  25. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/detect_and_train.py +0 -0
  26. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/regenerate_model.py +0 -0
  27. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/models.py +0 -0
  28. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/resources/model.pkl +0 -0
  29. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/templatetags/__init__.py +0 -0
  30. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/templatetags/aiwaf_tags.py +0 -0
  31. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/trainer.py +0 -0
  32. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/utils.py +0 -0
  33. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/dependency_links.txt +0 -0
  34. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/requires.txt +0 -0
  35. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/top_level.txt +0 -0
  36. {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.9.0.3
3
+ Version: 0.1.9.0.5
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -1,6 +1,6 @@
1
1
  default_app_config = "aiwaf.apps.AiwafConfig"
2
2
 
3
- __version__ = "0.1.9.0.3"
3
+ __version__ = "0.1.9.0.5"
4
4
 
5
5
  # Note: Middleware classes are available from aiwaf.middleware
6
6
  # Import them only when needed to avoid circular imports during Django app loading
@@ -0,0 +1,37 @@
1
+ # aiwaf/blacklist_manager.py
2
+
3
+ from .storage import get_blacklist_store, get_exemption_store
4
+
5
+ class BlacklistManager:
6
+ @staticmethod
7
+ def block(ip, reason):
8
+ """Add IP to blacklist, but only if it's not exempted"""
9
+ # Check if IP is exempted before blocking
10
+ exemption_store = get_exemption_store()
11
+ if exemption_store.is_exempted(ip):
12
+ return # Don't block exempted IPs
13
+
14
+ store = get_blacklist_store()
15
+ store.add_ip(ip, reason)
16
+
17
+ @staticmethod
18
+ def is_blocked(ip):
19
+ """Check if IP is blocked, but respect exemptions"""
20
+ # First check if IP is exempted - exemptions override blacklist
21
+ exemption_store = get_exemption_store()
22
+ if exemption_store.is_exempted(ip):
23
+ return False # Exempted IPs are never considered blocked
24
+
25
+ # If not exempted, check blacklist
26
+ store = get_blacklist_store()
27
+ return store.is_blocked(ip)
28
+
29
+ @staticmethod
30
+ def all_blocked():
31
+ store = get_blacklist_store()
32
+ return store.get_all()
33
+
34
+ @staticmethod
35
+ def unblock(ip):
36
+ store = get_blacklist_store()
37
+ store.remove_ip(ip)
@@ -0,0 +1,155 @@
1
+ from django.core.management.base import BaseCommand
2
+ import os
3
+ import csv
4
+
5
+ class Command(BaseCommand):
6
+ help = 'Debug and fix AI-WAF CSV functionality'
7
+
8
+ def add_arguments(self, parser):
9
+ parser.add_argument(
10
+ '--test-ip',
11
+ type=str,
12
+ help='Test IP address to add to exemption list',
13
+ default='127.0.0.1'
14
+ )
15
+ parser.add_argument(
16
+ '--fix',
17
+ action='store_true',
18
+ help='Attempt to fix identified issues',
19
+ )
20
+
21
+ def handle(self, *args, **options):
22
+ self.stdout.write(self.style.HTTP_INFO("🔍 AI-WAF CSV Debug & Fix"))
23
+ self.stdout.write("")
24
+
25
+ # Check storage mode
26
+ from django.conf import settings
27
+ storage_mode = getattr(settings, 'AIWAF_STORAGE_MODE', 'models')
28
+ csv_dir = getattr(settings, 'AIWAF_CSV_DATA_DIR', 'aiwaf_data')
29
+
30
+ self.stdout.write(f"Storage Mode: {storage_mode}")
31
+ self.stdout.write(f"CSV Directory: {csv_dir}")
32
+ self.stdout.write("")
33
+
34
+ # Check middleware logging
35
+ middleware_logging = getattr(settings, 'AIWAF_MIDDLEWARE_LOGGING', False)
36
+ middleware_log = getattr(settings, 'AIWAF_MIDDLEWARE_LOG', 'aiwaf_requests.log')
37
+
38
+ self.stdout.write(f"Middleware Logging: {middleware_logging}")
39
+ self.stdout.write(f"Middleware Log File: {middleware_log}")
40
+ self.stdout.write("")
41
+
42
+ # Check if CSV directory exists
43
+ if os.path.exists(csv_dir):
44
+ self.stdout.write(self.style.SUCCESS(f"✅ CSV directory exists: {csv_dir}"))
45
+ else:
46
+ self.stdout.write(self.style.ERROR(f"❌ CSV directory missing: {csv_dir}"))
47
+ if options['fix']:
48
+ os.makedirs(csv_dir, exist_ok=True)
49
+ self.stdout.write(self.style.SUCCESS(f"✅ Created CSV directory: {csv_dir}"))
50
+
51
+ # Check CSV files
52
+ csv_files = ['blacklist.csv', 'exemptions.csv', 'keywords.csv']
53
+ for filename in csv_files:
54
+ filepath = os.path.join(csv_dir, filename)
55
+ if os.path.exists(filepath):
56
+ # Count entries
57
+ try:
58
+ with open(filepath, 'r', newline='', encoding='utf-8') as f:
59
+ reader = csv.reader(f)
60
+ rows = list(reader)
61
+ entry_count = len(rows) - 1 if rows else 0 # Subtract header
62
+ self.stdout.write(self.style.SUCCESS(f"✅ {filename}: {entry_count} entries"))
63
+ except Exception as e:
64
+ self.stdout.write(self.style.ERROR(f"❌ {filename}: Error reading - {e}"))
65
+ else:
66
+ self.stdout.write(self.style.WARNING(f"⚠️ {filename}: Not found"))
67
+
68
+ self.stdout.write("")
69
+
70
+ # Test storage functionality
71
+ self.stdout.write(self.style.HTTP_INFO("🧪 Testing Storage Functions"))
72
+
73
+ try:
74
+ from aiwaf.storage import get_exemption_store, get_blacklist_store, get_keyword_store
75
+
76
+ # Test exemption store
77
+ exemption_store = get_exemption_store()
78
+ self.stdout.write(f"Exemption Store: {exemption_store.__name__}")
79
+
80
+ # Test blacklist store
81
+ blacklist_store = get_blacklist_store()
82
+ self.stdout.write(f"Blacklist Store: {blacklist_store.__name__}")
83
+
84
+ # Test keyword store
85
+ keyword_store = get_keyword_store()
86
+ self.stdout.write(f"Keyword Store: {keyword_store.__name__}")
87
+
88
+ except Exception as e:
89
+ self.stdout.write(self.style.ERROR(f"❌ Storage import failed: {e}"))
90
+ return
91
+
92
+ self.stdout.write("")
93
+
94
+ # Test exemption functionality
95
+ test_ip = options['test_ip']
96
+ self.stdout.write(f"🧪 Testing exemption with IP: {test_ip}")
97
+
98
+ try:
99
+ # Check if already exempted
100
+ is_exempted_before = exemption_store.is_exempted(test_ip)
101
+ self.stdout.write(f"Before: IP {test_ip} exempted = {is_exempted_before}")
102
+
103
+ # Add to exemption
104
+ exemption_store.add_ip(test_ip, "Test exemption from debug command")
105
+ self.stdout.write(f"✅ Added {test_ip} to exemption list")
106
+
107
+ # Check if now exempted
108
+ is_exempted_after = exemption_store.is_exempted(test_ip)
109
+ self.stdout.write(f"After: IP {test_ip} exempted = {is_exempted_after}")
110
+
111
+ if is_exempted_after:
112
+ self.stdout.write(self.style.SUCCESS("✅ Exemption functionality working!"))
113
+ else:
114
+ self.stdout.write(self.style.ERROR("❌ Exemption functionality not working!"))
115
+
116
+ # List all exemptions
117
+ all_exemptions = exemption_store.get_all()
118
+ self.stdout.write(f"Total exemptions: {len(all_exemptions)}")
119
+
120
+ for exemption in all_exemptions:
121
+ self.stdout.write(f" - {exemption.get('ip_address', exemption)}")
122
+
123
+ except Exception as e:
124
+ self.stdout.write(self.style.ERROR(f"❌ Exemption test failed: {e}"))
125
+
126
+ self.stdout.write("")
127
+
128
+ # Check middleware logger file
129
+ csv_log_file = middleware_log.replace('.log', '.csv')
130
+ if os.path.exists(csv_log_file):
131
+ try:
132
+ with open(csv_log_file, 'r', newline='', encoding='utf-8') as f:
133
+ reader = csv.reader(f)
134
+ rows = list(reader)
135
+ entry_count = len(rows) - 1 if rows else 0
136
+ self.stdout.write(self.style.SUCCESS(f"✅ Middleware CSV log: {entry_count} entries"))
137
+ except Exception as e:
138
+ self.stdout.write(self.style.ERROR(f"❌ Middleware CSV log error: {e}"))
139
+ else:
140
+ self.stdout.write(self.style.WARNING(f"⚠️ Middleware CSV log not found: {csv_log_file}"))
141
+ self.stdout.write(" Make some requests to generate log entries")
142
+
143
+ # Recommendations
144
+ self.stdout.write("")
145
+ self.stdout.write(self.style.HTTP_INFO("💡 Recommendations:"))
146
+
147
+ if storage_mode != 'csv':
148
+ self.stdout.write("1. Set AIWAF_STORAGE_MODE = 'csv' in settings.py")
149
+
150
+ if not middleware_logging:
151
+ self.stdout.write("2. Set AIWAF_MIDDLEWARE_LOGGING = True in settings.py")
152
+
153
+ self.stdout.write("3. Add AIWAFLoggerMiddleware to MIDDLEWARE in settings.py")
154
+ self.stdout.write("4. Make some requests to generate log data")
155
+ self.stdout.write("5. Run 'python manage.py detect_and_train' to train with data")
@@ -0,0 +1,120 @@
1
+ from django.core.management.base import BaseCommand
2
+ import os
3
+
4
+ class Command(BaseCommand):
5
+ help = 'Test AI-WAF exemption functionality step by step'
6
+
7
+ def add_arguments(self, parser):
8
+ parser.add_argument(
9
+ 'test_ip',
10
+ type=str,
11
+ help='IP address to test exemption for'
12
+ )
13
+
14
+ def handle(self, *args, **options):
15
+ test_ip = options['test_ip']
16
+
17
+ self.stdout.write(self.style.HTTP_INFO(f"🧪 Testing Exemption for IP: {test_ip}"))
18
+ self.stdout.write("=" * 50)
19
+
20
+ # Step 1: Check settings
21
+ from django.conf import settings
22
+ storage_mode = getattr(settings, 'AIWAF_STORAGE_MODE', 'models')
23
+ csv_dir = getattr(settings, 'AIWAF_CSV_DATA_DIR', 'aiwaf_data')
24
+
25
+ self.stdout.write(f"Storage Mode: {storage_mode}")
26
+ self.stdout.write(f"CSV Directory: {csv_dir}")
27
+ self.stdout.write("")
28
+
29
+ # Step 2: Check storage factory
30
+ try:
31
+ from aiwaf.storage import get_exemption_store, EXEMPTION_CSV, CSV_DATA_DIR, STORAGE_MODE
32
+ exemption_store = get_exemption_store()
33
+
34
+ self.stdout.write(f"Exemption Store Class: {exemption_store.__name__}")
35
+ self.stdout.write(f"Expected CSV File: {EXEMPTION_CSV}")
36
+ self.stdout.write(f"CSV Directory: {CSV_DATA_DIR}")
37
+ self.stdout.write(f"Storage Mode from storage.py: {STORAGE_MODE}")
38
+ self.stdout.write("")
39
+
40
+ except Exception as e:
41
+ self.stdout.write(self.style.ERROR(f"❌ Storage import failed: {e}"))
42
+ return
43
+
44
+ # Step 3: Check file existence
45
+ if os.path.exists(EXEMPTION_CSV):
46
+ self.stdout.write(self.style.SUCCESS(f"✅ Exemption CSV exists: {EXEMPTION_CSV}"))
47
+
48
+ # Read and display file contents
49
+ try:
50
+ with open(EXEMPTION_CSV, 'r', encoding='utf-8') as f:
51
+ content = f.read().strip()
52
+ if content:
53
+ self.stdout.write(f"📄 File contents:\n{content}")
54
+ self.stdout.write("")
55
+ else:
56
+ self.stdout.write("📄 File is empty")
57
+
58
+ except Exception as e:
59
+ self.stdout.write(self.style.ERROR(f"❌ Could not read file: {e}"))
60
+ else:
61
+ self.stdout.write(self.style.ERROR(f"❌ Exemption CSV not found: {EXEMPTION_CSV}"))
62
+ self.stdout.write("Creating test exemption...")
63
+
64
+ # Create the exemption
65
+ try:
66
+ exemption_store.add_ip(test_ip, "Test exemption from debug")
67
+ self.stdout.write(self.style.SUCCESS("✅ Created test exemption"))
68
+ except Exception as e:
69
+ self.stdout.write(self.style.ERROR(f"❌ Failed to create exemption: {e}"))
70
+ return
71
+
72
+ # Step 4: Test exemption check via storage
73
+ try:
74
+ is_exempted_storage = exemption_store.is_exempted(test_ip)
75
+ self.stdout.write(f"Direct storage check: {test_ip} exempted = {is_exempted_storage}")
76
+ except Exception as e:
77
+ self.stdout.write(self.style.ERROR(f"❌ Storage exemption check failed: {e}"))
78
+
79
+ # Step 5: Test exemption check via utils function
80
+ try:
81
+ from aiwaf.utils import is_ip_exempted
82
+ is_exempted_utils = is_ip_exempted(test_ip)
83
+ self.stdout.write(f"Utils function check: {test_ip} exempted = {is_exempted_utils}")
84
+ except Exception as e:
85
+ self.stdout.write(self.style.ERROR(f"❌ Utils exemption check failed: {e}"))
86
+
87
+ # Step 6: Test middleware import
88
+ try:
89
+ from aiwaf.middleware import IPAndKeywordBlockMiddleware
90
+ self.stdout.write("✅ Middleware import successful")
91
+ except Exception as e:
92
+ self.stdout.write(self.style.ERROR(f"❌ Middleware import failed: {e}"))
93
+
94
+ # Step 7: Test CSV reading manually
95
+ if os.path.exists(EXEMPTION_CSV):
96
+ try:
97
+ import csv
98
+ self.stdout.write("\n📋 Manual CSV parsing:")
99
+ with open(EXEMPTION_CSV, 'r', newline='', encoding='utf-8') as f:
100
+ reader = csv.DictReader(f)
101
+ found = False
102
+ for i, row in enumerate(reader):
103
+ ip_in_row = row.get('ip_address', 'N/A')
104
+ self.stdout.write(f" Row {i}: ip_address = '{ip_in_row}'")
105
+ if ip_in_row == test_ip:
106
+ found = True
107
+ self.stdout.write(f" ✅ Found match for {test_ip}")
108
+
109
+ if not found:
110
+ self.stdout.write(f" ❌ No match found for {test_ip}")
111
+
112
+ except Exception as e:
113
+ self.stdout.write(self.style.ERROR(f"❌ Manual CSV parsing failed: {e}"))
114
+
115
+ self.stdout.write("")
116
+ self.stdout.write(self.style.HTTP_INFO("💡 Debugging Tips:"))
117
+ self.stdout.write("1. Check that AIWAF_STORAGE_MODE = 'csv' in settings.py")
118
+ self.stdout.write("2. Ensure the CSV file has proper headers: ip_address,reason,created_at")
119
+ self.stdout.write("3. Check file permissions on the CSV directory")
120
+ self.stdout.write("4. Verify no trailing/leading spaces in IP addresses")
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from django.core.management.base import BaseCommand
4
+ from aiwaf.blacklist_manager import BlacklistManager
5
+ from aiwaf.storage import get_exemption_store
6
+
7
+ class Command(BaseCommand):
8
+ help = 'Test that exempted IPs are properly honored by BlacklistManager'
9
+
10
+ def add_arguments(self, parser):
11
+ parser.add_argument('--ip', default='97.187.30.95', help='IP address to test')
12
+
13
+ def handle(self, *args, **options):
14
+ test_ip = options['ip']
15
+
16
+ self.stdout.write(f"\n=== Testing Exemption Fix for IP: {test_ip} ===")
17
+
18
+ # Check exemption store
19
+ exemption_store = get_exemption_store()
20
+ is_exempted = exemption_store.is_exempted(test_ip)
21
+ self.stdout.write(f"1. Is IP exempted in storage? {is_exempted}")
22
+
23
+ # Test BlacklistManager.block() - should not block exempted IPs
24
+ self.stdout.write(f"\n2. Testing BlacklistManager.block() on exempted IP...")
25
+ BlacklistManager.block(test_ip, "Test block attempt")
26
+
27
+ # Check if actually blocked
28
+ is_blocked = BlacklistManager.is_blocked(test_ip)
29
+ self.stdout.write(f"3. Is IP blocked after block attempt? {is_blocked}")
30
+
31
+ if is_exempted and not is_blocked:
32
+ self.stdout.write(self.style.SUCCESS("✅ PASS: Exempted IP was NOT blocked"))
33
+ elif is_exempted and is_blocked:
34
+ self.stdout.write(self.style.ERROR("❌ FAIL: Exempted IP was blocked (this should not happen)"))
35
+ elif not is_exempted:
36
+ self.stdout.write(self.style.WARNING("⚠️ IP is not exempted, blocking behavior is normal"))
37
+
38
+ # Test with a non-exempted IP to verify blocking still works
39
+ test_non_exempted = "1.2.3.4"
40
+ self.stdout.write(f"\n4. Testing with non-exempted IP: {test_non_exempted}")
41
+
42
+ is_exempted_2 = exemption_store.is_exempted(test_non_exempted)
43
+ self.stdout.write(f" Is non-exempted IP exempted? {is_exempted_2}")
44
+
45
+ BlacklistManager.block(test_non_exempted, "Test block non-exempted")
46
+ is_blocked_2 = BlacklistManager.is_blocked(test_non_exempted)
47
+ self.stdout.write(f" Is non-exempted IP blocked? {is_blocked_2}")
48
+
49
+ if not is_exempted_2 and is_blocked_2:
50
+ self.stdout.write(self.style.SUCCESS("✅ PASS: Non-exempted IP was properly blocked"))
51
+ else:
52
+ self.stdout.write(self.style.ERROR("❌ FAIL: Non-exempted IP blocking failed"))
53
+
54
+ self.stdout.write(f"\n=== Test Complete ===")
@@ -107,8 +107,8 @@ class IPAndKeywordBlockMiddleware:
107
107
  return self.get_response(request)
108
108
  ip = get_ip(request)
109
109
  path = raw_path.lstrip("/")
110
- if is_ip_exempted(ip):
111
- return self.get_response(request)
110
+
111
+ # BlacklistManager now handles exemption checking internally
112
112
  if BlacklistManager.is_blocked(ip):
113
113
  return JsonResponse({"error": "blocked"}, status=403)
114
114
 
@@ -126,8 +126,10 @@ class IPAndKeywordBlockMiddleware:
126
126
  }
127
127
  for seg in segments:
128
128
  if seg in suspicious_kw:
129
- if not is_ip_exempted(ip):
130
- BlacklistManager.block(ip, f"Keyword block: {seg}")
129
+ # BlacklistManager.block() now checks exemptions internally
130
+ BlacklistManager.block(ip, f"Keyword block: {seg}")
131
+ # Check again after blocking attempt (exempted IPs won't be blocked)
132
+ if BlacklistManager.is_blocked(ip):
131
133
  return JsonResponse({"error": "blocked"}, status=403)
132
134
  return self.get_response(request)
133
135
 
@@ -152,8 +154,10 @@ class RateLimitMiddleware:
152
154
  timestamps.append(now)
153
155
  cache.set(key, timestamps, timeout=self.WINDOW)
154
156
  if len(timestamps) > self.FLOOD:
155
- if not is_ip_exempted(ip):
156
- BlacklistManager.block(ip, "Flood pattern")
157
+ # BlacklistManager.block() now checks exemptions internally
158
+ BlacklistManager.block(ip, "Flood pattern")
159
+ # Check if actually blocked (exempted IPs won't be blocked)
160
+ if BlacklistManager.is_blocked(ip):
157
161
  return JsonResponse({"error": "blocked"}, status=403)
158
162
  if len(timestamps) > self.MAX:
159
163
  return JsonResponse({"error": "too_many_requests"}, status=429)
@@ -174,8 +178,7 @@ class AIAnomalyMiddleware(MiddlewareMixin):
174
178
  return None
175
179
  request._start_time = time.time()
176
180
  ip = get_ip(request)
177
- if is_ip_exempted(ip):
178
- return None
181
+ # BlacklistManager now handles exemption checking internally
179
182
  if BlacklistManager.is_blocked(ip):
180
183
  return JsonResponse({"error": "blocked"}, status=403)
181
184
  return None
@@ -203,8 +206,10 @@ class AIAnomalyMiddleware(MiddlewareMixin):
203
206
 
204
207
  # Only use AI model if it's available
205
208
  if self.model is not None and self.model.predict(X)[0] == -1:
206
- if not is_ip_exempted(ip):
207
- BlacklistManager.block(ip, "AI anomaly")
209
+ # BlacklistManager.block() now checks exemptions internally
210
+ BlacklistManager.block(ip, "AI anomaly")
211
+ # Check if actually blocked (exempted IPs won't be blocked)
212
+ if BlacklistManager.is_blocked(ip):
208
213
  return JsonResponse({"error": "blocked"}, status=403)
209
214
 
210
215
  data.append((now, request.path, response.status_code, resp_time))
@@ -227,8 +232,7 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
227
232
  return None
228
233
 
229
234
  ip = get_ip(request)
230
- if is_ip_exempted(ip):
231
- return None
235
+ # BlacklistManager now handles exemption checking internally
232
236
 
233
237
  if request.method == "GET":
234
238
  # Store timestamp for this IP's GET request
@@ -245,8 +249,11 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
245
249
  if not any(request.path.lower().startswith(login_path) for login_path in [
246
250
  "/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
247
251
  ]):
252
+ # BlacklistManager.block() now checks exemptions internally
248
253
  BlacklistManager.block(ip, "Direct POST without GET")
249
- return JsonResponse({"error": "blocked"}, status=403)
254
+ # Check if actually blocked (exempted IPs won't be blocked)
255
+ if BlacklistManager.is_blocked(ip):
256
+ return JsonResponse({"error": "blocked"}, status=403)
250
257
  else:
251
258
  # Check timing - be more lenient for login paths
252
259
  time_diff = time.time() - get_time
@@ -259,8 +266,11 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
259
266
  min_time = 0.1 # Very short threshold for login forms
260
267
 
261
268
  if time_diff < min_time:
269
+ # BlacklistManager.block() now checks exemptions internally
262
270
  BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
263
- return JsonResponse({"error": "blocked"}, status=403)
271
+ # Check if actually blocked (exempted IPs won't be blocked)
272
+ if BlacklistManager.is_blocked(ip):
273
+ return JsonResponse({"error": "blocked"}, status=403)
264
274
 
265
275
  return None
266
276
 
@@ -284,6 +294,8 @@ class UUIDTamperMiddleware(MiddlewareMixin):
284
294
  except (ValueError, TypeError):
285
295
  continue
286
296
 
287
- if not is_ip_exempted(ip):
288
- BlacklistManager.block(ip, "UUID tampering")
297
+ # BlacklistManager.block() now checks exemptions internally
298
+ BlacklistManager.block(ip, "UUID tampering")
299
+ # Check if actually blocked (exempted IPs won't be blocked)
300
+ if BlacklistManager.is_blocked(ip):
289
301
  return JsonResponse({"error": "blocked"}, status=403)
@@ -28,7 +28,11 @@ class AIWAFLoggerMiddleware(MiddlewareMixin):
28
28
  def _ensure_csv_header(self):
29
29
  """Ensure CSV file has proper header row"""
30
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
31
+ # Create directory if it doesn't exist
32
+ csv_dir = os.path.dirname(self.csv_file)
33
+ if csv_dir and not os.path.exists(csv_dir):
34
+ os.makedirs(csv_dir, exist_ok=True)
35
+
32
36
  with open(self.csv_file, 'w', newline='', encoding='utf-8') as f:
33
37
  writer = csv.writer(f)
34
38
  writer.writerow([
@@ -73,6 +77,11 @@ class AIWAFLoggerMiddleware(MiddlewareMixin):
73
77
  def _log_to_csv(self, data):
74
78
  """Write log entry to CSV file"""
75
79
  try:
80
+ # Ensure directory exists before writing
81
+ csv_dir = os.path.dirname(self.csv_file)
82
+ if csv_dir and not os.path.exists(csv_dir):
83
+ os.makedirs(csv_dir, exist_ok=True)
84
+
76
85
  with open(self.csv_file, 'a', newline='', encoding='utf-8') as f:
77
86
  writer = csv.writer(f)
78
87
  writer.writerow([
@@ -167,28 +167,50 @@ class CsvExemptionStore:
167
167
  @staticmethod
168
168
  def add_ip(ip_address, reason=""):
169
169
  ensure_csv_directory()
170
- # Check if IP already exists
170
+
171
+ # Check if IP already exists to avoid duplicates
171
172
  if CsvExemptionStore.is_exempted(ip_address):
172
173
  return
173
174
 
174
175
  # Add new entry
175
176
  new_file = not os.path.exists(EXEMPTION_CSV)
176
- with open(EXEMPTION_CSV, "a", newline="", encoding="utf-8") as f:
177
- writer = csv.writer(f)
178
- if new_file:
179
- writer.writerow(["ip_address", "reason", "created_at"])
180
- writer.writerow([ip_address, reason, timezone.now().isoformat()])
177
+ try:
178
+ with open(EXEMPTION_CSV, "a", newline="", encoding="utf-8") as f:
179
+ writer = csv.writer(f)
180
+ if new_file:
181
+ writer.writerow(["ip_address", "reason", "created_at"])
182
+ writer.writerow([ip_address, reason, timezone.now().isoformat()])
183
+ except Exception as e:
184
+ print(f"Error writing to exemption CSV: {e}")
185
+ print(f"File path: {EXEMPTION_CSV}")
186
+ print(f"Directory exists: {os.path.exists(CSV_DATA_DIR)}")
187
+ raise
181
188
 
182
189
  @staticmethod
183
190
  def is_exempted(ip_address):
184
191
  if not os.path.exists(EXEMPTION_CSV):
192
+ # Debug: Let user know file doesn't exist
193
+ if getattr(settings, 'DEBUG', False):
194
+ print(f"DEBUG: Exemption CSV not found: {EXEMPTION_CSV}")
185
195
  return False
186
196
 
187
- with open(EXEMPTION_CSV, "r", newline="", encoding="utf-8") as f:
188
- reader = csv.DictReader(f)
189
- for row in reader:
190
- if row["ip_address"] == ip_address:
191
- return True
197
+ try:
198
+ with open(EXEMPTION_CSV, "r", newline="", encoding="utf-8") as f:
199
+ reader = csv.DictReader(f)
200
+ for row_num, row in enumerate(reader):
201
+ stored_ip = row.get("ip_address", "").strip()
202
+ if getattr(settings, 'DEBUG', False) and row_num < 5: # Show first 5 for debug
203
+ print(f"DEBUG: Row {row_num}: comparing '{stored_ip}' with '{ip_address}'")
204
+ if stored_ip == ip_address:
205
+ if getattr(settings, 'DEBUG', False):
206
+ print(f"DEBUG: Found exemption match for {ip_address}")
207
+ return True
208
+ except Exception as e:
209
+ print(f"Error reading exemption CSV: {e}")
210
+ return False
211
+
212
+ if getattr(settings, 'DEBUG', False):
213
+ print(f"DEBUG: No exemption found for {ip_address}")
192
214
  return False
193
215
 
194
216
  @staticmethod
@@ -298,12 +320,22 @@ def get_blacklist_store():
298
320
 
299
321
  def get_exemption_store():
300
322
  """Return appropriate exemption storage class based on settings"""
323
+ if getattr(settings, 'DEBUG', False):
324
+ print(f"DEBUG: Storage mode = {STORAGE_MODE}, CSV mode = {STORAGE_MODE == 'csv'}")
325
+
301
326
  if STORAGE_MODE == "csv":
327
+ if getattr(settings, 'DEBUG', False):
328
+ print("DEBUG: Using CsvExemptionStore")
302
329
  return CsvExemptionStore
303
330
  else:
331
+ _import_models()
304
332
  if IPExemption is not None:
333
+ if getattr(settings, 'DEBUG', False):
334
+ print("DEBUG: Using ModelExemptionStore")
305
335
  return ModelExemptionStore
306
336
  else:
337
+ if getattr(settings, 'DEBUG', False):
338
+ print("DEBUG: Falling back to CsvExemptionStore (models not available)")
307
339
  return CsvExemptionStore
308
340
 
309
341
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.9.0.3
3
+ Version: 0.1.9.0.5
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -23,8 +23,11 @@ aiwaf/management/commands/add_ipexemption.py
23
23
  aiwaf/management/commands/aiwaf_diagnose.py
24
24
  aiwaf/management/commands/aiwaf_logging.py
25
25
  aiwaf/management/commands/aiwaf_reset.py
26
+ aiwaf/management/commands/debug_csv.py
26
27
  aiwaf/management/commands/detect_and_train.py
27
28
  aiwaf/management/commands/regenerate_model.py
29
+ aiwaf/management/commands/test_exemption.py
30
+ aiwaf/management/commands/test_exemption_fix.py
28
31
  aiwaf/resources/model.pkl
29
32
  aiwaf/templatetags/__init__.py
30
33
  aiwaf/templatetags/aiwaf_tags.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiwaf"
3
- version = "0.1.9.0.3"
3
+ version = "0.1.9.0.5"
4
4
  description = "AI-powered Web Application Firewall"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -9,7 +9,7 @@ long_description = (HERE / "README.md").read_text(encoding="utf-8")
9
9
 
10
10
  setup(
11
11
  name="aiwaf",
12
- version="0.1.9.0.3",
12
+ version="0.1.9.0.5",
13
13
  description="AI‑driven, self‑learning Web Application Firewall for Django",
14
14
  long_description=long_description,
15
15
  long_description_content_type="text/markdown",
@@ -1,24 +0,0 @@
1
- # aiwaf/blacklist_manager.py
2
-
3
- from .storage import get_blacklist_store
4
-
5
- class BlacklistManager:
6
- @staticmethod
7
- def block(ip, reason):
8
- store = get_blacklist_store()
9
- store.add_ip(ip, reason)
10
-
11
- @staticmethod
12
- def is_blocked(ip):
13
- store = get_blacklist_store()
14
- return store.is_blocked(ip)
15
-
16
- @staticmethod
17
- def all_blocked():
18
- store = get_blacklist_store()
19
- return store.get_all()
20
-
21
- @staticmethod
22
- def unblock(ip):
23
- store = get_blacklist_store()
24
- store.remove_ip(ip)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes