mcp-security-framework 0.1.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.
- mcp_security_framework/__init__.py +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,571 @@
|
|
1
|
+
"""
|
2
|
+
Validation Utilities Module
|
3
|
+
|
4
|
+
This module provides comprehensive validation utilities for the
|
5
|
+
MCP Security Framework. It includes functions for validating
|
6
|
+
configuration files, input data, formats, and data normalization.
|
7
|
+
|
8
|
+
Key Features:
|
9
|
+
- Configuration file validation
|
10
|
+
- Input data validation
|
11
|
+
- Format validation utilities
|
12
|
+
- Data normalization functions
|
13
|
+
- Email and URL validation
|
14
|
+
- IP address validation
|
15
|
+
- File path validation
|
16
|
+
|
17
|
+
Functions:
|
18
|
+
validate_configuration_file: Validate configuration file
|
19
|
+
validate_input_data: Validate input data
|
20
|
+
validate_email: Validate email format
|
21
|
+
validate_url: Validate URL format
|
22
|
+
validate_ip_address: Validate IP address
|
23
|
+
validate_file_path: Validate file path
|
24
|
+
normalize_data: Normalize data format
|
25
|
+
validate_json_schema: Validate JSON against schema
|
26
|
+
|
27
|
+
Author: MCP Security Team
|
28
|
+
Version: 1.0.0
|
29
|
+
License: MIT
|
30
|
+
"""
|
31
|
+
|
32
|
+
import json
|
33
|
+
import os
|
34
|
+
import re
|
35
|
+
import socket
|
36
|
+
from pathlib import Path
|
37
|
+
from typing import Any, Dict, List, Optional, Union
|
38
|
+
from urllib.parse import urlparse
|
39
|
+
|
40
|
+
from pydantic import BaseModel
|
41
|
+
|
42
|
+
|
43
|
+
class ValidationError(Exception):
|
44
|
+
"""Raised when validation fails."""
|
45
|
+
|
46
|
+
def __init__(self, message: str, error_code: int = -32003):
|
47
|
+
self.message = message
|
48
|
+
self.error_code = error_code
|
49
|
+
super().__init__(self.message)
|
50
|
+
|
51
|
+
|
52
|
+
def validate_configuration_file(
|
53
|
+
config_path: Union[str, Path], config_model: type[BaseModel]
|
54
|
+
) -> Dict[str, Any]:
|
55
|
+
"""
|
56
|
+
Validate configuration file against Pydantic model.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
config_path: Path to configuration file
|
60
|
+
config_model: Pydantic model for validation
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Validated configuration data
|
64
|
+
|
65
|
+
Raises:
|
66
|
+
ValidationError: If configuration validation fails
|
67
|
+
"""
|
68
|
+
try:
|
69
|
+
config_path = Path(config_path)
|
70
|
+
|
71
|
+
if not config_path.exists():
|
72
|
+
raise ValidationError(f"Configuration file not found: {config_path}")
|
73
|
+
|
74
|
+
if not config_path.is_file():
|
75
|
+
raise ValidationError(f"Path is not a file: {config_path}")
|
76
|
+
|
77
|
+
# Read configuration file
|
78
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
79
|
+
if config_path.suffix.lower() == ".json":
|
80
|
+
config_data = json.load(f)
|
81
|
+
else:
|
82
|
+
raise ValidationError(
|
83
|
+
f"Unsupported configuration file format: {config_path.suffix}"
|
84
|
+
)
|
85
|
+
|
86
|
+
# Validate against model
|
87
|
+
validated_config = config_model.model_validate(config_data)
|
88
|
+
return validated_config.model_dump()
|
89
|
+
|
90
|
+
except json.JSONDecodeError as e:
|
91
|
+
raise ValidationError(f"Invalid JSON in configuration file: {str(e)}")
|
92
|
+
except ValidationError as e:
|
93
|
+
raise ValidationError(f"Configuration validation failed: {str(e)}")
|
94
|
+
except Exception as e:
|
95
|
+
raise ValidationError(f"Configuration file processing failed: {str(e)}")
|
96
|
+
|
97
|
+
|
98
|
+
def validate_input_data(
|
99
|
+
data: Any,
|
100
|
+
required_fields: Optional[List[str]] = None,
|
101
|
+
allowed_fields: Optional[List[str]] = None,
|
102
|
+
data_type: Optional[type] = None,
|
103
|
+
) -> bool:
|
104
|
+
"""
|
105
|
+
Validate input data structure and content.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
data: Data to validate
|
109
|
+
required_fields: List of required field names
|
110
|
+
allowed_fields: List of allowed field names
|
111
|
+
data_type: Expected data type
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
True if data is valid, False otherwise
|
115
|
+
|
116
|
+
Raises:
|
117
|
+
ValidationError: If validation fails
|
118
|
+
"""
|
119
|
+
try:
|
120
|
+
# Check data type
|
121
|
+
if data_type and not isinstance(data, data_type):
|
122
|
+
raise ValidationError(
|
123
|
+
f"Expected {data_type.__name__}, got {type(data).__name__}"
|
124
|
+
)
|
125
|
+
|
126
|
+
# Check required fields for dictionaries
|
127
|
+
if isinstance(data, dict) and required_fields:
|
128
|
+
missing_fields = [field for field in required_fields if field not in data]
|
129
|
+
if missing_fields:
|
130
|
+
raise ValidationError(f"Missing required fields: {missing_fields}")
|
131
|
+
|
132
|
+
# Check allowed fields for dictionaries
|
133
|
+
if isinstance(data, dict) and allowed_fields:
|
134
|
+
invalid_fields = [
|
135
|
+
field for field in data.keys() if field not in allowed_fields
|
136
|
+
]
|
137
|
+
if invalid_fields:
|
138
|
+
raise ValidationError(f"Invalid fields: {invalid_fields}")
|
139
|
+
|
140
|
+
return True
|
141
|
+
|
142
|
+
except ValidationError:
|
143
|
+
raise
|
144
|
+
except Exception as e:
|
145
|
+
raise ValidationError(f"Input data validation failed: {str(e)}")
|
146
|
+
|
147
|
+
|
148
|
+
def validate_email(email: str) -> bool:
|
149
|
+
"""
|
150
|
+
Validate email format.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
email: Email address to validate
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
True if email is valid, False otherwise
|
157
|
+
"""
|
158
|
+
if not email or not isinstance(email, str):
|
159
|
+
return False
|
160
|
+
|
161
|
+
# Basic email regex pattern
|
162
|
+
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
163
|
+
return bool(re.match(pattern, email)) and ".." not in email
|
164
|
+
|
165
|
+
|
166
|
+
def validate_url(url: str, allowed_schemes: Optional[List[str]] = None) -> bool:
|
167
|
+
"""
|
168
|
+
Validate URL format.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
url: URL to validate
|
172
|
+
allowed_schemes: List of allowed URL schemes
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
True if URL is valid, False otherwise
|
176
|
+
"""
|
177
|
+
if not url or not isinstance(url, str):
|
178
|
+
return False
|
179
|
+
|
180
|
+
try:
|
181
|
+
parsed = urlparse(url)
|
182
|
+
|
183
|
+
# Check if URL has required components
|
184
|
+
if not parsed.scheme or not parsed.netloc:
|
185
|
+
return False
|
186
|
+
|
187
|
+
# Check allowed schemes
|
188
|
+
if allowed_schemes and parsed.scheme not in allowed_schemes:
|
189
|
+
return False
|
190
|
+
|
191
|
+
return True
|
192
|
+
except Exception:
|
193
|
+
return False
|
194
|
+
|
195
|
+
|
196
|
+
def validate_ip_address(ip_address: str, ip_version: Optional[str] = None) -> bool:
|
197
|
+
"""
|
198
|
+
Validate IP address format.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
ip_address: IP address to validate
|
202
|
+
ip_version: IP version to validate (ipv4, ipv6)
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
True if IP address is valid, False otherwise
|
206
|
+
"""
|
207
|
+
if not ip_address or not isinstance(ip_address, str):
|
208
|
+
return False
|
209
|
+
|
210
|
+
try:
|
211
|
+
# Validate IPv4
|
212
|
+
if ip_version == "ipv4" or (ip_version is None and "." in ip_address):
|
213
|
+
socket.inet_pton(socket.AF_INET, ip_address)
|
214
|
+
return True
|
215
|
+
|
216
|
+
# Validate IPv6
|
217
|
+
elif ip_version == "ipv6" or (ip_version is None and ":" in ip_address):
|
218
|
+
socket.inet_pton(socket.AF_INET6, ip_address)
|
219
|
+
return True
|
220
|
+
|
221
|
+
return False
|
222
|
+
except socket.error:
|
223
|
+
return False
|
224
|
+
|
225
|
+
|
226
|
+
def validate_file_path(
|
227
|
+
file_path: Union[str, Path],
|
228
|
+
must_exist: bool = False,
|
229
|
+
must_be_file: bool = False,
|
230
|
+
must_be_directory: bool = False,
|
231
|
+
must_be_readable: bool = False,
|
232
|
+
must_be_writable: bool = False,
|
233
|
+
) -> bool:
|
234
|
+
"""
|
235
|
+
Validate file path.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
file_path: File path to validate
|
239
|
+
must_exist: Whether file must exist
|
240
|
+
must_be_file: Whether path must be a file
|
241
|
+
must_be_directory: Whether path must be a directory
|
242
|
+
must_be_readable: Whether file must be readable
|
243
|
+
must_be_writable: Whether file must be writable
|
244
|
+
|
245
|
+
Returns:
|
246
|
+
True if path is valid, False otherwise
|
247
|
+
"""
|
248
|
+
try:
|
249
|
+
path = Path(file_path)
|
250
|
+
|
251
|
+
# Check if file exists
|
252
|
+
if must_exist and not path.exists():
|
253
|
+
return False
|
254
|
+
|
255
|
+
# Check if it's a file
|
256
|
+
if must_be_file and not path.is_file():
|
257
|
+
return False
|
258
|
+
|
259
|
+
# Check if it's a directory
|
260
|
+
if must_be_directory and not path.is_dir():
|
261
|
+
return False
|
262
|
+
|
263
|
+
# Check readability
|
264
|
+
if must_be_readable and not os.access(path, os.R_OK):
|
265
|
+
return False
|
266
|
+
|
267
|
+
# Check writability
|
268
|
+
if must_be_writable and not os.access(path, os.W_OK):
|
269
|
+
return False
|
270
|
+
|
271
|
+
return True
|
272
|
+
except Exception:
|
273
|
+
return False
|
274
|
+
|
275
|
+
|
276
|
+
def normalize_data(data: Any, data_type: str = "string") -> Any:
|
277
|
+
"""
|
278
|
+
Normalize data format.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
data: Data to normalize
|
282
|
+
data_type: Target data type (string, integer, float, boolean)
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
Normalized data
|
286
|
+
|
287
|
+
Raises:
|
288
|
+
ValidationError: If normalization fails
|
289
|
+
"""
|
290
|
+
try:
|
291
|
+
if data_type == "string":
|
292
|
+
if data is None:
|
293
|
+
return ""
|
294
|
+
return str(data).strip()
|
295
|
+
|
296
|
+
elif data_type == "integer":
|
297
|
+
if data is None:
|
298
|
+
return 0
|
299
|
+
return int(float(data))
|
300
|
+
|
301
|
+
elif data_type == "float":
|
302
|
+
if data is None:
|
303
|
+
return 0.0
|
304
|
+
return float(data)
|
305
|
+
|
306
|
+
elif data_type == "boolean":
|
307
|
+
if data is None:
|
308
|
+
return False
|
309
|
+
if isinstance(data, bool):
|
310
|
+
return data
|
311
|
+
if isinstance(data, str):
|
312
|
+
return data.lower() in ("true", "1", "yes", "on")
|
313
|
+
return bool(data)
|
314
|
+
|
315
|
+
else:
|
316
|
+
raise ValidationError(f"Unsupported data type: {data_type}")
|
317
|
+
|
318
|
+
except (ValueError, TypeError) as e:
|
319
|
+
raise ValidationError(f"Data normalization failed: {str(e)}")
|
320
|
+
|
321
|
+
|
322
|
+
def validate_json_schema(data: Dict, schema: Dict) -> bool:
|
323
|
+
"""
|
324
|
+
Validate JSON data against schema.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
data: JSON data to validate
|
328
|
+
schema: JSON schema for validation
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
True if data matches schema, False otherwise
|
332
|
+
|
333
|
+
Raises:
|
334
|
+
ValidationError: If validation fails
|
335
|
+
"""
|
336
|
+
try:
|
337
|
+
# Simple schema validation (basic implementation)
|
338
|
+
# In production, use jsonschema library for full validation
|
339
|
+
|
340
|
+
if not isinstance(data, dict):
|
341
|
+
raise ValidationError("Data must be a dictionary")
|
342
|
+
|
343
|
+
# Check required fields
|
344
|
+
required_fields = schema.get("required", [])
|
345
|
+
for field in required_fields:
|
346
|
+
if field not in data:
|
347
|
+
raise ValidationError(f"Missing required field: {field}")
|
348
|
+
|
349
|
+
# Check field types
|
350
|
+
properties = schema.get("properties", {})
|
351
|
+
for field_name, field_value in data.items():
|
352
|
+
if field_name in properties:
|
353
|
+
field_schema = properties[field_name]
|
354
|
+
expected_type = field_schema.get("type")
|
355
|
+
|
356
|
+
if expected_type == "string" and not isinstance(field_value, str):
|
357
|
+
raise ValidationError(f"Field {field_name} must be a string")
|
358
|
+
elif expected_type == "integer" and not isinstance(field_value, int):
|
359
|
+
raise ValidationError(f"Field {field_name} must be an integer")
|
360
|
+
elif expected_type == "number" and not isinstance(
|
361
|
+
field_value, (int, float)
|
362
|
+
):
|
363
|
+
raise ValidationError(f"Field {field_name} must be a number")
|
364
|
+
elif expected_type == "boolean" and not isinstance(field_value, bool):
|
365
|
+
raise ValidationError(f"Field {field_name} must be a boolean")
|
366
|
+
elif expected_type == "array" and not isinstance(field_value, list):
|
367
|
+
raise ValidationError(f"Field {field_name} must be an array")
|
368
|
+
elif expected_type == "object" and not isinstance(field_value, dict):
|
369
|
+
raise ValidationError(f"Field {field_name} must be an object")
|
370
|
+
|
371
|
+
return True
|
372
|
+
|
373
|
+
except ValidationError:
|
374
|
+
raise
|
375
|
+
except Exception as e:
|
376
|
+
raise ValidationError(f"JSON schema validation failed: {str(e)}")
|
377
|
+
|
378
|
+
|
379
|
+
def validate_string_length(
|
380
|
+
value: str, min_length: Optional[int] = None, max_length: Optional[int] = None
|
381
|
+
) -> bool:
|
382
|
+
"""
|
383
|
+
Validate string length.
|
384
|
+
|
385
|
+
Args:
|
386
|
+
value: String to validate
|
387
|
+
min_length: Minimum length
|
388
|
+
max_length: Maximum length
|
389
|
+
|
390
|
+
Returns:
|
391
|
+
True if string length is valid, False otherwise
|
392
|
+
"""
|
393
|
+
if not isinstance(value, str):
|
394
|
+
return False
|
395
|
+
|
396
|
+
length = len(value)
|
397
|
+
|
398
|
+
if min_length is not None and length < min_length:
|
399
|
+
return False
|
400
|
+
|
401
|
+
if max_length is not None and length > max_length:
|
402
|
+
return False
|
403
|
+
|
404
|
+
return True
|
405
|
+
|
406
|
+
|
407
|
+
def validate_numeric_range(
|
408
|
+
value: Union[int, float],
|
409
|
+
min_value: Optional[Union[int, float]] = None,
|
410
|
+
max_value: Optional[Union[int, float]] = None,
|
411
|
+
) -> bool:
|
412
|
+
"""
|
413
|
+
Validate numeric value range.
|
414
|
+
|
415
|
+
Args:
|
416
|
+
value: Numeric value to validate
|
417
|
+
min_value: Minimum value
|
418
|
+
max_value: Maximum value
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
True if value is in range, False otherwise
|
422
|
+
"""
|
423
|
+
if not isinstance(value, (int, float)):
|
424
|
+
return False
|
425
|
+
|
426
|
+
if min_value is not None and value < min_value:
|
427
|
+
return False
|
428
|
+
|
429
|
+
if max_value is not None and value > max_value:
|
430
|
+
return False
|
431
|
+
|
432
|
+
return True
|
433
|
+
|
434
|
+
|
435
|
+
def validate_list_content(
|
436
|
+
value: List,
|
437
|
+
allowed_values: Optional[List] = None,
|
438
|
+
min_items: Optional[int] = None,
|
439
|
+
max_items: Optional[int] = None,
|
440
|
+
) -> bool:
|
441
|
+
"""
|
442
|
+
Validate list content and structure.
|
443
|
+
|
444
|
+
Args:
|
445
|
+
value: List to validate
|
446
|
+
allowed_values: List of allowed values
|
447
|
+
min_items: Minimum number of items
|
448
|
+
max_items: Maximum number of items
|
449
|
+
|
450
|
+
Returns:
|
451
|
+
True if list is valid, False otherwise
|
452
|
+
"""
|
453
|
+
if not isinstance(value, list):
|
454
|
+
return False
|
455
|
+
|
456
|
+
# Check item count
|
457
|
+
if min_items is not None and len(value) < min_items:
|
458
|
+
return False
|
459
|
+
|
460
|
+
if max_items is not None and len(value) > max_items:
|
461
|
+
return False
|
462
|
+
|
463
|
+
# Check allowed values
|
464
|
+
if allowed_values is not None:
|
465
|
+
for item in value:
|
466
|
+
if item not in allowed_values:
|
467
|
+
return False
|
468
|
+
|
469
|
+
return True
|
470
|
+
|
471
|
+
|
472
|
+
def sanitize_string(value: str, allowed_chars: Optional[str] = None) -> str:
|
473
|
+
"""
|
474
|
+
Sanitize string by removing or replacing invalid characters.
|
475
|
+
|
476
|
+
Args:
|
477
|
+
value: String to sanitize
|
478
|
+
allowed_chars: String of allowed characters (regex pattern)
|
479
|
+
|
480
|
+
Returns:
|
481
|
+
Sanitized string
|
482
|
+
"""
|
483
|
+
if not isinstance(value, str):
|
484
|
+
return ""
|
485
|
+
|
486
|
+
if allowed_chars is None:
|
487
|
+
# Default: allow alphanumeric, spaces, and common punctuation
|
488
|
+
allowed_chars = r"[a-zA-Z0-9\s\-_.,!?@#$%&*()+=:;]"
|
489
|
+
|
490
|
+
# Remove characters not in allowed set
|
491
|
+
sanitized = re.sub(f"[^{allowed_chars}]", "", value)
|
492
|
+
|
493
|
+
# Remove HTML tags and scripts
|
494
|
+
sanitized = re.sub(r"<[^>]*>", "", sanitized)
|
495
|
+
|
496
|
+
# Remove extra whitespace
|
497
|
+
sanitized = re.sub(r"\s+", " ", sanitized).strip()
|
498
|
+
|
499
|
+
return sanitized
|
500
|
+
|
501
|
+
|
502
|
+
def validate_file_extension(
|
503
|
+
file_path: Union[str, Path], allowed_extensions: List[str]
|
504
|
+
) -> bool:
|
505
|
+
"""
|
506
|
+
Validate file extension.
|
507
|
+
|
508
|
+
Args:
|
509
|
+
file_path: File path to validate
|
510
|
+
allowed_extensions: List of allowed file extensions
|
511
|
+
|
512
|
+
Returns:
|
513
|
+
True if file extension is allowed, False otherwise
|
514
|
+
"""
|
515
|
+
try:
|
516
|
+
path = Path(file_path)
|
517
|
+
extension = path.suffix.lower()
|
518
|
+
|
519
|
+
# Normalize allowed extensions (remove leading dot if present)
|
520
|
+
normalized_extensions = []
|
521
|
+
for ext in allowed_extensions:
|
522
|
+
ext_lower = ext.lower()
|
523
|
+
if ext_lower.startswith("."):
|
524
|
+
normalized_extensions.append(ext_lower)
|
525
|
+
else:
|
526
|
+
normalized_extensions.append("." + ext_lower)
|
527
|
+
|
528
|
+
return extension in normalized_extensions
|
529
|
+
except Exception:
|
530
|
+
return False
|
531
|
+
|
532
|
+
|
533
|
+
def validate_directory_structure(
|
534
|
+
directory_path: Union[str, Path],
|
535
|
+
required_files: Optional[List[str]] = None,
|
536
|
+
required_directories: Optional[List[str]] = None,
|
537
|
+
) -> bool:
|
538
|
+
"""
|
539
|
+
Validate directory structure.
|
540
|
+
|
541
|
+
Args:
|
542
|
+
directory_path: Directory path to validate
|
543
|
+
required_files: List of required files
|
544
|
+
required_directories: List of required subdirectories
|
545
|
+
|
546
|
+
Returns:
|
547
|
+
True if directory structure is valid, False otherwise
|
548
|
+
"""
|
549
|
+
try:
|
550
|
+
directory = Path(directory_path)
|
551
|
+
|
552
|
+
if not directory.exists() or not directory.is_dir():
|
553
|
+
return False
|
554
|
+
|
555
|
+
# Check required files
|
556
|
+
if required_files:
|
557
|
+
for file_name in required_files:
|
558
|
+
file_path = directory / file_name
|
559
|
+
if not file_path.exists() or not file_path.is_file():
|
560
|
+
return False
|
561
|
+
|
562
|
+
# Check required directories
|
563
|
+
if required_directories:
|
564
|
+
for dir_name in required_directories:
|
565
|
+
dir_path = directory / dir_name
|
566
|
+
if not dir_path.exists() or not dir_path.is_dir():
|
567
|
+
return False
|
568
|
+
|
569
|
+
return True
|
570
|
+
except Exception:
|
571
|
+
return False
|