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 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
- if recent_data:
568
- # Calculate behavior metrics similar to trainer.py
569
- recent_kw_hits = []
570
- recent_404s = 0
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
- for entry_time, entry_path, entry_status, entry_resp_time in recent_data:
574
- # Calculate keyword hits for this entry
575
- entry_known_path = path_exists_in_django(entry_path)
576
- entry_kw_hits = 0
577
- if not entry_known_path and not is_exempt_path(entry_path):
578
- entry_kw_hits = sum(1 for kw in STATIC_KW if kw in entry_path.lower())
579
- recent_kw_hits.append(entry_kw_hits)
580
-
581
- # Count 404s
582
- if entry_status == 404:
583
- recent_404s += 1
584
-
585
- # Calculate burst for this entry (requests within 10 seconds)
586
- entry_burst = sum(1 for (t, _, _, _) in recent_data if abs(entry_time - t) <= 10)
587
- recent_burst_counts.append(entry_burst)
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
- # Don't block if it looks like legitimate behavior (same thresholds as trainer.py):
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 < 2 and # Not hitting many malicious keywords
598
- max_404s < 10 and # Not excessive 404s
599
- avg_burst < 15 and # Not excessive burst activity
600
- total_requests < 100 # Not excessive total requests
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 + suspicious patterns (kw:{avg_kw_hits:.1f}, 404s:{max_404s}, burst:{avg_burst:.1f})")
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, only block on very suspicious current request
613
- if kw_hits >= 2 or status_idx == STATUS_IDX.index("404"):
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 + immediate suspicious behavior")
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
- """Check if the current view/URL pattern accepts the specified HTTP method"""
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
- # Check for method-handling methods
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
- 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
734
+ return has_handler
683
735
 
684
- # Default: assume method is allowed for class-based views
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
- # 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
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
- # 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
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
- # Default: assume function-based views accept common methods
742
- return method.upper() in ['GET', 'POST', 'HEAD', 'OPTIONS']
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 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}")
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
- # ENHANCEMENT: Check if this view accepts GET requests
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
- # 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
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
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=lRxi8M22Fp1fdhCWQ6XesbxX54aijH3tdSvjLNroQdE,49197
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.3.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
33
- aiwaf-0.1.9.3.3.dist-info/METADATA,sha256=GUXN2Lav1oOSfMnGTEd7ALU6-95yb7LJYbf4iZN-ukM,30989
34
- aiwaf-0.1.9.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- aiwaf-0.1.9.3.3.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
36
- aiwaf-0.1.9.3.3.dist-info/RECORD,,
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,,