aiwaf 0.1.7.7__py3-none-any.whl → 0.1.7.8__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/management/commands/add_ipexemption.py +22 -0
- aiwaf/middleware.py +22 -11
- aiwaf/models.py +11 -1
- aiwaf/trainer.py +6 -0
- {aiwaf-0.1.7.7.dist-info → aiwaf-0.1.7.8.dist-info}/METADATA +28 -4
- {aiwaf-0.1.7.7.dist-info → aiwaf-0.1.7.8.dist-info}/RECORD +9 -8
- {aiwaf-0.1.7.7.dist-info → aiwaf-0.1.7.8.dist-info}/WHEEL +1 -1
- {aiwaf-0.1.7.7.dist-info → aiwaf-0.1.7.8.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.7.7.dist-info → aiwaf-0.1.7.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
2
|
+
from aiwaf.models import IPExemption
|
|
3
|
+
|
|
4
|
+
class Command(BaseCommand):
|
|
5
|
+
help = 'Add an IP address to the IPExemption list (prevents blacklisting)'
|
|
6
|
+
|
|
7
|
+
def add_arguments(self, parser):
|
|
8
|
+
parser.add_argument('ip', type=str, help='IP address to exempt')
|
|
9
|
+
parser.add_argument('--reason', type=str, default='', help='Reason for exemption (optional)')
|
|
10
|
+
|
|
11
|
+
def handle(self, *args, **options):
|
|
12
|
+
ip = options['ip']
|
|
13
|
+
reason = options['reason']
|
|
14
|
+
obj, created = IPExemption.objects.get_or_create(ip_address=ip, defaults={'reason': reason})
|
|
15
|
+
if not created:
|
|
16
|
+
self.stdout.write(self.style.WARNING(f'IP {ip} is already exempted.'))
|
|
17
|
+
else:
|
|
18
|
+
self.stdout.write(self.style.SUCCESS(f'IP {ip} added to exemption list.'))
|
|
19
|
+
if reason:
|
|
20
|
+
obj.reason = reason
|
|
21
|
+
obj.save()
|
|
22
|
+
self.stdout.write(self.style.SUCCESS(f'Reason set to: {reason}'))
|
aiwaf/middleware.py
CHANGED
|
@@ -16,7 +16,9 @@ from django.apps import apps
|
|
|
16
16
|
from django.urls import get_resolver
|
|
17
17
|
from .trainer import STATIC_KW, STATUS_IDX, is_exempt_path, path_exists_in_django
|
|
18
18
|
from .blacklist_manager import BlacklistManager
|
|
19
|
-
from .models import DynamicKeyword
|
|
19
|
+
from .models import DynamicKeyword, IPExemption
|
|
20
|
+
def is_ip_exempted(ip):
|
|
21
|
+
return IPExemption.objects.filter(ip_address=ip).exists()
|
|
20
22
|
|
|
21
23
|
def is_exempt_path(path):
|
|
22
24
|
path = path.lower()
|
|
@@ -77,6 +79,8 @@ class IPAndKeywordBlockMiddleware:
|
|
|
77
79
|
return self.get_response(request)
|
|
78
80
|
ip = get_ip(request)
|
|
79
81
|
path = raw_path.lstrip("/")
|
|
82
|
+
if is_ip_exempted(ip):
|
|
83
|
+
return self.get_response(request)
|
|
80
84
|
if BlacklistManager.is_blocked(ip):
|
|
81
85
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
82
86
|
segments = [seg for seg in re.split(r"\W+", path) if len(seg) > 3]
|
|
@@ -95,8 +99,9 @@ class IPAndKeywordBlockMiddleware:
|
|
|
95
99
|
}
|
|
96
100
|
for seg in segments:
|
|
97
101
|
if seg in suspicious_kw:
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
if not is_ip_exempted(ip):
|
|
103
|
+
BlacklistManager.block(ip, f"Keyword block: {seg}")
|
|
104
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
100
105
|
return self.get_response(request)
|
|
101
106
|
|
|
102
107
|
|
|
@@ -120,8 +125,9 @@ class RateLimitMiddleware:
|
|
|
120
125
|
timestamps.append(now)
|
|
121
126
|
cache.set(key, timestamps, timeout=self.WINDOW)
|
|
122
127
|
if len(timestamps) > self.FLOOD:
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
if not is_ip_exempted(ip):
|
|
129
|
+
BlacklistManager.block(ip, "Flood pattern")
|
|
130
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
125
131
|
if len(timestamps) > self.MAX:
|
|
126
132
|
return JsonResponse({"error": "too_many_requests"}, status=429)
|
|
127
133
|
return self.get_response(request)
|
|
@@ -141,6 +147,8 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
141
147
|
return None
|
|
142
148
|
request._start_time = time.time()
|
|
143
149
|
ip = get_ip(request)
|
|
150
|
+
if is_ip_exempted(ip):
|
|
151
|
+
return None
|
|
144
152
|
if BlacklistManager.is_blocked(ip):
|
|
145
153
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
146
154
|
return None
|
|
@@ -166,8 +174,9 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
166
174
|
feats = [path_len, kw_hits, resp_time, status_idx, burst_count, total_404]
|
|
167
175
|
X = np.array(feats, dtype=float).reshape(1, -1)
|
|
168
176
|
if self.model.predict(X)[0] == -1:
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
if not is_ip_exempted(ip):
|
|
178
|
+
BlacklistManager.block(ip, "AI anomaly")
|
|
179
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
171
180
|
|
|
172
181
|
data.append((now, request.path, response.status_code, resp_time))
|
|
173
182
|
data = [d for d in data if now - d[0] < self.WINDOW]
|
|
@@ -187,8 +196,9 @@ class HoneypotMiddleware(MiddlewareMixin):
|
|
|
187
196
|
trap = request.POST.get(getattr(settings, "AIWAF_HONEYPOT_FIELD", "hp_field"), "")
|
|
188
197
|
if trap:
|
|
189
198
|
ip = get_ip(request)
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
if not is_ip_exempted(ip):
|
|
200
|
+
BlacklistManager.block(ip, "HONEYPOT triggered")
|
|
201
|
+
return JsonResponse({"error": "bot_detected"}, status=403)
|
|
192
202
|
return None
|
|
193
203
|
|
|
194
204
|
|
|
@@ -211,5 +221,6 @@ class UUIDTamperMiddleware(MiddlewareMixin):
|
|
|
211
221
|
except (ValueError, TypeError):
|
|
212
222
|
continue
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
if not is_ip_exempted(ip):
|
|
225
|
+
BlacklistManager.block(ip, "UUID tampering")
|
|
226
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
aiwaf/models.py
CHANGED
|
@@ -33,4 +33,14 @@ class DynamicKeyword(models.Model):
|
|
|
33
33
|
last_updated = models.DateTimeField(auto_now=True)
|
|
34
34
|
|
|
35
35
|
class Meta:
|
|
36
|
-
ordering = ['-count']
|
|
36
|
+
ordering = ['-count']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Model to store IP addresses that are exempt from blacklisting
|
|
40
|
+
class IPExemption(models.Model):
|
|
41
|
+
ip_address = models.GenericIPAddressField(unique=True, db_index=True)
|
|
42
|
+
reason = models.CharField(max_length=100, blank=True, default="")
|
|
43
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
return f"{self.ip_address} (Exempted: {self.reason})"
|
aiwaf/trainer.py
CHANGED
|
@@ -26,8 +26,10 @@ _LOG_RX = re.compile(
|
|
|
26
26
|
r'(\d{3}).*?"(.*?)" "(.*?)".*?response-time=(\d+\.\d+)'
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
|
|
29
30
|
BlacklistEntry = apps.get_model("aiwaf", "BlacklistEntry")
|
|
30
31
|
DynamicKeyword = apps.get_model("aiwaf", "DynamicKeyword")
|
|
32
|
+
IPExemption = apps.get_model("aiwaf", "IPExemption")
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def is_exempt_path(path: str) -> bool:
|
|
@@ -103,6 +105,10 @@ def _parse(line: str) -> dict | None:
|
|
|
103
105
|
|
|
104
106
|
def train() -> None:
|
|
105
107
|
remove_exempt_keywords()
|
|
108
|
+
# Remove any IPs in IPExemption from the blacklist
|
|
109
|
+
exempt_ips = set(IPExemption.objects.values_list("ip_address", flat=True))
|
|
110
|
+
if exempt_ips:
|
|
111
|
+
BlacklistEntry.objects.filter(ip_address__in=exempt_ips).delete()
|
|
106
112
|
raw_lines = _read_all_logs()
|
|
107
113
|
if not raw_lines:
|
|
108
114
|
print("No log lines found – check AIWAF_ACCESS_LOG setting.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.7.
|
|
3
|
+
Version: 0.1.7.8
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -9,6 +9,11 @@ License: MIT
|
|
|
9
9
|
Requires-Python: >=3.8
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
|
+
Requires-Dist: Django>=3.2
|
|
13
|
+
Requires-Dist: numpy>=1.21
|
|
14
|
+
Requires-Dist: pandas>=1.3
|
|
15
|
+
Requires-Dist: scikit-learn>=1.0
|
|
16
|
+
Requires-Dist: joblib>=1.1
|
|
12
17
|
Dynamic: author
|
|
13
18
|
Dynamic: home-page
|
|
14
19
|
Dynamic: license-file
|
|
@@ -78,13 +83,32 @@ aiwaf/
|
|
|
78
83
|
- **UUID Tampering Protection**
|
|
79
84
|
Blocks guessed or invalid UUIDs that don’t resolve to real models.
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
|
|
87
|
+
**Exempt Path & IP Awareness**
|
|
88
|
+
|
|
89
|
+
**Exempt Paths:**
|
|
90
|
+
Set `AIWAF_EXEMPT_PATHS` in your Django `settings.py` (not in your code). Fully respects this setting across all modules — exempt paths are:
|
|
83
91
|
- Skipped from keyword learning
|
|
84
92
|
- Immune to AI blocking
|
|
85
93
|
- Ignored in log training
|
|
86
94
|
- Cleaned from `DynamicKeyword` model automatically
|
|
87
95
|
|
|
96
|
+
**Exempt IPs:**
|
|
97
|
+
You can exempt specific IP addresses from all blocking and blacklisting logic. Exempted IPs will:
|
|
98
|
+
- Never be added to the blacklist (even if they trigger rules)
|
|
99
|
+
- Be automatically removed from the blacklist during retraining
|
|
100
|
+
- Bypass all block/deny logic in middleware
|
|
101
|
+
|
|
102
|
+
### Managing Exempt IPs
|
|
103
|
+
|
|
104
|
+
Add an IP to the exemption list using the management command:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python manage.py add_ipexemption <ip-address> --reason "optional reason"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This will ensure the IP is never blocked by AI‑WAF. You can also manage exemptions via the Django admin interface.
|
|
111
|
+
|
|
88
112
|
- **Daily Retraining**
|
|
89
113
|
Reads rotated logs, auto‑blocks 404 floods, retrains the IsolationForest, updates `model.pkl`, and evolves the keyword DB.
|
|
90
114
|
|
|
@@ -125,7 +149,7 @@ AIWAF_RATE_MAX = 20 # max requests per window
|
|
|
125
149
|
AIWAF_RATE_FLOOD = 10 # flood threshold
|
|
126
150
|
AIWAF_WINDOW_SECONDS = 60 # anomaly detection window
|
|
127
151
|
AIWAF_FILE_EXTENSIONS = [".php", ".asp", ".jsp"]
|
|
128
|
-
AIWAF_EXEMPT_PATHS
|
|
152
|
+
AIWAF_EXEMPT_PATHS = [ # optional but highly recommended
|
|
129
153
|
"/favicon.ico",
|
|
130
154
|
"/robots.txt",
|
|
131
155
|
"/static/",
|
|
@@ -1,19 +1,20 @@
|
|
|
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=
|
|
5
|
-
aiwaf/models.py,sha256=
|
|
4
|
+
aiwaf/middleware.py,sha256=8iMoJXGQ86HtvVbmAJa7ykZG-QRdXMIfYmD-hbF8TFg,8456
|
|
5
|
+
aiwaf/models.py,sha256=XaG1pd_oZu3y-fw66u4wblGlWcUY9gvsTNKGD0kQk7Y,1672
|
|
6
6
|
aiwaf/storage.py,sha256=bxCILzzvA1-q6nwclRE8WrfoRhe25H4VrsQDf0hl_lY,1903
|
|
7
|
-
aiwaf/trainer.py,sha256=
|
|
7
|
+
aiwaf/trainer.py,sha256=Xs_AuA7RCa1oyo5-lJYlnRYUiaq-HY2KXAviAdiGnzU,6217
|
|
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
|
|
11
|
+
aiwaf/management/commands/add_ipexemption.py,sha256=LWN21_ydqSjU3_hUnkou4Ciyrk_479zLvcKdWm8hkC0,988
|
|
11
12
|
aiwaf/management/commands/detect_and_train.py,sha256=-o-LZ7QZ5GeJPCekryox1DGXKMmFEkwwrcDsiM166K0,269
|
|
12
13
|
aiwaf/resources/model.pkl,sha256=5t6h9BX8yoh2xct85MXOO60jdlWyg1APskUOW0jZE1Y,1288265
|
|
13
14
|
aiwaf/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
15
|
aiwaf/templatetags/aiwaf_tags.py,sha256=1KGqeioYmgKACDUiPkykSqI7DLQ6-Ypy1k00weWj9iY,399
|
|
15
|
-
aiwaf-0.1.7.
|
|
16
|
-
aiwaf-0.1.7.
|
|
17
|
-
aiwaf-0.1.7.
|
|
18
|
-
aiwaf-0.1.7.
|
|
19
|
-
aiwaf-0.1.7.
|
|
16
|
+
aiwaf-0.1.7.8.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
|
|
17
|
+
aiwaf-0.1.7.8.dist-info/METADATA,sha256=Hthw0o3R1p6JeLo8qfHYKrVCYHB5RgD9lP1zTWlgA8s,6920
|
|
18
|
+
aiwaf-0.1.7.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
aiwaf-0.1.7.8.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
|
|
20
|
+
aiwaf-0.1.7.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|