aiwaf 0.1.8.3__tar.gz → 0.1.8.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiwaf might be problematic. Click here for more details.
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/PKG-INFO +12 -2
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/README.md +11 -1
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/middleware.py +40 -10
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/trainer.py +73 -11
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/PKG-INFO +12 -2
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/pyproject.toml +1 -1
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/setup.py +1 -1
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/LICENSE +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/__init__.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/apps.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/blacklist_manager.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/__init__.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/__init__.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/add_ipexemption.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/aiwaf_reset.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/detect_and_train.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/models.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/resources/model.pkl +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/storage.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/templatetags/__init__.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/templatetags/aiwaf_tags.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/utils.py +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/SOURCES.txt +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/dependency_links.txt +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/requires.txt +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/top_level.txt +0 -0
- {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/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.5
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -89,7 +89,17 @@ aiwaf/
|
|
|
89
89
|
**Exempt Path & IP Awareness**
|
|
90
90
|
|
|
91
91
|
**Exempt Paths:**
|
|
92
|
-
|
|
92
|
+
AI‑WAF automatically exempts common login paths (`/admin/`, `/login/`, `/accounts/login/`, etc.) from all blocking mechanisms. You can add additional exempt paths in your Django `settings.py`:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
AIWAF_EXEMPT_PATHS = [
|
|
96
|
+
"/api/webhooks/",
|
|
97
|
+
"/health/",
|
|
98
|
+
"/special-endpoint/",
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
All exempt paths are:
|
|
93
103
|
- Skipped from keyword learning
|
|
94
104
|
- Immune to AI blocking
|
|
95
105
|
- Ignored in log training
|
|
@@ -68,7 +68,17 @@ aiwaf/
|
|
|
68
68
|
**Exempt Path & IP Awareness**
|
|
69
69
|
|
|
70
70
|
**Exempt Paths:**
|
|
71
|
-
|
|
71
|
+
AI‑WAF automatically exempts common login paths (`/admin/`, `/login/`, `/accounts/login/`, etc.) from all blocking mechanisms. You can add additional exempt paths in your Django `settings.py`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
AIWAF_EXEMPT_PATHS = [
|
|
75
|
+
"/api/webhooks/",
|
|
76
|
+
"/health/",
|
|
77
|
+
"/special-endpoint/",
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
All exempt paths are:
|
|
72
82
|
- Skipped from keyword learning
|
|
73
83
|
- Immune to AI blocking
|
|
74
84
|
- Ignored in log training
|
|
@@ -22,10 +22,28 @@ def is_ip_exempted(ip):
|
|
|
22
22
|
|
|
23
23
|
def is_exempt_path(path):
|
|
24
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
|
|
25
42
|
exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
|
|
26
43
|
for exempt in exempt_paths:
|
|
27
44
|
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
28
45
|
return True
|
|
46
|
+
|
|
29
47
|
return False
|
|
30
48
|
|
|
31
49
|
MODEL_PATH = getattr(
|
|
@@ -201,7 +219,8 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
201
219
|
return None
|
|
202
220
|
|
|
203
221
|
if request.method == "GET":
|
|
204
|
-
# Store timestamp for this IP's GET request
|
|
222
|
+
# Store timestamp for this IP's GET request
|
|
223
|
+
# Use a general key for the IP, not path-specific
|
|
205
224
|
cache.set(f"honeypot_get:{ip}", time.time(), timeout=300) # 5 min timeout
|
|
206
225
|
|
|
207
226
|
elif request.method == "POST":
|
|
@@ -210,15 +229,26 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
210
229
|
|
|
211
230
|
if get_time is None:
|
|
212
231
|
# No GET request - likely bot posting directly
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
232
|
+
# But be more lenient for login paths since users might bookmark them
|
|
233
|
+
if not any(request.path.lower().startswith(login_path) for login_path in [
|
|
234
|
+
"/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
|
|
235
|
+
]):
|
|
236
|
+
BlacklistManager.block(ip, "Direct POST without GET")
|
|
237
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
238
|
+
else:
|
|
239
|
+
# Check timing - be more lenient for login paths
|
|
240
|
+
time_diff = time.time() - get_time
|
|
241
|
+
min_time = self.MIN_FORM_TIME
|
|
242
|
+
|
|
243
|
+
# Use shorter time threshold for login paths (users can login quickly)
|
|
244
|
+
if any(request.path.lower().startswith(login_path) for login_path in [
|
|
245
|
+
"/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
|
|
246
|
+
]):
|
|
247
|
+
min_time = 0.1 # Very short threshold for login forms
|
|
248
|
+
|
|
249
|
+
if time_diff < min_time:
|
|
250
|
+
BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
|
|
251
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
222
252
|
|
|
223
253
|
return None
|
|
224
254
|
|
|
@@ -34,6 +34,23 @@ IPExemption = apps.get_model("aiwaf", "IPExemption")
|
|
|
34
34
|
|
|
35
35
|
def is_exempt_path(path: str) -> bool:
|
|
36
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
|
|
37
54
|
for exempt in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
|
|
38
55
|
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
39
56
|
return True
|
|
@@ -116,6 +133,7 @@ def train() -> None:
|
|
|
116
133
|
|
|
117
134
|
parsed = []
|
|
118
135
|
ip_404 = defaultdict(int)
|
|
136
|
+
ip_404_login = defaultdict(int) # Track 404s on login paths separately
|
|
119
137
|
ip_times = defaultdict(list)
|
|
120
138
|
|
|
121
139
|
for line in raw_lines:
|
|
@@ -125,15 +143,24 @@ def train() -> None:
|
|
|
125
143
|
parsed.append(rec)
|
|
126
144
|
ip_times[rec["ip"]].append(rec["timestamp"])
|
|
127
145
|
if rec["status"] == "404":
|
|
128
|
-
|
|
146
|
+
if is_exempt_path(rec["path"]):
|
|
147
|
+
ip_404_login[rec["ip"]] += 1 # Login path 404s
|
|
148
|
+
else:
|
|
149
|
+
ip_404[rec["ip"]] += 1 # Non-login path 404s
|
|
129
150
|
|
|
130
|
-
# 3. Optional immediate 404‐flood blocking
|
|
151
|
+
# 3. Optional immediate 404‐flood blocking (only for non-login paths)
|
|
131
152
|
for ip, count in ip_404.items():
|
|
132
153
|
if count >= 6:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
# Only block if they have significant non-login 404s
|
|
155
|
+
login_404s = ip_404_login.get(ip, 0)
|
|
156
|
+
total_404s = count + login_404s
|
|
157
|
+
|
|
158
|
+
# Don't block if majority of 404s are on login paths
|
|
159
|
+
if count > login_404s: # More non-login 404s than login 404s
|
|
160
|
+
BlacklistEntry.objects.get_or_create(
|
|
161
|
+
ip_address=ip,
|
|
162
|
+
defaults={"reason": f"Excessive 404s (≥6 non-login, {count}/{total_404s})"}
|
|
163
|
+
)
|
|
137
164
|
|
|
138
165
|
feature_dicts = []
|
|
139
166
|
for r in parsed:
|
|
@@ -176,13 +203,48 @@ def train() -> None:
|
|
|
176
203
|
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
|
|
177
204
|
joblib.dump(model, MODEL_PATH)
|
|
178
205
|
print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
|
|
206
|
+
|
|
207
|
+
# Check for anomalies and intelligently decide which IPs to block
|
|
179
208
|
preds = model.predict(X)
|
|
180
209
|
anomalous_ips = set(df.loc[preds == -1, "ip"])
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
210
|
+
|
|
211
|
+
if anomalous_ips:
|
|
212
|
+
print(f"⚠️ Detected {len(anomalous_ips)} potentially anomalous IPs during training")
|
|
213
|
+
|
|
214
|
+
blocked_count = 0
|
|
215
|
+
for ip in anomalous_ips:
|
|
216
|
+
# Skip if IP is exempted
|
|
217
|
+
if IPExemption.objects.filter(ip_address=ip).exists():
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Get this IP's behavior from the data
|
|
221
|
+
ip_data = df[df["ip"] == ip]
|
|
222
|
+
|
|
223
|
+
# Criteria to determine if this is likely a legitimate user vs threat:
|
|
224
|
+
avg_kw_hits = ip_data["kw_hits"].mean()
|
|
225
|
+
max_404s = ip_data["total_404"].max()
|
|
226
|
+
avg_burst = ip_data["burst_count"].mean()
|
|
227
|
+
total_requests = len(ip_data)
|
|
228
|
+
|
|
229
|
+
# Don't block if it looks like legitimate behavior:
|
|
230
|
+
if (
|
|
231
|
+
avg_kw_hits < 2 and # Not hitting many malicious keywords
|
|
232
|
+
max_404s < 10 and # Not excessive 404s
|
|
233
|
+
avg_burst < 15 and # Not excessive burst activity
|
|
234
|
+
total_requests < 100 # Not excessive total requests
|
|
235
|
+
):
|
|
236
|
+
print(f" - {ip}: Anomalous but looks legitimate (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f}) - NOT blocking")
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# Block if it shows clear signs of malicious behavior
|
|
240
|
+
BlacklistEntry.objects.get_or_create(
|
|
241
|
+
ip_address=ip,
|
|
242
|
+
defaults={"reason": f"AI anomaly + suspicious patterns (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})"}
|
|
243
|
+
)
|
|
244
|
+
blocked_count += 1
|
|
245
|
+
print(f" - {ip}: Blocked for suspicious behavior (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})")
|
|
246
|
+
|
|
247
|
+
print(f" → Blocked {blocked_count}/{len(anomalous_ips)} anomalous IPs (others looked legitimate)")
|
|
186
248
|
|
|
187
249
|
tokens = Counter()
|
|
188
250
|
for r in parsed:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.8.
|
|
3
|
+
Version: 0.1.8.5
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -89,7 +89,17 @@ aiwaf/
|
|
|
89
89
|
**Exempt Path & IP Awareness**
|
|
90
90
|
|
|
91
91
|
**Exempt Paths:**
|
|
92
|
-
|
|
92
|
+
AI‑WAF automatically exempts common login paths (`/admin/`, `/login/`, `/accounts/login/`, etc.) from all blocking mechanisms. You can add additional exempt paths in your Django `settings.py`:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
AIWAF_EXEMPT_PATHS = [
|
|
96
|
+
"/api/webhooks/",
|
|
97
|
+
"/health/",
|
|
98
|
+
"/special-endpoint/",
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
All exempt paths are:
|
|
93
103
|
- Skipped from keyword learning
|
|
94
104
|
- Immune to AI blocking
|
|
95
105
|
- 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.5",
|
|
13
13
|
description="AI‑driven, self‑learning Web Application Firewall for Django",
|
|
14
14
|
long_description=long_description,
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|