aiwaf 0.1.8.4__tar.gz → 0.1.8.6__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.8.4 → aiwaf-0.1.8.6}/PKG-INFO +21 -2
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/README.md +20 -1
- aiwaf-0.1.8.6/aiwaf/decorators.py +28 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/middleware.py +31 -46
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/trainer.py +41 -30
- aiwaf-0.1.8.6/aiwaf/utils.py +105 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/PKG-INFO +21 -2
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/SOURCES.txt +1 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/pyproject.toml +1 -1
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/setup.py +1 -1
- aiwaf-0.1.8.4/aiwaf/utils.py +0 -50
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/LICENSE +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/__init__.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/apps.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/blacklist_manager.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/__init__.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/__init__.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/add_ipexemption.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/aiwaf_reset.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/detect_and_train.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/models.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/resources/model.pkl +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/storage.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/templatetags/__init__.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/templatetags/aiwaf_tags.py +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/dependency_links.txt +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/requires.txt +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/top_level.txt +0 -0
- {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.8.
|
|
3
|
+
Version: 0.1.8.6
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -99,7 +99,26 @@ AIWAF_EXEMPT_PATHS = [
|
|
|
99
99
|
]
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
**Exempt Views (Decorator):**
|
|
103
|
+
Use the `@aiwaf_exempt` decorator to exempt specific views from all AI-WAF protection:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from aiwaf.decorators import aiwaf_exempt
|
|
107
|
+
from django.http import JsonResponse
|
|
108
|
+
|
|
109
|
+
@aiwaf_exempt
|
|
110
|
+
def my_api_view(request):
|
|
111
|
+
"""This view will be exempt from all AI-WAF protection"""
|
|
112
|
+
return JsonResponse({"status": "success"})
|
|
113
|
+
|
|
114
|
+
# Works with class-based views too
|
|
115
|
+
@aiwaf_exempt
|
|
116
|
+
class MyAPIView(View):
|
|
117
|
+
def get(self, request):
|
|
118
|
+
return JsonResponse({"method": "GET"})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
All exempt paths and views are:
|
|
103
122
|
- Skipped from keyword learning
|
|
104
123
|
- Immune to AI blocking
|
|
105
124
|
- Ignored in log training
|
|
@@ -78,7 +78,26 @@ AIWAF_EXEMPT_PATHS = [
|
|
|
78
78
|
]
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
**Exempt Views (Decorator):**
|
|
82
|
+
Use the `@aiwaf_exempt` decorator to exempt specific views from all AI-WAF protection:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from aiwaf.decorators import aiwaf_exempt
|
|
86
|
+
from django.http import JsonResponse
|
|
87
|
+
|
|
88
|
+
@aiwaf_exempt
|
|
89
|
+
def my_api_view(request):
|
|
90
|
+
"""This view will be exempt from all AI-WAF protection"""
|
|
91
|
+
return JsonResponse({"status": "success"})
|
|
92
|
+
|
|
93
|
+
# Works with class-based views too
|
|
94
|
+
@aiwaf_exempt
|
|
95
|
+
class MyAPIView(View):
|
|
96
|
+
def get(self, request):
|
|
97
|
+
return JsonResponse({"method": "GET"})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
All exempt paths and views are:
|
|
82
101
|
- Skipped from keyword learning
|
|
83
102
|
- Immune to AI blocking
|
|
84
103
|
- Ignored in log training
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from django.utils.decorators import method_decorator
|
|
3
|
+
|
|
4
|
+
def aiwaf_exempt(view_func):
|
|
5
|
+
"""
|
|
6
|
+
Decorator to exempt a view from AI-WAF protection.
|
|
7
|
+
Can be used on function-based views or class-based views.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
@aiwaf_exempt
|
|
11
|
+
def my_view(request):
|
|
12
|
+
return HttpResponse("This view is exempt from AI-WAF")
|
|
13
|
+
|
|
14
|
+
# Or for class-based views:
|
|
15
|
+
@method_decorator(aiwaf_exempt, name='dispatch')
|
|
16
|
+
class MyView(View):
|
|
17
|
+
pass
|
|
18
|
+
"""
|
|
19
|
+
@wraps(view_func)
|
|
20
|
+
def wrapped_view(*args, **kwargs):
|
|
21
|
+
return view_func(*args, **kwargs)
|
|
22
|
+
|
|
23
|
+
# Mark the view as AI-WAF exempt
|
|
24
|
+
wrapped_view.aiwaf_exempt = True
|
|
25
|
+
return wrapped_view
|
|
26
|
+
|
|
27
|
+
# For class-based views
|
|
28
|
+
aiwaf_exempt_view = method_decorator(aiwaf_exempt, name='dispatch')
|
|
@@ -14,37 +14,10 @@ from django.core.cache import cache
|
|
|
14
14
|
from django.db.models import F
|
|
15
15
|
from django.apps import apps
|
|
16
16
|
from django.urls import get_resolver
|
|
17
|
-
from .trainer import STATIC_KW, STATUS_IDX,
|
|
17
|
+
from .trainer import STATIC_KW, STATUS_IDX, path_exists_in_django
|
|
18
18
|
from .blacklist_manager import BlacklistManager
|
|
19
19
|
from .models import DynamicKeyword, IPExemption
|
|
20
|
-
|
|
21
|
-
return IPExemption.objects.filter(ip_address=ip).exists()
|
|
22
|
-
|
|
23
|
-
def is_exempt_path(path):
|
|
24
|
-
path = path.lower()
|
|
25
|
-
|
|
26
|
-
# Default login paths that should always be exempt
|
|
27
|
-
default_login_paths = [
|
|
28
|
-
"/admin/login/",
|
|
29
|
-
"/admin/",
|
|
30
|
-
"/login/",
|
|
31
|
-
"/accounts/login/",
|
|
32
|
-
"/auth/login/",
|
|
33
|
-
"/signin/",
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
# Check default login paths
|
|
37
|
-
for login_path in default_login_paths:
|
|
38
|
-
if path.startswith(login_path):
|
|
39
|
-
return True
|
|
40
|
-
|
|
41
|
-
# Check user-configured exempt paths
|
|
42
|
-
exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
|
|
43
|
-
for exempt in exempt_paths:
|
|
44
|
-
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
45
|
-
return True
|
|
46
|
-
|
|
47
|
-
return False
|
|
20
|
+
from .utils import is_exempt, get_ip, is_ip_exempted
|
|
48
21
|
|
|
49
22
|
MODEL_PATH = getattr(
|
|
50
23
|
settings,
|
|
@@ -93,7 +66,7 @@ class IPAndKeywordBlockMiddleware:
|
|
|
93
66
|
|
|
94
67
|
def __call__(self, request):
|
|
95
68
|
raw_path = request.path.lower()
|
|
96
|
-
if
|
|
69
|
+
if is_exempt(request):
|
|
97
70
|
return self.get_response(request)
|
|
98
71
|
ip = get_ip(request)
|
|
99
72
|
path = raw_path.lstrip("/")
|
|
@@ -132,7 +105,7 @@ class RateLimitMiddleware:
|
|
|
132
105
|
self.get_response = get_response
|
|
133
106
|
|
|
134
107
|
def __call__(self, request):
|
|
135
|
-
if
|
|
108
|
+
if is_exempt(request):
|
|
136
109
|
return self.get_response(request)
|
|
137
110
|
|
|
138
111
|
ip = get_ip(request)
|
|
@@ -161,7 +134,7 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
161
134
|
self.model = joblib.load(model_path)
|
|
162
135
|
|
|
163
136
|
def process_request(self, request):
|
|
164
|
-
if
|
|
137
|
+
if is_exempt(request):
|
|
165
138
|
return None
|
|
166
139
|
request._start_time = time.time()
|
|
167
140
|
ip = get_ip(request)
|
|
@@ -172,14 +145,14 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
172
145
|
return None
|
|
173
146
|
|
|
174
147
|
def process_response(self, request, response):
|
|
175
|
-
if
|
|
148
|
+
if is_exempt(request):
|
|
176
149
|
return response
|
|
177
150
|
ip = get_ip(request)
|
|
178
151
|
now = time.time()
|
|
179
152
|
key = f"aiwaf:{ip}"
|
|
180
153
|
data = cache.get(key, [])
|
|
181
154
|
path_len = len(request.path)
|
|
182
|
-
if not path_exists_in_django(request.path) and not
|
|
155
|
+
if not path_exists_in_django(request.path) and not is_exempt(request):
|
|
183
156
|
kw_hits = sum(1 for kw in STATIC_KW if kw in request.path.lower())
|
|
184
157
|
else:
|
|
185
158
|
kw_hits = 0
|
|
@@ -211,7 +184,7 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
211
184
|
MIN_FORM_TIME = getattr(settings, "AIWAF_MIN_FORM_TIME", 1.0) # seconds
|
|
212
185
|
|
|
213
186
|
def process_request(self, request):
|
|
214
|
-
if
|
|
187
|
+
if is_exempt(request):
|
|
215
188
|
return None
|
|
216
189
|
|
|
217
190
|
ip = get_ip(request)
|
|
@@ -219,7 +192,8 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
219
192
|
return None
|
|
220
193
|
|
|
221
194
|
if request.method == "GET":
|
|
222
|
-
# Store timestamp for this IP's GET request
|
|
195
|
+
# Store timestamp for this IP's GET request
|
|
196
|
+
# Use a general key for the IP, not path-specific
|
|
223
197
|
cache.set(f"honeypot_get:{ip}", time.time(), timeout=300) # 5 min timeout
|
|
224
198
|
|
|
225
199
|
elif request.method == "POST":
|
|
@@ -228,22 +202,33 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
228
202
|
|
|
229
203
|
if get_time is None:
|
|
230
204
|
# No GET request - likely bot posting directly
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
205
|
+
# But be more lenient for login paths since users might bookmark them
|
|
206
|
+
if not any(request.path.lower().startswith(login_path) for login_path in [
|
|
207
|
+
"/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
|
|
208
|
+
]):
|
|
209
|
+
BlacklistManager.block(ip, "Direct POST without GET")
|
|
210
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
211
|
+
else:
|
|
212
|
+
# Check timing - be more lenient for login paths
|
|
213
|
+
time_diff = time.time() - get_time
|
|
214
|
+
min_time = self.MIN_FORM_TIME
|
|
215
|
+
|
|
216
|
+
# Use shorter time threshold for login paths (users can login quickly)
|
|
217
|
+
if any(request.path.lower().startswith(login_path) for login_path in [
|
|
218
|
+
"/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
|
|
219
|
+
]):
|
|
220
|
+
min_time = 0.1 # Very short threshold for login forms
|
|
221
|
+
|
|
222
|
+
if time_diff < min_time:
|
|
223
|
+
BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
|
|
224
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
240
225
|
|
|
241
226
|
return None
|
|
242
227
|
|
|
243
228
|
|
|
244
229
|
class UUIDTamperMiddleware(MiddlewareMixin):
|
|
245
230
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
246
|
-
if
|
|
231
|
+
if is_exempt(request):
|
|
247
232
|
return None
|
|
248
233
|
uid = view_kwargs.get("uuid")
|
|
249
234
|
if not uid:
|
|
@@ -13,6 +13,7 @@ from sklearn.ensemble import IsolationForest
|
|
|
13
13
|
from django.conf import settings
|
|
14
14
|
from django.apps import apps
|
|
15
15
|
from django.db.models import F
|
|
16
|
+
from .utils import is_exempt_path
|
|
16
17
|
|
|
17
18
|
# ─────────── Configuration ───────────
|
|
18
19
|
LOG_PATH = settings.AIWAF_ACCESS_LOG
|
|
@@ -32,31 +33,6 @@ DynamicKeyword = apps.get_model("aiwaf", "DynamicKeyword")
|
|
|
32
33
|
IPExemption = apps.get_model("aiwaf", "IPExemption")
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def is_exempt_path(path: str) -> bool:
|
|
36
|
-
path = path.lower()
|
|
37
|
-
|
|
38
|
-
# Default login paths that should always be exempt
|
|
39
|
-
default_login_paths = [
|
|
40
|
-
"/admin/login/",
|
|
41
|
-
"/admin/",
|
|
42
|
-
"/login/",
|
|
43
|
-
"/accounts/login/",
|
|
44
|
-
"/auth/login/",
|
|
45
|
-
"/signin/",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
# Check default login paths
|
|
49
|
-
for login_path in default_login_paths:
|
|
50
|
-
if path.startswith(login_path):
|
|
51
|
-
return True
|
|
52
|
-
|
|
53
|
-
# Check user-configured exempt paths
|
|
54
|
-
for exempt in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
|
|
55
|
-
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
56
|
-
return True
|
|
57
|
-
return False
|
|
58
|
-
|
|
59
|
-
|
|
60
36
|
def path_exists_in_django(path: str) -> bool:
|
|
61
37
|
from django.urls import get_resolver
|
|
62
38
|
from django.urls.resolvers import URLResolver
|
|
@@ -203,13 +179,48 @@ def train() -> None:
|
|
|
203
179
|
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
|
|
204
180
|
joblib.dump(model, MODEL_PATH)
|
|
205
181
|
print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
|
|
182
|
+
|
|
183
|
+
# Check for anomalies and intelligently decide which IPs to block
|
|
206
184
|
preds = model.predict(X)
|
|
207
185
|
anomalous_ips = set(df.loc[preds == -1, "ip"])
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
186
|
+
|
|
187
|
+
if anomalous_ips:
|
|
188
|
+
print(f"⚠️ Detected {len(anomalous_ips)} potentially anomalous IPs during training")
|
|
189
|
+
|
|
190
|
+
blocked_count = 0
|
|
191
|
+
for ip in anomalous_ips:
|
|
192
|
+
# Skip if IP is exempted
|
|
193
|
+
if IPExemption.objects.filter(ip_address=ip).exists():
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
# Get this IP's behavior from the data
|
|
197
|
+
ip_data = df[df["ip"] == ip]
|
|
198
|
+
|
|
199
|
+
# Criteria to determine if this is likely a legitimate user vs threat:
|
|
200
|
+
avg_kw_hits = ip_data["kw_hits"].mean()
|
|
201
|
+
max_404s = ip_data["total_404"].max()
|
|
202
|
+
avg_burst = ip_data["burst_count"].mean()
|
|
203
|
+
total_requests = len(ip_data)
|
|
204
|
+
|
|
205
|
+
# Don't block if it looks like legitimate behavior:
|
|
206
|
+
if (
|
|
207
|
+
avg_kw_hits < 2 and # Not hitting many malicious keywords
|
|
208
|
+
max_404s < 10 and # Not excessive 404s
|
|
209
|
+
avg_burst < 15 and # Not excessive burst activity
|
|
210
|
+
total_requests < 100 # Not excessive total requests
|
|
211
|
+
):
|
|
212
|
+
print(f" - {ip}: Anomalous but looks legitimate (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f}) - NOT blocking")
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Block if it shows clear signs of malicious behavior
|
|
216
|
+
BlacklistEntry.objects.get_or_create(
|
|
217
|
+
ip_address=ip,
|
|
218
|
+
defaults={"reason": f"AI anomaly + suspicious patterns (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})"}
|
|
219
|
+
)
|
|
220
|
+
blocked_count += 1
|
|
221
|
+
print(f" - {ip}: Blocked for suspicious behavior (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})")
|
|
222
|
+
|
|
223
|
+
print(f" → Blocked {blocked_count}/{len(anomalous_ips)} anomalous IPs (others looked legitimate)")
|
|
213
224
|
|
|
214
225
|
tokens = Counter()
|
|
215
226
|
for r in parsed:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import glob
|
|
4
|
+
import gzip
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from .models import IPExemption
|
|
8
|
+
|
|
9
|
+
_LOG_RX = re.compile(
|
|
10
|
+
r'(\d+\.\d+\.\d+\.\d+).*\[(.*?)\].*"(GET|POST) (.*?) HTTP/.*?" (\d{3}).*?"(.*?)" "(.*?)"'
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def get_ip(request):
|
|
14
|
+
xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
|
15
|
+
if xff:
|
|
16
|
+
return xff.split(",")[0].strip()
|
|
17
|
+
return request.META.get("REMOTE_ADDR", "")
|
|
18
|
+
|
|
19
|
+
def read_rotated_logs(base_path):
|
|
20
|
+
lines = []
|
|
21
|
+
if os.path.exists(base_path):
|
|
22
|
+
with open(base_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
23
|
+
lines.extend(f.readlines())
|
|
24
|
+
for path in sorted(glob.glob(base_path + ".*")):
|
|
25
|
+
opener = gzip.open if path.endswith(".gz") else open
|
|
26
|
+
try:
|
|
27
|
+
with opener(path, "rt", encoding="utf-8", errors="ignore") as f:
|
|
28
|
+
lines.extend(f.readlines())
|
|
29
|
+
except OSError:
|
|
30
|
+
continue
|
|
31
|
+
return lines
|
|
32
|
+
|
|
33
|
+
def parse_log_line(line):
|
|
34
|
+
m = _LOG_RX.search(line)
|
|
35
|
+
if not m:
|
|
36
|
+
return None
|
|
37
|
+
ip, ts_str, _, path, status, ref, ua = m.groups()
|
|
38
|
+
try:
|
|
39
|
+
ts = datetime.strptime(ts_str.split()[0], "%d/%b/%Y:%H:%M:%S")
|
|
40
|
+
except ValueError:
|
|
41
|
+
return None
|
|
42
|
+
rt_m = re.search(r'response-time=(\d+\.\d+)', line)
|
|
43
|
+
rt = float(rt_m.group(1)) if rt_m else 0.0
|
|
44
|
+
return {
|
|
45
|
+
"ip": ip,
|
|
46
|
+
"timestamp": ts,
|
|
47
|
+
"path": path,
|
|
48
|
+
"status": status,
|
|
49
|
+
"referer": ref,
|
|
50
|
+
"user_agent": ua,
|
|
51
|
+
"response_time": rt
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def is_ip_exempted(ip):
|
|
55
|
+
"""Check if IP is in exemption list"""
|
|
56
|
+
return IPExemption.objects.filter(ip_address=ip).exists()
|
|
57
|
+
|
|
58
|
+
def is_view_exempt(request):
|
|
59
|
+
"""Check if the current view is marked as AI-WAF exempt"""
|
|
60
|
+
if hasattr(request, 'resolver_match') and request.resolver_match:
|
|
61
|
+
view_func = request.resolver_match.func
|
|
62
|
+
|
|
63
|
+
# Check if view function has aiwaf_exempt attribute
|
|
64
|
+
if hasattr(view_func, 'aiwaf_exempt'):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
# For class-based views, check the view class
|
|
68
|
+
if hasattr(view_func, 'view_class'):
|
|
69
|
+
view_class = view_func.view_class
|
|
70
|
+
if hasattr(view_class, 'aiwaf_exempt'):
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
# Check dispatch method for method_decorator usage
|
|
74
|
+
dispatch_method = getattr(view_class, 'dispatch', None)
|
|
75
|
+
if dispatch_method and hasattr(dispatch_method, 'aiwaf_exempt'):
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def is_exempt_path(path):
|
|
81
|
+
"""Check if path should be exempt from AI-WAF"""
|
|
82
|
+
path = path.lower()
|
|
83
|
+
|
|
84
|
+
# Default login paths (always exempt)
|
|
85
|
+
default_exempt = [
|
|
86
|
+
"/admin/login/", "/admin/", "/login/", "/accounts/login/",
|
|
87
|
+
"/auth/login/", "/signin/"
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Check default exempt paths
|
|
91
|
+
for exempt_path in default_exempt:
|
|
92
|
+
if path.startswith(exempt_path):
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Check configured exempt paths
|
|
96
|
+
exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
|
|
97
|
+
for exempt_path in exempt_paths:
|
|
98
|
+
if path == exempt_path or path.startswith(exempt_path.rstrip("/") + "/"):
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def is_exempt(request):
|
|
104
|
+
"""Check if request should be exempt (either by path or view decorator)"""
|
|
105
|
+
return is_exempt_path(request.path) or is_view_exempt(request)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.8.
|
|
3
|
+
Version: 0.1.8.6
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -99,7 +99,26 @@ AIWAF_EXEMPT_PATHS = [
|
|
|
99
99
|
]
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
**Exempt Views (Decorator):**
|
|
103
|
+
Use the `@aiwaf_exempt` decorator to exempt specific views from all AI-WAF protection:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from aiwaf.decorators import aiwaf_exempt
|
|
107
|
+
from django.http import JsonResponse
|
|
108
|
+
|
|
109
|
+
@aiwaf_exempt
|
|
110
|
+
def my_api_view(request):
|
|
111
|
+
"""This view will be exempt from all AI-WAF protection"""
|
|
112
|
+
return JsonResponse({"status": "success"})
|
|
113
|
+
|
|
114
|
+
# Works with class-based views too
|
|
115
|
+
@aiwaf_exempt
|
|
116
|
+
class MyAPIView(View):
|
|
117
|
+
def get(self, request):
|
|
118
|
+
return JsonResponse({"method": "GET"})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
All exempt paths and views are:
|
|
103
122
|
- Skipped from keyword learning
|
|
104
123
|
- Immune to AI blocking
|
|
105
124
|
- Ignored in log training
|
|
@@ -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.8.
|
|
12
|
+
version="0.1.8.6",
|
|
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",
|
aiwaf-0.1.8.4/aiwaf/utils.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import glob
|
|
4
|
-
import gzip
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
_LOG_RX = re.compile(
|
|
8
|
-
r'(\d+\.\d+\.\d+\.\d+).*\[(.*?)\].*"(GET|POST) (.*?) HTTP/.*?" (\d{3}).*?"(.*?)" "(.*?)"'
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
def get_ip(request):
|
|
12
|
-
xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
|
13
|
-
if xff:
|
|
14
|
-
return xff.split(",")[0].strip()
|
|
15
|
-
return request.META.get("REMOTE_ADDR", "")
|
|
16
|
-
|
|
17
|
-
def read_rotated_logs(base_path):
|
|
18
|
-
lines = []
|
|
19
|
-
if os.path.exists(base_path):
|
|
20
|
-
with open(base_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
21
|
-
lines.extend(f.readlines())
|
|
22
|
-
for path in sorted(glob.glob(base_path + ".*")):
|
|
23
|
-
opener = gzip.open if path.endswith(".gz") else open
|
|
24
|
-
try:
|
|
25
|
-
with opener(path, "rt", encoding="utf-8", errors="ignore") as f:
|
|
26
|
-
lines.extend(f.readlines())
|
|
27
|
-
except OSError:
|
|
28
|
-
continue
|
|
29
|
-
return lines
|
|
30
|
-
|
|
31
|
-
def parse_log_line(line):
|
|
32
|
-
m = _LOG_RX.search(line)
|
|
33
|
-
if not m:
|
|
34
|
-
return None
|
|
35
|
-
ip, ts_str, _, path, status, ref, ua = m.groups()
|
|
36
|
-
try:
|
|
37
|
-
ts = datetime.strptime(ts_str.split()[0], "%d/%b/%Y:%H:%M:%S")
|
|
38
|
-
except ValueError:
|
|
39
|
-
return None
|
|
40
|
-
rt_m = re.search(r'response-time=(\d+\.\d+)', line)
|
|
41
|
-
rt = float(rt_m.group(1)) if rt_m else 0.0
|
|
42
|
-
return {
|
|
43
|
-
"ip": ip,
|
|
44
|
-
"timestamp": ts,
|
|
45
|
-
"path": path,
|
|
46
|
-
"status": status,
|
|
47
|
-
"referer": ref,
|
|
48
|
-
"user_agent": ua,
|
|
49
|
-
"response_time": rt
|
|
50
|
-
}
|
|
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
|