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.

Files changed (27) hide show
  1. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/PKG-INFO +12 -2
  2. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/README.md +11 -1
  3. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/middleware.py +40 -10
  4. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/trainer.py +73 -11
  5. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/PKG-INFO +12 -2
  6. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/pyproject.toml +1 -1
  7. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/setup.py +1 -1
  8. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/LICENSE +0 -0
  9. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/__init__.py +0 -0
  10. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/apps.py +0 -0
  11. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/blacklist_manager.py +0 -0
  12. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/__init__.py +0 -0
  13. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/__init__.py +0 -0
  14. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/add_ipexemption.py +0 -0
  15. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/aiwaf_reset.py +0 -0
  16. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/management/commands/detect_and_train.py +0 -0
  17. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/models.py +0 -0
  18. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/resources/model.pkl +0 -0
  19. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/storage.py +0 -0
  20. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/templatetags/__init__.py +0 -0
  21. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/templatetags/aiwaf_tags.py +0 -0
  22. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf/utils.py +0 -0
  23. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/SOURCES.txt +0 -0
  24. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/dependency_links.txt +0 -0
  25. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/requires.txt +0 -0
  26. {aiwaf-0.1.8.3 → aiwaf-0.1.8.5}/aiwaf.egg-info/top_level.txt +0 -0
  27. {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
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
- Set `AIWAF_EXEMPT_PATHS` in your Django `settings.py` (not in your code). Fully respects this setting across all modules exempt paths are:
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
- Set `AIWAF_EXEMPT_PATHS` in your Django `settings.py` (not in your code). Fully respects this setting across all modules exempt paths are:
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
- BlacklistManager.block(ip, "Direct POST without GET")
214
- return JsonResponse({"error": "blocked"}, status=403)
215
-
216
- # Check timing
217
- time_diff = time.time() - get_time
218
- if time_diff < self.MIN_FORM_TIME:
219
- # Posted too quickly - likely bot
220
- BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
221
- return JsonResponse({"error": "blocked"}, status=403)
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
- ip_404[rec["ip"]] += 1
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
- BlacklistEntry.objects.get_or_create(
134
- ip_address=ip,
135
- defaults={"reason": "Excessive 404s (≥6)"}
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
- for ip in anomalous_ips:
182
- BlacklistEntry.objects.get_or_create(
183
- ip_address=ip,
184
- defaults={"reason": "Anomalous behavior"}
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
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
- Set `AIWAF_EXEMPT_PATHS` in your Django `settings.py` (not in your code). Fully respects this setting across all modules exempt paths are:
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiwaf"
3
- version = "0.1.8.3"
3
+ version = "0.1.8.5"
4
4
  description = "AI-powered Web Application Firewall"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -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.3",
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