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.
Files changed (76) hide show
  1. mcp_security_framework/__init__.py +96 -0
  2. mcp_security_framework/cli/__init__.py +18 -0
  3. mcp_security_framework/cli/cert_cli.py +511 -0
  4. mcp_security_framework/cli/security_cli.py +791 -0
  5. mcp_security_framework/constants.py +209 -0
  6. mcp_security_framework/core/__init__.py +61 -0
  7. mcp_security_framework/core/auth_manager.py +1011 -0
  8. mcp_security_framework/core/cert_manager.py +1663 -0
  9. mcp_security_framework/core/permission_manager.py +735 -0
  10. mcp_security_framework/core/rate_limiter.py +602 -0
  11. mcp_security_framework/core/security_manager.py +943 -0
  12. mcp_security_framework/core/ssl_manager.py +735 -0
  13. mcp_security_framework/examples/__init__.py +75 -0
  14. mcp_security_framework/examples/django_example.py +615 -0
  15. mcp_security_framework/examples/fastapi_example.py +472 -0
  16. mcp_security_framework/examples/flask_example.py +506 -0
  17. mcp_security_framework/examples/gateway_example.py +803 -0
  18. mcp_security_framework/examples/microservice_example.py +690 -0
  19. mcp_security_framework/examples/standalone_example.py +576 -0
  20. mcp_security_framework/middleware/__init__.py +250 -0
  21. mcp_security_framework/middleware/auth_middleware.py +292 -0
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
  23. mcp_security_framework/middleware/fastapi_middleware.py +757 -0
  24. mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
  25. mcp_security_framework/middleware/flask_middleware.py +591 -0
  26. mcp_security_framework/middleware/mtls_middleware.py +439 -0
  27. mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
  28. mcp_security_framework/middleware/security_middleware.py +507 -0
  29. mcp_security_framework/schemas/__init__.py +109 -0
  30. mcp_security_framework/schemas/config.py +694 -0
  31. mcp_security_framework/schemas/models.py +709 -0
  32. mcp_security_framework/schemas/responses.py +686 -0
  33. mcp_security_framework/tests/__init__.py +0 -0
  34. mcp_security_framework/utils/__init__.py +121 -0
  35. mcp_security_framework/utils/cert_utils.py +525 -0
  36. mcp_security_framework/utils/crypto_utils.py +475 -0
  37. mcp_security_framework/utils/validation_utils.py +571 -0
  38. mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
  39. mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
  40. mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
  41. mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
  42. mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
  43. tests/__init__.py +0 -0
  44. tests/test_cli/__init__.py +0 -0
  45. tests/test_cli/test_cert_cli.py +379 -0
  46. tests/test_cli/test_security_cli.py +657 -0
  47. tests/test_core/__init__.py +0 -0
  48. tests/test_core/test_auth_manager.py +582 -0
  49. tests/test_core/test_cert_manager.py +795 -0
  50. tests/test_core/test_permission_manager.py +395 -0
  51. tests/test_core/test_rate_limiter.py +626 -0
  52. tests/test_core/test_security_manager.py +841 -0
  53. tests/test_core/test_ssl_manager.py +532 -0
  54. tests/test_examples/__init__.py +8 -0
  55. tests/test_examples/test_fastapi_example.py +264 -0
  56. tests/test_examples/test_flask_example.py +238 -0
  57. tests/test_examples/test_standalone_example.py +292 -0
  58. tests/test_integration/__init__.py +0 -0
  59. tests/test_integration/test_auth_flow.py +502 -0
  60. tests/test_integration/test_certificate_flow.py +527 -0
  61. tests/test_integration/test_fastapi_integration.py +341 -0
  62. tests/test_integration/test_flask_integration.py +398 -0
  63. tests/test_integration/test_standalone_integration.py +493 -0
  64. tests/test_middleware/__init__.py +0 -0
  65. tests/test_middleware/test_fastapi_middleware.py +523 -0
  66. tests/test_middleware/test_flask_middleware.py +582 -0
  67. tests/test_middleware/test_security_middleware.py +493 -0
  68. tests/test_schemas/__init__.py +0 -0
  69. tests/test_schemas/test_config.py +811 -0
  70. tests/test_schemas/test_models.py +879 -0
  71. tests/test_schemas/test_responses.py +1054 -0
  72. tests/test_schemas/test_serialization.py +493 -0
  73. tests/test_utils/__init__.py +0 -0
  74. tests/test_utils/test_cert_utils.py +510 -0
  75. tests/test_utils/test_crypto_utils.py +603 -0
  76. 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