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,307 @@
1
+ # """
2
+ # Elasticsearch logging backend for PyWebGuard.
3
+ # """
4
+
5
+ # import time
6
+ # import logging
7
+ # from typing import Dict, Any, Optional, TYPE_CHECKING
8
+
9
+ # # Try to import elasticsearch
10
+ # try:
11
+ # import elasticsearch
12
+ # from elasticsearch import AsyncElasticsearch
13
+
14
+ # ELASTICSEARCH_AVAILABLE = True
15
+ # except ImportError:
16
+ # ELASTICSEARCH_AVAILABLE = False
17
+ # if TYPE_CHECKING:
18
+ # import elasticsearch
19
+ # from elasticsearch import AsyncElasticsearch
20
+
21
+ # from ..base import LoggingBackend, AsyncLoggingBackend
22
+
23
+
24
+ # class ElasticsearchBackend(LoggingBackend):
25
+ # """
26
+ # Synchronous Elasticsearch logging backend implementation.
27
+ # """
28
+
29
+ # def __init__(self, config: Dict[str, Any]):
30
+ # """
31
+ # Initialize the Elasticsearch backend.
32
+
33
+ # Args:
34
+ # config: Configuration dictionary containing:
35
+ # - url: Elasticsearch server URL
36
+ # - username: Elasticsearch username (optional)
37
+ # - password: Elasticsearch password (optional)
38
+ # - index_name: Name of the index to store logs
39
+
40
+ # Raises:
41
+ # ImportError: If elasticsearch is not installed
42
+ # """
43
+ # if not ELASTICSEARCH_AVAILABLE:
44
+ # raise ImportError(
45
+ # "Elasticsearch backend requires elasticsearch. Install it with 'pip install elasticsearch'"
46
+ # )
47
+
48
+ # self.config = config
49
+ # self.client = None
50
+ # self.setup(config)
51
+
52
+ # def setup(self, config: Dict[str, Any]) -> None:
53
+ # """
54
+ # Set up the Elasticsearch client.
55
+
56
+ # Args:
57
+ # config: Configuration dictionary
58
+ # """
59
+ # # Create client with authentication if provided
60
+ # if "username" in config and "password" in config:
61
+ # self.client = elasticsearch.Elasticsearch(
62
+ # config["url"],
63
+ # basic_auth=(config["username"], config["password"]),
64
+ # )
65
+ # else:
66
+ # self.client = elasticsearch.Elasticsearch(config["url"])
67
+
68
+ # def _get_index_name(self) -> str:
69
+ # """
70
+ # Get the index name with date suffix.
71
+
72
+ # Returns:
73
+ # Index name with date suffix
74
+ # """
75
+ # return f"{self.config['index_name']}-{time.strftime('%Y.%m.%d')}"
76
+
77
+ # def _create_log_entry(self, **kwargs) -> Dict[str, Any]:
78
+ # """
79
+ # Create a standardized log entry.
80
+
81
+ # Returns:
82
+ # Dict containing the log entry
83
+ # """
84
+ # return {
85
+ # "@timestamp": time.time(),
86
+ # **kwargs,
87
+ # }
88
+
89
+ # def log_request(self, request_info: Dict[str, Any], response: Any) -> None:
90
+ # """
91
+ # Log a request to Elasticsearch.
92
+
93
+ # Args:
94
+ # request_info: Dict with request information
95
+ # response: The framework-specific response object
96
+ # """
97
+ # log_entry = self._create_log_entry(
98
+ # level="INFO",
99
+ # ip=request_info.get("ip", "unknown"),
100
+ # method=request_info.get("method", "unknown"),
101
+ # path=request_info.get("path", "unknown"),
102
+ # status_code=self._extract_status_code(response),
103
+ # user_agent=request_info.get("user_agent", "unknown"),
104
+ # event_type="request",
105
+ # )
106
+ # self.client.index(index=self._get_index_name(), document=log_entry)
107
+
108
+ # def log_blocked_request(
109
+ # self, request_info: Dict[str, Any], block_type: str, reason: str
110
+ # ) -> None:
111
+ # """
112
+ # Log a blocked request to Elasticsearch.
113
+
114
+ # Args:
115
+ # request_info: Dict with request information
116
+ # block_type: Type of block (IP filter, rate limit, etc.)
117
+ # reason: Reason for blocking
118
+ # """
119
+ # log_entry = self._create_log_entry(
120
+ # level="WARNING",
121
+ # ip=request_info.get("ip", "unknown"),
122
+ # method=request_info.get("method", "unknown"),
123
+ # path=request_info.get("path", "unknown"),
124
+ # block_type=block_type,
125
+ # reason=reason,
126
+ # user_agent=request_info.get("user_agent", "unknown"),
127
+ # event_type="blocked_request",
128
+ # )
129
+ # self.client.index(index=self._get_index_name(), document=log_entry)
130
+
131
+ # def log_security_event(
132
+ # self, level: str, message: str, extra: Optional[Dict[str, Any]] = None
133
+ # ) -> None:
134
+ # """
135
+ # Log a security event to Elasticsearch.
136
+
137
+ # Args:
138
+ # level: Log level (INFO, WARNING, ERROR, etc.)
139
+ # message: The log message
140
+ # extra: Additional information to log
141
+ # """
142
+ # log_entry = self._create_log_entry(
143
+ # level=level, message=message, event_type="security_event", **(extra or {})
144
+ # )
145
+ # self.client.index(index=self._get_index_name(), document=log_entry)
146
+
147
+ # def _extract_status_code(self, response: Any) -> int:
148
+ # """
149
+ # Extract status code from a response object.
150
+
151
+ # Args:
152
+ # response: The framework-specific response object
153
+
154
+ # Returns:
155
+ # The status code or 0 if not found
156
+ # """
157
+ # try:
158
+ # if hasattr(response, "status_code"):
159
+ # return response.status_code
160
+ # if isinstance(response, dict) and "status_code" in response:
161
+ # return response["status_code"]
162
+ # except:
163
+ # pass
164
+ # return 0
165
+
166
+
167
+ # class AsyncElasticsearchBackend(AsyncLoggingBackend):
168
+ # """
169
+ # Asynchronous Elasticsearch logging backend implementation.
170
+ # """
171
+
172
+ # def __init__(self, config: Dict[str, Any]):
173
+ # """
174
+ # Initialize the async Elasticsearch backend.
175
+
176
+ # Args:
177
+ # config: Configuration dictionary containing:
178
+ # - url: Elasticsearch server URL
179
+ # - username: Elasticsearch username (optional)
180
+ # - password: Elasticsearch password (optional)
181
+ # - index_name: Name of the index to store logs
182
+
183
+ # Raises:
184
+ # ImportError: If elasticsearch is not installed
185
+ # """
186
+ # if not ELASTICSEARCH_AVAILABLE:
187
+ # raise ImportError(
188
+ # "Elasticsearch backend requires elasticsearch. Install it with 'pip install elasticsearch'"
189
+ # )
190
+
191
+ # self.config = config
192
+ # self.client = None
193
+ # self.setup(config)
194
+
195
+ # def setup(self, config: Dict[str, Any]) -> None:
196
+ # """
197
+ # Set up the async Elasticsearch client.
198
+
199
+ # Args:
200
+ # config: Configuration dictionary
201
+ # """
202
+ # # Create client with authentication if provided
203
+ # if "username" in config and "password" in config:
204
+ # self.client = AsyncElasticsearch(
205
+ # config["url"],
206
+ # basic_auth=(config["username"], config["password"]),
207
+ # )
208
+ # else:
209
+ # self.client = AsyncElasticsearch(config["url"])
210
+
211
+ # def _get_index_name(self) -> str:
212
+ # """
213
+ # Get the index name with date suffix.
214
+
215
+ # Returns:
216
+ # Index name with date suffix
217
+ # """
218
+ # return f"{self.config['index_name']}-{time.strftime('%Y.%m.%d')}"
219
+
220
+ # def _create_log_entry(self, **kwargs) -> Dict[str, Any]:
221
+ # """
222
+ # Create a standardized log entry.
223
+
224
+ # Returns:
225
+ # Dict containing the log entry
226
+ # """
227
+ # return {
228
+ # "@timestamp": time.time(),
229
+ # **kwargs,
230
+ # }
231
+
232
+ # async def log_request(self, request_info: Dict[str, Any], response: Any) -> None:
233
+ # """
234
+ # Log a request to Elasticsearch asynchronously.
235
+
236
+ # Args:
237
+ # request_info: Dict with request information
238
+ # response: The framework-specific response object
239
+ # """
240
+ # log_entry = self._create_log_entry(
241
+ # level="INFO",
242
+ # ip=request_info.get("ip", "unknown"),
243
+ # method=request_info.get("method", "unknown"),
244
+ # path=request_info.get("path", "unknown"),
245
+ # status_code=self._extract_status_code(response),
246
+ # user_agent=request_info.get("user_agent", "unknown"),
247
+ # event_type="request",
248
+ # )
249
+ # await self.client.index(index=self._get_index_name(), document=log_entry)
250
+
251
+ # async def log_blocked_request(
252
+ # self, request_info: Dict[str, Any], block_type: str, reason: str
253
+ # ) -> None:
254
+ # """
255
+ # Log a blocked request to Elasticsearch asynchronously.
256
+
257
+ # Args:
258
+ # request_info: Dict with request information
259
+ # block_type: Type of block (IP filter, rate limit, etc.)
260
+ # reason: Reason for blocking
261
+ # """
262
+ # log_entry = self._create_log_entry(
263
+ # level="WARNING",
264
+ # ip=request_info.get("ip", "unknown"),
265
+ # method=request_info.get("method", "unknown"),
266
+ # path=request_info.get("path", "unknown"),
267
+ # block_type=block_type,
268
+ # reason=reason,
269
+ # user_agent=request_info.get("user_agent", "unknown"),
270
+ # event_type="blocked_request",
271
+ # )
272
+ # await self.client.index(index=self._get_index_name(), document=log_entry)
273
+
274
+ # async def log_security_event(
275
+ # self, level: str, message: str, extra: Optional[Dict[str, Any]] = None
276
+ # ) -> None:
277
+ # """
278
+ # Log a security event to Elasticsearch asynchronously.
279
+
280
+ # Args:
281
+ # level: Log level (INFO, WARNING, ERROR, etc.)
282
+ # message: The log message
283
+ # extra: Additional information to log
284
+ # """
285
+ # log_entry = self._create_log_entry(
286
+ # level=level, message=message, event_type="security_event", **(extra or {})
287
+ # )
288
+ # await self.client.index(index=self._get_index_name(), document=log_entry)
289
+
290
+ # def _extract_status_code(self, response: Any) -> int:
291
+ # """
292
+ # Extract status code from a response object.
293
+
294
+ # Args:
295
+ # response: The framework-specific response object
296
+
297
+ # Returns:
298
+ # The status code or 0 if not found
299
+ # """
300
+ # try:
301
+ # if hasattr(response, "status_code"):
302
+ # return response.status_code
303
+ # if isinstance(response, dict) and "status_code" in response:
304
+ # return response["status_code"]
305
+ # except:
306
+ # pass
307
+ # return 0
@@ -0,0 +1,310 @@
1
+ """
2
+ Meilisearch logging backend for PyWebGuard.
3
+ """
4
+
5
+ import time
6
+ import logging
7
+ from typing import Dict, Any, Optional, TYPE_CHECKING
8
+
9
+ # Disable urllib3 debug logs
10
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
11
+
12
+ # Try to import meilisearch
13
+ try:
14
+ import meilisearch
15
+
16
+ MEILISEARCH_AVAILABLE = True
17
+ except ImportError:
18
+ MEILISEARCH_AVAILABLE = False
19
+ if TYPE_CHECKING:
20
+ import meilisearch
21
+
22
+ from ..base import LoggingBackend, AsyncLoggingBackend
23
+
24
+
25
+ class MeilisearchBackend(LoggingBackend):
26
+ """
27
+ Synchronous Meilisearch logging backend implementation.
28
+ """
29
+
30
+ def __init__(self, config: Dict[str, Any]):
31
+ """
32
+ Initialize the Meilisearch backend.
33
+
34
+ Args:
35
+ config: Configuration dictionary containing:
36
+ - url: Meilisearch server URL
37
+ - api_key: Meilisearch API key
38
+ - index_name: Name of the index to store logs
39
+
40
+ Raises:
41
+ ImportError: If meilisearch is not installed
42
+ """
43
+ if not MEILISEARCH_AVAILABLE:
44
+ raise ImportError(
45
+ "Meilisearch backend requires meilisearch. Install it with 'pip install meilisearch'"
46
+ )
47
+
48
+ self.config = config
49
+ self.client = None
50
+ self.index = None
51
+ self.setup(config)
52
+
53
+ def setup(self, config: Dict[str, Any]) -> None:
54
+ """
55
+ Set up the Meilisearch client and index.
56
+
57
+ Args:
58
+ config: Configuration dictionary
59
+ """
60
+ self.client = meilisearch.Client(config["url"], config["api_key"])
61
+ self.index = self.client.index(config["index_name"])
62
+
63
+ # Ensure timestamp and other fields are filterable
64
+ self.index.update_filterable_attributes(
65
+ [
66
+ "timestamp",
67
+ "level",
68
+ "ip",
69
+ "method",
70
+ "path",
71
+ "status_code",
72
+ "block_type",
73
+ "reason",
74
+ ]
75
+ )
76
+
77
+ self.index.update_sortable_attributes(["timestamp", "level", "status_code"])
78
+
79
+ def _create_log_entry(self, **kwargs) -> Dict[str, Any]:
80
+ """
81
+ Create a standardized log entry.
82
+
83
+ Returns:
84
+ Dict containing the log entry
85
+ """
86
+ return {
87
+ "id": f"{int(time.time() * 1000)}-{kwargs.get('level', 'INFO')}",
88
+ "timestamp": int(time.time()),
89
+ **kwargs,
90
+ }
91
+
92
+ def log_request(self, request_info: Dict[str, Any], response: Any) -> None:
93
+ """
94
+ Log a request to Meilisearch.
95
+
96
+ Args:
97
+ request_info: Dict with request information
98
+ response: The framework-specific response object
99
+ """
100
+ log_entry = self._create_log_entry(
101
+ level="INFO",
102
+ ip=request_info.get("ip", "unknown"),
103
+ method=request_info.get("method", "unknown"),
104
+ path=request_info.get("path", "unknown"),
105
+ status_code=self._extract_status_code(response),
106
+ user_agent=request_info.get("user_agent", "unknown"),
107
+ event_type="request",
108
+ )
109
+ self.index.add_documents([log_entry])
110
+ print(f"[MeilisearchBackend::DEBUG] Logged request: {log_entry}")
111
+
112
+ def log_blocked_request(
113
+ self, request_info: Dict[str, Any], block_type: str, reason: str
114
+ ) -> None:
115
+ """
116
+ Log a blocked request to Meilisearch.
117
+
118
+ Args:
119
+ request_info: Dict with request information
120
+ block_type: Type of block (IP filter, rate limit, etc.)
121
+ reason: Reason for blocking
122
+ """
123
+ log_entry = self._create_log_entry(
124
+ level="WARNING",
125
+ ip=request_info.get("ip", "unknown"),
126
+ method=request_info.get("method", "unknown"),
127
+ path=request_info.get("path", "unknown"),
128
+ block_type=block_type,
129
+ reason=reason,
130
+ user_agent=request_info.get("user_agent", "unknown"),
131
+ event_type="blocked_request",
132
+ )
133
+ self.index.add_documents([log_entry])
134
+ print(f"[MeilisearchBackend::DEBUG] Logged blocked request: {log_entry}")
135
+
136
+ def log_security_event(
137
+ self, level: str, message: str, extra: Optional[Dict[str, Any]] = None
138
+ ) -> None:
139
+ """
140
+ Log a security event to Meilisearch.
141
+
142
+ Args:
143
+ level: Log level (INFO, WARNING, ERROR, etc.)
144
+ message: The log message
145
+ extra: Additional information to log
146
+ """
147
+ log_entry = self._create_log_entry(
148
+ level=level, message=message, event_type="security_event", **(extra or {})
149
+ )
150
+ self.index.add_documents([log_entry])
151
+ print(f"[MeilisearchBackend::DEBUG] Logged security event: {log_entry}")
152
+
153
+ def _extract_status_code(self, response: Any) -> int:
154
+ """
155
+ Extract status code from a response object.
156
+
157
+ Args:
158
+ response: The framework-specific response object
159
+
160
+ Returns:
161
+ The status code or 0 if not found
162
+ """
163
+ try:
164
+ if hasattr(response, "status_code"):
165
+ return response.status_code
166
+ if isinstance(response, dict) and "status_code" in response:
167
+ return response["status_code"]
168
+ except:
169
+ pass
170
+ return 0
171
+
172
+
173
+ class AsyncMeilisearchBackend(AsyncLoggingBackend):
174
+ """
175
+ Asynchronous Meilisearch logging backend implementation.
176
+ """
177
+
178
+ def __init__(self, config: Dict[str, Any]):
179
+ """
180
+ Initialize the async Meilisearch backend.
181
+
182
+ Args:
183
+ config: Configuration dictionary containing:
184
+ - url: Meilisearch server URL
185
+ - api_key: Meilisearch API key
186
+ - index_name: Name of the index to store logs
187
+
188
+ Raises:
189
+ ImportError: If meilisearch is not installed
190
+ """
191
+ if not MEILISEARCH_AVAILABLE:
192
+ raise ImportError(
193
+ "Meilisearch backend requires meilisearch. Install it with 'pip install meilisearch'"
194
+ )
195
+ self.config = config
196
+ self.client = meilisearch.Client(config["url"], config["api_key"])
197
+ self.index = self.client.index(config["index_name"])
198
+ self.setup(config)
199
+
200
+ def setup(self, config: Dict[str, Any]) -> None:
201
+ """
202
+ Set up the async Meilisearch client and index.
203
+
204
+ Args:
205
+ config: Configuration dictionary
206
+ """
207
+ # Ensure timestamp and other fields are filterable
208
+ self.index.update_filterable_attributes(
209
+ [
210
+ "timestamp",
211
+ "level",
212
+ "ip",
213
+ "method",
214
+ "path",
215
+ "status_code",
216
+ "block_type",
217
+ "reason",
218
+ ]
219
+ )
220
+ self.index.update_sortable_attributes(["timestamp", "level", "status_code"])
221
+
222
+ def _create_log_entry(self, **kwargs) -> Dict[str, Any]:
223
+ """
224
+ Create a standardized log entry.
225
+
226
+ Returns:
227
+ Dict containing the log entry
228
+ """
229
+ return {
230
+ "id": f"{int(time.time() * 1000)}-{kwargs.get('level', 'INFO')}",
231
+ "timestamp": int(time.time()),
232
+ **kwargs,
233
+ }
234
+
235
+ async def log_request(self, request_info: Dict[str, Any], response: Any) -> None:
236
+ """
237
+ Log a request to Meilisearch asynchronously.
238
+
239
+ Args:
240
+ request_info: Dict with request information
241
+ response: The framework-specific response object
242
+ """
243
+ log_entry = self._create_log_entry(
244
+ level="INFO",
245
+ ip=request_info.get("ip", "unknown"),
246
+ method=request_info.get("method", "unknown"),
247
+ path=request_info.get("path", "unknown"),
248
+ status_code=self._extract_status_code(response),
249
+ user_agent=request_info.get("user_agent", "unknown"),
250
+ event_type="request",
251
+ )
252
+ self.index.add_documents([log_entry])
253
+
254
+ async def log_blocked_request(
255
+ self, request_info: Dict[str, Any], block_type: str, reason: str
256
+ ) -> None:
257
+ """
258
+ Log a blocked request to Meilisearch asynchronously.
259
+
260
+ Args:
261
+ request_info: Dict with request information
262
+ block_type: Type of block (IP filter, rate limit, etc.)
263
+ reason: Reason for blocking
264
+ """
265
+ log_entry = self._create_log_entry(
266
+ level="WARNING",
267
+ ip=request_info.get("ip", "unknown"),
268
+ method=request_info.get("method", "unknown"),
269
+ path=request_info.get("path", "unknown"),
270
+ block_type=block_type,
271
+ reason=reason,
272
+ user_agent=request_info.get("user_agent", "unknown"),
273
+ event_type="blocked_request",
274
+ )
275
+ self.index.add_documents([log_entry])
276
+
277
+ async def log_security_event(
278
+ self, level: str, message: str, extra: Optional[Dict[str, Any]] = None
279
+ ) -> None:
280
+ """
281
+ Log a security event to Meilisearch asynchronously.
282
+
283
+ Args:
284
+ level: Log level (INFO, WARNING, ERROR, etc.)
285
+ message: The log message
286
+ extra: Additional information to log
287
+ """
288
+ log_entry = self._create_log_entry(
289
+ level=level, message=message, event_type="security_event", **(extra or {})
290
+ )
291
+ self.index.add_documents([log_entry])
292
+
293
+ def _extract_status_code(self, response: Any) -> int:
294
+ """
295
+ Extract status code from a response object.
296
+
297
+ Args:
298
+ response: The framework-specific response object
299
+
300
+ Returns:
301
+ The status code or 0 if not found
302
+ """
303
+ try:
304
+ if hasattr(response, "status_code"):
305
+ return response.status_code
306
+ if isinstance(response, dict) and "status_code" in response:
307
+ return response["status_code"]
308
+ except:
309
+ pass
310
+ return 0