aiwaf 0.1.6__py3-none-any.whl → 0.1.7.1__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/middleware.py +41 -17
- aiwaf/trainer.py +58 -10
- {aiwaf-0.1.6.dist-info → aiwaf-0.1.7.1.dist-info}/METADATA +55 -41
- {aiwaf-0.1.6.dist-info → aiwaf-0.1.7.1.dist-info}/RECORD +7 -7
- {aiwaf-0.1.6.dist-info → aiwaf-0.1.7.1.dist-info}/WHEEL +1 -1
- {aiwaf-0.1.6.dist-info → aiwaf-0.1.7.1.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.6.dist-info → aiwaf-0.1.7.1.dist-info}/top_level.txt +0 -0
aiwaf/middleware.py
CHANGED
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
import os
|
|
6
6
|
import numpy as np
|
|
7
7
|
import joblib
|
|
8
|
-
|
|
8
|
+
from django.db.models import UUIDField
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
from django.utils.deprecation import MiddlewareMixin
|
|
11
11
|
from django.http import JsonResponse
|
|
@@ -18,6 +18,14 @@ from django.urls import get_resolver
|
|
|
18
18
|
from .blacklist_manager import BlacklistManager
|
|
19
19
|
from .models import DynamicKeyword
|
|
20
20
|
|
|
21
|
+
def is_exempt_path(path):
|
|
22
|
+
path = path.lower()
|
|
23
|
+
exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
|
|
24
|
+
for exempt in exempt_paths:
|
|
25
|
+
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
26
|
+
return True
|
|
27
|
+
return False
|
|
28
|
+
|
|
21
29
|
MODEL_PATH = getattr(
|
|
22
30
|
settings,
|
|
23
31
|
"AIWAF_MODEL_PATH",
|
|
@@ -43,25 +51,32 @@ def get_ip(request):
|
|
|
43
51
|
class IPAndKeywordBlockMiddleware:
|
|
44
52
|
def __init__(self, get_response):
|
|
45
53
|
self.get_response = get_response
|
|
46
|
-
self.
|
|
54
|
+
self.safe_prefixes = self._collect_safe_prefixes()
|
|
47
55
|
|
|
48
|
-
def
|
|
56
|
+
def _collect_safe_prefixes(self):
|
|
49
57
|
resolver = get_resolver()
|
|
50
|
-
|
|
58
|
+
prefixes = set()
|
|
51
59
|
|
|
52
60
|
def extract(patterns_list, prefix=""):
|
|
53
61
|
for p in patterns_list:
|
|
54
|
-
if hasattr(p, "url_patterns"):
|
|
62
|
+
if hasattr(p, "url_patterns"): # include()
|
|
63
|
+
full_prefix = (prefix + str(p.pattern)).strip("^/").split("/")[0]
|
|
64
|
+
prefixes.add(full_prefix)
|
|
55
65
|
extract(p.url_patterns, prefix + str(p.pattern))
|
|
56
66
|
else:
|
|
57
67
|
pat = (prefix + str(p.pattern)).strip("^$")
|
|
58
|
-
|
|
68
|
+
path_parts = pat.strip("/").split("/")
|
|
69
|
+
if path_parts:
|
|
70
|
+
prefixes.add(path_parts[0])
|
|
59
71
|
extract(resolver.url_patterns)
|
|
60
|
-
return
|
|
72
|
+
return prefixes
|
|
61
73
|
|
|
62
74
|
def __call__(self, request):
|
|
75
|
+
raw_path = request.path.lower()
|
|
76
|
+
if is_exempt_path(raw_path):
|
|
77
|
+
return self.get_response(request)
|
|
63
78
|
ip = get_ip(request)
|
|
64
|
-
path =
|
|
79
|
+
path = raw_path.lstrip("/")
|
|
65
80
|
if BlacklistManager.is_blocked(ip):
|
|
66
81
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
67
82
|
segments = [seg for seg in re.split(r"\W+", path) if len(seg) > 3]
|
|
@@ -74,8 +89,10 @@ class IPAndKeywordBlockMiddleware:
|
|
|
74
89
|
.values_list("keyword", flat=True)[: getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10)]
|
|
75
90
|
)
|
|
76
91
|
all_kw = set(STATIC_KW) | set(dynamic_top)
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
suspicious_kw = {
|
|
93
|
+
kw for kw in all_kw
|
|
94
|
+
if not any(path.startswith(prefix) for prefix in self.safe_prefixes if prefix)
|
|
95
|
+
}
|
|
79
96
|
for seg in segments:
|
|
80
97
|
if seg in suspicious_kw:
|
|
81
98
|
BlacklistManager.block(ip, f"Keyword block: {seg}")
|
|
@@ -93,6 +110,8 @@ class RateLimitMiddleware:
|
|
|
93
110
|
self.logs = defaultdict(list)
|
|
94
111
|
|
|
95
112
|
def __call__(self, request):
|
|
113
|
+
if is_exempt_path(request.path):
|
|
114
|
+
return self.get_response(request)
|
|
96
115
|
ip = get_ip(request)
|
|
97
116
|
now = time.time()
|
|
98
117
|
recs = [t for t in self.logs[ip] if now - t < self.WINDOW]
|
|
@@ -113,6 +132,8 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
113
132
|
TOP_N = getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10)
|
|
114
133
|
|
|
115
134
|
def process_request(self, request):
|
|
135
|
+
if is_exempt_path(request.path):
|
|
136
|
+
return None
|
|
116
137
|
ip = get_ip(request)
|
|
117
138
|
if BlacklistManager.is_blocked(ip):
|
|
118
139
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
@@ -120,12 +141,9 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
120
141
|
now = time.time()
|
|
121
142
|
key = f"aiwaf:{ip}"
|
|
122
143
|
data = cache.get(key, [])
|
|
123
|
-
# TODO: you may want to capture real status & response_time in process_response
|
|
124
144
|
data.append((now, request.path, 0, 0.0))
|
|
125
145
|
data = [d for d in data if now - d[0] < self.WINDOW]
|
|
126
146
|
cache.set(key, data, timeout=self.WINDOW)
|
|
127
|
-
|
|
128
|
-
# update dynamic‐keyword counts
|
|
129
147
|
for seg in re.split(r"\W+", request.path.lower()):
|
|
130
148
|
if len(seg) > 3:
|
|
131
149
|
obj, _ = DynamicKeyword.objects.get_or_create(keyword=seg)
|
|
@@ -133,8 +151,6 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
133
151
|
|
|
134
152
|
if len(data) < 5:
|
|
135
153
|
return None
|
|
136
|
-
|
|
137
|
-
# pull top‐N dynamic tokens
|
|
138
154
|
top_dynamic = list(
|
|
139
155
|
DynamicKeyword.objects
|
|
140
156
|
.order_by("-count")
|
|
@@ -159,6 +175,8 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
159
175
|
|
|
160
176
|
class HoneypotMiddleware(MiddlewareMixin):
|
|
161
177
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
178
|
+
if is_exempt_path(request.path):
|
|
179
|
+
return None
|
|
162
180
|
trap = request.POST.get(getattr(settings, "AIWAF_HONEYPOT_FIELD", "hp_field"), "")
|
|
163
181
|
if trap:
|
|
164
182
|
ip = get_ip(request)
|
|
@@ -169,6 +187,8 @@ class HoneypotMiddleware(MiddlewareMixin):
|
|
|
169
187
|
|
|
170
188
|
class UUIDTamperMiddleware(MiddlewareMixin):
|
|
171
189
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
190
|
+
if is_exempt_path(request.path):
|
|
191
|
+
return None
|
|
172
192
|
uid = view_kwargs.get("uuid")
|
|
173
193
|
if not uid:
|
|
174
194
|
return None
|
|
@@ -177,8 +197,12 @@ class UUIDTamperMiddleware(MiddlewareMixin):
|
|
|
177
197
|
app_label = view_func.__module__.split(".")[0]
|
|
178
198
|
app_cfg = apps.get_app_config(app_label)
|
|
179
199
|
for Model in app_cfg.get_models():
|
|
180
|
-
if Model.
|
|
181
|
-
|
|
200
|
+
if isinstance(Model._meta.pk, UUIDField):
|
|
201
|
+
try:
|
|
202
|
+
if Model.objects.filter(pk=uid).exists():
|
|
203
|
+
return None
|
|
204
|
+
except (ValueError, TypeError):
|
|
205
|
+
continue
|
|
182
206
|
|
|
183
207
|
BlacklistManager.block(ip, "UUID tampering")
|
|
184
208
|
return JsonResponse({"error": "blocked"}, status=403)
|
aiwaf/trainer.py
CHANGED
|
@@ -12,6 +12,8 @@ import joblib
|
|
|
12
12
|
from django.conf import settings
|
|
13
13
|
from django.apps import apps
|
|
14
14
|
from django.db.models import F
|
|
15
|
+
from django.urls import get_resolver
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
LOG_PATH = settings.AIWAF_ACCESS_LOG
|
|
17
19
|
MODEL_PATH = os.path.join(os.path.dirname(__file__), "resources", "model.pkl")
|
|
@@ -27,6 +29,45 @@ _LOG_RX = re.compile(
|
|
|
27
29
|
BlacklistEntry = apps.get_model("aiwaf", "BlacklistEntry")
|
|
28
30
|
DynamicKeyword = apps.get_model("aiwaf", "DynamicKeyword")
|
|
29
31
|
|
|
32
|
+
def is_exempt_path(path):
|
|
33
|
+
path = path.lower()
|
|
34
|
+
exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
|
|
35
|
+
for exempt in exempt_paths:
|
|
36
|
+
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
37
|
+
return True
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
def path_exists_in_django(path):
|
|
41
|
+
from django.urls import get_resolver
|
|
42
|
+
from django.urls.resolvers import URLPattern, URLResolver
|
|
43
|
+
|
|
44
|
+
path = path.split("?")[0].lstrip("/")
|
|
45
|
+
try:
|
|
46
|
+
get_resolver().resolve(f"/{path}")
|
|
47
|
+
return True
|
|
48
|
+
except:
|
|
49
|
+
pass
|
|
50
|
+
parts = path.split("/")
|
|
51
|
+
root_resolver = get_resolver()
|
|
52
|
+
for pattern in root_resolver.url_patterns:
|
|
53
|
+
if isinstance(pattern, URLResolver):
|
|
54
|
+
prefix = pattern.pattern.describe().strip("^/")
|
|
55
|
+
if prefix and path.startswith(prefix):
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def remove_exempt_keywords():
|
|
60
|
+
exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
|
|
61
|
+
exempt_tokens = set()
|
|
62
|
+
|
|
63
|
+
for path in exempt_paths:
|
|
64
|
+
path = path.strip("/").lower()
|
|
65
|
+
segments = re.split(r"\W+", path)
|
|
66
|
+
exempt_tokens.update(seg for seg in segments if len(seg) > 3)
|
|
67
|
+
|
|
68
|
+
if exempt_tokens:
|
|
69
|
+
deleted_count, _ = DynamicKeyword.objects.filter(keyword__in=exempt_tokens).delete()
|
|
70
|
+
print(f"Removed {deleted_count} dynamic keywords that are now exempt: {list(exempt_tokens)}")
|
|
30
71
|
|
|
31
72
|
def _read_all_logs():
|
|
32
73
|
lines = []
|
|
@@ -62,10 +103,12 @@ def _parse(line):
|
|
|
62
103
|
}
|
|
63
104
|
|
|
64
105
|
|
|
106
|
+
|
|
65
107
|
def train():
|
|
108
|
+
remove_exempt_keywords()
|
|
66
109
|
raw_lines = _read_all_logs()
|
|
67
110
|
if not raw_lines:
|
|
68
|
-
print("
|
|
111
|
+
print("No log lines found – check AIWAF_ACCESS_LOG setting.")
|
|
69
112
|
return
|
|
70
113
|
parsed = []
|
|
71
114
|
ip_404 = defaultdict(int)
|
|
@@ -89,6 +132,7 @@ def train():
|
|
|
89
132
|
blocked_404.append(ip)
|
|
90
133
|
if blocked_404:
|
|
91
134
|
print(f"Blocked {len(blocked_404)} IPs for 404 flood: {blocked_404}")
|
|
135
|
+
|
|
92
136
|
feature_dicts = []
|
|
93
137
|
for r in parsed:
|
|
94
138
|
ip = r["ip"]
|
|
@@ -97,7 +141,10 @@ def train():
|
|
|
97
141
|
if (r["timestamp"] - t).total_seconds() <= 10
|
|
98
142
|
)
|
|
99
143
|
total404 = ip_404[ip]
|
|
100
|
-
|
|
144
|
+
is_known_path = path_exists_in_django(r["path"])
|
|
145
|
+
kw_hits = 0
|
|
146
|
+
if not is_known_path and not is_exempt_path(r["path"]):
|
|
147
|
+
kw_hits = sum(k in r["path"].lower() for k in STATIC_KW)
|
|
101
148
|
status_idx = STATUS_IDX.index(r["status"]) if r["status"] in STATUS_IDX else -1
|
|
102
149
|
feature_dicts.append({
|
|
103
150
|
"ip": ip,
|
|
@@ -112,7 +159,6 @@ def train():
|
|
|
112
159
|
if not feature_dicts:
|
|
113
160
|
print("⚠️ Nothing to train on – no valid log entries.")
|
|
114
161
|
return
|
|
115
|
-
|
|
116
162
|
df = pd.DataFrame(feature_dicts)
|
|
117
163
|
feature_cols = [c for c in df.columns if c != "ip"]
|
|
118
164
|
X = df[feature_cols].astype(float).values
|
|
@@ -120,8 +166,8 @@ def train():
|
|
|
120
166
|
model.fit(X)
|
|
121
167
|
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
|
|
122
168
|
joblib.dump(model, MODEL_PATH)
|
|
123
|
-
print(f"
|
|
124
|
-
preds = model.predict(X)
|
|
169
|
+
print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
|
|
170
|
+
preds = model.predict(X)
|
|
125
171
|
anomalous_ips = set(df.loc[preds == -1, 'ip'])
|
|
126
172
|
blocked_anom = []
|
|
127
173
|
for ip in anomalous_ips:
|
|
@@ -132,20 +178,22 @@ def train():
|
|
|
132
178
|
if created:
|
|
133
179
|
blocked_anom.append(ip)
|
|
134
180
|
if blocked_anom:
|
|
135
|
-
print(f" Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
|
|
136
|
-
|
|
181
|
+
print(f"🚫 Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
|
|
137
182
|
tokens = Counter()
|
|
138
183
|
for r in parsed:
|
|
139
|
-
if r["status"].startswith(("4", "5")):
|
|
184
|
+
if r["status"].startswith(("4", "5")) and not path_exists_in_django(r["path"]):
|
|
140
185
|
for seg in re.split(r"\W+", r["path"].lower()):
|
|
141
186
|
if len(seg) > 3 and seg not in STATIC_KW:
|
|
142
187
|
tokens[seg] += 1
|
|
188
|
+
|
|
143
189
|
top_tokens = tokens.most_common(10)
|
|
144
190
|
for kw, cnt in top_tokens:
|
|
145
191
|
obj, _ = DynamicKeyword.objects.get_or_create(keyword=kw)
|
|
146
192
|
DynamicKeyword.objects.filter(pk=obj.pk).update(count=F("count") + cnt)
|
|
147
|
-
|
|
193
|
+
|
|
194
|
+
print(f" DynamicKeyword DB updated with top tokens: {[kw for kw, _ in top_tokens]}")
|
|
195
|
+
|
|
148
196
|
|
|
149
197
|
|
|
150
198
|
if __name__ == "__main__":
|
|
151
|
-
train()
|
|
199
|
+
train()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7.1
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -15,14 +15,14 @@ Dynamic: license-file
|
|
|
15
15
|
Dynamic: requires-python
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
# AI‑WAF
|
|
18
|
+
# AI‑WAF
|
|
19
19
|
|
|
20
20
|
> A self‑learning, Django‑friendly Web Application Firewall
|
|
21
|
-
> with rate‑limiting, anomaly detection, honeypots, UUID‑tamper protection, dynamic keyword extraction, file‑extension probing detection, and daily retraining.
|
|
21
|
+
> with rate‑limiting, anomaly detection, honeypots, UUID‑tamper protection, dynamic keyword extraction, file‑extension probing detection, exempt path awareness, and daily retraining.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
## Package Structure
|
|
25
|
+
## 📁 Package Structure
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
aiwaf/
|
|
@@ -44,7 +44,7 @@ aiwaf/
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## Features
|
|
47
|
+
## 🚀 Features
|
|
48
48
|
|
|
49
49
|
- **IP Blocklist**
|
|
50
50
|
Instantly blocks suspicious IPs (supports CSV fallback or Django model).
|
|
@@ -53,7 +53,7 @@ aiwaf/
|
|
|
53
53
|
Sliding‑window blocks flooders (> `AIWAF_RATE_MAX` per `AIWAF_RATE_WINDOW`), then blacklists them.
|
|
54
54
|
|
|
55
55
|
- **AI Anomaly Detection**
|
|
56
|
-
IsolationForest on
|
|
56
|
+
IsolationForest trained on:
|
|
57
57
|
- Path length
|
|
58
58
|
- Keyword hits (static + dynamic)
|
|
59
59
|
- Response time
|
|
@@ -61,34 +61,28 @@ aiwaf/
|
|
|
61
61
|
- Burst count
|
|
62
62
|
- Total 404s
|
|
63
63
|
|
|
64
|
-
- **Dynamic Keyword Extraction**
|
|
65
|
-
Every retrain
|
|
64
|
+
- **Dynamic Keyword Extraction & Cleanup**
|
|
65
|
+
- Every retrain adds top 10 keyword segments from 4xx/5xx paths
|
|
66
|
+
- **If a path is added to `AIWAF_EXEMPT_PATHS`, its keywords are automatically removed from the database**
|
|
66
67
|
|
|
67
68
|
- **File‑Extension Probing Detection**
|
|
68
|
-
Tracks repeated 404s on common
|
|
69
|
+
Tracks repeated 404s on common extensions (e.g. `.php`, `.asp`) and blocks IPs.
|
|
69
70
|
|
|
70
71
|
- **Honeypot Field**
|
|
71
|
-
Hidden
|
|
72
|
+
Hidden field for bot detection → IP blacklisted on fill.
|
|
72
73
|
|
|
73
74
|
- **UUID Tampering Protection**
|
|
74
|
-
|
|
75
|
+
Blocks guessed or invalid UUIDs that don’t resolve to real models.
|
|
75
76
|
|
|
76
|
-
- **
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
# From PyPI
|
|
85
|
-
pip install aiwaf
|
|
77
|
+
- **Exempt Path Awareness**
|
|
78
|
+
Fully respects `AIWAF_EXEMPT_PATHS` across all modules — exempt paths are:
|
|
79
|
+
- Skipped from keyword learning
|
|
80
|
+
- Immune to AI blocking
|
|
81
|
+
- Ignored in log training
|
|
82
|
+
- Cleaned from `DynamicKeyword` model automatically
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cd aiwaf
|
|
90
|
-
pip install -e .
|
|
91
|
-
```
|
|
84
|
+
- **Daily Retraining**
|
|
85
|
+
Reads rotated logs, auto‑blocks 404 floods, retrains the IsolationForest, updates `model.pkl`, and evolves the keyword DB.
|
|
92
86
|
|
|
93
87
|
---
|
|
94
88
|
|
|
@@ -96,33 +90,51 @@ pip install -e .
|
|
|
96
90
|
|
|
97
91
|
```python
|
|
98
92
|
INSTALLED_APPS += ["aiwaf"]
|
|
93
|
+
```
|
|
99
94
|
|
|
100
95
|
### Database Setup
|
|
101
96
|
|
|
102
|
-
After adding `aiwaf` to your `INSTALLED_APPS`,
|
|
97
|
+
After adding `aiwaf` to your `INSTALLED_APPS`, run the following to create the necessary tables:
|
|
103
98
|
|
|
104
99
|
```bash
|
|
105
100
|
python manage.py makemigrations aiwaf
|
|
106
101
|
python manage.py migrate
|
|
102
|
+
```
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Required
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
109
|
AIWAF_ACCESS_LOG = "/var/log/nginx/access.log"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Optional (defaults shown)
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
```python
|
|
112
117
|
AIWAF_MODEL_PATH = BASE_DIR / "aiwaf" / "resources" / "model.pkl"
|
|
113
118
|
AIWAF_HONEYPOT_FIELD = "hp_field"
|
|
114
119
|
AIWAF_RATE_WINDOW = 10 # seconds
|
|
115
|
-
AIWAF_RATE_MAX = 20 # max
|
|
120
|
+
AIWAF_RATE_MAX = 20 # max requests per window
|
|
116
121
|
AIWAF_RATE_FLOOD = 10 # flood threshold
|
|
117
|
-
AIWAF_WINDOW_SECONDS = 60 # anomaly window
|
|
118
|
-
AIWAF_FILE_EXTENSIONS = [".php", ".asp", ".jsp"]
|
|
122
|
+
AIWAF_WINDOW_SECONDS = 60 # anomaly detection window
|
|
123
|
+
AIWAF_FILE_EXTENSIONS = [".php", ".asp", ".jsp"]
|
|
124
|
+
AIWAF_EXEMPT_PATHS = [ # optional but highly recommended
|
|
125
|
+
"/favicon.ico",
|
|
126
|
+
"/robots.txt",
|
|
127
|
+
"/static/",
|
|
128
|
+
"/media/",
|
|
129
|
+
"/health/",
|
|
130
|
+
]
|
|
119
131
|
```
|
|
120
132
|
|
|
121
|
-
> **Note:** You no longer need to define `AIWAF_MALICIOUS_KEYWORDS` or `AIWAF_STATUS_CODES`
|
|
133
|
+
> **Note:** You no longer need to define `AIWAF_MALICIOUS_KEYWORDS` or `AIWAF_STATUS_CODES` — they evolve dynamically.
|
|
122
134
|
|
|
123
135
|
---
|
|
124
136
|
|
|
125
|
-
## Middleware Setup
|
|
137
|
+
## 🧱 Middleware Setup
|
|
126
138
|
|
|
127
139
|
Add in **this** order to your `MIDDLEWARE` list:
|
|
128
140
|
|
|
@@ -139,7 +151,7 @@ MIDDLEWARE = [
|
|
|
139
151
|
|
|
140
152
|
---
|
|
141
153
|
|
|
142
|
-
## Honeypot Field (in your template)
|
|
154
|
+
## 🕵️ Honeypot Field (in your template)
|
|
143
155
|
|
|
144
156
|
```django
|
|
145
157
|
{% load aiwaf_tags %}
|
|
@@ -156,22 +168,23 @@ MIDDLEWARE = [
|
|
|
156
168
|
|
|
157
169
|
---
|
|
158
170
|
|
|
159
|
-
## Running Detection & Training
|
|
171
|
+
## 🔁 Running Detection & Training
|
|
160
172
|
|
|
161
173
|
```bash
|
|
162
174
|
python manage.py detect_and_train
|
|
163
175
|
```
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
1. Read access logs
|
|
177
|
+
### What happens:
|
|
178
|
+
1. Read access logs (incl. rotated or gzipped)
|
|
167
179
|
2. Auto‑block IPs with ≥ 6 total 404s
|
|
168
180
|
3. Extract features & train IsolationForest
|
|
169
181
|
4. Save `model.pkl`
|
|
170
182
|
5. Extract top 10 dynamic keywords from 4xx/5xx
|
|
183
|
+
6. Remove any keywords associated with newly exempt paths
|
|
171
184
|
|
|
172
185
|
---
|
|
173
186
|
|
|
174
|
-
## How It Works
|
|
187
|
+
## 🧠 How It Works
|
|
175
188
|
|
|
176
189
|
| Middleware | Purpose |
|
|
177
190
|
|------------------------------------|-----------------------------------------------------------------|
|
|
@@ -180,15 +193,16 @@ python manage.py detect_and_train
|
|
|
180
193
|
| AIAnomalyMiddleware | ML‑driven behavior analysis + block on anomaly |
|
|
181
194
|
| HoneypotMiddleware | Detects bots filling hidden inputs in forms |
|
|
182
195
|
| UUIDTamperMiddleware | Blocks guessed/nonexistent UUIDs across all models in an app |
|
|
196
|
+
|
|
183
197
|
---
|
|
184
198
|
|
|
185
|
-
## License
|
|
199
|
+
## 📄 License
|
|
186
200
|
|
|
187
201
|
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
|
188
202
|
|
|
189
203
|
---
|
|
190
204
|
|
|
191
|
-
## Credits
|
|
205
|
+
## 👤 Credits
|
|
192
206
|
|
|
193
207
|
**AI‑WAF** by [Aayush Gauba](https://github.com/aayushgauba)
|
|
194
208
|
> “Let your firewall learn and evolve — keep your site a fortress.”
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
aiwaf/__init__.py,sha256=nQFpJ1YpX48snzLjEQCf8zD2YNh8v0b_kPTrXx8uBYc,46
|
|
2
2
|
aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
|
|
3
3
|
aiwaf/blacklist_manager.py,sha256=sM6uTH7zD6MOPGb0kzqV2aFut2vxKgft_UVeRJr7klw,392
|
|
4
|
-
aiwaf/middleware.py,sha256=
|
|
4
|
+
aiwaf/middleware.py,sha256=LTLHmQYIQ36WwfR9FEPLrmTbYgqxIh4X5Aen4VJ-vN0,7350
|
|
5
5
|
aiwaf/models.py,sha256=8au1umopgCo0lthztTTRrYRJQUM7uX8eAeXgs3z45K4,1282
|
|
6
6
|
aiwaf/storage.py,sha256=bxCILzzvA1-q6nwclRE8WrfoRhe25H4VrsQDf0hl_lY,1903
|
|
7
|
-
aiwaf/trainer.py,sha256=
|
|
7
|
+
aiwaf/trainer.py,sha256=ir5kFTeLQuhMd2h094ct03Wr-rNZsX-mZHwjLx29F54,6422
|
|
8
8
|
aiwaf/utils.py,sha256=RkEUWhhHy6tOk7V0UYv3cN4xhOR_7aBy9bjhwuV2cdA,1436
|
|
9
9
|
aiwaf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -12,8 +12,8 @@ aiwaf/management/commands/detect_and_train.py,sha256=-o-LZ7QZ5GeJPCekryox1DGXKMm
|
|
|
12
12
|
aiwaf/resources/model.pkl,sha256=rCCXH38SJrnaOba2WZrU1LQVzWT34x6bTVkq20XJU-Q,1091129
|
|
13
13
|
aiwaf/template_tags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
aiwaf/template_tags/aiwaf_tags.py,sha256=1KGqeioYmgKACDUiPkykSqI7DLQ6-Ypy1k00weWj9iY,399
|
|
15
|
-
aiwaf-0.1.
|
|
16
|
-
aiwaf-0.1.
|
|
17
|
-
aiwaf-0.1.
|
|
18
|
-
aiwaf-0.1.
|
|
19
|
-
aiwaf-0.1.
|
|
15
|
+
aiwaf-0.1.7.1.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
|
|
16
|
+
aiwaf-0.1.7.1.dist-info/METADATA,sha256=aO_1D_qSP_s4vKUj60a8VmsFcCLCyhBZii1tpbo3HqE,5790
|
|
17
|
+
aiwaf-0.1.7.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
18
|
+
aiwaf-0.1.7.1.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
|
|
19
|
+
aiwaf-0.1.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|