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.
- pywebguard/__init__.py +87 -0
- pywebguard/cli.py +1022 -0
- pywebguard/core/__init__.py +0 -0
- pywebguard/core/base.py +792 -0
- pywebguard/core/config.py +455 -0
- pywebguard/filters/__init__.py +0 -0
- pywebguard/filters/base.py +44 -0
- pywebguard/filters/ip_filter.py +233 -0
- pywebguard/filters/user_agent.py +102 -0
- pywebguard/frameworks/__init__.py +0 -0
- pywebguard/frameworks/_fastapi.py +191 -0
- pywebguard/frameworks/_flask.py +182 -0
- pywebguard/limiters/__init__.py +0 -0
- pywebguard/limiters/base.py +46 -0
- pywebguard/limiters/rate_limit.py +415 -0
- pywebguard/logging/__init__.py +0 -0
- pywebguard/logging/backends/__init__.py +17 -0
- pywebguard/logging/backends/_elasticsearch.py +307 -0
- pywebguard/logging/backends/_meilisearch.py +310 -0
- pywebguard/logging/backends/_mongodb.py +303 -0
- pywebguard/logging/base.py +117 -0
- pywebguard/logging/logger.py +408 -0
- pywebguard/security/__init__.py +0 -0
- pywebguard/security/base.py +44 -0
- pywebguard/security/cors.py +204 -0
- pywebguard/security/penetration.py +265 -0
- pywebguard/storage/__init__.py +0 -0
- pywebguard/storage/_mongodb.py +396 -0
- pywebguard/storage/_postgresql.py +539 -0
- pywebguard/storage/_redis.py +314 -0
- pywebguard/storage/_sqlite.py +374 -0
- pywebguard/storage/_tinydb.py +271 -0
- pywebguard/storage/base.py +327 -0
- pywebguard/storage/memory.py +245 -0
- pywebguard/utils/__init__.py +0 -0
- pywebguard/utils/ip.py +84 -0
- pywebguard/utils/request.py +65 -0
- pywebguard-0.0.0.dist-info/LICENSE +21 -0
- pywebguard-0.0.0.dist-info/METADATA +368 -0
- pywebguard-0.0.0.dist-info/RECORD +43 -0
- pywebguard-0.0.0.dist-info/WHEEL +5 -0
- pywebguard-0.0.0.dist-info/entry_points.txt +2 -0
- 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
|