aiwaf 0.1.7.6__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/resources/model.pkl +0 -0
- aiwaf/trainer.py +74 -77
- {aiwaf-0.1.7.6.dist-info → aiwaf-0.1.7.8.dist-info}/METADATA +28 -4
- {aiwaf-0.1.7.6.dist-info → aiwaf-0.1.7.8.dist-info}/RECORD +10 -9
- {aiwaf-0.1.7.6.dist-info → aiwaf-0.1.7.8.dist-info}/WHEEL +1 -1
- {aiwaf-0.1.7.6.dist-info → aiwaf-0.1.7.8.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.7.6.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/resources/model.pkl
CHANGED
|
Binary file
|
aiwaf/trainer.py
CHANGED
|
@@ -2,23 +2,23 @@ import os
|
|
|
2
2
|
import glob
|
|
3
3
|
import gzip
|
|
4
4
|
import re
|
|
5
|
+
import joblib
|
|
6
|
+
|
|
5
7
|
from datetime import datetime
|
|
6
8
|
from collections import defaultdict, Counter
|
|
7
9
|
|
|
8
10
|
import pandas as pd
|
|
9
11
|
from sklearn.ensemble import IsolationForest
|
|
10
|
-
import joblib
|
|
11
12
|
|
|
12
13
|
from django.conf import settings
|
|
13
14
|
from django.apps import apps
|
|
14
15
|
from django.db.models import F
|
|
15
|
-
from django.urls import get_resolver
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
LOG_PATH
|
|
17
|
+
# ─────────── Configuration ───────────
|
|
18
|
+
LOG_PATH = settings.AIWAF_ACCESS_LOG
|
|
19
19
|
MODEL_PATH = os.path.join(os.path.dirname(__file__), "resources", "model.pkl")
|
|
20
20
|
|
|
21
|
-
STATIC_KW
|
|
21
|
+
STATIC_KW = [".php", "xmlrpc", "wp-", ".env", ".git", ".bak", "conflg", "shell", "filemanager"]
|
|
22
22
|
STATUS_IDX = ["200", "403", "404", "500"]
|
|
23
23
|
|
|
24
24
|
_LOG_RX = re.compile(
|
|
@@ -26,112 +26,114 @@ _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")
|
|
33
|
+
|
|
31
34
|
|
|
32
|
-
def is_exempt_path(path):
|
|
35
|
+
def is_exempt_path(path: str) -> bool:
|
|
33
36
|
path = path.lower()
|
|
34
|
-
|
|
35
|
-
for exempt in exempt_paths:
|
|
37
|
+
for exempt in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
|
|
36
38
|
if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
|
|
37
39
|
return True
|
|
38
40
|
return False
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
def path_exists_in_django(path: str) -> bool:
|
|
41
44
|
from django.urls import get_resolver
|
|
42
|
-
from django.urls.resolvers import
|
|
45
|
+
from django.urls.resolvers import URLResolver
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
candidate = path.split("?")[0].lstrip("/")
|
|
45
48
|
try:
|
|
46
|
-
get_resolver().resolve(f"/{
|
|
49
|
+
get_resolver().resolve(f"/{candidate}")
|
|
47
50
|
return True
|
|
48
51
|
except:
|
|
49
52
|
pass
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for
|
|
53
|
-
if isinstance(
|
|
54
|
-
prefix =
|
|
55
|
-
if prefix and
|
|
53
|
+
|
|
54
|
+
root = get_resolver()
|
|
55
|
+
for p in root.url_patterns:
|
|
56
|
+
if isinstance(p, URLResolver):
|
|
57
|
+
prefix = p.pattern.describe().strip("^/")
|
|
58
|
+
if prefix and candidate.startswith(prefix):
|
|
56
59
|
return True
|
|
57
60
|
return False
|
|
58
61
|
|
|
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
62
|
|
|
63
|
+
def remove_exempt_keywords() -> None:
|
|
64
|
+
exempt_tokens = set()
|
|
65
|
+
for path in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
|
|
66
|
+
for seg in re.split(r"\W+", path.strip("/").lower()):
|
|
67
|
+
if len(seg) > 3:
|
|
68
|
+
exempt_tokens.add(seg)
|
|
68
69
|
if exempt_tokens:
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
DynamicKeyword.objects.filter(keyword__in=exempt_tokens).delete()
|
|
71
|
+
|
|
71
72
|
|
|
72
|
-
def _read_all_logs():
|
|
73
|
+
def _read_all_logs() -> list[str]:
|
|
73
74
|
lines = []
|
|
74
75
|
if LOG_PATH and os.path.exists(LOG_PATH):
|
|
75
76
|
with open(LOG_PATH, "r", errors="ignore") as f:
|
|
76
77
|
lines.extend(f.readlines())
|
|
77
|
-
for
|
|
78
|
-
opener = gzip.open if
|
|
78
|
+
for p in sorted(glob.glob(f"{LOG_PATH}.*")):
|
|
79
|
+
opener = gzip.open if p.endswith(".gz") else open
|
|
79
80
|
try:
|
|
80
|
-
with opener(
|
|
81
|
+
with opener(p, "rt", errors="ignore") as f:
|
|
81
82
|
lines.extend(f.readlines())
|
|
82
83
|
except OSError:
|
|
83
84
|
continue
|
|
84
85
|
return lines
|
|
85
86
|
|
|
86
87
|
|
|
87
|
-
def _parse(line):
|
|
88
|
+
def _parse(line: str) -> dict | None:
|
|
88
89
|
m = _LOG_RX.search(line)
|
|
89
90
|
if not m:
|
|
90
91
|
return None
|
|
91
|
-
ip, ts_str, path, status,
|
|
92
|
+
ip, ts_str, path, status, *_ , rt = m.groups()
|
|
92
93
|
try:
|
|
93
94
|
ts = datetime.strptime(ts_str.split()[0], "%d/%b/%Y:%H:%M:%S")
|
|
94
95
|
except ValueError:
|
|
95
96
|
return None
|
|
96
97
|
return {
|
|
97
|
-
"ip":
|
|
98
|
-
"timestamp":
|
|
99
|
-
"path":
|
|
100
|
-
"status":
|
|
101
|
-
"ua": ua,
|
|
98
|
+
"ip": ip,
|
|
99
|
+
"timestamp": ts,
|
|
100
|
+
"path": path,
|
|
101
|
+
"status": status,
|
|
102
102
|
"response_time": float(rt),
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
def train():
|
|
106
|
+
def train() -> None:
|
|
108
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()
|
|
109
112
|
raw_lines = _read_all_logs()
|
|
110
113
|
if not raw_lines:
|
|
111
114
|
print("No log lines found – check AIWAF_ACCESS_LOG setting.")
|
|
112
115
|
return
|
|
116
|
+
|
|
113
117
|
parsed = []
|
|
114
|
-
ip_404
|
|
118
|
+
ip_404 = defaultdict(int)
|
|
115
119
|
ip_times = defaultdict(list)
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
|
|
121
|
+
for line in raw_lines:
|
|
122
|
+
rec = _parse(line)
|
|
118
123
|
if not rec:
|
|
119
124
|
continue
|
|
120
125
|
parsed.append(rec)
|
|
121
126
|
ip_times[rec["ip"]].append(rec["timestamp"])
|
|
122
127
|
if rec["status"] == "404":
|
|
123
128
|
ip_404[rec["ip"]] += 1
|
|
124
|
-
|
|
129
|
+
|
|
130
|
+
# 3. Optional immediate 404‐flood blocking
|
|
125
131
|
for ip, count in ip_404.items():
|
|
126
132
|
if count >= 6:
|
|
127
|
-
|
|
133
|
+
BlacklistEntry.objects.get_or_create(
|
|
128
134
|
ip_address=ip,
|
|
129
135
|
defaults={"reason": "Excessive 404s (≥6)"}
|
|
130
136
|
)
|
|
131
|
-
if created:
|
|
132
|
-
blocked_404.append(ip)
|
|
133
|
-
if blocked_404:
|
|
134
|
-
print(f"Blocked {len(blocked_404)} IPs for 404 flood: {blocked_404}")
|
|
135
137
|
|
|
136
138
|
feature_dicts = []
|
|
137
139
|
for r in parsed:
|
|
@@ -140,60 +142,55 @@ def train():
|
|
|
140
142
|
1 for t in ip_times[ip]
|
|
141
143
|
if (r["timestamp"] - t).total_seconds() <= 10
|
|
142
144
|
)
|
|
143
|
-
total404
|
|
144
|
-
|
|
145
|
-
kw_hits
|
|
146
|
-
if not
|
|
145
|
+
total404 = ip_404[ip]
|
|
146
|
+
known_path = path_exists_in_django(r["path"])
|
|
147
|
+
kw_hits = 0
|
|
148
|
+
if not known_path and not is_exempt_path(r["path"]):
|
|
147
149
|
kw_hits = sum(k in r["path"].lower() for k in STATIC_KW)
|
|
150
|
+
|
|
148
151
|
status_idx = STATUS_IDX.index(r["status"]) if r["status"] in STATUS_IDX else -1
|
|
152
|
+
|
|
149
153
|
feature_dicts.append({
|
|
150
|
-
"ip":
|
|
151
|
-
"path_len":
|
|
152
|
-
"kw_hits":
|
|
153
|
-
"resp_time":
|
|
154
|
-
"status_idx":
|
|
155
|
-
"burst_count":
|
|
156
|
-
"total_404":
|
|
154
|
+
"ip": ip,
|
|
155
|
+
"path_len": len(r["path"]),
|
|
156
|
+
"kw_hits": kw_hits,
|
|
157
|
+
"resp_time": r["response_time"],
|
|
158
|
+
"status_idx": status_idx,
|
|
159
|
+
"burst_count": burst,
|
|
160
|
+
"total_404": total404,
|
|
157
161
|
})
|
|
158
162
|
|
|
159
163
|
if not feature_dicts:
|
|
160
164
|
print("⚠️ Nothing to train on – no valid log entries.")
|
|
161
165
|
return
|
|
166
|
+
|
|
162
167
|
df = pd.DataFrame(feature_dicts)
|
|
163
168
|
feature_cols = [c for c in df.columns if c != "ip"]
|
|
164
169
|
X = df[feature_cols].astype(float).values
|
|
165
170
|
model = IsolationForest(contamination=0.01, random_state=42)
|
|
166
171
|
model.fit(X)
|
|
172
|
+
|
|
167
173
|
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
|
|
168
174
|
joblib.dump(model, MODEL_PATH)
|
|
169
175
|
print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
|
|
170
176
|
preds = model.predict(X)
|
|
171
|
-
anomalous_ips = set(df.loc[preds == -1,
|
|
172
|
-
blocked_anom = []
|
|
177
|
+
anomalous_ips = set(df.loc[preds == -1, "ip"])
|
|
173
178
|
for ip in anomalous_ips:
|
|
174
|
-
|
|
179
|
+
BlacklistEntry.objects.get_or_create(
|
|
175
180
|
ip_address=ip,
|
|
176
181
|
defaults={"reason": "Anomalous behavior"}
|
|
177
182
|
)
|
|
178
|
-
|
|
179
|
-
blocked_anom.append(ip)
|
|
180
|
-
if blocked_anom:
|
|
181
|
-
print(f"🚫 Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
|
|
183
|
+
|
|
182
184
|
tokens = Counter()
|
|
183
185
|
for r in parsed:
|
|
184
|
-
if r["status"].startswith(("4", "5"))
|
|
186
|
+
if (r["status"].startswith(("4", "5"))
|
|
187
|
+
and not path_exists_in_django(r["path"])):
|
|
185
188
|
for seg in re.split(r"\W+", r["path"].lower()):
|
|
186
189
|
if len(seg) > 3 and seg not in STATIC_KW:
|
|
187
190
|
tokens[seg] += 1
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
for kw, cnt in top_tokens:
|
|
192
|
+
for kw, cnt in tokens.most_common(10):
|
|
191
193
|
obj, _ = DynamicKeyword.objects.get_or_create(keyword=kw)
|
|
192
194
|
DynamicKeyword.objects.filter(pk=obj.pk).update(count=F("count") + cnt)
|
|
193
195
|
|
|
194
|
-
print(f"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if __name__ == "__main__":
|
|
199
|
-
train()
|
|
196
|
+
print(f"DynamicKeyword DB updated with top tokens: {[kw for kw, _ in tokens.most_common(10)]}")
|
|
@@ -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
|
-
aiwaf/resources/model.pkl,sha256=
|
|
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
|