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,233 @@
1
+ """
2
+ IP filtering functionality for PyWebGuard with both sync and async support.
3
+ """
4
+
5
+ from typing import Dict, List, Optional, Union, Any
6
+ import ipaddress
7
+ from pywebguard.core.config import IPFilterConfig
8
+ from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
9
+ from pywebguard.filters.base import BaseFilter, AsyncBaseFilter
10
+
11
+
12
+ class IPFilter(BaseFilter):
13
+ """
14
+ Filter requests based on IP addresses (synchronous).
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ config: IPFilterConfig,
20
+ storage: BaseStorage,
21
+ ):
22
+ """
23
+ Initialize the IP filter.
24
+
25
+ Args:
26
+ config: IP filter configuration
27
+ storage: Storage backend for persistent data
28
+ """
29
+ self.config = config
30
+ self.storage = storage
31
+
32
+ # Parse IP networks for efficient matching
33
+ self.whitelist_networks = self._parse_ip_networks(config.whitelist)
34
+ self.blacklist_networks = self._parse_ip_networks(config.blacklist)
35
+
36
+ def _parse_ip_networks(self, ip_list: List[str]) -> List:
37
+ """
38
+ Parse IP addresses and CIDR ranges into network objects.
39
+
40
+ Args:
41
+ ip_list: List of IP addresses and CIDR ranges
42
+
43
+ Returns:
44
+ List of network objects
45
+ """
46
+ networks = []
47
+ for ip in ip_list:
48
+ try:
49
+ # Handle CIDR notation
50
+ if "/" in ip:
51
+ networks.append(ipaddress.ip_network(ip, strict=False))
52
+ else:
53
+ # Convert single IP to network with /32 or /128 mask
54
+ networks.append(ipaddress.ip_address(ip))
55
+ except ValueError:
56
+ # Log invalid IP address
57
+ pass
58
+ return networks
59
+
60
+ def is_allowed(self, ip_address: str) -> Dict[str, Union[bool, str]]:
61
+ """
62
+ Check if an IP address is allowed.
63
+
64
+ Args:
65
+ ip_address: The IP address to check
66
+
67
+ Returns:
68
+ Dict with allowed status and reason
69
+ """
70
+ if not self.config.enabled:
71
+ return {"allowed": True, "reason": ""}
72
+
73
+ # Re-parse networks in case config changed at runtime
74
+ self.whitelist_networks = self._parse_ip_networks(self.config.whitelist)
75
+ self.blacklist_networks = self._parse_ip_networks(self.config.blacklist)
76
+
77
+ try:
78
+ ip = ipaddress.ip_address(ip_address)
79
+
80
+ # Check if IP is banned (highest priority)
81
+ if self.storage.exists(f"banned_ip:{ip_address}"):
82
+ return {"allowed": False, "reason": "IP is banned"}
83
+
84
+ # Check if IP is in blacklist (second priority)
85
+ if self._is_ip_in_networks(ip, self.blacklist_networks):
86
+ return {"allowed": False, "reason": "IP in blacklist"}
87
+
88
+ # Check if IP is in whitelist (lowest priority)
89
+ if self.whitelist_networks and not self._is_ip_in_networks(
90
+ ip, self.whitelist_networks
91
+ ):
92
+ return {"allowed": False, "reason": "IP not in whitelist"}
93
+
94
+ # Additional checks for cloud providers, geolocation, etc. would go here
95
+
96
+ return {"allowed": True, "reason": ""}
97
+
98
+ except ValueError:
99
+ # Invalid IP address
100
+ return {"allowed": False, "reason": "Invalid IP address"}
101
+
102
+ def _is_ip_in_networks(self, ip, networks) -> bool:
103
+ """
104
+ Check if an IP is in a list of networks.
105
+
106
+ Args:
107
+ ip: IP address to check
108
+ networks: List of networks to check against
109
+
110
+ Returns:
111
+ True if IP is in any network, False otherwise
112
+ """
113
+ for network in networks:
114
+ if isinstance(network, ipaddress.IPv4Network) or isinstance(
115
+ network, ipaddress.IPv6Network
116
+ ):
117
+ if ip in network:
118
+ return True
119
+ elif ip == network: # Direct comparison for single IP addresses
120
+ return True
121
+ return False
122
+
123
+
124
+ class AsyncIPFilter(AsyncBaseFilter):
125
+ """
126
+ Filter requests based on IP addresses (asynchronous).
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ config: IPFilterConfig,
132
+ storage: AsyncBaseStorage,
133
+ ):
134
+ """
135
+ Initialize the async IP filter.
136
+
137
+ Args:
138
+ config: IP filter configuration
139
+ storage: Async storage backend for persistent data
140
+ """
141
+ self.config = config
142
+ self.storage = storage
143
+
144
+ # Parse IP networks for efficient matching
145
+ self.whitelist_networks = self._parse_ip_networks(config.whitelist)
146
+ self.blacklist_networks = self._parse_ip_networks(config.blacklist)
147
+
148
+ def _parse_ip_networks(self, ip_list: List[str]) -> List:
149
+ """
150
+ Parse IP addresses and CIDR ranges into network objects.
151
+
152
+ Args:
153
+ ip_list: List of IP addresses and CIDR ranges
154
+
155
+ Returns:
156
+ List of network objects
157
+ """
158
+ networks = []
159
+ for ip in ip_list:
160
+ try:
161
+ # Handle CIDR notation
162
+ if "/" in ip:
163
+ networks.append(ipaddress.ip_network(ip, strict=False))
164
+ else:
165
+ # Convert single IP to network with /32 or /128 mask
166
+ networks.append(ipaddress.ip_address(ip))
167
+ except ValueError:
168
+ # Log invalid IP address
169
+ pass
170
+ return networks
171
+
172
+ async def is_allowed(self, ip_address: str) -> Dict[str, Union[bool, str]]:
173
+ """
174
+ Check if an IP address is allowed asynchronously.
175
+
176
+ Args:
177
+ ip_address: The IP address to check
178
+
179
+ Returns:
180
+ Dict with allowed status and reason
181
+ """
182
+ if not self.config.enabled:
183
+ return {"allowed": True, "reason": ""}
184
+
185
+ # Re-parse networks in case config changed at runtime
186
+ self.whitelist_networks = self._parse_ip_networks(self.config.whitelist)
187
+ self.blacklist_networks = self._parse_ip_networks(self.config.blacklist)
188
+
189
+ try:
190
+ ip = ipaddress.ip_address(ip_address)
191
+
192
+ # Check if IP is banned (highest priority)
193
+ if await self.storage.exists(f"banned_ip:{ip_address}"):
194
+ return {"allowed": False, "reason": "IP is banned"}
195
+
196
+ # Check if IP is in blacklist (second priority)
197
+ if self._is_ip_in_networks(ip, self.blacklist_networks):
198
+ return {"allowed": False, "reason": "IP in blacklist"}
199
+
200
+ # Check if IP is in whitelist (lowest priority)
201
+ if self.whitelist_networks and not self._is_ip_in_networks(
202
+ ip, self.whitelist_networks
203
+ ):
204
+ return {"allowed": False, "reason": "IP not in whitelist"}
205
+
206
+ # Additional checks for cloud providers, geolocation, etc. would go here
207
+
208
+ return {"allowed": True, "reason": ""}
209
+
210
+ except ValueError:
211
+ # Invalid IP address
212
+ return {"allowed": False, "reason": "Invalid IP address"}
213
+
214
+ def _is_ip_in_networks(self, ip, networks) -> bool:
215
+ """
216
+ Check if an IP is in a list of networks.
217
+
218
+ Args:
219
+ ip: IP address to check
220
+ networks: List of networks to check against
221
+
222
+ Returns:
223
+ True if IP is in any network, False otherwise
224
+ """
225
+ for network in networks:
226
+ if isinstance(network, ipaddress.IPv4Network) or isinstance(
227
+ network, ipaddress.IPv6Network
228
+ ):
229
+ if ip in network:
230
+ return True
231
+ elif ip == network: # Direct comparison for single IP addresses
232
+ return True
233
+ return False
@@ -0,0 +1,102 @@
1
+ """
2
+ User agent filtering functionality for PyWebGuard with both sync and async support.
3
+ """
4
+
5
+ from typing import Dict, Union
6
+ from pywebguard.core.config import UserAgentConfig
7
+ from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
8
+ from pywebguard.filters.base import BaseFilter, AsyncBaseFilter
9
+
10
+
11
+ class UserAgentFilter(BaseFilter):
12
+ """
13
+ Filter requests based on user agent (synchronous).
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ config: UserAgentConfig,
19
+ storage: BaseStorage,
20
+ ):
21
+ """
22
+ Initialize the user agent filter.
23
+
24
+ Args:
25
+ config: User agent filter configuration
26
+ storage: Storage backend for persistent data
27
+ """
28
+ self.config = config
29
+ self.storage = storage
30
+
31
+ def is_allowed(self, user_agent: str) -> Dict[str, Union[bool, str]]:
32
+ """
33
+ Check if a user agent is allowed.
34
+
35
+ Args:
36
+ user_agent: The user agent string to check
37
+
38
+ Returns:
39
+ Dict with allowed status and reason
40
+ """
41
+ if not self.config.enabled:
42
+ return {"allowed": True, "reason": ""}
43
+
44
+ if not user_agent:
45
+ return {"allowed": False, "reason": "Empty user agent"}
46
+
47
+ # Check if user agent is in blocked list
48
+ for blocked_agent in self.config.blocked_agents:
49
+ if blocked_agent.lower() in user_agent.lower():
50
+ return {
51
+ "allowed": False,
52
+ "reason": f"Blocked user agent: {blocked_agent}",
53
+ }
54
+
55
+ return {"allowed": True, "reason": ""}
56
+
57
+
58
+ class AsyncUserAgentFilter(AsyncBaseFilter):
59
+ """
60
+ Filter requests based on user agent (asynchronous).
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ config: UserAgentConfig,
66
+ storage: AsyncBaseStorage,
67
+ ):
68
+ """
69
+ Initialize the async user agent filter.
70
+
71
+ Args:
72
+ config: User agent filter configuration
73
+ storage: Async storage backend for persistent data
74
+ """
75
+ self.config = config
76
+ self.storage = storage
77
+
78
+ async def is_allowed(self, user_agent: str) -> Dict[str, Union[bool, str]]:
79
+ """
80
+ Check if a user agent is allowed asynchronously.
81
+
82
+ Args:
83
+ user_agent: The user agent string to check
84
+
85
+ Returns:
86
+ Dict with allowed status and reason
87
+ """
88
+ if not self.config.enabled:
89
+ return {"allowed": True, "reason": ""}
90
+
91
+ if not user_agent:
92
+ return {"allowed": False, "reason": "Empty user agent"}
93
+
94
+ # Check if user agent is in blocked list
95
+ for blocked_agent in self.config.blocked_agents:
96
+ if blocked_agent.lower() in user_agent.lower():
97
+ return {
98
+ "allowed": False,
99
+ "reason": f"Blocked user agent: {blocked_agent}",
100
+ }
101
+
102
+ return {"allowed": True, "reason": ""}
File without changes
@@ -0,0 +1,191 @@
1
+ """
2
+ FastAPI integration for PyWebGuard.
3
+
4
+ This module provides middleware for integrating PyWebGuard with FastAPI applications.
5
+ It handles request interception, security checks, and response processing.
6
+
7
+ Classes:
8
+ FastAPIGuard: FastAPI middleware for PyWebGuard
9
+ """
10
+
11
+ from typing import Callable, Dict, Any, Optional, List, Union
12
+ import time
13
+
14
+ # Check if FastAPI is installed
15
+ try:
16
+ from fastapi import FastAPI, Request, Response
17
+ from starlette.middleware.base import BaseHTTPMiddleware
18
+ from fastapi.responses import JSONResponse
19
+
20
+ FASTAPI_AVAILABLE = True
21
+ except ImportError:
22
+ FASTAPI_AVAILABLE = False
23
+
24
+ # Create dummy classes for type checking
25
+ class BaseHTTPMiddleware:
26
+ def __init__(self, app):
27
+ self.app = app
28
+
29
+ class Request:
30
+ pass
31
+
32
+ class Response:
33
+ pass
34
+
35
+ class JSONResponse:
36
+ pass
37
+
38
+
39
+ from pywebguard.core.base import Guard, AsyncGuard
40
+ from pywebguard.core.config import GuardConfig
41
+ from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
42
+ from pywebguard.storage.memory import MemoryStorage, AsyncMemoryStorage
43
+
44
+
45
+ class FastAPIGuard(BaseHTTPMiddleware):
46
+ """
47
+ FastAPI middleware for PyWebGuard supporting both sync and async guards.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ app,
53
+ config: Optional[GuardConfig] = None,
54
+ storage: Optional[Union[AsyncBaseStorage, BaseStorage]] = None,
55
+ route_rate_limits: Optional[List[Dict[str, Any]]] = None,
56
+ custom_response_handler: Optional[Callable] = None,
57
+ ):
58
+ """
59
+ Initialize the FastAPI guard.
60
+
61
+ Args:
62
+ app: The FastAPI application
63
+ config: Guard configuration
64
+ storage: Storage backend
65
+ route_rate_limits: Optional list of route-specific rate limits
66
+ custom_response_handler: Optional custom response handler for blocked requests
67
+ """
68
+ if not FASTAPI_AVAILABLE:
69
+ raise ImportError(
70
+ "FastAPI is not installed. Install it with 'pip install pywebguard[fastapi]' "
71
+ "or 'pip install fastapi>=0.68.0 starlette>=0.14.0'"
72
+ )
73
+
74
+ super().__init__(app)
75
+ self.app = app
76
+
77
+ # Use AsyncGuard instead of Guard
78
+ if isinstance(storage, AsyncBaseStorage):
79
+ self.guard = AsyncGuard(config, storage)
80
+ else:
81
+ raise ValueError(
82
+ "Storage must be an instance of AsyncBaseStorage for FastAPI integration"
83
+ )
84
+
85
+ # Add route-specific rate limits if provided
86
+ if route_rate_limits:
87
+ for route in route_rate_limits:
88
+ self.guard.rate_limiter.add_route_config(route["endpoint"], route)
89
+
90
+ # Set up default response handler if none provided
91
+ if custom_response_handler is None:
92
+ custom_response_handler = self._default_response_handler
93
+ self.custom_response_handler = custom_response_handler
94
+
95
+ async def _default_response_handler(
96
+ self, request: Request, reason: str
97
+ ) -> Response:
98
+ """
99
+ Default response handler for blocked requests.
100
+
101
+ Args:
102
+ request: The FastAPI request object
103
+ reason: The reason the request was blocked
104
+
105
+ Returns:
106
+ A JSON response with details about why the request was blocked
107
+ """
108
+ status_code = 429 if "rate limit" in reason.lower() else 403
109
+
110
+ return JSONResponse(
111
+ status_code=status_code,
112
+ content={
113
+ "error": "Request blocked",
114
+ "reason": reason,
115
+ "timestamp": time.time(),
116
+ "path": request.url.path,
117
+ "method": request.method,
118
+ },
119
+ )
120
+
121
+ async def dispatch(self, request: Request, call_next):
122
+ """
123
+ Process the request through the middleware.
124
+
125
+ Args:
126
+ request: The FastAPI request object
127
+ call_next: The next middleware/handler in the chain
128
+
129
+ Returns:
130
+ The response from the next middleware/handler
131
+ """
132
+ # Store the guard instance in the app's state if not already stored
133
+ if not hasattr(request.app.state, "guard"):
134
+ request.app.state.guard = self
135
+
136
+ client_ip = request.client.host
137
+
138
+ # Check if IP is banned first
139
+ is_banned = await self.guard.is_ip_banned(client_ip)
140
+ if is_banned:
141
+ ban_info = await self.guard.storage.get(f"banned_ip:{client_ip}")
142
+ response = await self.custom_response_handler(
143
+ request,
144
+ f"IP is banned: {ban_info.get('reason', 'Unknown reason')}",
145
+ )
146
+ # Log blocked request
147
+ request_info = {
148
+ "ip": client_ip,
149
+ "method": request.method,
150
+ "path": request.url.path,
151
+ "user_agent": request.headers.get("user-agent", "unknown"),
152
+ }
153
+ await self.guard.logger.log_blocked_request(
154
+ request_info,
155
+ "ip_ban",
156
+ f"IP is banned: {ban_info.get('reason', 'Unknown reason')}",
157
+ )
158
+ return response
159
+
160
+ # Check rate limits
161
+ rate_info = await self.guard.rate_limiter.check_limit(
162
+ client_ip, request.url.path
163
+ )
164
+ if not rate_info["allowed"]:
165
+ response = await self.custom_response_handler(request, rate_info["reason"])
166
+ # Log blocked request
167
+ request_info = {
168
+ "ip": client_ip,
169
+ "method": request.method,
170
+ "path": request.url.path,
171
+ "user_agent": request.headers.get("user-agent", "unknown"),
172
+ }
173
+ await self.guard.logger.log_security_event(
174
+ "WARNING",
175
+ f"Blocked request: {request.method} {request.url.path} - {rate_info['reason']}",
176
+ )
177
+ return response
178
+
179
+ # Continue with the request
180
+ response = await call_next(request)
181
+
182
+ # Log successful request
183
+ request_info = {
184
+ "ip": client_ip,
185
+ "method": request.method,
186
+ "path": request.url.path,
187
+ "user_agent": request.headers.get("user-agent", "unknown"),
188
+ }
189
+ await self.guard.logger.log_request(request_info, response)
190
+
191
+ return response