aiwaf 0.1.9.3.3__py3-none-any.whl → 0.1.9.3.4__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/middleware.py +124 -104
- {aiwaf-0.1.9.3.3.dist-info → aiwaf-0.1.9.3.4.dist-info}/METADATA +1 -5
- {aiwaf-0.1.9.3.3.dist-info → aiwaf-0.1.9.3.4.dist-info}/RECORD +6 -6
- {aiwaf-0.1.9.3.3.dist-info → aiwaf-0.1.9.3.4.dist-info}/WHEEL +0 -0
- {aiwaf-0.1.9.3.3.dist-info → aiwaf-0.1.9.3.4.dist-info}/licenses/LICENSE +0 -0
- {aiwaf-0.1.9.3.3.dist-info → aiwaf-0.1.9.3.4.dist-info}/top_level.txt +0 -0
aiwaf/middleware.py
CHANGED
|
@@ -504,6 +504,51 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
504
504
|
|
|
505
505
|
return any(malicious_indicators)
|
|
506
506
|
|
|
507
|
+
def _is_scanning_path(self, path):
|
|
508
|
+
"""
|
|
509
|
+
Determine if a 404 path looks like automated scanning vs legitimate browsing.
|
|
510
|
+
Focus on common scanner patterns that indicate malicious intent.
|
|
511
|
+
"""
|
|
512
|
+
path_lower = path.lower()
|
|
513
|
+
|
|
514
|
+
# Common scanning patterns that are clear indicators of malicious activity
|
|
515
|
+
scanning_patterns = [
|
|
516
|
+
# WordPress scanning
|
|
517
|
+
'wp-admin', 'wp-content', 'wp-includes', 'wp-config', 'xmlrpc.php',
|
|
518
|
+
|
|
519
|
+
# Admin/config scanning
|
|
520
|
+
'admin', 'phpmyadmin', 'adminer', 'config', 'configuration',
|
|
521
|
+
'settings', 'setup', 'install', 'installer',
|
|
522
|
+
|
|
523
|
+
# Database/backup scanning
|
|
524
|
+
'backup', 'database', 'db', 'mysql', 'sql', 'dump',
|
|
525
|
+
|
|
526
|
+
# System files scanning
|
|
527
|
+
'.env', '.git', '.htaccess', '.htpasswd', 'passwd', 'shadow',
|
|
528
|
+
'robots.txt', 'sitemap.xml',
|
|
529
|
+
|
|
530
|
+
# Common vulnerabilities
|
|
531
|
+
'cgi-bin', 'scripts', 'shell', 'cmd', 'exec',
|
|
532
|
+
|
|
533
|
+
# File extensions that shouldn't exist on most sites
|
|
534
|
+
'.php', '.asp', '.aspx', '.jsp', '.cgi', '.pl'
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
# Check for scanning patterns
|
|
538
|
+
for pattern in scanning_patterns:
|
|
539
|
+
if pattern in path_lower:
|
|
540
|
+
return True
|
|
541
|
+
|
|
542
|
+
# Check for directory traversal attempts
|
|
543
|
+
if '../' in path or '..' in path:
|
|
544
|
+
return True
|
|
545
|
+
|
|
546
|
+
# Check for encoded attack patterns
|
|
547
|
+
if any(encoded in path for encoded in ['%2e%2e', '%252e', '%c0%ae']):
|
|
548
|
+
return True
|
|
549
|
+
|
|
550
|
+
return False
|
|
551
|
+
|
|
507
552
|
def process_request(self, request):
|
|
508
553
|
# First exemption check - early exit for exempt requests
|
|
509
554
|
if is_exempt(request):
|
|
@@ -564,27 +609,27 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
564
609
|
# Get recent behavior data for this IP to make intelligent blocking decision
|
|
565
610
|
recent_data = [d for d in data if now - d[0] <= 300] # Last 5 minutes
|
|
566
611
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
recent_burst_counts = []
|
|
612
|
+
# Always initialize variables before use
|
|
613
|
+
recent_kw_hits = []
|
|
614
|
+
recent_404s = 0
|
|
615
|
+
recent_burst_counts = []
|
|
572
616
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
617
|
+
if recent_data:
|
|
618
|
+
for entry_time, entry_path, entry_status, entry_resp_time in recent_data:
|
|
619
|
+
# Calculate keyword hits for this entry
|
|
620
|
+
entry_known_path = path_exists_in_django(entry_path)
|
|
621
|
+
entry_kw_hits = 0
|
|
622
|
+
if not entry_known_path and not is_exempt_path(entry_path):
|
|
623
|
+
entry_kw_hits = sum(1 for kw in STATIC_KW if kw in entry_path.lower())
|
|
624
|
+
recent_kw_hits.append(entry_kw_hits)
|
|
625
|
+
|
|
626
|
+
# Count 404s
|
|
627
|
+
if entry_status == 404:
|
|
628
|
+
recent_404s += 1
|
|
629
|
+
|
|
630
|
+
# Calculate burst for this entry (requests within 10 seconds)
|
|
631
|
+
entry_burst = sum(1 for (t, _, _, _) in recent_data if abs(entry_time - t) <= 10)
|
|
632
|
+
recent_burst_counts.append(entry_burst)
|
|
588
633
|
|
|
589
634
|
# Calculate averages and maximums
|
|
590
635
|
avg_kw_hits = sum(recent_kw_hits) / len(recent_kw_hits) if recent_kw_hits else 0
|
|
@@ -592,28 +637,37 @@ class AIAnomalyMiddleware(MiddlewareMixin):
|
|
|
592
637
|
avg_burst = sum(recent_burst_counts) / len(recent_burst_counts) if recent_burst_counts else 0
|
|
593
638
|
total_requests = len(recent_data)
|
|
594
639
|
|
|
595
|
-
#
|
|
640
|
+
# Enhanced 404 analysis - focus on scanning patterns
|
|
641
|
+
scanning_404s = sum(1 for (_, path, status, _) in recent_data
|
|
642
|
+
if status == 404 and self._is_scanning_path(path))
|
|
643
|
+
legitimate_404s = max_404s - scanning_404s
|
|
644
|
+
|
|
645
|
+
# Don't block if it looks like legitimate behavior:
|
|
596
646
|
if (
|
|
597
|
-
avg_kw_hits <
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
647
|
+
avg_kw_hits < 3 and # Allow some keyword hits (increased from 2)
|
|
648
|
+
scanning_404s < 5 and # Focus on scanning 404s, not all 404s
|
|
649
|
+
legitimate_404s < 20 and # Allow more legitimate 404s (typos, old links)
|
|
650
|
+
avg_burst < 25 and # Allow higher burst (increased from 15)
|
|
651
|
+
total_requests < 150 # Allow more total requests (increased from 100)
|
|
601
652
|
):
|
|
602
653
|
# Anomalous but looks legitimate - don't block
|
|
603
654
|
pass
|
|
604
655
|
else:
|
|
605
656
|
# Double-check exemption before blocking
|
|
606
657
|
if not exemption_store.is_exempted(ip):
|
|
607
|
-
BlacklistManager.block(ip, f"AI anomaly +
|
|
658
|
+
BlacklistManager.block(ip, f"AI anomaly + scanning 404s (total:{max_404s}, scanning:{scanning_404s}, kw:{avg_kw_hits:.1f}, burst:{avg_burst:.1f})")
|
|
608
659
|
# Check if actually blocked (exempted IPs won't be blocked)
|
|
609
660
|
if BlacklistManager.is_blocked(ip):
|
|
610
661
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
611
662
|
else:
|
|
612
|
-
# No recent data to analyze - be more conservative
|
|
613
|
-
|
|
663
|
+
# No recent data to analyze - be more conservative
|
|
664
|
+
# Only block on multiple suspicious indicators, not single 404
|
|
665
|
+
current_scanning = self._is_scanning_path(request.path)
|
|
666
|
+
|
|
667
|
+
if kw_hits >= 3 and current_scanning: # Require both high keywords AND scanning pattern
|
|
614
668
|
# Double-check exemption before blocking
|
|
615
669
|
if not exemption_store.is_exempted(ip):
|
|
616
|
-
BlacklistManager.block(ip, "AI anomaly +
|
|
670
|
+
BlacklistManager.block(ip, f"AI anomaly + scanning behavior (kw:{kw_hits}, scanning_path:{request.path})")
|
|
617
671
|
if BlacklistManager.is_blocked(ip):
|
|
618
672
|
return JsonResponse({"error": "blocked"}, status=403)
|
|
619
673
|
|
|
@@ -644,10 +698,13 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
644
698
|
MAX_PAGE_TIME = getattr(settings, "AIWAF_MAX_PAGE_TIME", 240) # 4 minutes default
|
|
645
699
|
|
|
646
700
|
def _view_accepts_method(self, request, method):
|
|
647
|
-
"""
|
|
701
|
+
"""
|
|
702
|
+
Check if the current view accepts the specified HTTP method.
|
|
703
|
+
Be very conservative - only block when we're absolutely certain.
|
|
704
|
+
Handle decorator issues by being permissive when detection fails.
|
|
705
|
+
"""
|
|
648
706
|
try:
|
|
649
707
|
from django.urls import resolve
|
|
650
|
-
from django.urls.resolvers import URLResolver, URLPattern
|
|
651
708
|
|
|
652
709
|
# Resolve the current URL to get the view
|
|
653
710
|
resolved = resolve(request.path)
|
|
@@ -657,12 +714,12 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
657
714
|
if hasattr(view_func, 'cls'):
|
|
658
715
|
view_class = view_func.cls
|
|
659
716
|
|
|
660
|
-
# Check http_method_names attribute (most reliable)
|
|
717
|
+
# Check http_method_names attribute (most reliable for CBVs)
|
|
661
718
|
if hasattr(view_class, 'http_method_names'):
|
|
662
719
|
allowed_methods = [m.upper() for m in view_class.http_method_names]
|
|
663
720
|
return method.upper() in allowed_methods
|
|
664
721
|
|
|
665
|
-
#
|
|
722
|
+
# For CBVs without http_method_names, check for method handlers
|
|
666
723
|
method_handlers = {
|
|
667
724
|
'GET': ['get'],
|
|
668
725
|
'POST': ['post', 'form_valid', 'form_invalid'],
|
|
@@ -674,76 +731,30 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
674
731
|
if method.upper() in method_handlers:
|
|
675
732
|
handlers = method_handlers[method.upper()]
|
|
676
733
|
has_handler = any(hasattr(view_class, handler) for handler in handlers)
|
|
677
|
-
|
|
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
|
|
734
|
+
return has_handler
|
|
683
735
|
|
|
684
|
-
# Default
|
|
736
|
+
# Default for CBVs: be permissive
|
|
685
737
|
return True
|
|
686
738
|
|
|
687
|
-
# Handle function-based views
|
|
739
|
+
# Handle function-based views (including decorated ones)
|
|
688
740
|
else:
|
|
689
|
-
#
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
741
|
+
# Try to unwrap decorators to get the actual view function
|
|
742
|
+
actual_func = view_func
|
|
743
|
+
while hasattr(actual_func, '__wrapped__'):
|
|
744
|
+
actual_func = actual_func.__wrapped__
|
|
693
745
|
|
|
694
|
-
#
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
|
746
|
+
# Check if the actual function has explicit allowed methods
|
|
747
|
+
if hasattr(actual_func, 'http_method_names'):
|
|
748
|
+
allowed_methods = [m.upper() for m in actual_func.http_method_names]
|
|
749
|
+
return method.upper() in allowed_methods
|
|
740
750
|
|
|
741
|
-
#
|
|
742
|
-
|
|
751
|
+
# For function-based views, be very conservative
|
|
752
|
+
# Most Django views accept both GET and POST, so default to allowing
|
|
753
|
+
return True
|
|
743
754
|
|
|
744
755
|
except Exception as e:
|
|
745
|
-
# If
|
|
746
|
-
|
|
756
|
+
# If anything fails (decorators, imports, etc.), be permissive
|
|
757
|
+
# Better to allow a legitimate request than block it
|
|
747
758
|
return True
|
|
748
759
|
|
|
749
760
|
def process_request(self, request):
|
|
@@ -759,16 +770,25 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
|
|
|
759
770
|
return None
|
|
760
771
|
|
|
761
772
|
if request.method == "GET":
|
|
762
|
-
#
|
|
773
|
+
# CONSERVATIVE: Only block GET if we're absolutely certain it's POST-only
|
|
774
|
+
# Most Django views accept both GET and POST (forms show on GET, process on POST)
|
|
763
775
|
if not self._view_accepts_method(request, 'GET'):
|
|
764
|
-
#
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
776
|
+
# EXTRA CHECK: Only block if path looks like obvious POST-only API endpoint
|
|
777
|
+
path_lower = request.path.lower()
|
|
778
|
+
obvious_post_only = any(path_lower.endswith(pattern) for pattern in [
|
|
779
|
+
'/create/', '/submit/', '/upload/', '/delete/', '/process/'
|
|
780
|
+
])
|
|
781
|
+
|
|
782
|
+
if obvious_post_only:
|
|
783
|
+
# This is very likely a POST-only endpoint getting a GET
|
|
784
|
+
if not exemption_store.is_exempted(ip):
|
|
785
|
+
BlacklistManager.block(ip, f"GET to obvious POST-only endpoint: {request.path}")
|
|
786
|
+
if BlacklistManager.is_blocked(ip):
|
|
787
|
+
return JsonResponse({
|
|
788
|
+
"error": "blocked",
|
|
789
|
+
"message": f"GET not allowed for {request.path}"
|
|
790
|
+
}, status=405) # Method Not Allowed
|
|
791
|
+
# Otherwise, don't block - could be a decorated view or complex form
|
|
772
792
|
|
|
773
793
|
# Store timestamp for this IP's GET request
|
|
774
794
|
# Use a general key for the IP, not path-specific
|
|
@@ -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.4
|
|
4
4
|
Summary: AI-powered Web Application Firewall
|
|
5
5
|
Home-page: https://github.com/aayushgauba/aiwaf
|
|
6
6
|
Author: Aayush Gauba
|
|
@@ -904,7 +904,3 @@ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) f
|
|
|
904
904
|
|
|
905
905
|
---
|
|
906
906
|
|
|
907
|
-
## Credits
|
|
908
|
-
|
|
909
|
-
**AI‑WAF** by [Aayush Gauba](https://github.com/aayushgauba)
|
|
910
|
-
> "Let your firewall learn and evolve — keep your site a fortress." your Django `INSTALLED_APPS` to avoid setup errors.
|
|
@@ -2,7 +2,7 @@ aiwaf/__init__.py,sha256=Rnla6te9DNqQBP_HMEdhUdQdj9dd4ECcAr6F62Xs4-A,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=_Erl9GGf1nrfywfghX1NU4CTuveugDlyTgP3sxu6h_A,49928
|
|
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
|
|
@@ -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.4.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
|
|
33
|
+
aiwaf-0.1.9.3.4.dist-info/METADATA,sha256=bgaJr_xz1U7y_wXrB0xkgXn_LPJknN_9FeTN5Bahe3c,30790
|
|
34
|
+
aiwaf-0.1.9.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
aiwaf-0.1.9.3.4.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
|
|
36
|
+
aiwaf-0.1.9.3.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|