aiwaf 0.1.8.4__tar.gz → 0.1.8.6__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 (29) hide show
  1. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/PKG-INFO +21 -2
  2. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/README.md +20 -1
  3. aiwaf-0.1.8.6/aiwaf/decorators.py +28 -0
  4. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/middleware.py +31 -46
  5. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/trainer.py +41 -30
  6. aiwaf-0.1.8.6/aiwaf/utils.py +105 -0
  7. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/PKG-INFO +21 -2
  8. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/SOURCES.txt +1 -0
  9. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/pyproject.toml +1 -1
  10. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/setup.py +1 -1
  11. aiwaf-0.1.8.4/aiwaf/utils.py +0 -50
  12. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/LICENSE +0 -0
  13. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/__init__.py +0 -0
  14. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/apps.py +0 -0
  15. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/blacklist_manager.py +0 -0
  16. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/__init__.py +0 -0
  17. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/__init__.py +0 -0
  18. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/add_ipexemption.py +0 -0
  19. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/aiwaf_reset.py +0 -0
  20. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/management/commands/detect_and_train.py +0 -0
  21. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/models.py +0 -0
  22. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/resources/model.pkl +0 -0
  23. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/storage.py +0 -0
  24. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/templatetags/__init__.py +0 -0
  25. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf/templatetags/aiwaf_tags.py +0 -0
  26. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/dependency_links.txt +0 -0
  27. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/requires.txt +0 -0
  28. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/aiwaf.egg-info/top_level.txt +0 -0
  29. {aiwaf-0.1.8.4 → aiwaf-0.1.8.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.8.4
3
+ Version: 0.1.8.6
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -99,7 +99,26 @@ AIWAF_EXEMPT_PATHS = [
99
99
  ]
100
100
  ```
101
101
 
102
- All exempt paths are:
102
+ **Exempt Views (Decorator):**
103
+ Use the `@aiwaf_exempt` decorator to exempt specific views from all AI-WAF protection:
104
+
105
+ ```python
106
+ from aiwaf.decorators import aiwaf_exempt
107
+ from django.http import JsonResponse
108
+
109
+ @aiwaf_exempt
110
+ def my_api_view(request):
111
+ """This view will be exempt from all AI-WAF protection"""
112
+ return JsonResponse({"status": "success"})
113
+
114
+ # Works with class-based views too
115
+ @aiwaf_exempt
116
+ class MyAPIView(View):
117
+ def get(self, request):
118
+ return JsonResponse({"method": "GET"})
119
+ ```
120
+
121
+ All exempt paths and views are:
103
122
  - Skipped from keyword learning
104
123
  - Immune to AI blocking
105
124
  - Ignored in log training
@@ -78,7 +78,26 @@ AIWAF_EXEMPT_PATHS = [
78
78
  ]
79
79
  ```
80
80
 
81
- All exempt paths are:
81
+ **Exempt Views (Decorator):**
82
+ Use the `@aiwaf_exempt` decorator to exempt specific views from all AI-WAF protection:
83
+
84
+ ```python
85
+ from aiwaf.decorators import aiwaf_exempt
86
+ from django.http import JsonResponse
87
+
88
+ @aiwaf_exempt
89
+ def my_api_view(request):
90
+ """This view will be exempt from all AI-WAF protection"""
91
+ return JsonResponse({"status": "success"})
92
+
93
+ # Works with class-based views too
94
+ @aiwaf_exempt
95
+ class MyAPIView(View):
96
+ def get(self, request):
97
+ return JsonResponse({"method": "GET"})
98
+ ```
99
+
100
+ All exempt paths and views are:
82
101
  - Skipped from keyword learning
83
102
  - Immune to AI blocking
84
103
  - Ignored in log training
@@ -0,0 +1,28 @@
1
+ from functools import wraps
2
+ from django.utils.decorators import method_decorator
3
+
4
+ def aiwaf_exempt(view_func):
5
+ """
6
+ Decorator to exempt a view from AI-WAF protection.
7
+ Can be used on function-based views or class-based views.
8
+
9
+ Usage:
10
+ @aiwaf_exempt
11
+ def my_view(request):
12
+ return HttpResponse("This view is exempt from AI-WAF")
13
+
14
+ # Or for class-based views:
15
+ @method_decorator(aiwaf_exempt, name='dispatch')
16
+ class MyView(View):
17
+ pass
18
+ """
19
+ @wraps(view_func)
20
+ def wrapped_view(*args, **kwargs):
21
+ return view_func(*args, **kwargs)
22
+
23
+ # Mark the view as AI-WAF exempt
24
+ wrapped_view.aiwaf_exempt = True
25
+ return wrapped_view
26
+
27
+ # For class-based views
28
+ aiwaf_exempt_view = method_decorator(aiwaf_exempt, name='dispatch')
@@ -14,37 +14,10 @@ from django.core.cache import cache
14
14
  from django.db.models import F
15
15
  from django.apps import apps
16
16
  from django.urls import get_resolver
17
- from .trainer import STATIC_KW, STATUS_IDX, is_exempt_path, path_exists_in_django
17
+ from .trainer import STATIC_KW, STATUS_IDX, path_exists_in_django
18
18
  from .blacklist_manager import BlacklistManager
19
19
  from .models import DynamicKeyword, IPExemption
20
- def is_ip_exempted(ip):
21
- return IPExemption.objects.filter(ip_address=ip).exists()
22
-
23
- def is_exempt_path(path):
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
42
- exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
43
- for exempt in exempt_paths:
44
- if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
45
- return True
46
-
47
- return False
20
+ from .utils import is_exempt, get_ip, is_ip_exempted
48
21
 
49
22
  MODEL_PATH = getattr(
50
23
  settings,
@@ -93,7 +66,7 @@ class IPAndKeywordBlockMiddleware:
93
66
 
94
67
  def __call__(self, request):
95
68
  raw_path = request.path.lower()
96
- if is_exempt_path(raw_path):
69
+ if is_exempt(request):
97
70
  return self.get_response(request)
98
71
  ip = get_ip(request)
99
72
  path = raw_path.lstrip("/")
@@ -132,7 +105,7 @@ class RateLimitMiddleware:
132
105
  self.get_response = get_response
133
106
 
134
107
  def __call__(self, request):
135
- if is_exempt_path(request.path):
108
+ if is_exempt(request):
136
109
  return self.get_response(request)
137
110
 
138
111
  ip = get_ip(request)
@@ -161,7 +134,7 @@ class AIAnomalyMiddleware(MiddlewareMixin):
161
134
  self.model = joblib.load(model_path)
162
135
 
163
136
  def process_request(self, request):
164
- if is_exempt_path(request.path):
137
+ if is_exempt(request):
165
138
  return None
166
139
  request._start_time = time.time()
167
140
  ip = get_ip(request)
@@ -172,14 +145,14 @@ class AIAnomalyMiddleware(MiddlewareMixin):
172
145
  return None
173
146
 
174
147
  def process_response(self, request, response):
175
- if is_exempt_path(request.path):
148
+ if is_exempt(request):
176
149
  return response
177
150
  ip = get_ip(request)
178
151
  now = time.time()
179
152
  key = f"aiwaf:{ip}"
180
153
  data = cache.get(key, [])
181
154
  path_len = len(request.path)
182
- if not path_exists_in_django(request.path) and not is_exempt_path(request.path):
155
+ if not path_exists_in_django(request.path) and not is_exempt(request):
183
156
  kw_hits = sum(1 for kw in STATIC_KW if kw in request.path.lower())
184
157
  else:
185
158
  kw_hits = 0
@@ -211,7 +184,7 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
211
184
  MIN_FORM_TIME = getattr(settings, "AIWAF_MIN_FORM_TIME", 1.0) # seconds
212
185
 
213
186
  def process_request(self, request):
214
- if is_exempt_path(request.path):
187
+ if is_exempt(request):
215
188
  return None
216
189
 
217
190
  ip = get_ip(request)
@@ -219,7 +192,8 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
219
192
  return None
220
193
 
221
194
  if request.method == "GET":
222
- # Store timestamp for this IP's GET request
195
+ # Store timestamp for this IP's GET request
196
+ # Use a general key for the IP, not path-specific
223
197
  cache.set(f"honeypot_get:{ip}", time.time(), timeout=300) # 5 min timeout
224
198
 
225
199
  elif request.method == "POST":
@@ -228,22 +202,33 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
228
202
 
229
203
  if get_time is None:
230
204
  # No GET request - likely bot posting directly
231
- BlacklistManager.block(ip, "Direct POST without GET")
232
- return JsonResponse({"error": "blocked"}, status=403)
233
-
234
- # Check timing
235
- time_diff = time.time() - get_time
236
- if time_diff < self.MIN_FORM_TIME:
237
- # Posted too quickly - likely bot
238
- BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
239
- return JsonResponse({"error": "blocked"}, status=403)
205
+ # But be more lenient for login paths since users might bookmark them
206
+ if not any(request.path.lower().startswith(login_path) for login_path in [
207
+ "/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
208
+ ]):
209
+ BlacklistManager.block(ip, "Direct POST without GET")
210
+ return JsonResponse({"error": "blocked"}, status=403)
211
+ else:
212
+ # Check timing - be more lenient for login paths
213
+ time_diff = time.time() - get_time
214
+ min_time = self.MIN_FORM_TIME
215
+
216
+ # Use shorter time threshold for login paths (users can login quickly)
217
+ if any(request.path.lower().startswith(login_path) for login_path in [
218
+ "/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
219
+ ]):
220
+ min_time = 0.1 # Very short threshold for login forms
221
+
222
+ if time_diff < min_time:
223
+ BlacklistManager.block(ip, f"Form submitted too quickly ({time_diff:.2f}s)")
224
+ return JsonResponse({"error": "blocked"}, status=403)
240
225
 
241
226
  return None
242
227
 
243
228
 
244
229
  class UUIDTamperMiddleware(MiddlewareMixin):
245
230
  def process_view(self, request, view_func, view_args, view_kwargs):
246
- if is_exempt_path(request.path):
231
+ if is_exempt(request):
247
232
  return None
248
233
  uid = view_kwargs.get("uuid")
249
234
  if not uid:
@@ -13,6 +13,7 @@ from sklearn.ensemble import IsolationForest
13
13
  from django.conf import settings
14
14
  from django.apps import apps
15
15
  from django.db.models import F
16
+ from .utils import is_exempt_path
16
17
 
17
18
  # ─────────── Configuration ───────────
18
19
  LOG_PATH = settings.AIWAF_ACCESS_LOG
@@ -32,31 +33,6 @@ DynamicKeyword = apps.get_model("aiwaf", "DynamicKeyword")
32
33
  IPExemption = apps.get_model("aiwaf", "IPExemption")
33
34
 
34
35
 
35
- def is_exempt_path(path: str) -> bool:
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
54
- for exempt in getattr(settings, "AIWAF_EXEMPT_PATHS", []):
55
- if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
56
- return True
57
- return False
58
-
59
-
60
36
  def path_exists_in_django(path: str) -> bool:
61
37
  from django.urls import get_resolver
62
38
  from django.urls.resolvers import URLResolver
@@ -203,13 +179,48 @@ def train() -> None:
203
179
  os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
204
180
  joblib.dump(model, MODEL_PATH)
205
181
  print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
182
+
183
+ # Check for anomalies and intelligently decide which IPs to block
206
184
  preds = model.predict(X)
207
185
  anomalous_ips = set(df.loc[preds == -1, "ip"])
208
- for ip in anomalous_ips:
209
- BlacklistEntry.objects.get_or_create(
210
- ip_address=ip,
211
- defaults={"reason": "Anomalous behavior"}
212
- )
186
+
187
+ if anomalous_ips:
188
+ print(f"⚠️ Detected {len(anomalous_ips)} potentially anomalous IPs during training")
189
+
190
+ blocked_count = 0
191
+ for ip in anomalous_ips:
192
+ # Skip if IP is exempted
193
+ if IPExemption.objects.filter(ip_address=ip).exists():
194
+ continue
195
+
196
+ # Get this IP's behavior from the data
197
+ ip_data = df[df["ip"] == ip]
198
+
199
+ # Criteria to determine if this is likely a legitimate user vs threat:
200
+ avg_kw_hits = ip_data["kw_hits"].mean()
201
+ max_404s = ip_data["total_404"].max()
202
+ avg_burst = ip_data["burst_count"].mean()
203
+ total_requests = len(ip_data)
204
+
205
+ # Don't block if it looks like legitimate behavior:
206
+ if (
207
+ avg_kw_hits < 2 and # Not hitting many malicious keywords
208
+ max_404s < 10 and # Not excessive 404s
209
+ avg_burst < 15 and # Not excessive burst activity
210
+ total_requests < 100 # Not excessive total requests
211
+ ):
212
+ print(f" - {ip}: Anomalous but looks legitimate (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f}) - NOT blocking")
213
+ continue
214
+
215
+ # Block if it shows clear signs of malicious behavior
216
+ BlacklistEntry.objects.get_or_create(
217
+ ip_address=ip,
218
+ defaults={"reason": f"AI anomaly + suspicious patterns (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})"}
219
+ )
220
+ blocked_count += 1
221
+ print(f" - {ip}: Blocked for suspicious behavior (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})")
222
+
223
+ print(f" → Blocked {blocked_count}/{len(anomalous_ips)} anomalous IPs (others looked legitimate)")
213
224
 
214
225
  tokens = Counter()
215
226
  for r in parsed:
@@ -0,0 +1,105 @@
1
+ import os
2
+ import re
3
+ import glob
4
+ import gzip
5
+ from datetime import datetime
6
+ from django.conf import settings
7
+ from .models import IPExemption
8
+
9
+ _LOG_RX = re.compile(
10
+ r'(\d+\.\d+\.\d+\.\d+).*\[(.*?)\].*"(GET|POST) (.*?) HTTP/.*?" (\d{3}).*?"(.*?)" "(.*?)"'
11
+ )
12
+
13
+ def get_ip(request):
14
+ xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
15
+ if xff:
16
+ return xff.split(",")[0].strip()
17
+ return request.META.get("REMOTE_ADDR", "")
18
+
19
+ def read_rotated_logs(base_path):
20
+ lines = []
21
+ if os.path.exists(base_path):
22
+ with open(base_path, "r", encoding="utf-8", errors="ignore") as f:
23
+ lines.extend(f.readlines())
24
+ for path in sorted(glob.glob(base_path + ".*")):
25
+ opener = gzip.open if path.endswith(".gz") else open
26
+ try:
27
+ with opener(path, "rt", encoding="utf-8", errors="ignore") as f:
28
+ lines.extend(f.readlines())
29
+ except OSError:
30
+ continue
31
+ return lines
32
+
33
+ def parse_log_line(line):
34
+ m = _LOG_RX.search(line)
35
+ if not m:
36
+ return None
37
+ ip, ts_str, _, path, status, ref, ua = m.groups()
38
+ try:
39
+ ts = datetime.strptime(ts_str.split()[0], "%d/%b/%Y:%H:%M:%S")
40
+ except ValueError:
41
+ return None
42
+ rt_m = re.search(r'response-time=(\d+\.\d+)', line)
43
+ rt = float(rt_m.group(1)) if rt_m else 0.0
44
+ return {
45
+ "ip": ip,
46
+ "timestamp": ts,
47
+ "path": path,
48
+ "status": status,
49
+ "referer": ref,
50
+ "user_agent": ua,
51
+ "response_time": rt
52
+ }
53
+
54
+ def is_ip_exempted(ip):
55
+ """Check if IP is in exemption list"""
56
+ return IPExemption.objects.filter(ip_address=ip).exists()
57
+
58
+ def is_view_exempt(request):
59
+ """Check if the current view is marked as AI-WAF exempt"""
60
+ if hasattr(request, 'resolver_match') and request.resolver_match:
61
+ view_func = request.resolver_match.func
62
+
63
+ # Check if view function has aiwaf_exempt attribute
64
+ if hasattr(view_func, 'aiwaf_exempt'):
65
+ return True
66
+
67
+ # For class-based views, check the view class
68
+ if hasattr(view_func, 'view_class'):
69
+ view_class = view_func.view_class
70
+ if hasattr(view_class, 'aiwaf_exempt'):
71
+ return True
72
+
73
+ # Check dispatch method for method_decorator usage
74
+ dispatch_method = getattr(view_class, 'dispatch', None)
75
+ if dispatch_method and hasattr(dispatch_method, 'aiwaf_exempt'):
76
+ return True
77
+
78
+ return False
79
+
80
+ def is_exempt_path(path):
81
+ """Check if path should be exempt from AI-WAF"""
82
+ path = path.lower()
83
+
84
+ # Default login paths (always exempt)
85
+ default_exempt = [
86
+ "/admin/login/", "/admin/", "/login/", "/accounts/login/",
87
+ "/auth/login/", "/signin/"
88
+ ]
89
+
90
+ # Check default exempt paths
91
+ for exempt_path in default_exempt:
92
+ if path.startswith(exempt_path):
93
+ return True
94
+
95
+ # Check configured exempt paths
96
+ exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
97
+ for exempt_path in exempt_paths:
98
+ if path == exempt_path or path.startswith(exempt_path.rstrip("/") + "/"):
99
+ return True
100
+
101
+ return False
102
+
103
+ def is_exempt(request):
104
+ """Check if request should be exempt (either by path or view decorator)"""
105
+ return is_exempt_path(request.path) or is_view_exempt(request)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.8.4
3
+ Version: 0.1.8.6
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -99,7 +99,26 @@ AIWAF_EXEMPT_PATHS = [
99
99
  ]
100
100
  ```
101
101
 
102
- All exempt paths are:
102
+ **Exempt Views (Decorator):**
103
+ Use the `@aiwaf_exempt` decorator to exempt specific views from all AI-WAF protection:
104
+
105
+ ```python
106
+ from aiwaf.decorators import aiwaf_exempt
107
+ from django.http import JsonResponse
108
+
109
+ @aiwaf_exempt
110
+ def my_api_view(request):
111
+ """This view will be exempt from all AI-WAF protection"""
112
+ return JsonResponse({"status": "success"})
113
+
114
+ # Works with class-based views too
115
+ @aiwaf_exempt
116
+ class MyAPIView(View):
117
+ def get(self, request):
118
+ return JsonResponse({"method": "GET"})
119
+ ```
120
+
121
+ All exempt paths and views are:
103
122
  - Skipped from keyword learning
104
123
  - Immune to AI blocking
105
124
  - Ignored in log training
@@ -5,6 +5,7 @@ setup.py
5
5
  aiwaf/__init__.py
6
6
  aiwaf/apps.py
7
7
  aiwaf/blacklist_manager.py
8
+ aiwaf/decorators.py
8
9
  aiwaf/middleware.py
9
10
  aiwaf/models.py
10
11
  aiwaf/storage.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiwaf"
3
- version = "0.1.8.4"
3
+ version = "0.1.8.6"
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.4",
12
+ version="0.1.8.6",
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",
@@ -1,50 +0,0 @@
1
- import os
2
- import re
3
- import glob
4
- import gzip
5
- from datetime import datetime
6
-
7
- _LOG_RX = re.compile(
8
- r'(\d+\.\d+\.\d+\.\d+).*\[(.*?)\].*"(GET|POST) (.*?) HTTP/.*?" (\d{3}).*?"(.*?)" "(.*?)"'
9
- )
10
-
11
- def get_ip(request):
12
- xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
13
- if xff:
14
- return xff.split(",")[0].strip()
15
- return request.META.get("REMOTE_ADDR", "")
16
-
17
- def read_rotated_logs(base_path):
18
- lines = []
19
- if os.path.exists(base_path):
20
- with open(base_path, "r", encoding="utf-8", errors="ignore") as f:
21
- lines.extend(f.readlines())
22
- for path in sorted(glob.glob(base_path + ".*")):
23
- opener = gzip.open if path.endswith(".gz") else open
24
- try:
25
- with opener(path, "rt", encoding="utf-8", errors="ignore") as f:
26
- lines.extend(f.readlines())
27
- except OSError:
28
- continue
29
- return lines
30
-
31
- def parse_log_line(line):
32
- m = _LOG_RX.search(line)
33
- if not m:
34
- return None
35
- ip, ts_str, _, path, status, ref, ua = m.groups()
36
- try:
37
- ts = datetime.strptime(ts_str.split()[0], "%d/%b/%Y:%H:%M:%S")
38
- except ValueError:
39
- return None
40
- rt_m = re.search(r'response-time=(\d+\.\d+)', line)
41
- rt = float(rt_m.group(1)) if rt_m else 0.0
42
- return {
43
- "ip": ip,
44
- "timestamp": ts,
45
- "path": path,
46
- "status": status,
47
- "referer": ref,
48
- "user_agent": ua,
49
- "response_time": rt
50
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes