aiwaf 0.1.9.0.4__py3-none-any.whl → 0.1.9.0.6__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/storage.py CHANGED
@@ -1,4 +1,3 @@
1
- import os, csv, gzip, glob
2
1
  import numpy as np
3
2
  import pandas as pd
4
3
  from django.conf import settings
@@ -22,402 +21,209 @@ def _import_models():
22
21
  # Keep models as None if can't import
23
22
  pass
24
23
 
25
- # Configuration
26
- STORAGE_MODE = getattr(settings, "AIWAF_STORAGE_MODE", "models") # "models" or "csv"
27
- CSV_DATA_DIR = getattr(settings, "AIWAF_CSV_DATA_DIR", "aiwaf_data")
28
- FEATURE_CSV = getattr(settings, "AIWAF_CSV_PATH", os.path.join(CSV_DATA_DIR, "access_samples.csv"))
29
- BLACKLIST_CSV = os.path.join(CSV_DATA_DIR, "blacklist.csv")
30
- EXEMPTION_CSV = os.path.join(CSV_DATA_DIR, "exemptions.csv")
31
- KEYWORDS_CSV = os.path.join(CSV_DATA_DIR, "keywords.csv")
32
-
33
- CSV_HEADER = [
34
- "ip","path_len","kw_hits","resp_time",
35
- "status_idx","burst_count","total_404","label"
36
- ]
37
-
38
- def ensure_csv_directory():
39
- """Ensure the CSV data directory exists"""
40
- if STORAGE_MODE == "csv" and not os.path.exists(CSV_DATA_DIR):
41
- os.makedirs(CSV_DATA_DIR)
42
-
43
- class CsvFeatureStore:
44
- @staticmethod
45
- def persist_rows(rows):
46
- ensure_csv_directory()
47
- new_file = not os.path.exists(FEATURE_CSV)
48
- with open(FEATURE_CSV, "a", newline="", encoding="utf-8") as f:
49
- w = csv.writer(f)
50
- if new_file:
51
- w.writerow(CSV_HEADER)
52
- w.writerows(rows)
53
-
54
- @staticmethod
55
- def load_matrix():
56
- if not os.path.exists(FEATURE_CSV):
57
- return np.empty((0,6))
58
- df = pd.read_csv(
59
- FEATURE_CSV,
60
- names=CSV_HEADER,
61
- skiprows=1,
62
- engine="python",
63
- on_bad_lines="skip"
64
- )
65
- feature_cols = CSV_HEADER[1:7]
66
- df[feature_cols] = df[feature_cols].apply(pd.to_numeric, errors="coerce").fillna(0)
67
- return df[feature_cols].to_numpy()
68
-
69
- class DbFeatureStore:
24
+ class ModelFeatureStore:
70
25
  @staticmethod
71
26
  def persist_rows(rows):
27
+ """Persist feature data to Django models"""
72
28
  _import_models()
73
- if FeatureSample is not None:
74
- objs = []
75
- for ip,pl,kw,rt,si,bc,t404,label in rows:
76
- objs.append(FeatureSample(
77
- ip=ip, path_len=pl, kw_hits=kw,
78
- resp_time=rt, status_idx=si,
79
- burst_count=bc, total_404=t404,
80
- label=label
81
- ))
82
- FeatureSample.objects.bulk_create(objs, ignore_conflicts=True)
83
-
84
- @staticmethod
85
- def load_matrix():
29
+ if FeatureSample is None:
30
+ print("Warning: Django models not available, skipping feature storage")
31
+ return
32
+
33
+ for row in rows:
34
+ try:
35
+ FeatureSample.objects.create(
36
+ ip=row[0],
37
+ path_len=int(row[1]),
38
+ kw_hits=int(row[2]),
39
+ resp_time=float(row[3]),
40
+ status_idx=int(row[4]),
41
+ burst_count=int(row[5]),
42
+ total_404=int(row[6]),
43
+ label=int(row[7]),
44
+ created_at=timezone.now()
45
+ )
46
+ except Exception as e:
47
+ print(f"Error saving feature sample: {e}")
48
+
49
+ @staticmethod
50
+ def get_all_data():
51
+ """Get all feature data as DataFrame"""
86
52
  _import_models()
87
- if FeatureSample is not None:
88
- qs = FeatureSample.objects.all().values_list(
89
- "path_len","kw_hits","resp_time","status_idx","burst_count","total_404"
53
+ if FeatureSample is None:
54
+ return pd.DataFrame()
55
+
56
+ try:
57
+ queryset = FeatureSample.objects.all().values(
58
+ 'ip', 'path_len', 'kw_hits', 'resp_time',
59
+ 'status_idx', 'burst_count', 'total_404', 'label'
90
60
  )
91
- return np.array(list(qs), dtype=float)
92
- return np.empty((0,6))
93
-
94
- def get_store():
95
- if getattr(settings, "AIWAF_FEATURE_STORE", "csv") == "db":
96
- return DbFeatureStore
97
- return CsvFeatureStore
98
-
99
-
100
- # ============= CSV Storage Classes =============
61
+ df = pd.DataFrame(list(queryset))
62
+ if df.empty:
63
+ return df
64
+
65
+ # Ensure proper column order and types
66
+ feature_cols = ['path_len', 'kw_hits', 'resp_time', 'status_idx', 'burst_count', 'total_404']
67
+ for col in feature_cols:
68
+ if col in df.columns:
69
+ df[col] = pd.to_numeric(df[col], errors='coerce')
70
+
71
+ return df
72
+ except Exception as e:
73
+ print(f"Error loading feature data: {e}")
74
+ return pd.DataFrame()
101
75
 
102
- class CsvBlacklistStore:
103
- """CSV-based storage for IP blacklist entries"""
104
-
105
- @staticmethod
106
- def add_ip(ip_address, reason):
107
- ensure_csv_directory()
108
- # Check if IP already exists
109
- if CsvBlacklistStore.is_blocked(ip_address):
110
- return
111
-
112
- # Add new entry
113
- new_file = not os.path.exists(BLACKLIST_CSV)
114
- with open(BLACKLIST_CSV, "a", newline="", encoding="utf-8") as f:
115
- writer = csv.writer(f)
116
- if new_file:
117
- writer.writerow(["ip_address", "reason", "created_at"])
118
- writer.writerow([ip_address, reason, timezone.now().isoformat()])
119
-
76
+ class ModelBlacklistStore:
120
77
  @staticmethod
121
- def is_blocked(ip_address):
122
- if not os.path.exists(BLACKLIST_CSV):
78
+ def is_blocked(ip):
79
+ """Check if IP is in blacklist"""
80
+ _import_models()
81
+ if BlacklistEntry is None:
82
+ return False
83
+ try:
84
+ return BlacklistEntry.objects.filter(ip_address=ip).exists()
85
+ except Exception:
123
86
  return False
124
-
125
- with open(BLACKLIST_CSV, "r", newline="", encoding="utf-8") as f:
126
- reader = csv.DictReader(f)
127
- for row in reader:
128
- if row["ip_address"] == ip_address:
129
- return True
130
- return False
131
-
132
- @staticmethod
133
- def get_all():
134
- """Return list of dictionaries with blacklist entries"""
135
- if not os.path.exists(BLACKLIST_CSV):
136
- return []
137
-
138
- entries = []
139
- with open(BLACKLIST_CSV, "r", newline="", encoding="utf-8") as f:
140
- reader = csv.DictReader(f)
141
- for row in reader:
142
- entries.append(row)
143
- return entries
144
-
145
- @staticmethod
146
- def remove_ip(ip_address):
147
- if not os.path.exists(BLACKLIST_CSV):
148
- return
149
-
150
- # Read all entries except the one to remove
151
- entries = []
152
- with open(BLACKLIST_CSV, "r", newline="", encoding="utf-8") as f:
153
- reader = csv.DictReader(f)
154
- entries = [row for row in reader if row["ip_address"] != ip_address]
155
-
156
- # Write back the filtered entries
157
- with open(BLACKLIST_CSV, "w", newline="", encoding="utf-8") as f:
158
- if entries:
159
- writer = csv.DictWriter(f, fieldnames=["ip_address", "reason", "created_at"])
160
- writer.writeheader()
161
- writer.writerows(entries)
162
-
163
87
 
164
- class CsvExemptionStore:
165
- """CSV-based storage for IP exemption entries"""
166
-
167
88
  @staticmethod
168
- def add_ip(ip_address, reason=""):
169
- ensure_csv_directory()
170
-
171
- # Check if IP already exists to avoid duplicates
172
- if CsvExemptionStore.is_exempted(ip_address):
89
+ def block_ip(ip, reason="Automated block"):
90
+ """Add IP to blacklist"""
91
+ _import_models()
92
+ if BlacklistEntry is None:
93
+ print(f"Warning: Cannot block IP {ip}, models not available")
173
94
  return
174
-
175
- # Add new entry
176
- new_file = not os.path.exists(EXEMPTION_CSV)
177
95
  try:
178
- with open(EXEMPTION_CSV, "a", newline="", encoding="utf-8") as f:
179
- writer = csv.writer(f)
180
- if new_file:
181
- writer.writerow(["ip_address", "reason", "created_at"])
182
- writer.writerow([ip_address, reason, timezone.now().isoformat()])
96
+ BlacklistEntry.objects.get_or_create(
97
+ ip_address=ip,
98
+ defaults={'reason': reason, 'created_at': timezone.now()}
99
+ )
183
100
  except Exception as e:
184
- print(f"Error writing to exemption CSV: {e}")
185
- print(f"File path: {EXEMPTION_CSV}")
186
- print(f"Directory exists: {os.path.exists(CSV_DATA_DIR)}")
187
- raise
188
-
101
+ print(f"Error blocking IP {ip}: {e}")
102
+
189
103
  @staticmethod
190
- def is_exempted(ip_address):
191
- if not os.path.exists(EXEMPTION_CSV):
192
- return False
193
-
104
+ def unblock_ip(ip):
105
+ """Remove IP from blacklist"""
106
+ _import_models()
107
+ if BlacklistEntry is None:
108
+ return
194
109
  try:
195
- with open(EXEMPTION_CSV, "r", newline="", encoding="utf-8") as f:
196
- reader = csv.DictReader(f)
197
- for row in reader:
198
- if row["ip_address"] == ip_address:
199
- return True
110
+ BlacklistEntry.objects.filter(ip_address=ip).delete()
200
111
  except Exception as e:
201
- print(f"Error reading exemption CSV: {e}")
202
- return False
203
- return False
204
-
205
- @staticmethod
206
- def get_all():
207
- """Return list of dictionaries with exemption entries"""
208
- if not os.path.exists(EXEMPTION_CSV):
209
- return []
210
-
211
- entries = []
212
- with open(EXEMPTION_CSV, "r", newline="", encoding="utf-8") as f:
213
- reader = csv.DictReader(f)
214
- for row in reader:
215
- entries.append(row)
216
- return entries
217
-
218
- @staticmethod
219
- def remove_ip(ip_address):
220
- if not os.path.exists(EXEMPTION_CSV):
221
- return
222
-
223
- # Read all entries except the one to remove
224
- entries = []
225
- with open(EXEMPTION_CSV, "r", newline="", encoding="utf-8") as f:
226
- reader = csv.DictReader(f)
227
- entries = [row for row in reader if row["ip_address"] != ip_address]
228
-
229
- # Write back the filtered entries
230
- with open(EXEMPTION_CSV, "w", newline="", encoding="utf-8") as f:
231
- if entries:
232
- writer = csv.DictWriter(f, fieldnames=["ip_address", "reason", "created_at"])
233
- writer.writeheader()
234
- writer.writerows(entries)
235
-
112
+ print(f"Error unblocking IP {ip}: {e}")
236
113
 
237
- class CsvKeywordStore:
238
- """CSV-based storage for dynamic keywords"""
239
-
240
- @staticmethod
241
- def add_keyword(keyword, count=1):
242
- ensure_csv_directory()
243
-
244
- # Read existing keywords
245
- keywords = CsvKeywordStore._load_keywords()
246
-
247
- # Update or add keyword
248
- keywords[keyword] = keywords.get(keyword, 0) + count
249
-
250
- # Save back to file
251
- CsvKeywordStore._save_keywords(keywords)
252
-
253
114
  @staticmethod
254
- def get_top_keywords(limit=10):
255
- keywords = CsvKeywordStore._load_keywords()
256
- # Sort by count in descending order and return top N
257
- sorted_keywords = sorted(keywords.items(), key=lambda x: x[1], reverse=True)
258
- return [kw for kw, count in sorted_keywords[:limit]]
259
-
260
- @staticmethod
261
- def remove_keyword(keyword):
262
- keywords = CsvKeywordStore._load_keywords()
263
- if keyword in keywords:
264
- del keywords[keyword]
265
- CsvKeywordStore._save_keywords(keywords)
266
-
267
- @staticmethod
268
- def clear_all():
269
- if os.path.exists(KEYWORDS_CSV):
270
- os.remove(KEYWORDS_CSV)
271
-
272
- @staticmethod
273
- def _load_keywords():
274
- """Load keywords from CSV file as a dictionary"""
275
- if not os.path.exists(KEYWORDS_CSV):
276
- return {}
277
-
278
- keywords = {}
279
- with open(KEYWORDS_CSV, "r", newline="", encoding="utf-8") as f:
280
- reader = csv.DictReader(f)
281
- for row in reader:
282
- keywords[row["keyword"]] = int(row["count"])
283
- return keywords
284
-
285
- @staticmethod
286
- def _save_keywords(keywords):
287
- """Save keywords dictionary to CSV file"""
288
- with open(KEYWORDS_CSV, "w", newline="", encoding="utf-8") as f:
289
- writer = csv.writer(f)
290
- writer.writerow(["keyword", "count", "last_updated"])
291
- for keyword, count in keywords.items():
292
- writer.writerow([keyword, count, timezone.now().isoformat()])
293
-
294
-
295
- # ============= Storage Factory Functions =============
296
-
297
- def get_blacklist_store():
298
- """Return appropriate blacklist storage class based on settings"""
299
- if STORAGE_MODE == "csv":
300
- return CsvBlacklistStore
301
- else:
302
- # Return a wrapper for Django models (only if models are available)
303
- if BlacklistEntry is not None:
304
- return ModelBlacklistStore
305
- else:
306
- # Fallback to CSV if models aren't available
307
- return CsvBlacklistStore
308
-
309
-
310
- def get_exemption_store():
311
- """Return appropriate exemption storage class based on settings"""
312
- if STORAGE_MODE == "csv":
313
- return CsvExemptionStore
314
- else:
315
- if IPExemption is not None:
316
- return ModelExemptionStore
317
- else:
318
- return CsvExemptionStore
319
-
320
-
321
- def get_keyword_store():
322
- """Return appropriate keyword storage class based on settings"""
323
- if STORAGE_MODE == "csv":
324
- return CsvKeywordStore
325
- else:
326
- if DynamicKeyword is not None:
327
- return ModelKeywordStore
328
- else:
329
- return CsvKeywordStore
330
-
331
-
332
- # ============= Django Model Wrappers =============
333
-
334
- class ModelBlacklistStore:
335
- """Django model-based storage for blacklist entries"""
336
-
337
- @staticmethod
338
- def add_ip(ip_address, reason):
115
+ def get_all_blocked_ips():
116
+ """Get all blocked IPs"""
339
117
  _import_models()
340
- if BlacklistEntry is not None:
341
- BlacklistEntry.objects.get_or_create(ip_address=ip_address, defaults={"reason": reason})
342
-
343
- @staticmethod
344
- def is_blocked(ip_address):
345
- _import_models()
346
- if BlacklistEntry is not None:
347
- return BlacklistEntry.objects.filter(ip_address=ip_address).exists()
348
- return False
349
-
350
- @staticmethod
351
- def get_all():
352
- _import_models()
353
- if BlacklistEntry is not None:
354
- return list(BlacklistEntry.objects.values("ip_address", "reason", "created_at"))
355
- return []
356
-
357
- @staticmethod
358
- def remove_ip(ip_address):
359
- _import_models()
360
- if BlacklistEntry is not None:
361
- BlacklistEntry.objects.filter(ip_address=ip_address).delete()
362
-
118
+ if BlacklistEntry is None:
119
+ return []
120
+ try:
121
+ return list(BlacklistEntry.objects.values_list('ip_address', flat=True))
122
+ except Exception:
123
+ return []
363
124
 
364
125
  class ModelExemptionStore:
365
- """Django model-based storage for exemption entries"""
366
-
367
126
  @staticmethod
368
- def add_ip(ip_address, reason=""):
127
+ def is_exempted(ip):
128
+ """Check if IP is exempted"""
369
129
  _import_models()
370
- if IPExemption is not None:
371
- IPExemption.objects.get_or_create(ip_address=ip_address, defaults={"reason": reason})
372
-
130
+ if IPExemption is None:
131
+ return False
132
+ try:
133
+ return IPExemption.objects.filter(ip_address=ip).exists()
134
+ except Exception:
135
+ return False
136
+
373
137
  @staticmethod
374
- def is_exempted(ip_address):
138
+ def add_exemption(ip, reason="Manual exemption"):
139
+ """Add IP to exemption list"""
375
140
  _import_models()
376
- if IPExemption is not None:
377
- return IPExemption.objects.filter(ip_address=ip_address).exists()
378
- return False
379
-
141
+ if IPExemption is None:
142
+ print(f"Warning: Cannot exempt IP {ip}, models not available")
143
+ return
144
+ try:
145
+ IPExemption.objects.get_or_create(
146
+ ip_address=ip,
147
+ defaults={'reason': reason, 'created_at': timezone.now()}
148
+ )
149
+ except Exception as e:
150
+ print(f"Error exempting IP {ip}: {e}")
151
+
380
152
  @staticmethod
381
- def get_all():
153
+ def remove_exemption(ip):
154
+ """Remove IP from exemption list"""
382
155
  _import_models()
383
- if IPExemption is not None:
384
- return list(IPExemption.objects.values("ip_address", "reason", "created_at"))
385
- return []
386
-
156
+ if IPExemption is None:
157
+ return
158
+ try:
159
+ IPExemption.objects.filter(ip_address=ip).delete()
160
+ except Exception as e:
161
+ print(f"Error removing exemption for IP {ip}: {e}")
162
+
387
163
  @staticmethod
388
- def remove_ip(ip_address):
164
+ def get_all_exempted_ips():
165
+ """Get all exempted IPs"""
389
166
  _import_models()
390
- if IPExemption is not None:
391
- IPExemption.objects.filter(ip_address=ip_address).delete()
392
-
167
+ if IPExemption is None:
168
+ return []
169
+ try:
170
+ return list(IPExemption.objects.values_list('ip_address', flat=True))
171
+ except Exception:
172
+ return []
393
173
 
394
174
  class ModelKeywordStore:
395
- """Django model-based storage for dynamic keywords"""
396
-
397
175
  @staticmethod
398
- def add_keyword(keyword, count=1):
176
+ def add_keyword(keyword):
177
+ """Add a keyword to the dynamic keyword list"""
399
178
  _import_models()
400
- if DynamicKeyword is not None:
401
- obj, created = DynamicKeyword.objects.get_or_create(keyword=keyword, defaults={"count": count})
179
+ if DynamicKeyword is None:
180
+ return
181
+ try:
182
+ obj, created = DynamicKeyword.objects.get_or_create(keyword=keyword)
402
183
  if not created:
403
- obj.count += count
184
+ obj.count += 1
404
185
  obj.save()
405
-
406
- @staticmethod
407
- def get_top_keywords(limit=10):
408
- _import_models()
409
- if DynamicKeyword is not None:
410
- return list(DynamicKeyword.objects.order_by("-count").values_list("keyword", flat=True)[:limit])
411
- return []
412
-
186
+ except Exception as e:
187
+ print(f"Error adding keyword {keyword}: {e}")
188
+
413
189
  @staticmethod
414
- def remove_keyword(keyword):
190
+ def get_top_keywords(n=10):
191
+ """Get top N keywords by count"""
415
192
  _import_models()
416
- if DynamicKeyword is not None:
417
- DynamicKeyword.objects.filter(keyword=keyword).delete()
418
-
193
+ if DynamicKeyword is None:
194
+ return []
195
+ try:
196
+ return list(
197
+ DynamicKeyword.objects.order_by('-count')[:n]
198
+ .values_list('keyword', flat=True)
199
+ )
200
+ except Exception:
201
+ return []
202
+
419
203
  @staticmethod
420
- def clear_all():
204
+ def reset_keywords():
205
+ """Reset all keyword counts"""
421
206
  _import_models()
422
- if DynamicKeyword is not None:
207
+ if DynamicKeyword is None:
208
+ return
209
+ try:
423
210
  DynamicKeyword.objects.all().delete()
211
+ except Exception as e:
212
+ print(f"Error resetting keywords: {e}")
213
+
214
+ # Factory functions that only return Django model stores
215
+ def get_feature_store():
216
+ """Get the feature store (Django models only)"""
217
+ return ModelFeatureStore()
218
+
219
+ def get_blacklist_store():
220
+ """Get the blacklist store (Django models only)"""
221
+ return ModelBlacklistStore()
222
+
223
+ def get_exemption_store():
224
+ """Get the exemption store (Django models only)"""
225
+ return ModelExemptionStore()
226
+
227
+ def get_keyword_store():
228
+ """Get the keyword store (Django models only)"""
229
+ return ModelKeywordStore()
aiwaf/trainer.py CHANGED
@@ -78,18 +78,6 @@ def _read_all_logs() -> list[str]:
78
78
  except OSError:
79
79
  continue
80
80
 
81
- # If no lines found from main log, try AI-WAF middleware CSV log
82
- if not lines:
83
- middleware_csv = getattr(settings, "AIWAF_MIDDLEWARE_LOG", "aiwaf_requests.log").replace('.log', '.csv')
84
- if os.path.exists(middleware_csv):
85
- try:
86
- from .middleware_logger import AIWAFCSVLogParser
87
- csv_lines = AIWAFCSVLogParser.get_log_lines_for_trainer(middleware_csv)
88
- lines.extend(csv_lines)
89
- print(f"📋 Using AI-WAF middleware CSV log: {middleware_csv} ({len(csv_lines)} entries)")
90
- except Exception as e:
91
- print(f"⚠️ Failed to read middleware CSV log: {e}")
92
-
93
81
  return lines
94
82
 
95
83