aiwaf 0.1.9.1.8__py3-none-any.whl → 0.1.9.2.0__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.
- aiwaf/__init__.py +1 -1
- aiwaf/management/commands/aiwaf_reset.py +107 -60
- aiwaf/middleware.py +210 -42
- aiwaf/storage.py +23 -0
- aiwaf/trainer.py +90 -9
- {aiwaf-0.1.9.1.8.dist-info → aiwaf-0.1.9.2.0.dist-info}/METADATA +121 -12
- {aiwaf-0.1.9.1.8.dist-info → aiwaf-0.1.9.2.0.dist-info}/RECORD +10 -10
- {aiwaf-0.1.9.1.8.dist-info → aiwaf-0.1.9.2.0.dist-info}/WHEEL +0 -0
- {aiwaf-0.1.9.1.8.dist-info → aiwaf-0.1.9.2.0.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.9.1.8.dist-info → aiwaf-0.1.9.2.0.dist-info}/top_level.txt +0 -0
aiwaf/__init__.py
CHANGED
|
@@ -1,76 +1,115 @@
|
|
|
1
1
|
from django.core.management.base import BaseCommand
|
|
2
|
-
from aiwaf.storage import get_blacklist_store, get_exemption_store
|
|
2
|
+
from aiwaf.storage import get_blacklist_store, get_exemption_store, get_keyword_store
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
class Command(BaseCommand):
|
|
6
|
-
help = 'Reset AI-WAF by clearing
|
|
6
|
+
help = 'Reset AI-WAF by clearing blacklist, exemption, and/or keyword entries'
|
|
7
7
|
|
|
8
8
|
def add_arguments(self, parser):
|
|
9
9
|
parser.add_argument(
|
|
10
|
-
'--blacklist
|
|
10
|
+
'--blacklist',
|
|
11
11
|
action='store_true',
|
|
12
|
-
help='Clear
|
|
12
|
+
help='Clear blacklist entries (default: all)'
|
|
13
13
|
)
|
|
14
14
|
parser.add_argument(
|
|
15
|
-
'--exemptions
|
|
15
|
+
'--exemptions',
|
|
16
|
+
action='store_true',
|
|
17
|
+
help='Clear exemption entries (default: all)'
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
'--keywords',
|
|
16
21
|
action='store_true',
|
|
17
|
-
help='Clear
|
|
22
|
+
help='Clear learned dynamic keywords (default: all)'
|
|
18
23
|
)
|
|
19
24
|
parser.add_argument(
|
|
20
25
|
'--confirm',
|
|
21
26
|
action='store_true',
|
|
22
27
|
help='Skip confirmation prompt'
|
|
23
28
|
)
|
|
29
|
+
|
|
30
|
+
# Legacy flags for backward compatibility
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
'--blacklist-only',
|
|
33
|
+
action='store_true',
|
|
34
|
+
help='(Legacy) Clear only blacklist entries'
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
'--exemptions-only',
|
|
38
|
+
action='store_true',
|
|
39
|
+
help='(Legacy) Clear only exemption entries'
|
|
40
|
+
)
|
|
24
41
|
|
|
25
42
|
def handle(self, *args, **options):
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
# Parse arguments
|
|
44
|
+
blacklist_flag = options.get('blacklist', False)
|
|
45
|
+
exemptions_flag = options.get('exemptions', False)
|
|
46
|
+
keywords_flag = options.get('keywords', False)
|
|
47
|
+
confirm = options.get('confirm', False)
|
|
48
|
+
|
|
49
|
+
# Legacy support
|
|
50
|
+
blacklist_only = options.get('blacklist_only', False)
|
|
51
|
+
exemptions_only = options.get('exemptions_only', False)
|
|
52
|
+
|
|
53
|
+
# Handle legacy flags
|
|
54
|
+
if blacklist_only:
|
|
55
|
+
blacklist_flag = True
|
|
56
|
+
exemptions_flag = False
|
|
57
|
+
keywords_flag = False
|
|
58
|
+
elif exemptions_only:
|
|
59
|
+
blacklist_flag = False
|
|
60
|
+
exemptions_flag = True
|
|
61
|
+
keywords_flag = False
|
|
62
|
+
|
|
63
|
+
# If no specific flags, clear everything
|
|
64
|
+
if not (blacklist_flag or exemptions_flag or keywords_flag):
|
|
65
|
+
blacklist_flag = exemptions_flag = keywords_flag = True
|
|
29
66
|
|
|
30
67
|
try:
|
|
31
68
|
blacklist_store = get_blacklist_store()
|
|
32
69
|
exemption_store = get_exemption_store()
|
|
70
|
+
keyword_store = get_keyword_store()
|
|
33
71
|
except Exception as e:
|
|
34
72
|
self.stdout.write(self.style.ERROR(f'Error initializing stores: {e}'))
|
|
35
73
|
return
|
|
36
74
|
|
|
37
75
|
# Count current entries safely
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
blacklist_count = len(blacklist_entries)
|
|
41
|
-
except Exception as e:
|
|
42
|
-
self.stdout.write(self.style.WARNING(f'Warning: Could not count blacklist entries: {e}'))
|
|
43
|
-
blacklist_count = 0
|
|
44
|
-
blacklist_entries = []
|
|
76
|
+
counts = {'blacklist': 0, 'exemptions': 0, 'keywords': 0}
|
|
77
|
+
entries = {'blacklist': [], 'exemptions': [], 'keywords': []}
|
|
45
78
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
exemption_entries = []
|
|
79
|
+
if blacklist_flag:
|
|
80
|
+
try:
|
|
81
|
+
entries['blacklist'] = blacklist_store.get_all()
|
|
82
|
+
counts['blacklist'] = len(entries['blacklist'])
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.stdout.write(self.style.WARNING(f'Warning: Could not count blacklist entries: {e}'))
|
|
53
85
|
|
|
54
|
-
if
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
if exemptions_flag:
|
|
87
|
+
try:
|
|
88
|
+
entries['exemptions'] = exemption_store.get_all()
|
|
89
|
+
counts['exemptions'] = len(entries['exemptions'])
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.stdout.write(self.style.WARNING(f'Warning: Could not count exemption entries: {e}'))
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
if keywords_flag:
|
|
94
|
+
try:
|
|
95
|
+
entries['keywords'] = keyword_store.get_all_keywords()
|
|
96
|
+
counts['keywords'] = len(entries['keywords'])
|
|
97
|
+
except Exception as e:
|
|
98
|
+
self.stdout.write(self.style.WARNING(f'Warning: Could not count keyword entries: {e}'))
|
|
99
|
+
|
|
100
|
+
# Build action description
|
|
101
|
+
actions = []
|
|
102
|
+
if blacklist_flag:
|
|
103
|
+
actions.append(f"{counts['blacklist']} blacklist entries")
|
|
104
|
+
if exemptions_flag:
|
|
105
|
+
actions.append(f"{counts['exemptions']} exemption entries")
|
|
106
|
+
if keywords_flag:
|
|
107
|
+
actions.append(f"{counts['keywords']} learned keywords")
|
|
108
|
+
|
|
109
|
+
action = "Clear " + ", ".join(actions)
|
|
71
110
|
|
|
72
111
|
# Show what will be cleared
|
|
73
|
-
self.stdout.write(f"AI-WAF Reset: {action}")
|
|
112
|
+
self.stdout.write(f"🔧 AI-WAF Reset: {action}")
|
|
74
113
|
|
|
75
114
|
if not confirm:
|
|
76
115
|
try:
|
|
@@ -83,12 +122,12 @@ class Command(BaseCommand):
|
|
|
83
122
|
return
|
|
84
123
|
|
|
85
124
|
# Perform the reset
|
|
86
|
-
deleted_counts = {'blacklist': 0, 'exemptions': 0, 'errors': []}
|
|
125
|
+
deleted_counts = {'blacklist': 0, 'exemptions': 0, 'keywords': 0, 'errors': []}
|
|
87
126
|
|
|
88
|
-
if
|
|
127
|
+
if blacklist_flag:
|
|
89
128
|
# Clear blacklist entries
|
|
90
129
|
try:
|
|
91
|
-
for entry in
|
|
130
|
+
for entry in entries['blacklist']:
|
|
92
131
|
try:
|
|
93
132
|
blacklist_store.remove_ip(entry['ip_address'])
|
|
94
133
|
deleted_counts['blacklist'] += 1
|
|
@@ -97,10 +136,10 @@ class Command(BaseCommand):
|
|
|
97
136
|
except Exception as e:
|
|
98
137
|
deleted_counts['errors'].append(f"Error clearing blacklist: {e}")
|
|
99
138
|
|
|
100
|
-
if
|
|
139
|
+
if exemptions_flag:
|
|
101
140
|
# Clear exemption entries
|
|
102
141
|
try:
|
|
103
|
-
for entry in
|
|
142
|
+
for entry in entries['exemptions']:
|
|
104
143
|
try:
|
|
105
144
|
exemption_store.remove_ip(entry['ip_address'])
|
|
106
145
|
deleted_counts['exemptions'] += 1
|
|
@@ -109,26 +148,34 @@ class Command(BaseCommand):
|
|
|
109
148
|
except Exception as e:
|
|
110
149
|
deleted_counts['errors'].append(f"Error clearing exemptions: {e}")
|
|
111
150
|
|
|
151
|
+
if keywords_flag:
|
|
152
|
+
# Clear keyword entries
|
|
153
|
+
try:
|
|
154
|
+
for keyword in entries['keywords']:
|
|
155
|
+
try:
|
|
156
|
+
keyword_store.remove_keyword(keyword)
|
|
157
|
+
deleted_counts['keywords'] += 1
|
|
158
|
+
except Exception as e:
|
|
159
|
+
deleted_counts['errors'].append(f"Error removing keyword '{keyword}': {e}")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
deleted_counts['errors'].append(f"Error clearing keywords: {e}")
|
|
162
|
+
|
|
112
163
|
# Report results
|
|
113
164
|
if deleted_counts['errors']:
|
|
114
165
|
for error in deleted_counts['errors']:
|
|
115
166
|
self.stdout.write(self.style.WARNING(f"⚠️ {error}"))
|
|
116
167
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
elif clear_exemptions:
|
|
129
|
-
self.stdout.write(
|
|
130
|
-
self.style.SUCCESS(f"✅ Exemptions cleared: Deleted {deleted_counts['exemptions']} entries")
|
|
131
|
-
)
|
|
168
|
+
# Build success message
|
|
169
|
+
success_parts = []
|
|
170
|
+
if blacklist_flag:
|
|
171
|
+
success_parts.append(f"{deleted_counts['blacklist']} blacklist entries")
|
|
172
|
+
if exemptions_flag:
|
|
173
|
+
success_parts.append(f"{deleted_counts['exemptions']} exemption entries")
|
|
174
|
+
if keywords_flag:
|
|
175
|
+
success_parts.append(f"{deleted_counts['keywords']} learned keywords")
|
|
176
|
+
|
|
177
|
+
success_message = "✅ Reset complete: Deleted " + ", ".join(success_parts)
|
|
178
|
+
self.stdout.write(self.style.SUCCESS(success_message))
|
|
132
179
|
|
|
133
180
|
if deleted_counts['errors']:
|
|
134
181
|
self.stdout.write(
|
aiwaf/middleware.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import time
|
|
4
4
|
import re
|
|
5
5
|
import os
|
|
6
|
+
import warnings
|
|
6
7
|
import numpy as np
|
|
7
8
|
import joblib
|
|
8
9
|
from django.db.models import UUIDField
|
|
@@ -82,6 +83,93 @@ class IPAndKeywordBlockMiddleware:
|
|
|
82
83
|
def __init__(self, get_response):
|
|
83
84
|
self.get_response = get_response
|
|
84
85
|
self.safe_prefixes = self._collect_safe_prefixes()
|
|
86
|
+
self.exempt_keywords = self._get_exempt_keywords()
|
|
87
|
+
self.legitimate_path_keywords = self._get_legitimate_path_keywords()
|
|
88
|
+
|
|
89
|
+
def _get_exempt_keywords(self):
|
|
90
|
+
"""Get keywords that should be exempt from blocking"""
|
|
91
|
+
exempt_tokens = set()
|
|
92
|
+
|
|
93
|
+
# Extract from exempt paths
|
|
94
|
+
for path in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
|
|
95
|
+
for seg in re.split(r"\W+", path.strip("/").lower()):
|
|
96
|
+
if len(seg) > 3:
|
|
97
|
+
exempt_tokens.add(seg)
|
|
98
|
+
|
|
99
|
+
# Add explicit exempt keywords from settings
|
|
100
|
+
exempt_keywords = getattr(settings, "AIWAF_EXEMPT_KEYWORDS", [])
|
|
101
|
+
exempt_tokens.update(exempt_keywords)
|
|
102
|
+
|
|
103
|
+
return exempt_tokens
|
|
104
|
+
|
|
105
|
+
def _get_legitimate_path_keywords(self):
|
|
106
|
+
"""Get keywords that are legitimate in URL paths"""
|
|
107
|
+
# Extract from Django URL patterns
|
|
108
|
+
legitimate_keywords = set()
|
|
109
|
+
|
|
110
|
+
# Add common legitimate path segments
|
|
111
|
+
default_legitimate = {
|
|
112
|
+
"profile", "user", "account", "settings", "dashboard",
|
|
113
|
+
"home", "about", "contact", "help", "search", "list",
|
|
114
|
+
"view", "edit", "create", "update", "delete", "detail",
|
|
115
|
+
"api", "auth", "login", "logout", "register", "signup",
|
|
116
|
+
"reset", "confirm", "activate", "verify", "page",
|
|
117
|
+
"category", "tag", "post", "article", "blog", "news"
|
|
118
|
+
}
|
|
119
|
+
legitimate_keywords.update(default_legitimate)
|
|
120
|
+
|
|
121
|
+
# Add from Django settings
|
|
122
|
+
allowed_path_keywords = getattr(settings, "AIWAF_ALLOWED_PATH_KEYWORDS", [])
|
|
123
|
+
legitimate_keywords.update(allowed_path_keywords)
|
|
124
|
+
|
|
125
|
+
# Extract from actual Django URL patterns
|
|
126
|
+
resolver = get_resolver()
|
|
127
|
+
self._extract_path_keywords_from_urls(resolver.url_patterns, legitimate_keywords)
|
|
128
|
+
|
|
129
|
+
return legitimate_keywords
|
|
130
|
+
|
|
131
|
+
def _extract_path_keywords_from_urls(self, url_patterns, keywords, prefix=""):
|
|
132
|
+
"""Extract legitimate keywords from Django URL patterns"""
|
|
133
|
+
for pattern in url_patterns:
|
|
134
|
+
if hasattr(pattern, 'url_patterns'): # include()
|
|
135
|
+
new_prefix = prefix + str(pattern.pattern).strip('^$/')
|
|
136
|
+
self._extract_path_keywords_from_urls(pattern.url_patterns, keywords, new_prefix)
|
|
137
|
+
else:
|
|
138
|
+
# Extract static path segments from URL pattern
|
|
139
|
+
pattern_str = str(pattern.pattern).strip('^$/')
|
|
140
|
+
full_path = (prefix + '/' + pattern_str).strip('/')
|
|
141
|
+
|
|
142
|
+
# Extract meaningful segments (not regex patterns)
|
|
143
|
+
segments = re.findall(r'[a-zA-Z]{3,}', full_path)
|
|
144
|
+
for seg in segments:
|
|
145
|
+
if seg.lower() not in {'http', 'https', 'www'}:
|
|
146
|
+
keywords.add(seg.lower())
|
|
147
|
+
|
|
148
|
+
def _is_malicious_context(self, request, segment):
|
|
149
|
+
"""Determine if a keyword appears in a malicious context"""
|
|
150
|
+
path = request.path.lower()
|
|
151
|
+
|
|
152
|
+
# Check if this is a query parameter attack
|
|
153
|
+
query_string = request.META.get('QUERY_STRING', '').lower()
|
|
154
|
+
if segment in query_string and any(attack_pattern in query_string for attack_pattern in [
|
|
155
|
+
'union', 'select', 'drop', 'insert', 'script', 'alert', 'eval'
|
|
156
|
+
]):
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
# Check if this looks like a file extension attack
|
|
160
|
+
if segment.startswith('.') and not path_exists_in_django(request.path):
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
# Check if this looks like a directory traversal
|
|
164
|
+
if '../' in path or '..\\' in path:
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
# Check if accessing non-existent paths with suspicious extensions
|
|
168
|
+
if (not path_exists_in_django(request.path) and
|
|
169
|
+
any(ext in segment for ext in ['.php', '.asp', '.jsp', '.cgi'])):
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
return False
|
|
85
173
|
|
|
86
174
|
def _collect_safe_prefixes(self):
|
|
87
175
|
resolver = get_resolver()
|
|
@@ -102,35 +190,68 @@ class IPAndKeywordBlockMiddleware:
|
|
|
102
190
|
return prefixes
|
|
103
191
|
|
|
104
192
|
def __call__(self, request):
|
|
105
|
-
|
|
193
|
+
# First exemption check - early exit for exempt requests
|
|
106
194
|
if is_exempt(request):
|
|
107
195
|
return self.get_response(request)
|
|
196
|
+
|
|
197
|
+
raw_path = request.path.lower()
|
|
108
198
|
ip = get_ip(request)
|
|
109
199
|
path = raw_path.lstrip("/")
|
|
110
200
|
|
|
111
|
-
#
|
|
201
|
+
# Additional IP-level exemption check
|
|
202
|
+
from .storage import get_exemption_store
|
|
203
|
+
exemption_store = get_exemption_store()
|
|
204
|
+
if exemption_store.is_exempted(ip):
|
|
205
|
+
return self.get_response(request)
|
|
206
|
+
|
|
207
|
+
# BlacklistManager handles exemption checking internally
|
|
112
208
|
if BlacklistManager.is_blocked(ip):
|
|
113
209
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
114
210
|
|
|
211
|
+
# Check if path exists in Django - if yes, be more lenient
|
|
212
|
+
path_exists = path_exists_in_django(request.path)
|
|
213
|
+
|
|
115
214
|
keyword_store = get_keyword_store()
|
|
116
215
|
segments = [seg for seg in re.split(r"\W+", path) if len(seg) > 3]
|
|
117
216
|
|
|
217
|
+
# Only learn keywords from non-existent paths or suspicious contexts
|
|
118
218
|
for seg in segments:
|
|
119
|
-
|
|
219
|
+
if not path_exists or self._is_malicious_context(request, seg):
|
|
220
|
+
keyword_store.add_keyword(seg)
|
|
120
221
|
|
|
121
222
|
dynamic_top = keyword_store.get_top_keywords(getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10))
|
|
122
223
|
all_kw = set(STATIC_KW) | set(dynamic_top)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
224
|
+
|
|
225
|
+
# Enhanced filtering logic
|
|
226
|
+
suspicious_kw = set()
|
|
227
|
+
for kw in all_kw:
|
|
228
|
+
# Skip if keyword is explicitly exempted
|
|
229
|
+
if kw in self.exempt_keywords:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
# Skip if this is a legitimate path keyword and path exists in Django
|
|
233
|
+
if (kw in self.legitimate_path_keywords and
|
|
234
|
+
path_exists and
|
|
235
|
+
not self._is_malicious_context(request, kw)):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Skip if path starts with safe prefix
|
|
239
|
+
if any(path.startswith(prefix) for prefix in self.safe_prefixes if prefix):
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
suspicious_kw.add(kw)
|
|
243
|
+
|
|
244
|
+
# Check segments against suspicious keywords
|
|
127
245
|
for seg in segments:
|
|
128
246
|
if seg in suspicious_kw:
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
247
|
+
# Additional context check before blocking
|
|
248
|
+
if self._is_malicious_context(request, seg) or not path_exists:
|
|
249
|
+
# Double-check exemption before blocking
|
|
250
|
+
if not exemption_store.is_exempted(ip):
|
|
251
|
+
BlacklistManager.block(ip, f"Keyword block: {seg} (context: malicious)")
|
|
252
|
+
# Check again after blocking attempt (exempted IPs won't be blocked)
|
|
253
|
+
if BlacklistManager.is_blocked(ip):
|
|
254
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
134
255
|
return self.get_response(request)
|
|
135
256
|
|
|
136
257
|
|
|
@@ -143,22 +264,32 @@ class RateLimitMiddleware:
|
|
|
143
264
|
self.FLOOD = getattr(settings, "AIWAF_RATE_FLOOD", 40) # hard limit
|
|
144
265
|
|
|
145
266
|
def __call__(self, request):
|
|
267
|
+
# First exemption check - early exit for exempt requests
|
|
146
268
|
if is_exempt(request):
|
|
147
269
|
return self.get_response(request)
|
|
148
270
|
|
|
149
271
|
ip = get_ip(request)
|
|
272
|
+
|
|
273
|
+
# Additional IP-level exemption check
|
|
274
|
+
from .storage import get_exemption_store
|
|
275
|
+
exemption_store = get_exemption_store()
|
|
276
|
+
if exemption_store.is_exempted(ip):
|
|
277
|
+
return self.get_response(request)
|
|
278
|
+
|
|
150
279
|
key = f"ratelimit:{ip}"
|
|
151
280
|
now = time.time()
|
|
152
281
|
timestamps = cache.get(key, [])
|
|
153
282
|
timestamps = [t for t in timestamps if now - t < self.WINDOW]
|
|
154
283
|
timestamps.append(now)
|
|
155
284
|
cache.set(key, timestamps, timeout=self.WINDOW)
|
|
285
|
+
|
|
156
286
|
if len(timestamps) > self.FLOOD:
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
287
|
+
# Double-check exemption before blocking
|
|
288
|
+
if not exemption_store.is_exempted(ip):
|
|
289
|
+
BlacklistManager.block(ip, "Flood pattern")
|
|
290
|
+
# Check if actually blocked (exempted IPs won't be blocked)
|
|
291
|
+
if BlacklistManager.is_blocked(ip):
|
|
292
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
162
293
|
if len(timestamps) > self.MAX:
|
|
163
294
|
return JsonResponse({"error": "too_many_requests"}, status=429)
|
|
164
295
|
return self.get_response(request)
|
|
@@ -174,19 +305,37 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
174
305
|
self.model = MODEL
|
|
175
306
|
|
|
176
307
|
def process_request(self, request):
|
|
308
|
+
# First exemption check - early exit for exempt requests
|
|
177
309
|
if is_exempt(request):
|
|
178
310
|
return None
|
|
311
|
+
|
|
179
312
|
request._start_time = time.time()
|
|
180
313
|
ip = get_ip(request)
|
|
181
|
-
|
|
314
|
+
|
|
315
|
+
# Additional IP-level exemption check
|
|
316
|
+
from .storage import get_exemption_store
|
|
317
|
+
exemption_store = get_exemption_store()
|
|
318
|
+
if exemption_store.is_exempted(ip):
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
# BlacklistManager handles exemption checking internally
|
|
182
322
|
if BlacklistManager.is_blocked(ip):
|
|
183
323
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
184
324
|
return None
|
|
185
325
|
|
|
186
326
|
def process_response(self, request, response):
|
|
327
|
+
# First exemption check - early exit for exempt requests
|
|
187
328
|
if is_exempt(request):
|
|
188
329
|
return response
|
|
330
|
+
|
|
189
331
|
ip = get_ip(request)
|
|
332
|
+
|
|
333
|
+
# Additional IP-level exemption check
|
|
334
|
+
from .storage import get_exemption_store
|
|
335
|
+
exemption_store = get_exemption_store()
|
|
336
|
+
if exemption_store.is_exempted(ip):
|
|
337
|
+
return response
|
|
338
|
+
|
|
190
339
|
now = time.time()
|
|
191
340
|
key = f"aiwaf:{ip}"
|
|
192
341
|
data = cache.get(key, [])
|
|
@@ -251,17 +400,20 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
251
400
|
# Anomalous but looks legitimate - don't block
|
|
252
401
|
pass
|
|
253
402
|
else:
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
403
|
+
# Double-check exemption before blocking
|
|
404
|
+
if not exemption_store.is_exempted(ip):
|
|
405
|
+
BlacklistManager.block(ip, f"AI anomaly + suspicious patterns (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})")
|
|
406
|
+
# Check if actually blocked (exempted IPs won't be blocked)
|
|
407
|
+
if BlacklistManager.is_blocked(ip):
|
|
408
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
259
409
|
else:
|
|
260
410
|
# No recent data to analyze - be more conservative, only block on very suspicious current request
|
|
261
411
|
if kw_hits >= 2 or status_idx == STATUS_IDX.index("404"):
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
|
|
412
|
+
# Double-check exemption before blocking
|
|
413
|
+
if not exemption_store.is_exempted(ip):
|
|
414
|
+
BlacklistManager.block(ip, "AI anomaly + immediate suspicious behavior")
|
|
415
|
+
if BlacklistManager.is_blocked(ip):
|
|
416
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
265
417
|
|
|
266
418
|
data.append((now, request.path, response.status_code, resp_time))
|
|
267
419
|
data = [d for d in data if now - d[0] < self.WINDOW]
|
|
@@ -283,7 +435,12 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
283
435
|
return None
|
|
284
436
|
|
|
285
437
|
ip = get_ip(request)
|
|
286
|
-
|
|
438
|
+
|
|
439
|
+
# Additional IP-level exemption check
|
|
440
|
+
from .storage import get_exemption_store
|
|
441
|
+
exemption_store = get_exemption_store()
|
|
442
|
+
if exemption_store.is_exempted(ip):
|
|
443
|
+
return None
|
|
287
444
|
|
|
288
445
|
if request.method == "GET":
|
|
289
446
|
# Store timestamp for this IP's GET request
|
|
@@ -300,11 +457,12 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
300
457
|
if not any(request.path.lower().startswith(login_path) for login_path in [
|
|
301
458
|
"/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
|
|
302
459
|
]):
|
|
303
|
-
#
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
460
|
+
# Double-check exemption before blocking
|
|
461
|
+
if not exemption_store.is_exempted(ip):
|
|
462
|
+
BlacklistManager.block(ip, "Direct POST without GET")
|
|
463
|
+
# Check if actually blocked (exempted IPs won't be blocked)
|
|
464
|
+
if BlacklistManager.is_blocked(ip):
|
|
465
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
308
466
|
else:
|
|
309
467
|
# Check timing - be more lenient for login paths
|
|
310
468
|
time_diff = time.time() - get_time
|
|
@@ -317,11 +475,12 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
317
475
|
min_time = 0.1 # Very short threshold for login forms
|
|
318
476
|
|
|
319
477
|
if time_diff < min_time:
|
|
320
|
-
#
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
478
|
+
# Double-check exemption before blocking
|
|
479
|
+
if not exemption_store.is_exempted(ip):
|
|
480
|
+
BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
|
|
481
|
+
# Check if actually blocked (exempted IPs won't be blocked)
|
|
482
|
+
if BlacklistManager.is_blocked(ip):
|
|
483
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
325
484
|
|
|
326
485
|
return None
|
|
327
486
|
|
|
@@ -330,11 +489,19 @@ class UUIDTamperMiddleware(MiddlewareMixin):
|
|
|
330
489
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
331
490
|
if is_exempt(request):
|
|
332
491
|
return None
|
|
492
|
+
|
|
333
493
|
uid = view_kwargs.get("uuid")
|
|
334
494
|
if not uid:
|
|
335
495
|
return None
|
|
336
496
|
|
|
337
497
|
ip = get_ip(request)
|
|
498
|
+
|
|
499
|
+
# Additional IP-level exemption check
|
|
500
|
+
from .storage import get_exemption_store
|
|
501
|
+
exemption_store = get_exemption_store()
|
|
502
|
+
if exemption_store.is_exempted(ip):
|
|
503
|
+
return None
|
|
504
|
+
|
|
338
505
|
app_label = view_func.__module__.split(".")[0]
|
|
339
506
|
app_cfg = apps.get_app_config(app_label)
|
|
340
507
|
for Model in app_cfg.get_models():
|
|
@@ -345,8 +512,9 @@ class UUIDTamperMiddleware(MiddlewareMixin):
|
|
|
345
512
|
except (ValueError, TypeError):
|
|
346
513
|
continue
|
|
347
514
|
|
|
348
|
-
#
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
515
|
+
# Double-check exemption before blocking
|
|
516
|
+
if not exemption_store.is_exempted(ip):
|
|
517
|
+
BlacklistManager.block(ip, "UUID tampering")
|
|
518
|
+
# Check if actually blocked (exempted IPs won't be blocked)
|
|
519
|
+
if BlacklistManager.is_blocked(ip):
|
|
520
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
aiwaf/storage.py
CHANGED
|
@@ -195,6 +195,16 @@ class ModelExemptionStore:
|
|
|
195
195
|
except Exception as e:
|
|
196
196
|
print(f"Error removing exemption for IP {ip}: {e}")
|
|
197
197
|
|
|
198
|
+
@staticmethod
|
|
199
|
+
def remove_ip(ip):
|
|
200
|
+
"""Remove IP from exemption list (alias for remove_exemption)"""
|
|
201
|
+
ModelExemptionStore.remove_exemption(ip)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def add_ip(ip, reason="Manual exemption"):
|
|
205
|
+
"""Add IP to exemption list (alias for add_exemption)"""
|
|
206
|
+
ModelExemptionStore.add_exemption(ip, reason)
|
|
207
|
+
|
|
198
208
|
@staticmethod
|
|
199
209
|
def get_all_exempted_ips():
|
|
200
210
|
"""Get all exempted IPs"""
|
|
@@ -274,6 +284,19 @@ class ModelKeywordStore:
|
|
|
274
284
|
except Exception:
|
|
275
285
|
return []
|
|
276
286
|
|
|
287
|
+
@staticmethod
|
|
288
|
+
def get_all_keywords():
|
|
289
|
+
"""Get all keywords"""
|
|
290
|
+
_import_models()
|
|
291
|
+
if DynamicKeyword is None:
|
|
292
|
+
return []
|
|
293
|
+
try:
|
|
294
|
+
return list(
|
|
295
|
+
DynamicKeyword.objects.all().values_list('keyword', flat=True)
|
|
296
|
+
)
|
|
297
|
+
except Exception:
|
|
298
|
+
return []
|
|
299
|
+
|
|
277
300
|
@staticmethod
|
|
278
301
|
def reset_keywords():
|
|
279
302
|
"""Reset all keyword counts"""
|
aiwaf/trainer.py
CHANGED
|
@@ -51,17 +51,59 @@ def path_exists_in_django(path: str) -> bool:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def remove_exempt_keywords() -> None:
|
|
54
|
+
"""Remove exempt keywords from dynamic keyword storage"""
|
|
54
55
|
keyword_store = get_keyword_store()
|
|
55
56
|
exempt_tokens = set()
|
|
56
57
|
|
|
58
|
+
# Extract tokens from exempt paths
|
|
57
59
|
for path in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
|
|
58
60
|
for seg in re.split(r"\W+", path.strip("/").lower()):
|
|
59
61
|
if len(seg) > 3:
|
|
60
62
|
exempt_tokens.add(seg)
|
|
61
63
|
|
|
64
|
+
# Add explicit exempt keywords from settings
|
|
65
|
+
explicit_exempt = getattr(settings, "AIWAF_EXEMPT_KEYWORDS", [])
|
|
66
|
+
exempt_tokens.update(explicit_exempt)
|
|
67
|
+
|
|
68
|
+
# Add legitimate path keywords to prevent them from being learned as suspicious
|
|
69
|
+
allowed_path_keywords = getattr(settings, "AIWAF_ALLOWED_PATH_KEYWORDS", [])
|
|
70
|
+
exempt_tokens.update(allowed_path_keywords)
|
|
71
|
+
|
|
62
72
|
# Remove exempt tokens from keyword storage
|
|
63
73
|
for token in exempt_tokens:
|
|
64
74
|
keyword_store.remove_keyword(token)
|
|
75
|
+
|
|
76
|
+
if exempt_tokens:
|
|
77
|
+
print(f"🧹 Removed {len(exempt_tokens)} exempt keywords from learning: {list(exempt_tokens)[:10]}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_legitimate_keywords() -> set:
|
|
81
|
+
"""Get all legitimate keywords that shouldn't be learned as suspicious"""
|
|
82
|
+
legitimate = set()
|
|
83
|
+
|
|
84
|
+
# Common legitimate path segments
|
|
85
|
+
default_legitimate = {
|
|
86
|
+
"profile", "user", "users", "account", "accounts", "settings", "dashboard",
|
|
87
|
+
"home", "about", "contact", "help", "search", "list", "lists",
|
|
88
|
+
"view", "views", "edit", "create", "update", "delete", "detail", "details",
|
|
89
|
+
"api", "auth", "login", "logout", "register", "signup", "signin",
|
|
90
|
+
"reset", "confirm", "activate", "verify", "page", "pages",
|
|
91
|
+
"category", "categories", "tag", "tags", "post", "posts",
|
|
92
|
+
"article", "articles", "blog", "blogs", "news", "item", "items",
|
|
93
|
+
"admin", "administration", "manage", "manager", "control", "panel",
|
|
94
|
+
"config", "configuration", "option", "options", "preference", "preferences"
|
|
95
|
+
}
|
|
96
|
+
legitimate.update(default_legitimate)
|
|
97
|
+
|
|
98
|
+
# Add from Django settings
|
|
99
|
+
allowed_path_keywords = getattr(settings, "AIWAF_ALLOWED_PATH_KEYWORDS", [])
|
|
100
|
+
legitimate.update(allowed_path_keywords)
|
|
101
|
+
|
|
102
|
+
# Add exempt keywords
|
|
103
|
+
exempt_keywords = getattr(settings, "AIWAF_EXEMPT_KEYWORDS", [])
|
|
104
|
+
legitimate.update(exempt_keywords)
|
|
105
|
+
|
|
106
|
+
return legitimate
|
|
65
107
|
|
|
66
108
|
|
|
67
109
|
def _read_all_logs() -> list[str]:
|
|
@@ -137,14 +179,20 @@ def _parse(line: str) -> dict | None:
|
|
|
137
179
|
|
|
138
180
|
|
|
139
181
|
def train() -> None:
|
|
182
|
+
"""Enhanced training with improved keyword filtering and exemption handling"""
|
|
183
|
+
print("🚀 Starting AIWAF enhanced training...")
|
|
184
|
+
|
|
185
|
+
# Remove exempt keywords first
|
|
140
186
|
remove_exempt_keywords()
|
|
141
187
|
|
|
142
188
|
# Remove any IPs in IPExemption from the blacklist using BlacklistManager
|
|
143
189
|
exemption_store = get_exemption_store()
|
|
144
190
|
|
|
145
191
|
exempted_ips = [entry['ip_address'] for entry in exemption_store.get_all()]
|
|
146
|
-
|
|
147
|
-
|
|
192
|
+
if exempted_ips:
|
|
193
|
+
print(f"🛡️ Found {len(exempted_ips)} exempted IPs - clearing from blacklist")
|
|
194
|
+
for ip in exempted_ips:
|
|
195
|
+
BlacklistManager.unblock(ip)
|
|
148
196
|
|
|
149
197
|
raw_lines = _read_all_logs()
|
|
150
198
|
if not raw_lines:
|
|
@@ -281,17 +329,50 @@ def train() -> None:
|
|
|
281
329
|
print(f" → Blocked {blocked_count}/{len(anomalous_ips)} anomalous IPs (others looked legitimate)")
|
|
282
330
|
|
|
283
331
|
tokens = Counter()
|
|
332
|
+
legitimate_keywords = get_legitimate_keywords()
|
|
333
|
+
|
|
334
|
+
print(f"🔍 Learning keywords from {len(parsed)} parsed requests...")
|
|
335
|
+
|
|
284
336
|
for r in parsed:
|
|
285
|
-
|
|
286
|
-
|
|
337
|
+
# Only learn from suspicious requests (errors on non-existent paths)
|
|
338
|
+
if (r["status"].startswith(("4", "5")) and
|
|
339
|
+
not path_exists_in_django(r["path"]) and
|
|
340
|
+
not is_exempt_path(r["path"])):
|
|
341
|
+
|
|
287
342
|
for seg in re.split(r"\W+", r["path"].lower()):
|
|
288
|
-
if len(seg) > 3 and
|
|
343
|
+
if (len(seg) > 3 and
|
|
344
|
+
seg not in STATIC_KW and
|
|
345
|
+
seg not in legitimate_keywords): # Don't learn legitimate keywords
|
|
289
346
|
tokens[seg] += 1
|
|
290
347
|
|
|
291
348
|
keyword_store = get_keyword_store()
|
|
292
|
-
top_tokens = tokens.most_common(10)
|
|
349
|
+
top_tokens = tokens.most_common(getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10))
|
|
293
350
|
|
|
351
|
+
# Additional filtering: only add keywords that appear suspicious enough
|
|
352
|
+
filtered_tokens = []
|
|
294
353
|
for kw, cnt in top_tokens:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
354
|
+
# Don't add keywords that might be legitimate
|
|
355
|
+
if (cnt >= 2 and # Must appear at least twice
|
|
356
|
+
len(kw) >= 4 and # Must be at least 4 characters
|
|
357
|
+
kw not in legitimate_keywords): # Not in legitimate set
|
|
358
|
+
filtered_tokens.append((kw, cnt))
|
|
359
|
+
keyword_store.add_keyword(kw, cnt)
|
|
360
|
+
|
|
361
|
+
if filtered_tokens:
|
|
362
|
+
print(f"📝 Added {len(filtered_tokens)} suspicious keywords: {[kw for kw, _ in filtered_tokens]}")
|
|
363
|
+
else:
|
|
364
|
+
print("✅ No new suspicious keywords learned (good sign!)")
|
|
365
|
+
|
|
366
|
+
print(f"🎯 Dynamic keyword learning complete. Excluded {len(legitimate_keywords)} legitimate keywords.")
|
|
367
|
+
|
|
368
|
+
# Training summary
|
|
369
|
+
print("\n" + "="*60)
|
|
370
|
+
print("🎉 AIWAF ENHANCED TRAINING COMPLETE")
|
|
371
|
+
print("="*60)
|
|
372
|
+
print(f"📊 Training Data: {len(parsed)} log entries processed")
|
|
373
|
+
print(f"🤖 AI Model: Trained with {len(feature_cols)} features")
|
|
374
|
+
print(f"🚫 Blocked IPs: {blocked_count if 'blocked_count' in locals() else 0} suspicious IPs blocked")
|
|
375
|
+
print(f"🔑 Keywords: {len(filtered_tokens)} new suspicious keywords learned")
|
|
376
|
+
print(f"🛡️ Exemptions: {len(exempted_ips)} IPs protected from blocking")
|
|
377
|
+
print(f"✅ Enhanced protection now active with context-aware filtering!")
|
|
378
|
+
print("="*60)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.9.
|
|
3
|
+
Version: 0.1.9.2.0
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -25,7 +25,13 @@ Dynamic: requires-python
|
|
|
25
25
|
# AI‑WAF
|
|
26
26
|
|
|
27
27
|
> A self‑learning, Django‑friendly Web Application Firewall
|
|
28
|
-
> with rate‑limiting, anomaly detection, honeypots, UUID‑tamper protection,
|
|
28
|
+
> with **enhanced context-aware protection**, rate‑limiting, anomaly detection, honeypots, UUID‑tamper protection, **smart keyword learning**, file‑extension probing detection, exempt path awareness, and daily retraining.
|
|
29
|
+
|
|
30
|
+
**🆕 Latest Enhancements:**
|
|
31
|
+
- ✅ **Smart Keyword Filtering** - Prevents blocking legitimate pages like `/profile/`
|
|
32
|
+
- ✅ **Granular Reset Commands** - Clear specific data types (`--blacklist`, `--keywords`, `--exemptions`)
|
|
33
|
+
- ✅ **Context-Aware Learning** - Only learns from suspicious requests, not legitimate site functionality
|
|
34
|
+
- ✅ **Enhanced Configuration** - `AIWAF_ALLOWED_PATH_KEYWORDS` and `AIWAF_EXEMPT_KEYWORDS`
|
|
29
35
|
|
|
30
36
|
---
|
|
31
37
|
|
|
@@ -88,9 +94,14 @@ aiwaf/
|
|
|
88
94
|
- Burst count
|
|
89
95
|
- Total 404s
|
|
90
96
|
|
|
91
|
-
- **Dynamic Keyword
|
|
92
|
-
-
|
|
93
|
-
- **
|
|
97
|
+
- **Enhanced Dynamic Keyword Learning**
|
|
98
|
+
- **Smart Context-Aware Learning**: Only learns keywords from suspicious requests on non-existent paths
|
|
99
|
+
- **Legitimate Path Protection**: Automatically excludes keywords from valid Django URLs (like `/profile/`, `/admin/`)
|
|
100
|
+
- **Configuration Options**:
|
|
101
|
+
- `AIWAF_ALLOWED_PATH_KEYWORDS` - Explicitly allow certain keywords in legitimate paths
|
|
102
|
+
- `AIWAF_EXEMPT_KEYWORDS` - Keywords that should never trigger blocking
|
|
103
|
+
- **Automatic Cleanup**: Keywords from `AIWAF_EXEMPT_PATHS` are automatically removed from the database
|
|
104
|
+
- **False Positive Prevention**: Stops learning legitimate site functionality as "malicious"
|
|
94
105
|
|
|
95
106
|
- **File‑Extension Probing Detection**
|
|
96
107
|
Tracks repeated 404s on common extensions (e.g. `.php`, `.asp`) and blocks IPs.
|
|
@@ -196,20 +207,44 @@ python manage.py add_ipexemption <ip-address> --reason "optional reason"
|
|
|
196
207
|
|
|
197
208
|
### Resetting AI-WAF
|
|
198
209
|
|
|
199
|
-
|
|
210
|
+
The `aiwaf_reset` command provides **granular control** for clearing different types of data:
|
|
200
211
|
|
|
201
212
|
```bash
|
|
202
|
-
# Clear everything (
|
|
213
|
+
# Clear everything (default - backward compatible)
|
|
203
214
|
python manage.py aiwaf_reset
|
|
204
215
|
|
|
205
|
-
# Clear everything without confirmation
|
|
216
|
+
# Clear everything without confirmation prompt
|
|
206
217
|
python manage.py aiwaf_reset --confirm
|
|
207
218
|
|
|
208
|
-
# Clear
|
|
209
|
-
python manage.py aiwaf_reset --blacklist
|
|
219
|
+
# 🆕 GRANULAR CONTROL - Clear specific data types
|
|
220
|
+
python manage.py aiwaf_reset --blacklist # Clear only blocked IPs
|
|
221
|
+
python manage.py aiwaf_reset --exemptions # Clear only exempted IPs
|
|
222
|
+
python manage.py aiwaf_reset --keywords # Clear only learned keywords
|
|
223
|
+
|
|
224
|
+
# 🔧 COMBINE OPTIONS - Mix and match as needed
|
|
225
|
+
python manage.py aiwaf_reset --blacklist --keywords # Keep exemptions
|
|
226
|
+
python manage.py aiwaf_reset --exemptions --keywords # Keep blacklist
|
|
227
|
+
python manage.py aiwaf_reset --blacklist --exemptions # Keep keywords
|
|
228
|
+
|
|
229
|
+
# 🚀 COMMON USE CASES
|
|
230
|
+
# Fix false positive keywords (like "profile" blocking legitimate pages)
|
|
231
|
+
python manage.py aiwaf_reset --keywords --confirm
|
|
232
|
+
python manage.py detect_and_train # Retrain with enhanced filtering
|
|
233
|
+
|
|
234
|
+
# Clear blocked IPs but preserve exemptions and learning
|
|
235
|
+
python manage.py aiwaf_reset --blacklist --confirm
|
|
236
|
+
|
|
237
|
+
# Legacy support (still works for backward compatibility)
|
|
238
|
+
python manage.py aiwaf_reset --blacklist-only # Legacy: blacklist only
|
|
239
|
+
python manage.py aiwaf_reset --exemptions-only # Legacy: exemptions only
|
|
240
|
+
```
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
|
|
242
|
+
**Enhanced Feedback:**
|
|
243
|
+
```bash
|
|
244
|
+
$ python manage.py aiwaf_reset --keywords
|
|
245
|
+
🔧 AI-WAF Reset: Clear 15 learned keywords
|
|
246
|
+
Are you sure you want to proceed? [y/N]: y
|
|
247
|
+
✅ Reset complete: Deleted 15 learned keywords
|
|
213
248
|
```
|
|
214
249
|
|
|
215
250
|
### Checking Dependencies
|
|
@@ -482,6 +517,21 @@ AIWAF_EXEMPT_PATHS = [ # optional but highly recommended
|
|
|
482
517
|
"/media/",
|
|
483
518
|
"/health/",
|
|
484
519
|
]
|
|
520
|
+
|
|
521
|
+
# 🆕 ENHANCED KEYWORD FILTERING OPTIONS
|
|
522
|
+
AIWAF_ALLOWED_PATH_KEYWORDS = [ # Keywords allowed in legitimate paths
|
|
523
|
+
"profile", "user", "account", "settings", "dashboard",
|
|
524
|
+
"admin", "api", "auth", "search", "contact", "about",
|
|
525
|
+
# Add your site-specific legitimate keywords
|
|
526
|
+
"buddycraft", "sc2", "starcraft", # Example: gaming site keywords
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
AIWAF_EXEMPT_KEYWORDS = [ # Keywords that never trigger blocking
|
|
530
|
+
"api", "webhook", "health", "static", "media",
|
|
531
|
+
"upload", "download", "backup", "profile"
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
AIWAF_DYNAMIC_TOP_N = 10 # Number of dynamic keywords to learn (default: 10)
|
|
485
535
|
```
|
|
486
536
|
|
|
487
537
|
> **Note:** You no longer need to define `AIWAF_MALICIOUS_KEYWORDS` or `AIWAF_STATUS_CODES` — they evolve dynamically.
|
|
@@ -680,6 +730,65 @@ python manage.py detect_and_train
|
|
|
680
730
|
|
|
681
731
|
---
|
|
682
732
|
|
|
733
|
+
## 🔧 Troubleshooting
|
|
734
|
+
|
|
735
|
+
### Legitimate Pages Being Blocked
|
|
736
|
+
|
|
737
|
+
**Problem**: Users can't access legitimate pages like `/en/profile/` due to keyword blocking.
|
|
738
|
+
|
|
739
|
+
**Cause**: AIWAF learned legitimate keywords (like "profile") as suspicious from previous traffic.
|
|
740
|
+
|
|
741
|
+
**Solution**:
|
|
742
|
+
```bash
|
|
743
|
+
# 1. Clear problematic learned keywords
|
|
744
|
+
python manage.py aiwaf_reset --keywords --confirm
|
|
745
|
+
|
|
746
|
+
# 2. Add legitimate keywords to settings
|
|
747
|
+
# In settings.py:
|
|
748
|
+
AIWAF_ALLOWED_PATH_KEYWORDS = [
|
|
749
|
+
"profile", "user", "account", "dashboard",
|
|
750
|
+
# Add your site-specific keywords
|
|
751
|
+
]
|
|
752
|
+
|
|
753
|
+
# 3. Retrain with enhanced filtering (won't learn legitimate keywords)
|
|
754
|
+
python manage.py detect_and_train
|
|
755
|
+
|
|
756
|
+
# 4. Test - legitimate pages should now work!
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Preventing Future False Positives
|
|
760
|
+
|
|
761
|
+
Configure AIWAF to recognize your site's legitimate keywords:
|
|
762
|
+
|
|
763
|
+
```python
|
|
764
|
+
# settings.py
|
|
765
|
+
AIWAF_ALLOWED_PATH_KEYWORDS = [
|
|
766
|
+
# Common legitimate keywords
|
|
767
|
+
"profile", "user", "account", "settings", "dashboard",
|
|
768
|
+
"admin", "search", "contact", "about", "help",
|
|
769
|
+
|
|
770
|
+
# Your site-specific keywords
|
|
771
|
+
"buddycraft", "sc2", "starcraft", # Gaming site example
|
|
772
|
+
"shop", "cart", "checkout", # E-commerce example
|
|
773
|
+
"blog", "article", "news", # Content site example
|
|
774
|
+
]
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Reset Command Options
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
# Clear everything (safest for troubleshooting)
|
|
781
|
+
python manage.py aiwaf_reset --confirm
|
|
782
|
+
|
|
783
|
+
# Clear only problematic keywords
|
|
784
|
+
python manage.py aiwaf_reset --keywords --confirm
|
|
785
|
+
|
|
786
|
+
# Clear blocked IPs but keep exemptions
|
|
787
|
+
python manage.py aiwaf_reset --blacklist --confirm
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
683
792
|
## 🧠 How It Works
|
|
684
793
|
|
|
685
794
|
| Middleware | Purpose |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
aiwaf/__init__.py,sha256=
|
|
1
|
+
aiwaf/__init__.py,sha256=Bn2DcnLiYvx-vkOUfIbQtnKUDidKK1rAUNuXuPa36MM,220
|
|
2
2
|
aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
|
|
3
3
|
aiwaf/blacklist_manager.py,sha256=LYCeKFB-7e_C6Bg2WeFJWFIIQlrfRMPuGp30ivrnhQY,1196
|
|
4
4
|
aiwaf/decorators.py,sha256=IUKOdM_gdroffImRZep1g1wT6gNqD10zGwcp28hsJCs,825
|
|
5
|
-
aiwaf/middleware.py,sha256=
|
|
5
|
+
aiwaf/middleware.py,sha256=D1HavBGJbpPneOtkkCVFddlOQwCdoWcugmHOvn5THDU,22614
|
|
6
6
|
aiwaf/middleware_logger.py,sha256=LWZVDAnjh6CGESirA8eMbhGgJKB7lVDGRQqVroH95Lo,4742
|
|
7
7
|
aiwaf/models.py,sha256=vQxgY19BDVMjoO903UNrTZC1pNoLltMU6wbyWPoAEns,2719
|
|
8
|
-
aiwaf/storage.py,sha256=
|
|
9
|
-
aiwaf/trainer.py,sha256=
|
|
8
|
+
aiwaf/storage.py,sha256=5ImrZMRn3u7HNsPH0fDjWhDrD2tgG2IHVnOXtLz0fk4,10253
|
|
9
|
+
aiwaf/trainer.py,sha256=UHkfrbJI47bGJPCz0Vws6r23WvGpemMHf5ScHWG_I1I,14568
|
|
10
10
|
aiwaf/utils.py,sha256=BJk5vJCYdGPl_4QQiknjhCbkzv5HZCXgFcBJDMJpHok,3390
|
|
11
11
|
aiwaf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -14,7 +14,7 @@ aiwaf/management/commands/add_exemption.py,sha256=U_ByfJw1EstAZ8DaSoRb97IGwYzXs0
|
|
|
14
14
|
aiwaf/management/commands/add_ipexemption.py,sha256=sSf3d9hGK9RqqlBYkCrnrd8KZWGT-derSpoWnEY4H60,952
|
|
15
15
|
aiwaf/management/commands/aiwaf_diagnose.py,sha256=nXFRhq66N4QC3e4scYJ2sUngJce-0yDxtBO3R2BllRM,6134
|
|
16
16
|
aiwaf/management/commands/aiwaf_logging.py,sha256=FCIqULn2tii2vD9VxL7vk3PV4k4vr7kaA00KyaCExYY,7692
|
|
17
|
-
aiwaf/management/commands/aiwaf_reset.py,sha256=
|
|
17
|
+
aiwaf/management/commands/aiwaf_reset.py,sha256=pcF0zOYDSqjpCwDtk2HYJZLgr76td8OFRENtl20c1dQ,7472
|
|
18
18
|
aiwaf/management/commands/check_dependencies.py,sha256=GOZl00pDwW2cJjDvIaCeB3yWxmeYcJDRTIpmOTLvy2c,37204
|
|
19
19
|
aiwaf/management/commands/clear_blacklist.py,sha256=Tisedg0EVlc3E01mA3hBZQorwMzc5j1cns-oYshja0g,2770
|
|
20
20
|
aiwaf/management/commands/clear_cache.py,sha256=cdnuTgxkhKLqT_6k6yTcEBlREovNRQxAE51ceXlGYMA,647
|
|
@@ -28,8 +28,8 @@ aiwaf/management/commands/test_exemption_fix.py,sha256=ngyGaHUCmQQ6y--6j4q1viZJt
|
|
|
28
28
|
aiwaf/resources/model.pkl,sha256=5t6h9BX8yoh2xct85MXOO60jdlWyg1APskUOW0jZE1Y,1288265
|
|
29
29
|
aiwaf/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
30
|
aiwaf/templatetags/aiwaf_tags.py,sha256=XXfb7Tl4DjU3Sc40GbqdaqOEtKTUKELBEk58u83wBNw,357
|
|
31
|
-
aiwaf-0.1.9.
|
|
32
|
-
aiwaf-0.1.9.
|
|
33
|
-
aiwaf-0.1.9.
|
|
34
|
-
aiwaf-0.1.9.
|
|
35
|
-
aiwaf-0.1.9.
|
|
31
|
+
aiwaf-0.1.9.2.0.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
|
|
32
|
+
aiwaf-0.1.9.2.0.dist-info/METADATA,sha256=HM_8Dh89XWhMKtLDkqrvA7fxzR97F9Ph1sY2bYOk9Mc,26414
|
|
33
|
+
aiwaf-0.1.9.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
34
|
+
aiwaf-0.1.9.2.0.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
|
|
35
|
+
aiwaf-0.1.9.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|