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.
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/PKG-INFO +1 -1
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/__init__.py +1 -1
- aiwaf-0.1.9.0.5/aiwaf/blacklist_manager.py +37 -0
- aiwaf-0.1.9.0.5/aiwaf/management/commands/debug_csv.py +155 -0
- aiwaf-0.1.9.0.5/aiwaf/management/commands/test_exemption.py +120 -0
- aiwaf-0.1.9.0.5/aiwaf/management/commands/test_exemption_fix.py +54 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/middleware.py +28 -16
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/middleware_logger.py +10 -1
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/storage.py +43 -11
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/PKG-INFO +1 -1
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/SOURCES.txt +3 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/pyproject.toml +1 -1
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/setup.py +1 -1
- aiwaf-0.1.9.0.3/aiwaf/blacklist_manager.py +0 -24
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/LICENSE +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/README.md +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/apps.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/decorators.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/__init__.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/__init__.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/add_ipexemption.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/aiwaf_diagnose.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/aiwaf_logging.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/aiwaf_reset.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/detect_and_train.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/management/commands/regenerate_model.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/models.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/resources/model.pkl +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/templatetags/__init__.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/templatetags/aiwaf_tags.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/trainer.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf/utils.py +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/dependency_links.txt +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/requires.txt +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/aiwaf.egg-info/top_level.txt +0 -0
- {aiwaf-0.1.9.0.3 → aiwaf-0.1.9.0.5}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
|
@@ -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
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|