pywebguard 0.0.0__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.
Files changed (43) hide show
  1. pywebguard/__init__.py +87 -0
  2. pywebguard/cli.py +1022 -0
  3. pywebguard/core/__init__.py +0 -0
  4. pywebguard/core/base.py +792 -0
  5. pywebguard/core/config.py +455 -0
  6. pywebguard/filters/__init__.py +0 -0
  7. pywebguard/filters/base.py +44 -0
  8. pywebguard/filters/ip_filter.py +233 -0
  9. pywebguard/filters/user_agent.py +102 -0
  10. pywebguard/frameworks/__init__.py +0 -0
  11. pywebguard/frameworks/_fastapi.py +191 -0
  12. pywebguard/frameworks/_flask.py +182 -0
  13. pywebguard/limiters/__init__.py +0 -0
  14. pywebguard/limiters/base.py +46 -0
  15. pywebguard/limiters/rate_limit.py +415 -0
  16. pywebguard/logging/__init__.py +0 -0
  17. pywebguard/logging/backends/__init__.py +17 -0
  18. pywebguard/logging/backends/_elasticsearch.py +307 -0
  19. pywebguard/logging/backends/_meilisearch.py +310 -0
  20. pywebguard/logging/backends/_mongodb.py +303 -0
  21. pywebguard/logging/base.py +117 -0
  22. pywebguard/logging/logger.py +408 -0
  23. pywebguard/security/__init__.py +0 -0
  24. pywebguard/security/base.py +44 -0
  25. pywebguard/security/cors.py +204 -0
  26. pywebguard/security/penetration.py +265 -0
  27. pywebguard/storage/__init__.py +0 -0
  28. pywebguard/storage/_mongodb.py +396 -0
  29. pywebguard/storage/_postgresql.py +539 -0
  30. pywebguard/storage/_redis.py +314 -0
  31. pywebguard/storage/_sqlite.py +374 -0
  32. pywebguard/storage/_tinydb.py +271 -0
  33. pywebguard/storage/base.py +327 -0
  34. pywebguard/storage/memory.py +245 -0
  35. pywebguard/utils/__init__.py +0 -0
  36. pywebguard/utils/ip.py +84 -0
  37. pywebguard/utils/request.py +65 -0
  38. pywebguard-0.0.0.dist-info/LICENSE +21 -0
  39. pywebguard-0.0.0.dist-info/METADATA +368 -0
  40. pywebguard-0.0.0.dist-info/RECORD +43 -0
  41. pywebguard-0.0.0.dist-info/WHEEL +5 -0
  42. pywebguard-0.0.0.dist-info/entry_points.txt +2 -0
  43. pywebguard-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,792 @@
1
+ """
2
+ Base classes for PyWebGuard with both sync and async support.
3
+
4
+ This module provides the core Guard classes that handle security features
5
+ for both synchronous and asynchronous web applications. It includes:
6
+ - IP filtering
7
+ - User agent filtering
8
+ - Rate limiting
9
+ - Penetration detection
10
+ - CORS handling
11
+ - Logging
12
+
13
+ Classes:
14
+ RequestProtocol: Protocol defining the minimum required attributes for a request object
15
+ ResponseProtocol: Protocol defining the minimum required attributes for a response object
16
+ Guard: Base Guard class for synchronous web applications
17
+ AsyncGuard: Base Guard class for asynchronous web applications
18
+ """
19
+
20
+ import os
21
+ from typing import List, Optional, Dict, Any, Union, Type, Protocol, TypeVar, cast
22
+ from datetime import datetime
23
+
24
+ from pywebguard.core.config import GuardConfig, RateLimitConfig
25
+ from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
26
+ from pywebguard.storage.memory import MemoryStorage, AsyncMemoryStorage
27
+
28
+ # Conditional imports to handle optional dependencies
29
+ try:
30
+ from pywebguard.storage._redis import RedisStorage, AsyncRedisStorage
31
+
32
+ REDIS_AVAILABLE = True
33
+ except ImportError:
34
+ REDIS_AVAILABLE = False
35
+
36
+ # Create dummy classes for type checking
37
+ class RedisStorage(BaseStorage):
38
+ pass
39
+
40
+ class AsyncRedisStorage(AsyncBaseStorage):
41
+ pass
42
+
43
+
44
+ try:
45
+ from pywebguard.storage._sqlite import SQLiteStorage, AsyncSQLiteStorage
46
+
47
+ SQLITE_AVAILABLE = True
48
+ except ImportError:
49
+ SQLITE_AVAILABLE = False
50
+
51
+ # Create dummy classes for type checking
52
+ class SQLiteStorage(BaseStorage):
53
+ pass
54
+
55
+ class AsyncSQLiteStorage(AsyncBaseStorage):
56
+ pass
57
+
58
+
59
+ try:
60
+ from pywebguard.storage._tinydb import TinyDBStorage, AsyncTinyDBStorage
61
+
62
+ TINYDB_AVAILABLE = True
63
+ except ImportError:
64
+ TINYDB_AVAILABLE = False
65
+
66
+ # Create dummy classes for type checking
67
+ class TinyDBStorage(BaseStorage):
68
+ pass
69
+
70
+ class AsyncTinyDBStorage(AsyncBaseStorage):
71
+ pass
72
+
73
+
74
+ try:
75
+ from pywebguard.storage._mongodb import MongoDBStorage, AsyncMongoDBStorage
76
+
77
+ MONGODB_AVAILABLE = True
78
+ except ImportError:
79
+ MONGODB_AVAILABLE = False
80
+
81
+ # Create dummy classes for type checking
82
+ class MongoDBStorage(BaseStorage):
83
+ pass
84
+
85
+ class AsyncMongoDBStorage(AsyncBaseStorage):
86
+ pass
87
+
88
+
89
+ try:
90
+ from pywebguard.storage._postgresql import PostgreSQLStorage, AsyncPostgreSQLStorage
91
+
92
+ POSTGRESQL_AVAILABLE = True
93
+ except ImportError:
94
+ POSTGRESQL_AVAILABLE = False
95
+
96
+ # Create dummy classes for type checking
97
+ class PostgreSQLStorage(BaseStorage):
98
+ pass
99
+
100
+ class AsyncPostgreSQLStorage(AsyncBaseStorage):
101
+ pass
102
+
103
+
104
+ from pywebguard.filters.ip_filter import IPFilter, AsyncIPFilter
105
+ from pywebguard.filters.user_agent import UserAgentFilter, AsyncUserAgentFilter
106
+ from pywebguard.limiters.rate_limit import RateLimiter, AsyncRateLimiter
107
+ from pywebguard.security.penetration import (
108
+ PenetrationDetector,
109
+ AsyncPenetrationDetector,
110
+ )
111
+ from pywebguard.security.cors import CORSHandler, AsyncCORSHandler
112
+ from pywebguard.logging.logger import SecurityLogger, AsyncSecurityLogger
113
+
114
+
115
+ class RequestProtocol(Protocol):
116
+ """Protocol defining the minimum required attributes for a request object."""
117
+
118
+ remote_addr: str
119
+ user_agent: str
120
+ method: str
121
+ path: str
122
+ query_string: str
123
+ headers: Dict[str, str]
124
+
125
+
126
+ class ResponseProtocol(Protocol):
127
+ """Protocol defining the minimum required attributes for a response object."""
128
+
129
+ status_code: int
130
+ headers: Dict[str, str]
131
+
132
+
133
+ class Guard:
134
+ """
135
+ Base Guard class that handles security features (synchronous).
136
+
137
+ This is the main class that users will interact with for synchronous
138
+ web applications. It provides a unified interface for all security
139
+ features including IP filtering, rate limiting, and more.
140
+
141
+ Attributes:
142
+ config: The GuardConfig instance containing all security settings
143
+ storage: The storage backend for persistent data
144
+ ip_filter: IP filtering component
145
+ user_agent_filter: User agent filtering component
146
+ rate_limiter: Rate limiting component
147
+ penetration_detector: Penetration detection component
148
+ cors_handler: CORS handling component
149
+ logger: Security logging component
150
+ """
151
+
152
+ def __init__(
153
+ self,
154
+ config: Optional[GuardConfig] = None,
155
+ storage: Optional[BaseStorage] = None,
156
+ route_rate_limits: Optional[List[Dict[str, Any]]] = None,
157
+ ) -> None:
158
+ """
159
+ Initialize the Guard with configuration and storage.
160
+
161
+ Args:
162
+ config: GuardConfig object with security settings. If None, default config is used.
163
+ storage: Storage backend for persistent data. If None, storage is determined from config.
164
+ route_rate_limits: List of dictionaries with route-specific rate limits.
165
+ Each dict should have: endpoint, requests_per_minute, burst_size, auto_ban_threshold (optional)
166
+
167
+ Raises:
168
+ ImportError: If the required storage backend is not installed
169
+ """
170
+ # Use environment variables if no config is provided
171
+ if config is None:
172
+ config = GuardConfig.from_env()
173
+ self.config = config
174
+
175
+ # Initialize storage
176
+ if storage is not None:
177
+ self.storage = storage
178
+ else:
179
+ self.storage = self._create_storage_from_config()
180
+
181
+ # Initialize components based on config
182
+ self._initialize_components()
183
+
184
+ # Add route-specific rate limits if provided
185
+ if route_rate_limits:
186
+ self.add_route_rate_limits(route_rate_limits)
187
+
188
+ def _create_storage_from_config(self) -> BaseStorage:
189
+ """
190
+ Create a storage backend based on configuration.
191
+
192
+ Returns:
193
+ BaseStorage: The storage backend instance
194
+
195
+ Raises:
196
+ ImportError: If the required storage backend is not installed
197
+ ValueError: If the storage type is invalid
198
+ """
199
+ storage_type = self.config.storage.type.lower()
200
+
201
+ if storage_type == "memory":
202
+ return MemoryStorage()
203
+
204
+ elif storage_type == "redis":
205
+ if not REDIS_AVAILABLE:
206
+ raise ImportError(
207
+ "Redis storage is not available. Install it with 'pip install pywebguard[redis]'"
208
+ )
209
+ return RedisStorage(
210
+ url=self.config.storage.url or "redis://localhost:6379/0",
211
+ prefix=self.config.storage.prefix,
212
+ ttl=self.config.storage.ttl,
213
+ )
214
+
215
+ elif storage_type == "sqlite":
216
+ if not SQLITE_AVAILABLE:
217
+ raise ImportError(
218
+ "SQLite storage is not available. Install it with 'pip install pywebguard[sqlite]'"
219
+ )
220
+ return SQLiteStorage(
221
+ db_path=self.config.storage.url or "pywebguard.db",
222
+ table_prefix=self.config.storage.prefix,
223
+ ttl=self.config.storage.ttl,
224
+ )
225
+
226
+ elif storage_type == "tinydb":
227
+ if not TINYDB_AVAILABLE:
228
+ raise ImportError(
229
+ "TinyDB storage is not available. Install it with 'pip install pywebguard[tinydb]'"
230
+ )
231
+ return TinyDBStorage(
232
+ db_path=self.config.storage.url or "pywebguard.json",
233
+ ttl=self.config.storage.ttl,
234
+ )
235
+
236
+ elif storage_type == "mongodb":
237
+ if not MONGODB_AVAILABLE:
238
+ raise ImportError(
239
+ "MongoDB storage is not available. Install it with 'pip install pywebguard[mongodb]'"
240
+ )
241
+ return MongoDBStorage(
242
+ url=self.config.storage.url or "mongodb://localhost:27017/pywebguard",
243
+ collection_name=self.config.storage.table_name,
244
+ ttl=self.config.storage.ttl,
245
+ )
246
+
247
+ elif storage_type == "postgresql":
248
+ if not POSTGRESQL_AVAILABLE:
249
+ raise ImportError(
250
+ "PostgreSQL storage is not available. Install it with 'pip install pywebguard[postgresql]'"
251
+ )
252
+ return PostgreSQLStorage(
253
+ url=self.config.storage.url
254
+ or "postgresql://postgres:postgres@localhost:5432/pywebguard",
255
+ table_name=self.config.storage.table_name,
256
+ ttl=self.config.storage.ttl,
257
+ )
258
+
259
+ else:
260
+ raise ValueError(f"Invalid storage type: {storage_type}")
261
+
262
+ def _initialize_components(self) -> None:
263
+ """
264
+ Initialize all security components based on configuration.
265
+
266
+ This method sets up all the security components (IP filter, rate limiter,
267
+ etc.) using the configuration provided during initialization.
268
+ """
269
+ self.ip_filter = IPFilter(self.config.ip_filter, self.storage)
270
+ self.user_agent_filter = UserAgentFilter(self.config.user_agent, self.storage)
271
+ self.rate_limiter = RateLimiter(self.config.rate_limit, self.storage)
272
+ self.penetration_detector = PenetrationDetector(
273
+ self.config.penetration, self.storage
274
+ )
275
+ self.cors_handler = CORSHandler(self.config.cors)
276
+ self.logger = SecurityLogger(self.config.logging)
277
+
278
+ def add_route_rate_limit(
279
+ self, route_pattern: str, config: Union[RateLimitConfig, Dict[str, Any]]
280
+ ) -> None:
281
+ """
282
+ Add a custom rate limit configuration for a specific route pattern.
283
+
284
+ Args:
285
+ route_pattern: The route pattern to match (can include wildcards like * and **)
286
+ config: Custom rate limit configuration for this route
287
+ """
288
+ self.rate_limiter.add_route_config(route_pattern, config)
289
+
290
+ def add_route_rate_limits(self, route_configs: List[Dict[str, Any]]) -> None:
291
+ """
292
+ Add multiple custom rate limit configurations for specific route patterns.
293
+
294
+ Args:
295
+ route_configs: List of dictionaries with route-specific rate limits.
296
+ Each dict should have: endpoint, requests_per_minute, burst_size, auto_ban_threshold (optional)
297
+ """
298
+ for config in route_configs:
299
+ # Make a deep copy of the config to prevent mutation
300
+ config_copy = {k: v for k, v in config.items()}
301
+ if (
302
+ "endpoint" not in config_copy
303
+ or "requests_per_minute" not in config_copy
304
+ ):
305
+ self.logger.logger.warning(
306
+ f"Skipping invalid route config: {config_copy}"
307
+ )
308
+ continue
309
+ endpoint = config_copy.pop("endpoint")
310
+ self.add_route_rate_limit(endpoint, config_copy)
311
+
312
+ def check_request(self, request: RequestProtocol) -> Dict[str, Any]:
313
+ """
314
+ Check if a request should be allowed based on security rules.
315
+
316
+ This method performs all security checks in sequence:
317
+ 1. IP filtering
318
+ 2. User agent filtering
319
+ 3. Rate limiting
320
+ 4. Penetration detection
321
+
322
+ Args:
323
+ request: The framework-specific request object
324
+
325
+ Returns:
326
+ Dict with status and details about the request check:
327
+ {
328
+ "allowed": bool,
329
+ "details": {
330
+ "type": str, # Type of check that failed (if any)
331
+ "reason": str # Reason for failure (if any)
332
+ }
333
+ }
334
+ """
335
+ # Extract request information
336
+ request_info = self._extract_request_info(request)
337
+
338
+ # Check IP filter
339
+ ip_result = self.ip_filter.is_allowed(request_info["ip"])
340
+ if not ip_result["allowed"]:
341
+ self.logger.log_blocked_request(
342
+ request_info, "IP filter", ip_result["reason"]
343
+ )
344
+ return {
345
+ "allowed": False,
346
+ "details": {"type": "IP filter", "reason": ip_result["reason"]},
347
+ }
348
+
349
+ # Check user agent filter
350
+ ua_result = self.user_agent_filter.is_allowed(request_info["user_agent"])
351
+ if not ua_result["allowed"]:
352
+ self.logger.log_blocked_request(
353
+ request_info, "User agent filter", ua_result["reason"]
354
+ )
355
+ return {
356
+ "allowed": False,
357
+ "details": {"type": "User agent filter", "reason": ua_result["reason"]},
358
+ }
359
+
360
+ # Check rate limit (now with path for route-specific rate limiting)
361
+ rate_result = self.rate_limiter.check_limit(
362
+ request_info["ip"], request_info["path"]
363
+ )
364
+ if not rate_result["allowed"]:
365
+ self.logger.log_blocked_request(
366
+ request_info, "Rate limit", rate_result["reason"]
367
+ )
368
+ return {
369
+ "allowed": False,
370
+ "details": {"type": "Rate limit", "reason": rate_result["reason"]},
371
+ }
372
+
373
+ # Check for penetration attempts
374
+ pen_result = self.penetration_detector.check_request(request_info)
375
+ if not pen_result["allowed"]:
376
+ self.logger.log_blocked_request(
377
+ request_info, "Penetration detection", pen_result["reason"]
378
+ )
379
+ return {
380
+ "allowed": False,
381
+ "details": {
382
+ "type": "Penetration detection",
383
+ "reason": pen_result["reason"],
384
+ },
385
+ }
386
+
387
+ # All checks passed
388
+ return {"allowed": True, "details": {}}
389
+
390
+ def update_metrics(
391
+ self, request: RequestProtocol, response: ResponseProtocol
392
+ ) -> None:
393
+ """
394
+ Update metrics based on request and response.
395
+
396
+ This method is called after a request has been processed to update
397
+ various metrics and logs.
398
+
399
+ Args:
400
+ request: The framework-specific request object
401
+ response: The framework-specific response object
402
+ """
403
+ # Extract request information
404
+ request_info = self._extract_request_info(request)
405
+
406
+ # Log the request
407
+ self.logger.log_request(request_info, response)
408
+
409
+ def _extract_request_info(
410
+ self, request: Union[RequestProtocol, Dict[str, Any]]
411
+ ) -> Dict[str, Any]:
412
+ """
413
+ Extract information from a request object.
414
+
415
+ This method should be overridden by framework-specific implementations
416
+ to properly extract request information from the framework's request object.
417
+
418
+ Args:
419
+ request: The framework-specific request object or a dictionary with request info
420
+
421
+ Returns:
422
+ Dict with request information:
423
+ {
424
+ "ip": str,
425
+ "user_agent": str,
426
+ "method": str,
427
+ "path": str,
428
+ "query": Dict[str, str],
429
+ "headers": Dict[str, str]
430
+ }
431
+ """
432
+ if isinstance(request, dict):
433
+ return request
434
+
435
+ # Parse query string into a dictionary
436
+ query_dict = {}
437
+ if hasattr(request, "query_string") and request.query_string:
438
+ query_dict = dict(
439
+ pair.split("=")
440
+ for pair in request.query_string.split("&")
441
+ if "=" in pair
442
+ )
443
+
444
+ return {
445
+ "ip": getattr(request, "remote_addr", ""),
446
+ "user_agent": getattr(request, "user_agent", ""),
447
+ "method": getattr(request, "method", ""),
448
+ "path": getattr(request, "path", ""),
449
+ "query": query_dict,
450
+ "headers": getattr(request, "headers", {}),
451
+ }
452
+
453
+ def is_ip_banned(self, ip: str) -> bool:
454
+ """Return True if the IP is banned, else False."""
455
+ result = self.ip_filter.is_allowed(ip)
456
+ return not result["allowed"] and result["reason"] == "IP is banned"
457
+
458
+ def check_rate_limit(self, ip: str, path: str = None) -> Dict[str, Any]:
459
+ """Check if the request is allowed by the rate limiter."""
460
+ return self.rate_limiter.check_limit(ip, path)
461
+
462
+
463
+ class AsyncGuard:
464
+ """
465
+ Base Guard class that handles security features (asynchronous).
466
+
467
+ This is the main class that users will interact with for asynchronous
468
+ web applications. It provides a unified interface for all security
469
+ features including IP filtering, rate limiting, and more.
470
+
471
+ Attributes:
472
+ config: The GuardConfig instance containing all security settings
473
+ storage: The async storage backend for persistent data
474
+ ip_filter: Async IP filtering component
475
+ user_agent_filter: Async user agent filtering component
476
+ rate_limiter: Async rate limiting component
477
+ penetration_detector: Async penetration detection component
478
+ cors_handler: Async CORS handling component
479
+ logger: Async security logging component
480
+ """
481
+
482
+ def __init__(
483
+ self,
484
+ config: Optional[GuardConfig] = None,
485
+ storage: Optional[AsyncBaseStorage] = None,
486
+ route_rate_limits: Optional[List[Dict[str, Any]]] = None,
487
+ ) -> None:
488
+ """
489
+ Initialize the AsyncGuard with configuration and storage.
490
+
491
+ Args:
492
+ config: GuardConfig object with security settings. If None, default config is used.
493
+ storage: Async storage backend for persistent data. If None, storage is determined from config.
494
+ route_rate_limits: List of dictionaries with route-specific rate limits.
495
+ Each dict should have: endpoint, requests_per_minute, burst_size, auto_ban_threshold (optional)
496
+
497
+ Raises:
498
+ ImportError: If the required storage backend is not installed
499
+ """
500
+ # Use environment variables if no config is provided
501
+ if config is None:
502
+ config = GuardConfig.from_env()
503
+ self.config = config
504
+
505
+ # Initialize storage
506
+ if storage is not None:
507
+ self.storage = storage
508
+ else:
509
+ self.storage = self._create_storage_from_config()
510
+
511
+ # Initialize components based on config
512
+ self._initialize_components()
513
+
514
+ # Add route-specific rate limits if provided
515
+ if route_rate_limits:
516
+ self.add_route_rate_limits(route_rate_limits)
517
+
518
+ def _create_storage_from_config(self) -> AsyncBaseStorage:
519
+ """
520
+ Create an async storage backend based on configuration.
521
+
522
+ Returns:
523
+ AsyncBaseStorage: The async storage backend instance
524
+
525
+ Raises:
526
+ ImportError: If the required storage backend is not installed
527
+ ValueError: If the storage type is invalid
528
+ """
529
+ storage_type = self.config.storage.type.lower()
530
+
531
+ if storage_type == "memory":
532
+ return AsyncMemoryStorage()
533
+
534
+ elif storage_type == "redis":
535
+ if not REDIS_AVAILABLE:
536
+ raise ImportError(
537
+ "Redis storage is not available. Install it with 'pip install pywebguard[redis]'"
538
+ )
539
+ return AsyncRedisStorage(
540
+ url=self.config.storage.url or "redis://localhost:6379/0",
541
+ prefix=self.config.storage.prefix,
542
+ ttl=self.config.storage.ttl,
543
+ )
544
+
545
+ elif storage_type == "sqlite":
546
+ if not SQLITE_AVAILABLE:
547
+ raise ImportError(
548
+ "SQLite storage is not available. Install it with 'pip install pywebguard[sqlite]'"
549
+ )
550
+ return AsyncSQLiteStorage(
551
+ db_path=self.config.storage.url or "pywebguard.db",
552
+ table_prefix=self.config.storage.prefix,
553
+ ttl=self.config.storage.ttl,
554
+ )
555
+
556
+ elif storage_type == "tinydb":
557
+ if not TINYDB_AVAILABLE:
558
+ raise ImportError(
559
+ "TinyDB storage is not available. Install it with 'pip install pywebguard[tinydb]'"
560
+ )
561
+ return AsyncTinyDBStorage(
562
+ db_path=self.config.storage.url or "pywebguard.json",
563
+ ttl=self.config.storage.ttl,
564
+ )
565
+
566
+ elif storage_type == "mongodb":
567
+ if not MONGODB_AVAILABLE:
568
+ raise ImportError(
569
+ "MongoDB storage is not available. Install it with 'pip install pywebguard[mongodb]'"
570
+ )
571
+ return AsyncMongoDBStorage(
572
+ url=self.config.storage.url or "mongodb://localhost:27017/pywebguard",
573
+ collection_name=self.config.storage.table_name,
574
+ ttl=self.config.storage.ttl,
575
+ )
576
+
577
+ elif storage_type == "postgresql":
578
+ if not POSTGRESQL_AVAILABLE:
579
+ raise ImportError(
580
+ "PostgreSQL storage is not available. Install it with 'pip install pywebguard[postgresql]'"
581
+ )
582
+ return AsyncPostgreSQLStorage(
583
+ url=self.config.storage.url
584
+ or "postgresql://postgres:postgres@localhost:5432/pywebguard",
585
+ table_name=self.config.storage.table_name,
586
+ ttl=self.config.storage.ttl,
587
+ )
588
+
589
+ else:
590
+ raise ValueError(f"Invalid storage type: {storage_type}")
591
+
592
+ def _initialize_components(self) -> None:
593
+ """
594
+ Initialize all security components based on configuration.
595
+
596
+ This method sets up all the security components (IP filter, rate limiter,
597
+ etc.) using the configuration provided during initialization.
598
+ """
599
+ self.ip_filter = AsyncIPFilter(self.config.ip_filter, self.storage)
600
+ self.user_agent_filter = AsyncUserAgentFilter(
601
+ self.config.user_agent, self.storage
602
+ )
603
+ self.rate_limiter = AsyncRateLimiter(self.config.rate_limit, self.storage)
604
+ self.penetration_detector = AsyncPenetrationDetector(
605
+ self.config.penetration, self.storage
606
+ )
607
+ self.cors_handler = AsyncCORSHandler(self.config.cors)
608
+ self.logger = AsyncSecurityLogger(self.config.logging)
609
+
610
+ def add_route_rate_limit(
611
+ self, route_pattern: str, config: Union[RateLimitConfig, Dict[str, Any]]
612
+ ) -> None:
613
+ """
614
+ Add a custom rate limit configuration for a specific route pattern.
615
+
616
+ Args:
617
+ route_pattern: The route pattern to match (can include wildcards like * and **)
618
+ config: Custom rate limit configuration for this route
619
+ """
620
+ self.rate_limiter.add_route_config(route_pattern, config)
621
+
622
+ def add_route_rate_limits(self, route_configs: List[Dict[str, Any]]) -> None:
623
+ """
624
+ Add multiple custom rate limit configurations for specific route patterns.
625
+
626
+ Args:
627
+ route_configs: List of dictionaries with route-specific rate limits.
628
+ Each dict should have: endpoint, requests_per_minute, burst_size, auto_ban_threshold (optional)
629
+ """
630
+ for config in route_configs:
631
+ # Make a deep copy of the config to prevent mutation
632
+ config_copy = {k: v for k, v in config.items()}
633
+ if (
634
+ "endpoint" not in config_copy
635
+ or "requests_per_minute" not in config_copy
636
+ ):
637
+ self.logger.logger.warning(
638
+ f"Skipping invalid route config: {config_copy}"
639
+ )
640
+ continue
641
+ endpoint = config_copy.pop("endpoint")
642
+ self.add_route_rate_limit(endpoint, config_copy)
643
+
644
+ async def check_request(self, request: RequestProtocol) -> Dict[str, Any]:
645
+ """
646
+ Check if a request should be allowed based on security rules.
647
+
648
+ This method performs all security checks in sequence:
649
+ 1. IP filtering
650
+ 2. User agent filtering
651
+ 3. Rate limiting
652
+ 4. Penetration detection
653
+
654
+ Args:
655
+ request: The framework-specific request object
656
+
657
+ Returns:
658
+ Dict with status and details about the request check:
659
+ {
660
+ "allowed": bool,
661
+ "details": {
662
+ "type": str, # Type of check that failed (if any)
663
+ "reason": str # Reason for failure (if any)
664
+ }
665
+ }
666
+ """
667
+ # Extract request information
668
+ request_info = self._extract_request_info(request)
669
+
670
+ # Check IP filter
671
+ ip_result = await self.ip_filter.is_allowed(request_info["ip"])
672
+ if not ip_result["allowed"]:
673
+ await self.logger.log_blocked_request(
674
+ request_info, "IP filter", ip_result["reason"]
675
+ )
676
+ return {
677
+ "allowed": False,
678
+ "details": {"type": "IP filter", "reason": ip_result["reason"]},
679
+ }
680
+
681
+ # Check user agent filter
682
+ ua_result = await self.user_agent_filter.is_allowed(request_info["user_agent"])
683
+ if not ua_result["allowed"]:
684
+ await self.logger.log_blocked_request(
685
+ request_info, "User agent filter", ua_result["reason"]
686
+ )
687
+ return {
688
+ "allowed": False,
689
+ "details": {"type": "User agent filter", "reason": ua_result["reason"]},
690
+ }
691
+
692
+ # Check rate limit (now with path for route-specific rate limiting)
693
+ rate_result = await self.rate_limiter.check_limit(
694
+ request_info["ip"], request_info["path"]
695
+ )
696
+ if not rate_result["allowed"]:
697
+ await self.logger.log_blocked_request(
698
+ request_info, "Rate limit", rate_result["reason"]
699
+ )
700
+ return {
701
+ "allowed": False,
702
+ "details": {"type": "Rate limit", "reason": rate_result["reason"]},
703
+ }
704
+
705
+ # Check for penetration attempts
706
+ pen_result = await self.penetration_detector.check_request(request_info)
707
+ if not pen_result["allowed"]:
708
+ await self.logger.log_blocked_request(
709
+ request_info, "Penetration detection", pen_result["reason"]
710
+ )
711
+ return {
712
+ "allowed": False,
713
+ "details": {
714
+ "type": "Penetration detection",
715
+ "reason": pen_result["reason"],
716
+ },
717
+ }
718
+
719
+ # All checks passed
720
+ return {"allowed": True, "details": {}}
721
+
722
+ async def update_metrics(
723
+ self, request: RequestProtocol, response: ResponseProtocol
724
+ ) -> None:
725
+ """
726
+ Update metrics based on request and response.
727
+
728
+ This method is called after a request has been processed to update
729
+ various metrics and logs.
730
+
731
+ Args:
732
+ request: The framework-specific request object
733
+ response: The framework-specific response object
734
+ """
735
+ # Extract request information
736
+ request_info = self._extract_request_info(request)
737
+
738
+ # Log the request
739
+ await self.logger.log_request(request_info, response)
740
+
741
+ def _extract_request_info(
742
+ self, request: Union[RequestProtocol, Dict[str, Any]]
743
+ ) -> Dict[str, Any]:
744
+ """
745
+ Extract information from a request object.
746
+
747
+ This method should be overridden by framework-specific implementations
748
+ to properly extract request information from the framework's request object.
749
+
750
+ Args:
751
+ request: The framework-specific request object or a dictionary with request info
752
+
753
+ Returns:
754
+ Dict with request information:
755
+ {
756
+ "ip": str,
757
+ "user_agent": str,
758
+ "method": str,
759
+ "path": str,
760
+ "query": Dict[str, str],
761
+ "headers": Dict[str, str]
762
+ }
763
+ """
764
+ if isinstance(request, dict):
765
+ return request
766
+
767
+ # Parse query string into a dictionary
768
+ query_dict = {}
769
+ if hasattr(request, "query_string") and request.query_string:
770
+ query_dict = dict(
771
+ pair.split("=")
772
+ for pair in request.query_string.split("&")
773
+ if "=" in pair
774
+ )
775
+
776
+ return {
777
+ "ip": getattr(request, "remote_addr", ""),
778
+ "user_agent": getattr(request, "user_agent", ""),
779
+ "method": getattr(request, "method", ""),
780
+ "path": getattr(request, "path", ""),
781
+ "query": query_dict,
782
+ "headers": getattr(request, "headers", {}),
783
+ }
784
+
785
+ async def is_ip_banned(self, ip: str) -> bool:
786
+ """Return True if the IP is banned, else False (async)."""
787
+ result = await self.ip_filter.is_allowed(ip)
788
+ return not result["allowed"] and result["reason"] == "IP is banned"
789
+
790
+ async def check_rate_limit(self, ip: str, path: str = None) -> Dict[str, Any]:
791
+ """Check if the request is allowed by the rate limiter (async)."""
792
+ return await self.rate_limiter.check_limit(ip, path)