aiwaf 0.1.9.3.0__py3-none-any.whl → 0.1.9.3.1__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/__init__.py +1 -1
- aiwaf/management/commands/aiwaf_list.py +67 -44
- aiwaf/middleware.py +196 -20
- {aiwaf-0.1.9.3.0.dist-info → aiwaf-0.1.9.3.1.dist-info}/METADATA +40 -4
- {aiwaf-0.1.9.3.0.dist-info → aiwaf-0.1.9.3.1.dist-info}/RECORD +8 -8
- {aiwaf-0.1.9.3.0.dist-info → aiwaf-0.1.9.3.1.dist-info}/WHEEL +0 -0
- {aiwaf-0.1.9.3.0.dist-info → aiwaf-0.1.9.3.1.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.9.3.0.dist-info → aiwaf-0.1.9.3.1.dist-info}/top_level.txt +0 -0
aiwaf/__init__.py
CHANGED
|
@@ -10,7 +10,8 @@ def _sort(items, order):
|
|
|
10
10
|
reverse=reverse)
|
|
11
11
|
|
|
12
12
|
def _filter_since(items, seconds):
|
|
13
|
-
if not seconds:
|
|
13
|
+
if not seconds:
|
|
14
|
+
return items
|
|
14
15
|
cutoff = timezone.now() - timedelta(seconds=seconds)
|
|
15
16
|
return [it for it in items if it.get("created_at") and it["created_at"] >= cutoff]
|
|
16
17
|
|
|
@@ -25,57 +26,79 @@ def _print_table(rows, headers):
|
|
|
25
26
|
print(" | ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(r)))
|
|
26
27
|
|
|
27
28
|
class Command(BaseCommand):
|
|
28
|
-
help = "
|
|
29
|
+
help = "AIWAF list (v2): blocked IPs, exempted IPs, monitored keywords."
|
|
29
30
|
|
|
30
31
|
def add_arguments(self, parser):
|
|
31
32
|
grp = parser.add_mutually_exclusive_group()
|
|
32
|
-
grp.add_argument("--ips", action="store_true", help="
|
|
33
|
-
grp.add_argument("--
|
|
34
|
-
grp.add_argument("--keywords", action="store_true", help="
|
|
35
|
-
grp.add_argument("--all", action="store_true", help="
|
|
36
|
-
|
|
37
|
-
parser.add_argument("--
|
|
38
|
-
parser.add_argument("--
|
|
39
|
-
parser.add_argument("--
|
|
33
|
+
grp.add_argument("--ips-blocked", action="store_true", help="List blocked IPs (blacklist).")
|
|
34
|
+
grp.add_argument("--ips-exempted", action="store_true", help="List exempted IPs (whitelist).")
|
|
35
|
+
grp.add_argument("--keywords-monitored", action="store_true", help="List monitored dynamic keywords.")
|
|
36
|
+
grp.add_argument("--all", action="store_true", help="List everything.")
|
|
37
|
+
|
|
38
|
+
parser.add_argument("--format", choices=["table", "json"], default="table", help="Output format.")
|
|
39
|
+
parser.add_argument("--limit", type=int, default=100, help="Max items to display.")
|
|
40
|
+
parser.add_argument("--order", choices=["newest", "oldest"], default="newest", help="Sort order for IP entries.")
|
|
41
|
+
parser.add_argument("--since", type=int, help="Time window in seconds (e.g. 86400 = last 24h) for IP entries.")
|
|
42
|
+
parser.add_argument("--only-ip", action="store_true", help="For IP lists: print only the IP column.")
|
|
43
|
+
|
|
44
|
+
def handle(self, *args, **options):
|
|
45
|
+
# default: show blocked IPs if nothing specific is requested
|
|
46
|
+
if not any([options["ips_blocked"], options["ips_exempted"], options["keywords_monitored"], options["all"]]):
|
|
47
|
+
options["ips_blocked"] = True
|
|
40
48
|
|
|
41
|
-
def handle(self, *args, **o):
|
|
42
|
-
if not any([o["exemptions"], o["keywords"], o["all"]]): # défaut = ips
|
|
43
|
-
o["ips"] = True
|
|
44
49
|
payload = {}
|
|
45
50
|
|
|
46
|
-
if
|
|
47
|
-
data = get_blacklist_store().get_all()
|
|
48
|
-
data = _filter_since(data,
|
|
49
|
-
data = _sort(data,
|
|
50
|
-
payload["
|
|
51
|
+
if options["all"] or options["ips_blocked"]:
|
|
52
|
+
data = get_blacklist_store().get_all() # [{ip_address, reason, created_at}]
|
|
53
|
+
data = _filter_since(data, options.get("since"))
|
|
54
|
+
data = _sort(data, options["order"])[: options["limit"]]
|
|
55
|
+
payload["ips_blocked"] = data
|
|
51
56
|
|
|
52
|
-
if
|
|
53
|
-
data = get_exemption_store().get_all()
|
|
54
|
-
data = _filter_since(data,
|
|
55
|
-
data = _sort(data,
|
|
56
|
-
payload["
|
|
57
|
+
if options["all"] or options["ips_exempted"]:
|
|
58
|
+
data = get_exemption_store().get_all() # [{ip_address, reason, created_at}]
|
|
59
|
+
data = _filter_since(data, options.get("since"))
|
|
60
|
+
data = _sort(data, options["order"])[: options["limit"]]
|
|
61
|
+
payload["ips_exempted"] = data
|
|
57
62
|
|
|
58
|
-
if
|
|
59
|
-
kws = get_keyword_store().get_top_keywords(
|
|
60
|
-
payload["
|
|
63
|
+
if options["all"] or options["keywords_monitored"]:
|
|
64
|
+
kws = get_keyword_store().get_top_keywords(options["limit"]) # [str]
|
|
65
|
+
payload["keywords_monitored"] = [{"keyword": k} for k in kws]
|
|
61
66
|
|
|
62
|
-
if
|
|
67
|
+
if options["format"] == "json":
|
|
63
68
|
def _default(v):
|
|
64
|
-
try:
|
|
65
|
-
|
|
69
|
+
try:
|
|
70
|
+
return v.isoformat()
|
|
71
|
+
except Exception:
|
|
72
|
+
return str(v)
|
|
66
73
|
self.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2, default=_default))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if "
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
_print_table(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# table output
|
|
77
|
+
if "ips_blocked" in payload:
|
|
78
|
+
print("\n== IPs blocked ==")
|
|
79
|
+
rows = payload["ips_blocked"]
|
|
80
|
+
if options["only_ip"]:
|
|
81
|
+
for r in rows:
|
|
82
|
+
print(r.get("ip_address", ""))
|
|
83
|
+
else:
|
|
84
|
+
_print_table(
|
|
85
|
+
[[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")] for r in rows],
|
|
86
|
+
["ip_address", "reason", "created_at"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if "ips_exempted" in payload:
|
|
90
|
+
print("\n== IPs exempted ==")
|
|
91
|
+
rows = payload["ips_exempted"]
|
|
92
|
+
if options["only_ip"]:
|
|
93
|
+
for r in rows:
|
|
94
|
+
print(r.get("ip_address", ""))
|
|
95
|
+
else:
|
|
96
|
+
_print_table(
|
|
97
|
+
[[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")] for r in rows],
|
|
98
|
+
["ip_address", "reason", "created_at"],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if "keywords_monitored" in payload:
|
|
102
|
+
print("\n== Keywords monitored ==")
|
|
103
|
+
rows = payload["keywords_monitored"]
|
|
104
|
+
_print_table([[r["keyword"]] for r in rows], ["keyword"])
|
aiwaf/middleware.py
CHANGED
|
@@ -4,17 +4,30 @@ import time
|
|
|
4
4
|
import re
|
|
5
5
|
import os
|
|
6
6
|
import warnings
|
|
7
|
-
import numpy as np
|
|
8
|
-
import joblib
|
|
9
|
-
from django.db.models import UUIDField
|
|
10
7
|
from collections import defaultdict
|
|
11
8
|
from django.utils.deprecation import MiddlewareMixin
|
|
12
9
|
from django.http import JsonResponse
|
|
13
10
|
from django.conf import settings
|
|
14
11
|
from django.core.cache import cache
|
|
15
|
-
from django.db.models import F
|
|
12
|
+
from django.db.models import F, UUIDField
|
|
16
13
|
from django.apps import apps
|
|
17
14
|
from django.urls import get_resolver
|
|
15
|
+
|
|
16
|
+
# Optional dependencies with graceful fallbacks
|
|
17
|
+
try:
|
|
18
|
+
import numpy as np
|
|
19
|
+
NUMPY_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
np = None
|
|
22
|
+
NUMPY_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import joblib
|
|
26
|
+
JOBLIB_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
joblib = None
|
|
29
|
+
JOBLIB_AVAILABLE = False
|
|
30
|
+
|
|
18
31
|
from .trainer import STATIC_KW, STATUS_IDX, path_exists_in_django
|
|
19
32
|
from .blacklist_manager import BlacklistManager
|
|
20
33
|
from .models import IPExemption
|
|
@@ -30,12 +43,22 @@ MODEL_PATH = getattr(
|
|
|
30
43
|
def load_model_safely():
|
|
31
44
|
"""Load the AI model with version compatibility checking."""
|
|
32
45
|
import warnings
|
|
33
|
-
import sklearn
|
|
34
46
|
|
|
35
47
|
# Check if AI is disabled globally
|
|
36
48
|
ai_disabled = getattr(settings, "AIWAF_DISABLE_AI", False)
|
|
37
49
|
if ai_disabled:
|
|
38
|
-
print("AI functionality disabled via AIWAF_DISABLE_AI setting")
|
|
50
|
+
print("ℹ️ AI functionality disabled via AIWAF_DISABLE_AI setting")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
# Check if required dependencies are available
|
|
54
|
+
if not JOBLIB_AVAILABLE:
|
|
55
|
+
print("ℹ️ joblib not available, AI functionality disabled")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
import sklearn
|
|
60
|
+
except ImportError:
|
|
61
|
+
print("ℹ️ sklearn not available, AI functionality disabled")
|
|
39
62
|
return None
|
|
40
63
|
|
|
41
64
|
try:
|
|
@@ -52,13 +75,13 @@ def load_model_safely():
|
|
|
52
75
|
current_version = sklearn.__version__
|
|
53
76
|
|
|
54
77
|
if stored_version != current_version:
|
|
55
|
-
print(f"Model was trained with sklearn v{stored_version}, current v{current_version}")
|
|
78
|
+
print(f"ℹ️ Model was trained with sklearn v{stored_version}, current v{current_version}")
|
|
56
79
|
print(" Run 'python manage.py detect_and_train' to update model if needed.")
|
|
57
80
|
|
|
58
81
|
return model
|
|
59
82
|
else:
|
|
60
83
|
# Old format - direct model object
|
|
61
|
-
print("Using legacy model format. Consider retraining for better compatibility.")
|
|
84
|
+
print("ℹ️ Using legacy model format. Consider retraining for better compatibility.")
|
|
62
85
|
return model_data
|
|
63
86
|
|
|
64
87
|
except Exception as e:
|
|
@@ -66,6 +89,7 @@ def load_model_safely():
|
|
|
66
89
|
print("AI anomaly detection will be disabled until model is retrained.")
|
|
67
90
|
print("Run 'python manage.py detect_and_train' to regenerate the model.")
|
|
68
91
|
return None
|
|
92
|
+
return None
|
|
69
93
|
|
|
70
94
|
# Load model with safety checks
|
|
71
95
|
MODEL = load_model_safely()
|
|
@@ -529,20 +553,22 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
529
553
|
burst_count = sum(1 for (t, _, _, _) in data if now - t <= 10)
|
|
530
554
|
total_404 = sum(1 for (_, _, st, _) in data if st == 404)
|
|
531
555
|
feats = [path_len, kw_hits, resp_time, status_idx, burst_count, total_404]
|
|
532
|
-
X = np.array(feats, dtype=float).reshape(1, -1)
|
|
533
556
|
|
|
534
|
-
# Only use AI model if it's available
|
|
535
|
-
if self.model is not None and
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
# Get recent behavior data for this IP to make intelligent blocking decision
|
|
539
|
-
recent_data = [d for d in data if now - d[0] <= 300] # Last 5 minutes
|
|
557
|
+
# Only use AI model if it's available and numpy is available
|
|
558
|
+
if self.model is not None and NUMPY_AVAILABLE:
|
|
559
|
+
X = np.array(feats, dtype=float).reshape(1, -1)
|
|
540
560
|
|
|
541
|
-
if
|
|
542
|
-
#
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
561
|
+
if self.model.predict(X)[0] == -1:
|
|
562
|
+
# AI detected anomaly - but analyze patterns before blocking (like trainer.py)
|
|
563
|
+
|
|
564
|
+
# Get recent behavior data for this IP to make intelligent blocking decision
|
|
565
|
+
recent_data = [d for d in data if now - d[0] <= 300] # Last 5 minutes
|
|
566
|
+
|
|
567
|
+
if recent_data:
|
|
568
|
+
# Calculate behavior metrics similar to trainer.py
|
|
569
|
+
recent_kw_hits = []
|
|
570
|
+
recent_404s = 0
|
|
571
|
+
recent_burst_counts = []
|
|
546
572
|
|
|
547
573
|
for entry_time, entry_path, entry_status, entry_resp_time in recent_data:
|
|
548
574
|
# Calculate keyword hits for this entry
|
|
@@ -615,6 +641,110 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
615
641
|
|
|
616
642
|
class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
617
643
|
MIN_FORM_TIME = getattr(settings, "AIWAF_MIN_FORM_TIME", 1.0) # seconds
|
|
644
|
+
MAX_PAGE_TIME = getattr(settings, "AIWAF_MAX_PAGE_TIME", 240) # 4 minutes default
|
|
645
|
+
|
|
646
|
+
def _view_accepts_method(self, request, method):
|
|
647
|
+
"""Check if the current view/URL pattern accepts the specified HTTP method"""
|
|
648
|
+
try:
|
|
649
|
+
from django.urls import resolve
|
|
650
|
+
from django.urls.resolvers import URLResolver, URLPattern
|
|
651
|
+
|
|
652
|
+
# Resolve the current URL to get the view
|
|
653
|
+
resolved = resolve(request.path)
|
|
654
|
+
view_func = resolved.func
|
|
655
|
+
|
|
656
|
+
# Handle class-based views
|
|
657
|
+
if hasattr(view_func, 'cls'):
|
|
658
|
+
view_class = view_func.cls
|
|
659
|
+
|
|
660
|
+
# Check http_method_names attribute (most reliable)
|
|
661
|
+
if hasattr(view_class, 'http_method_names'):
|
|
662
|
+
allowed_methods = [m.upper() for m in view_class.http_method_names]
|
|
663
|
+
return method.upper() in allowed_methods
|
|
664
|
+
|
|
665
|
+
# Check for method-handling methods
|
|
666
|
+
method_handlers = {
|
|
667
|
+
'GET': ['get'],
|
|
668
|
+
'POST': ['post', 'form_valid', 'form_invalid'],
|
|
669
|
+
'PUT': ['put'],
|
|
670
|
+
'PATCH': ['patch'],
|
|
671
|
+
'DELETE': ['delete']
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if method.upper() in method_handlers:
|
|
675
|
+
handlers = method_handlers[method.upper()]
|
|
676
|
+
has_handler = any(hasattr(view_class, handler) for handler in handlers)
|
|
677
|
+
if has_handler:
|
|
678
|
+
return True
|
|
679
|
+
|
|
680
|
+
# If no handler found, check if it's a common method that should be rejected
|
|
681
|
+
if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
|
|
682
|
+
return False
|
|
683
|
+
|
|
684
|
+
# Default: assume method is allowed for class-based views
|
|
685
|
+
return True
|
|
686
|
+
|
|
687
|
+
# Handle function-based views
|
|
688
|
+
else:
|
|
689
|
+
# Check if view has explicit allowed methods
|
|
690
|
+
if hasattr(view_func, 'http_method_names'):
|
|
691
|
+
allowed_methods = [m.upper() for m in view_func.http_method_names]
|
|
692
|
+
return method.upper() in allowed_methods
|
|
693
|
+
|
|
694
|
+
# For function-based views, inspect the source code
|
|
695
|
+
import inspect
|
|
696
|
+
try:
|
|
697
|
+
source = inspect.getsource(view_func)
|
|
698
|
+
method_upper = method.upper()
|
|
699
|
+
|
|
700
|
+
# Look for method handling in the source
|
|
701
|
+
if f'request.method' in source and method_upper in source:
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
# Look for method-specific patterns
|
|
705
|
+
method_patterns = {
|
|
706
|
+
'GET': ['request.GET', 'GET'],
|
|
707
|
+
'POST': ['request.POST', 'POST', 'form.is_valid()'],
|
|
708
|
+
'PUT': ['PUT', 'request.PUT'],
|
|
709
|
+
'DELETE': ['DELETE', 'request.DELETE']
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if method.upper() in method_patterns:
|
|
713
|
+
patterns = method_patterns[method.upper()]
|
|
714
|
+
if any(pattern in source for pattern in patterns):
|
|
715
|
+
return True
|
|
716
|
+
|
|
717
|
+
except (OSError, TypeError):
|
|
718
|
+
# Can't get source, make educated guess
|
|
719
|
+
pass
|
|
720
|
+
|
|
721
|
+
# Check URL pattern name for method-specific endpoints
|
|
722
|
+
if resolved.url_name:
|
|
723
|
+
url_name_lower = resolved.url_name.lower()
|
|
724
|
+
|
|
725
|
+
# POST-only patterns
|
|
726
|
+
post_only_patterns = ['create', 'submit', 'upload', 'process']
|
|
727
|
+
# GET-only patterns
|
|
728
|
+
get_only_patterns = ['list', 'detail', 'view', 'display']
|
|
729
|
+
|
|
730
|
+
if method.upper() == 'POST':
|
|
731
|
+
if any(pattern in url_name_lower for pattern in post_only_patterns):
|
|
732
|
+
return True
|
|
733
|
+
if any(pattern in url_name_lower for pattern in get_only_patterns):
|
|
734
|
+
return False
|
|
735
|
+
elif method.upper() == 'GET':
|
|
736
|
+
if any(pattern in url_name_lower for pattern in get_only_patterns):
|
|
737
|
+
return True
|
|
738
|
+
if any(pattern in url_name_lower for pattern in post_only_patterns):
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
# Default: assume function-based views accept common methods
|
|
742
|
+
return method.upper() in ['GET', 'POST', 'HEAD', 'OPTIONS']
|
|
743
|
+
|
|
744
|
+
except Exception as e:
|
|
745
|
+
# If we can't determine, err on the side of caution and allow
|
|
746
|
+
print(f"AIWAF: Could not determine {method} capability for {request.path}: {e}")
|
|
747
|
+
return True
|
|
618
748
|
|
|
619
749
|
def process_request(self, request):
|
|
620
750
|
if is_exempt(request):
|
|
@@ -629,11 +759,33 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
629
759
|
return None
|
|
630
760
|
|
|
631
761
|
if request.method == "GET":
|
|
762
|
+
# ENHANCEMENT: Check if this view accepts GET requests
|
|
763
|
+
if not self._view_accepts_method(request, 'GET'):
|
|
764
|
+
# This view is POST-only, but received a GET - likely scanning/probing
|
|
765
|
+
if not exemption_store.is_exempted(ip):
|
|
766
|
+
BlacklistManager.block(ip, f"GET to POST-only view: {request.path}")
|
|
767
|
+
if BlacklistManager.is_blocked(ip):
|
|
768
|
+
return JsonResponse({
|
|
769
|
+
"error": "blocked",
|
|
770
|
+
"message": f"GET not allowed for {request.path}"
|
|
771
|
+
}, status=405) # Method Not Allowed
|
|
772
|
+
|
|
632
773
|
# Store timestamp for this IP's GET request
|
|
633
774
|
# Use a general key for the IP, not path-specific
|
|
634
775
|
cache.set(f"honeypot_get:{ip}", time.time(), timeout=300) # 5 min timeout
|
|
635
776
|
|
|
636
777
|
elif request.method == "POST":
|
|
778
|
+
# ENHANCEMENT: Check if this view actually accepts POST requests
|
|
779
|
+
if not self._view_accepts_method(request, 'POST'):
|
|
780
|
+
# This view is GET-only, but received a POST - likely malicious
|
|
781
|
+
if not exemption_store.is_exempted(ip):
|
|
782
|
+
BlacklistManager.block(ip, f"POST to GET-only view: {request.path}")
|
|
783
|
+
if BlacklistManager.is_blocked(ip):
|
|
784
|
+
return JsonResponse({
|
|
785
|
+
"error": "blocked",
|
|
786
|
+
"message": f"POST not allowed for {request.path}"
|
|
787
|
+
}, status=405) # Method Not Allowed
|
|
788
|
+
|
|
637
789
|
# Check if there was a preceding GET request
|
|
638
790
|
get_time = cache.get(f"honeypot_get:{ip}")
|
|
639
791
|
|
|
@@ -654,6 +806,17 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
654
806
|
time_diff = time.time() - get_time
|
|
655
807
|
min_time = self.MIN_FORM_TIME
|
|
656
808
|
|
|
809
|
+
# ENHANCEMENT 2: Check for page timeout (4+ minutes)
|
|
810
|
+
if time_diff > self.MAX_PAGE_TIME:
|
|
811
|
+
# Page has been open too long - suspicious or stale session
|
|
812
|
+
# Don't block immediately, but require a fresh page load
|
|
813
|
+
cache.delete(f"honeypot_get:{ip}") # Force fresh GET
|
|
814
|
+
return JsonResponse({
|
|
815
|
+
"error": "page_expired",
|
|
816
|
+
"message": "Page has expired. Please reload and try again.",
|
|
817
|
+
"reload_required": True
|
|
818
|
+
}, status=409) # 409 Conflict - client should reload
|
|
819
|
+
|
|
657
820
|
# Use shorter time threshold for login paths (users can login quickly)
|
|
658
821
|
if any(request.path.lower().startswith(login_path) for login_path in [
|
|
659
822
|
"/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
|
|
@@ -668,6 +831,19 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
668
831
|
if BlacklistManager.is_blocked(ip):
|
|
669
832
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
670
833
|
|
|
834
|
+
else:
|
|
835
|
+
# Handle other HTTP methods (PUT, DELETE, PATCH, etc.)
|
|
836
|
+
if request.method not in ['GET', 'POST', 'HEAD', 'OPTIONS']:
|
|
837
|
+
# Check if this view supports the requested method
|
|
838
|
+
if not self._view_accepts_method(request, request.method):
|
|
839
|
+
if not exemption_store.is_exempted(ip):
|
|
840
|
+
BlacklistManager.block(ip, f"{request.method} to view that doesn't support it: {request.path}")
|
|
841
|
+
if BlacklistManager.is_blocked(ip):
|
|
842
|
+
return JsonResponse({
|
|
843
|
+
"error": "blocked",
|
|
844
|
+
"message": f"{request.method} not allowed for {request.path}"
|
|
845
|
+
}, status=405) # Method Not Allowed
|
|
846
|
+
|
|
671
847
|
return None
|
|
672
848
|
|
|
673
849
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiwaf
|
|
3
|
-
Version: 0.1.9.3.
|
|
3
|
+
Version: 0.1.9.3.1
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -32,6 +32,8 @@ Dynamic: requires-python
|
|
|
32
32
|
- ✅ **Granular Reset Commands** - Clear specific data types (`--blacklist`, `--keywords`, `--exemptions`)
|
|
33
33
|
- ✅ **Context-Aware Learning** - Only learns from suspicious requests, not legitimate site functionality
|
|
34
34
|
- ✅ **Enhanced Configuration** - `AIWAF_ALLOWED_PATH_KEYWORDS` and `AIWAF_EXEMPT_KEYWORDS`
|
|
35
|
+
- ✅ **Comprehensive HTTP Method Validation** - Blocks GET→POST-only, POST→GET-only, unsupported REST methods
|
|
36
|
+
- ✅ **Enhanced Honeypot Protection** - POST validation & 4-minute page timeout with smart reload detection
|
|
35
37
|
|
|
36
38
|
---
|
|
37
39
|
|
|
@@ -111,10 +113,16 @@ aiwaf/
|
|
|
111
113
|
- **File‑Extension Probing Detection**
|
|
112
114
|
Tracks repeated 404s on common extensions (e.g. `.php`, `.asp`) and blocks IPs.
|
|
113
115
|
|
|
114
|
-
- **Timing-Based Honeypot**
|
|
115
|
-
|
|
116
|
+
- **Enhanced Timing-Based Honeypot**
|
|
117
|
+
Advanced GET→POST timing analysis with comprehensive HTTP method validation:
|
|
116
118
|
- POST directly without a preceding GET request
|
|
117
119
|
- Submit forms faster than `AIWAF_MIN_FORM_TIME` seconds (default: 1 second)
|
|
120
|
+
- **🆕 Smart HTTP Method Validation** - Comprehensive protection against method misuse:
|
|
121
|
+
- Blocks GET requests to POST-only views (form endpoints, API creates)
|
|
122
|
+
- Blocks POST requests to GET-only views (list pages, read-only content)
|
|
123
|
+
- Blocks unsupported REST methods (PUT/DELETE to non-REST views)
|
|
124
|
+
- Uses Django view analysis: class-based views, method handlers, URL patterns
|
|
125
|
+
- **🆕 Page expiration** after `AIWAF_MAX_PAGE_TIME` (4 minutes) with smart reload
|
|
118
126
|
|
|
119
127
|
- **UUID Tampering Protection**
|
|
120
128
|
Blocks guessed or invalid UUIDs that don't resolve to real models.
|
|
@@ -509,6 +517,7 @@ python manage.py aiwaf_logging --clear # Clear log files
|
|
|
509
517
|
```python
|
|
510
518
|
AIWAF_MODEL_PATH = BASE_DIR / "aiwaf" / "resources" / "model.pkl"
|
|
511
519
|
AIWAF_MIN_FORM_TIME = 1.0 # minimum seconds between GET and POST
|
|
520
|
+
AIWAF_MAX_PAGE_TIME = 240 # maximum page age before requiring reload (4 minutes)
|
|
512
521
|
AIWAF_AI_CONTAMINATION = 0.05 # AI anomaly detection sensitivity (5%)
|
|
513
522
|
AIWAF_RATE_WINDOW = 10 # seconds
|
|
514
523
|
AIWAF_RATE_MAX = 20 # max requests per window
|
|
@@ -801,9 +810,36 @@ python manage.py aiwaf_reset --blacklist --confirm
|
|
|
801
810
|
| IPAndKeywordBlockMiddleware | Blocks requests from known blacklisted IPs and Keywords |
|
|
802
811
|
| RateLimitMiddleware | Enforces burst & flood thresholds |
|
|
803
812
|
| AIAnomalyMiddleware | ML‑driven behavior analysis + block on anomaly |
|
|
804
|
-
| HoneypotTimingMiddleware |
|
|
813
|
+
| HoneypotTimingMiddleware | Enhanced bot detection: GET→POST timing, POST validation, page timeouts |
|
|
805
814
|
| UUIDTamperMiddleware | Blocks guessed/nonexistent UUIDs across all models in an app |
|
|
806
815
|
|
|
816
|
+
### 🍯 Enhanced Honeypot Protection
|
|
817
|
+
|
|
818
|
+
The **HoneypotTimingMiddleware** now includes advanced bot detection capabilities:
|
|
819
|
+
|
|
820
|
+
#### 🚫 Smart POST Request Validation
|
|
821
|
+
- **Analyzes Django views** to determine actual allowed HTTP methods
|
|
822
|
+
- **Intelligent detection** of GET-only vs POST-capable views
|
|
823
|
+
- **Example**: `POST` to view with `http_method_names = ['get']` → `403 Blocked`
|
|
824
|
+
|
|
825
|
+
#### ⏰ Page Timeout with Smart Reload
|
|
826
|
+
- **4-minute page expiration** prevents stale session attacks
|
|
827
|
+
- **HTTP 409 response** with reload instructions instead of immediate blocking
|
|
828
|
+
- **CSRF protection** by forcing fresh page loads for old sessions
|
|
829
|
+
|
|
830
|
+
```python
|
|
831
|
+
# Configuration
|
|
832
|
+
AIWAF_MIN_FORM_TIME = 1.0 # Minimum form submission time
|
|
833
|
+
AIWAF_MAX_PAGE_TIME = 240 # Page timeout (4 minutes)
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
**Timeline Example**:
|
|
837
|
+
```
|
|
838
|
+
12:00:00 - GET /contact/ ✅ Page loaded
|
|
839
|
+
12:02:00 - POST /contact/ ✅ Valid submission (2 minutes)
|
|
840
|
+
12:04:30 - POST /contact/ ❌ 409 Conflict (page expired, reload required)
|
|
841
|
+
```
|
|
842
|
+
|
|
807
843
|
---
|
|
808
844
|
|
|
809
845
|
## Sponsors
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
aiwaf/__init__.py,sha256=
|
|
1
|
+
aiwaf/__init__.py,sha256=VlFbI8uqJmi1V0hsKasasV1BFglekVX0R5jvEOwXGzE,220
|
|
2
2
|
aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
|
|
3
3
|
aiwaf/blacklist_manager.py,sha256=LYCeKFB-7e_C6Bg2WeFJWFIIQlrfRMPuGp30ivrnhQY,1196
|
|
4
4
|
aiwaf/decorators.py,sha256=IUKOdM_gdroffImRZep1g1wT6gNqD10zGwcp28hsJCs,825
|
|
5
|
-
aiwaf/middleware.py,sha256=
|
|
5
|
+
aiwaf/middleware.py,sha256=BnVdA4g2YTDo5g_H1Q8EE-ctVR4JF_yV3PaLHMgYZ-E,40804
|
|
6
6
|
aiwaf/middleware_logger.py,sha256=LWZVDAnjh6CGESirA8eMbhGgJKB7lVDGRQqVroH95Lo,4742
|
|
7
7
|
aiwaf/models.py,sha256=vQxgY19BDVMjoO903UNrTZC1pNoLltMU6wbyWPoAEns,2719
|
|
8
8
|
aiwaf/storage.py,sha256=pUXE3bm7aRrABh_B6jTOBUQOYK67oQmHaR9EqyOasis,14038
|
|
@@ -13,7 +13,7 @@ aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
13
13
|
aiwaf/management/commands/add_exemption.py,sha256=U_ByfJw1EstAZ8DaSoRb97IGwYzXs0DBJkVAqeN4Wak,1128
|
|
14
14
|
aiwaf/management/commands/add_ipexemption.py,sha256=sSf3d9hGK9RqqlBYkCrnrd8KZWGT-derSpoWnEY4H60,952
|
|
15
15
|
aiwaf/management/commands/aiwaf_diagnose.py,sha256=nXFRhq66N4QC3e4scYJ2sUngJce-0yDxtBO3R2BllRM,6134
|
|
16
|
-
aiwaf/management/commands/aiwaf_list.py,sha256=
|
|
16
|
+
aiwaf/management/commands/aiwaf_list.py,sha256=DVuhrQ4OUKWlzcQERadhedSRBhedt6OGJrrUkKFIoG0,4821
|
|
17
17
|
aiwaf/management/commands/aiwaf_logging.py,sha256=FCIqULn2tii2vD9VxL7vk3PV4k4vr7kaA00KyaCExYY,7692
|
|
18
18
|
aiwaf/management/commands/aiwaf_reset.py,sha256=pcF0zOYDSqjpCwDtk2HYJZLgr76td8OFRENtl20c1dQ,7472
|
|
19
19
|
aiwaf/management/commands/check_dependencies.py,sha256=GOZl00pDwW2cJjDvIaCeB3yWxmeYcJDRTIpmOTLvy2c,37204
|
|
@@ -29,8 +29,8 @@ aiwaf/management/commands/test_exemption_fix.py,sha256=ngyGaHUCmQQ6y--6j4q1viZJt
|
|
|
29
29
|
aiwaf/resources/model.pkl,sha256=5t6h9BX8yoh2xct85MXOO60jdlWyg1APskUOW0jZE1Y,1288265
|
|
30
30
|
aiwaf/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
aiwaf/templatetags/aiwaf_tags.py,sha256=XXfb7Tl4DjU3Sc40GbqdaqOEtKTUKELBEk58u83wBNw,357
|
|
32
|
-
aiwaf-0.1.9.3.
|
|
33
|
-
aiwaf-0.1.9.3.
|
|
34
|
-
aiwaf-0.1.9.3.
|
|
35
|
-
aiwaf-0.1.9.3.
|
|
36
|
-
aiwaf-0.1.9.3.
|
|
32
|
+
aiwaf-0.1.9.3.1.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
|
|
33
|
+
aiwaf-0.1.9.3.1.dist-info/METADATA,sha256=TQO1y9t5sRQY7zBNyJc6dvTfJ1JXC1vBTf1RjH8O8m0,29037
|
|
34
|
+
aiwaf-0.1.9.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
aiwaf-0.1.9.3.1.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
|
|
36
|
+
aiwaf-0.1.9.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|