aiwaf 0.1.5__py3-none-any.whl → 0.1.7__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 +22 -17
- aiwaf/trainer.py +39 -9
- {aiwaf-0.1.5.dist-info → aiwaf-0.1.7.dist-info}/METADATA +2 -3
- {aiwaf-0.1.5.dist-info → aiwaf-0.1.7.dist-info}/RECORD +7 -7
- {aiwaf-0.1.5.dist-info → aiwaf-0.1.7.dist-info}/WHEEL +1 -1
- {aiwaf-0.1.5.dist-info → aiwaf-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.5.dist-info → aiwaf-0.1.7.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
|
|
@@ -43,25 +43,29 @@ def get_ip(request):
|
|
|
43
43
|
class IPAndKeywordBlockMiddleware:
|
|
44
44
|
def __init__(self, get_response):
|
|
45
45
|
self.get_response = get_response
|
|
46
|
-
self.
|
|
46
|
+
self.safe_prefixes = self._collect_safe_prefixes()
|
|
47
47
|
|
|
48
|
-
def
|
|
48
|
+
def _collect_safe_prefixes(self):
|
|
49
49
|
resolver = get_resolver()
|
|
50
|
-
|
|
50
|
+
prefixes = set()
|
|
51
51
|
|
|
52
52
|
def extract(patterns_list, prefix=""):
|
|
53
53
|
for p in patterns_list:
|
|
54
|
-
if hasattr(p, "url_patterns"):
|
|
54
|
+
if hasattr(p, "url_patterns"): # include()
|
|
55
|
+
full_prefix = (prefix + str(p.pattern)).strip("^/").split("/")[0]
|
|
56
|
+
prefixes.add(full_prefix)
|
|
55
57
|
extract(p.url_patterns, prefix + str(p.pattern))
|
|
56
58
|
else:
|
|
57
59
|
pat = (prefix + str(p.pattern)).strip("^$")
|
|
58
|
-
|
|
60
|
+
path_parts = pat.strip("/").split("/")
|
|
61
|
+
if path_parts:
|
|
62
|
+
prefixes.add(path_parts[0])
|
|
59
63
|
extract(resolver.url_patterns)
|
|
60
|
-
return
|
|
64
|
+
return prefixes
|
|
61
65
|
|
|
62
66
|
def __call__(self, request):
|
|
63
67
|
ip = get_ip(request)
|
|
64
|
-
path = request.path.lower()
|
|
68
|
+
path = request.path.lower().lstrip("/")
|
|
65
69
|
if BlacklistManager.is_blocked(ip):
|
|
66
70
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
67
71
|
segments = [seg for seg in re.split(r"\W+", path) if len(seg) > 3]
|
|
@@ -74,8 +78,10 @@ class IPAndKeywordBlockMiddleware:
|
|
|
74
78
|
.values_list("keyword", flat=True)[: getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10)]
|
|
75
79
|
)
|
|
76
80
|
all_kw = set(STATIC_KW) | set(dynamic_top)
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
suspicious_kw = {
|
|
82
|
+
kw for kw in all_kw
|
|
83
|
+
if not any(path.startswith(prefix) for prefix in self.safe_prefixes if prefix)
|
|
84
|
+
}
|
|
79
85
|
for seg in segments:
|
|
80
86
|
if seg in suspicious_kw:
|
|
81
87
|
BlacklistManager.block(ip, f"Keyword block: {seg}")
|
|
@@ -120,12 +126,9 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
120
126
|
now = time.time()
|
|
121
127
|
key = f"aiwaf:{ip}"
|
|
122
128
|
data = cache.get(key, [])
|
|
123
|
-
# TODO: you may want to capture real status & response_time in process_response
|
|
124
129
|
data.append((now, request.path, 0, 0.0))
|
|
125
130
|
data = [d for d in data if now - d[0] < self.WINDOW]
|
|
126
131
|
cache.set(key, data, timeout=self.WINDOW)
|
|
127
|
-
|
|
128
|
-
# update dynamic‐keyword counts
|
|
129
132
|
for seg in re.split(r"\W+", request.path.lower()):
|
|
130
133
|
if len(seg) > 3:
|
|
131
134
|
obj, _ = DynamicKeyword.objects.get_or_create(keyword=seg)
|
|
@@ -133,8 +136,6 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
133
136
|
|
|
134
137
|
if len(data) < 5:
|
|
135
138
|
return None
|
|
136
|
-
|
|
137
|
-
# pull top‐N dynamic tokens
|
|
138
139
|
top_dynamic = list(
|
|
139
140
|
DynamicKeyword.objects
|
|
140
141
|
.order_by("-count")
|
|
@@ -177,8 +178,12 @@ class UUIDTamperMiddleware(MiddlewareMixin):
|
|
|
177
178
|
app_label = view_func.__module__.split(".")[0]
|
|
178
179
|
app_cfg = apps.get_app_config(app_label)
|
|
179
180
|
for Model in app_cfg.get_models():
|
|
180
|
-
if Model.
|
|
181
|
-
|
|
181
|
+
if isinstance(Model._meta.pk, UUIDField):
|
|
182
|
+
try:
|
|
183
|
+
if Model.objects.filter(pk=uid).exists():
|
|
184
|
+
return None
|
|
185
|
+
except (ValueError, TypeError):
|
|
186
|
+
continue
|
|
182
187
|
|
|
183
188
|
BlacklistManager.block(ip, "UUID tampering")
|
|
184
189
|
return JsonResponse({"error": "blocked"}, status=403)
|
aiwaf/trainer.py
CHANGED
|
@@ -12,6 +12,9 @@ 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
|
+
|
|
17
|
+
# ─── CONFIG ────────────────────────────────────────────────────────────────
|
|
15
18
|
|
|
16
19
|
LOG_PATH = settings.AIWAF_ACCESS_LOG
|
|
17
20
|
MODEL_PATH = os.path.join(os.path.dirname(__file__), "resources", "model.pkl")
|
|
@@ -28,6 +31,27 @@ BlacklistEntry = apps.get_model("aiwaf", "BlacklistEntry")
|
|
|
28
31
|
DynamicKeyword = apps.get_model("aiwaf", "DynamicKeyword")
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
|
|
35
|
+
def path_exists_in_django(path):
|
|
36
|
+
from django.urls import get_resolver
|
|
37
|
+
from django.urls.resolvers import URLPattern, URLResolver
|
|
38
|
+
|
|
39
|
+
path = path.split("?")[0].lstrip("/")
|
|
40
|
+
try:
|
|
41
|
+
get_resolver().resolve(f"/{path}")
|
|
42
|
+
return True
|
|
43
|
+
except:
|
|
44
|
+
pass
|
|
45
|
+
parts = path.split("/")
|
|
46
|
+
root_resolver = get_resolver()
|
|
47
|
+
for pattern in root_resolver.url_patterns:
|
|
48
|
+
if isinstance(pattern, URLResolver):
|
|
49
|
+
prefix = pattern.pattern.describe().strip("^/")
|
|
50
|
+
if prefix and path.startswith(prefix):
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
31
55
|
def _read_all_logs():
|
|
32
56
|
lines = []
|
|
33
57
|
if LOG_PATH and os.path.exists(LOG_PATH):
|
|
@@ -62,10 +86,11 @@ def _parse(line):
|
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
|
|
89
|
+
|
|
65
90
|
def train():
|
|
66
91
|
raw_lines = _read_all_logs()
|
|
67
92
|
if not raw_lines:
|
|
68
|
-
print("
|
|
93
|
+
print("No log lines found – check AIWAF_ACCESS_LOG setting.")
|
|
69
94
|
return
|
|
70
95
|
parsed = []
|
|
71
96
|
ip_404 = defaultdict(int)
|
|
@@ -89,6 +114,7 @@ def train():
|
|
|
89
114
|
blocked_404.append(ip)
|
|
90
115
|
if blocked_404:
|
|
91
116
|
print(f"Blocked {len(blocked_404)} IPs for 404 flood: {blocked_404}")
|
|
117
|
+
|
|
92
118
|
feature_dicts = []
|
|
93
119
|
for r in parsed:
|
|
94
120
|
ip = r["ip"]
|
|
@@ -97,7 +123,10 @@ def train():
|
|
|
97
123
|
if (r["timestamp"] - t).total_seconds() <= 10
|
|
98
124
|
)
|
|
99
125
|
total404 = ip_404[ip]
|
|
100
|
-
|
|
126
|
+
is_known_path = path_exists_in_django(r["path"])
|
|
127
|
+
kw_hits = 0
|
|
128
|
+
if not is_known_path:
|
|
129
|
+
kw_hits = sum(k in r["path"].lower() for k in STATIC_KW)
|
|
101
130
|
status_idx = STATUS_IDX.index(r["status"]) if r["status"] in STATUS_IDX else -1
|
|
102
131
|
feature_dicts.append({
|
|
103
132
|
"ip": ip,
|
|
@@ -112,7 +141,6 @@ def train():
|
|
|
112
141
|
if not feature_dicts:
|
|
113
142
|
print("⚠️ Nothing to train on – no valid log entries.")
|
|
114
143
|
return
|
|
115
|
-
|
|
116
144
|
df = pd.DataFrame(feature_dicts)
|
|
117
145
|
feature_cols = [c for c in df.columns if c != "ip"]
|
|
118
146
|
X = df[feature_cols].astype(float).values
|
|
@@ -120,8 +148,8 @@ def train():
|
|
|
120
148
|
model.fit(X)
|
|
121
149
|
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
|
|
122
150
|
joblib.dump(model, MODEL_PATH)
|
|
123
|
-
print(f"
|
|
124
|
-
preds = model.predict(X)
|
|
151
|
+
print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
|
|
152
|
+
preds = model.predict(X)
|
|
125
153
|
anomalous_ips = set(df.loc[preds == -1, 'ip'])
|
|
126
154
|
blocked_anom = []
|
|
127
155
|
for ip in anomalous_ips:
|
|
@@ -132,19 +160,21 @@ def train():
|
|
|
132
160
|
if created:
|
|
133
161
|
blocked_anom.append(ip)
|
|
134
162
|
if blocked_anom:
|
|
135
|
-
print(f" Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
|
|
136
|
-
|
|
163
|
+
print(f"🚫 Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
|
|
137
164
|
tokens = Counter()
|
|
138
165
|
for r in parsed:
|
|
139
|
-
if r["status"].startswith(("4", "5")):
|
|
166
|
+
if r["status"].startswith(("4", "5")) and not path_exists_in_django(r["path"]):
|
|
140
167
|
for seg in re.split(r"\W+", r["path"].lower()):
|
|
141
168
|
if len(seg) > 3 and seg not in STATIC_KW:
|
|
142
169
|
tokens[seg] += 1
|
|
170
|
+
|
|
143
171
|
top_tokens = tokens.most_common(10)
|
|
144
172
|
for kw, cnt in top_tokens:
|
|
145
173
|
obj, _ = DynamicKeyword.objects.get_or_create(keyword=kw)
|
|
146
174
|
DynamicKeyword.objects.filter(pk=obj.pk).update(count=F("count") + cnt)
|
|
147
|
-
|
|
175
|
+
|
|
176
|
+
print(f" DynamicKeyword DB updated with top tokens: {[kw for kw, _ in top_tokens]}")
|
|
177
|
+
|
|
148
178
|
|
|
149
179
|
|
|
150
180
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -128,7 +128,7 @@ Add in **this** order to your `MIDDLEWARE` list:
|
|
|
128
128
|
|
|
129
129
|
```python
|
|
130
130
|
MIDDLEWARE = [
|
|
131
|
-
"aiwaf.middleware.
|
|
131
|
+
"aiwaf.middleware.IPAndKeywordBlockMiddleware",
|
|
132
132
|
"aiwaf.middleware.RateLimitMiddleware",
|
|
133
133
|
"aiwaf.middleware.AIAnomalyMiddleware",
|
|
134
134
|
"aiwaf.middleware.HoneypotMiddleware",
|
|
@@ -180,7 +180,6 @@ python manage.py detect_and_train
|
|
|
180
180
|
| AIAnomalyMiddleware | ML‑driven behavior analysis + block on anomaly |
|
|
181
181
|
| HoneypotMiddleware | Detects bots filling hidden inputs in forms |
|
|
182
182
|
| UUIDTamperMiddleware | Blocks guessed/nonexistent UUIDs across all models in an app |
|
|
183
|
-
|
|
184
183
|
---
|
|
185
184
|
|
|
186
185
|
## License
|
|
@@ -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=2sNCqDULvuASo6dlbvrGpLzwhgHtHXwgVR8u3IhvrDI,6698
|
|
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=IwL-BHbjGunOLX2HuGE12-W_PB0aDwbiZ62izPpfOEo,5796
|
|
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.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
|
|
16
|
+
aiwaf-0.1.7.dist-info/METADATA,sha256=uyKj5eHph-ufrCwZWOtGWxMZD1OtQOXu_6JXz0SRB2Q,5414
|
|
17
|
+
aiwaf-0.1.7.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
18
|
+
aiwaf-0.1.7.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
|
|
19
|
+
aiwaf-0.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|