mcp-proxy-adapter 6.9.28__py3-none-any.whl ā 6.9.29__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.
Potentially problematic release.
This version of mcp-proxy-adapter might be problematic. Click here for more details.
- mcp_proxy_adapter/__init__.py +10 -0
- mcp_proxy_adapter/__main__.py +8 -21
- mcp_proxy_adapter/api/app.py +10 -913
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +243 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_manager.py +166 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +78 -199
- mcp_proxy_adapter/api/middleware/__init__.py +1 -44
- mcp_proxy_adapter/api/middleware/base.py +0 -42
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
- mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
- mcp_proxy_adapter/api/middleware/factory.py +0 -94
- mcp_proxy_adapter/api/middleware/logging.py +0 -112
- mcp_proxy_adapter/api/middleware/performance.py +0 -35
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
- mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
- mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +0 -61
- mcp_proxy_adapter/api/tool_integration.py +0 -117
- mcp_proxy_adapter/api/tools.py +0 -46
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +324 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
- mcp_proxy_adapter/client/proxy.py +45 -0
- mcp_proxy_adapter/commands/__init__.py +44 -28
- mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
- mcp_proxy_adapter/commands/base.py +19 -43
- mcp_proxy_adapter/commands/builtin_commands.py +0 -75
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +58 -928
- mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
- mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
- mcp_proxy_adapter/commands/command_registry.py +172 -904
- mcp_proxy_adapter/commands/config_command.py +0 -28
- mcp_proxy_adapter/commands/dependency_container.py +1 -70
- mcp_proxy_adapter/commands/dependency_manager.py +0 -128
- mcp_proxy_adapter/commands/echo_command.py +0 -34
- mcp_proxy_adapter/commands/health_command.py +0 -3
- mcp_proxy_adapter/commands/help_command.py +0 -159
- mcp_proxy_adapter/commands/hooks.py +0 -137
- mcp_proxy_adapter/commands/key_management_command.py +0 -25
- mcp_proxy_adapter/commands/load_command.py +7 -78
- mcp_proxy_adapter/commands/plugins_command.py +0 -16
- mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
- mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +0 -43
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +0 -80
- mcp_proxy_adapter/commands/result.py +25 -77
- mcp_proxy_adapter/commands/role_test_command.py +0 -44
- mcp_proxy_adapter/commands/roles_management_command.py +0 -199
- mcp_proxy_adapter/commands/security_command.py +0 -30
- mcp_proxy_adapter/commands/settings_command.py +0 -68
- mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
- mcp_proxy_adapter/commands/token_management_command.py +0 -1
- mcp_proxy_adapter/commands/transport_management_command.py +0 -20
- mcp_proxy_adapter/commands/unload_command.py +0 -71
- mcp_proxy_adapter/config.py +15 -626
- mcp_proxy_adapter/core/__init__.py +5 -39
- mcp_proxy_adapter/core/app_factory.py +14 -36
- mcp_proxy_adapter/core/app_runner.py +0 -27
- mcp_proxy_adapter/core/auth_validator.py +1 -93
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
- mcp_proxy_adapter/core/certificate_utils.py +64 -903
- mcp_proxy_adapter/core/client.py +0 -6
- mcp_proxy_adapter/core/client_manager.py +0 -19
- mcp_proxy_adapter/core/client_security.py +0 -2
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +195 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +112 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
- mcp_proxy_adapter/core/config_converter.py +0 -186
- mcp_proxy_adapter/core/config_validator.py +96 -1238
- mcp_proxy_adapter/core/errors.py +7 -42
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +2 -22
- mcp_proxy_adapter/core/mtls_asgi.py +0 -20
- mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
- mcp_proxy_adapter/core/mtls_proxy.py +0 -80
- mcp_proxy_adapter/core/mtls_server.py +3 -173
- mcp_proxy_adapter/core/protocol_manager.py +1 -191
- mcp_proxy_adapter/core/proxy/__init__.py +22 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +0 -1
- mcp_proxy_adapter/core/proxy_registration.py +36 -913
- mcp_proxy_adapter/core/role_utils.py +0 -308
- mcp_proxy_adapter/core/security_adapter.py +1 -36
- mcp_proxy_adapter/core/security_factory.py +1 -150
- mcp_proxy_adapter/core/security_integration.py +0 -33
- mcp_proxy_adapter/core/server_adapter.py +1 -40
- mcp_proxy_adapter/core/server_engine.py +2 -173
- mcp_proxy_adapter/core/settings.py +0 -127
- mcp_proxy_adapter/core/signal_handler.py +0 -65
- mcp_proxy_adapter/core/ssl_utils.py +19 -137
- mcp_proxy_adapter/core/transport_manager.py +0 -151
- mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
- mcp_proxy_adapter/core/utils.py +1 -182
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +211 -0
- mcp_proxy_adapter/core/validation/file_validator.py +73 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
- mcp_proxy_adapter/core/validation/security_validator.py +58 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +33 -652
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
- mcp_proxy_adapter/examples/check_config.py +0 -2
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/config_builder.py +13 -2
- mcp_proxy_adapter/examples/config_cli.py +0 -1
- mcp_proxy_adapter/examples/create_test_configs.py +0 -46
- mcp_proxy_adapter/examples/debug_request_state.py +0 -1
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
- mcp_proxy_adapter/examples/full_application/main.py +186 -150
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
- mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
- mcp_proxy_adapter/examples/generate_config.py +65 -11
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +0 -2
- mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
- mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +24 -1075
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
- mcp_proxy_adapter/examples/test_config.py +0 -3
- mcp_proxy_adapter/examples/test_config_builder.py +25 -405
- mcp_proxy_adapter/examples/test_examples.py +0 -1
- mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
- mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
- mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
- mcp_proxy_adapter/examples/universal_client.py +0 -6
- mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +70 -62
- mcp_proxy_adapter/openapi.py +0 -22
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.9.28.dist-info ā mcp_proxy_adapter-6.9.29.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.9.29.dist-info/RECORD +235 -0
- {mcp_proxy_adapter-6.9.28.dist-info ā mcp_proxy_adapter-6.9.29.dist-info}/entry_points.txt +1 -1
- mcp_proxy_adapter-6.9.28.dist-info/RECORD +0 -149
- {mcp_proxy_adapter-6.9.28.dist-info ā mcp_proxy_adapter-6.9.29.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.9.28.dist-info ā mcp_proxy_adapter-6.9.29.dist-info}/top_level.txt +0 -0
|
@@ -1,35 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Configuration Validator for MCP Proxy Adapter
|
|
3
|
-
Validates configuration files and ensures all required settings are present and correct.
|
|
4
|
-
|
|
5
2
|
Author: Vasiliy Zdanovskiy
|
|
6
3
|
email: vasilyvz@gmail.com
|
|
4
|
+
|
|
5
|
+
Main configuration validator for MCP Proxy Adapter.
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
8
|
import json
|
|
10
|
-
import os
|
|
11
9
|
import logging
|
|
12
|
-
import re
|
|
13
10
|
from pathlib import Path
|
|
14
|
-
from typing import Dict, List, Any, Optional
|
|
15
|
-
from enum import Enum
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
from datetime import datetime, timezone
|
|
18
|
-
import ssl
|
|
19
|
-
import socket
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ValidationLevel(Enum):
|
|
25
|
-
"""Validation severity levels."""
|
|
26
|
-
ERROR = "error"
|
|
27
|
-
WARNING = "warning"
|
|
28
|
-
INFO = "info"
|
|
11
|
+
from typing import Dict, List, Any, Optional
|
|
29
12
|
|
|
13
|
+
from .file_validator import FileValidator
|
|
14
|
+
from .security_validator import SecurityValidator
|
|
15
|
+
from .protocol_validator import ProtocolValidator
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
from .errors import ValidationResult, MissingConfigKeyError
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
33
18
|
|
|
34
19
|
|
|
35
20
|
class ConfigValidator:
|
|
@@ -49,138 +34,12 @@ class ConfigValidator:
|
|
|
49
34
|
Initialize configuration validator.
|
|
50
35
|
|
|
51
36
|
Args:
|
|
52
|
-
config_path: Path to configuration file
|
|
37
|
+
config_path: Path to configuration file (optional)
|
|
53
38
|
"""
|
|
54
39
|
self.config_path = config_path
|
|
55
40
|
self.config_data: Dict[str, Any] = {}
|
|
56
41
|
self.validation_results: List[ValidationResult] = []
|
|
57
|
-
|
|
58
|
-
# Define required sections and their keys
|
|
59
|
-
self.required_sections = {
|
|
60
|
-
"server": {
|
|
61
|
-
"host": str,
|
|
62
|
-
"port": int,
|
|
63
|
-
"protocol": str,
|
|
64
|
-
"debug": bool,
|
|
65
|
-
"log_level": str
|
|
66
|
-
},
|
|
67
|
-
"logging": {
|
|
68
|
-
"level": str,
|
|
69
|
-
"log_dir": str,
|
|
70
|
-
"log_file": str,
|
|
71
|
-
"error_log_file": str,
|
|
72
|
-
"access_log_file": str,
|
|
73
|
-
"max_file_size": (str, int),
|
|
74
|
-
"backup_count": int,
|
|
75
|
-
"format": str,
|
|
76
|
-
"date_format": str,
|
|
77
|
-
"console_output": bool,
|
|
78
|
-
"file_output": bool
|
|
79
|
-
},
|
|
80
|
-
"commands": {
|
|
81
|
-
"auto_discovery": bool,
|
|
82
|
-
"commands_directory": str,
|
|
83
|
-
"catalog_directory": str,
|
|
84
|
-
"plugin_servers": list,
|
|
85
|
-
"auto_install_dependencies": bool,
|
|
86
|
-
"enabled_commands": list,
|
|
87
|
-
"disabled_commands": list,
|
|
88
|
-
"custom_commands_path": str
|
|
89
|
-
},
|
|
90
|
-
"transport": {
|
|
91
|
-
"type": str,
|
|
92
|
-
"port": (int, type(None)),
|
|
93
|
-
"verify_client": bool,
|
|
94
|
-
"chk_hostname": bool
|
|
95
|
-
},
|
|
96
|
-
"proxy_registration": {
|
|
97
|
-
"enabled": bool,
|
|
98
|
-
"proxy_url": str,
|
|
99
|
-
"server_id": str,
|
|
100
|
-
"server_name": str,
|
|
101
|
-
"description": str,
|
|
102
|
-
"version": str,
|
|
103
|
-
"registration_timeout": int,
|
|
104
|
-
"retry_attempts": int,
|
|
105
|
-
"retry_delay": int,
|
|
106
|
-
"auto_register_on_startup": bool,
|
|
107
|
-
"auto_unregister_on_shutdown": bool
|
|
108
|
-
},
|
|
109
|
-
"debug": {
|
|
110
|
-
"enabled": bool,
|
|
111
|
-
"level": str
|
|
112
|
-
},
|
|
113
|
-
"security": {
|
|
114
|
-
"enabled": bool,
|
|
115
|
-
"tokens": dict,
|
|
116
|
-
"roles": dict,
|
|
117
|
-
"roles_file": (str, type(None))
|
|
118
|
-
},
|
|
119
|
-
"roles": {
|
|
120
|
-
"enabled": bool,
|
|
121
|
-
"config_file": (str, type(None)),
|
|
122
|
-
"default_policy": dict,
|
|
123
|
-
"auto_load": bool,
|
|
124
|
-
"validation_enabled": bool
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
# Define feature flags and their dependencies
|
|
129
|
-
self.feature_flags = {
|
|
130
|
-
"security": {
|
|
131
|
-
"enabled_key": "security.enabled",
|
|
132
|
-
"dependencies": ["security.tokens", "security.roles"],
|
|
133
|
-
"required_files": ["security.roles_file"],
|
|
134
|
-
"optional_files": []
|
|
135
|
-
},
|
|
136
|
-
"roles": {
|
|
137
|
-
"enabled_key": "roles.enabled",
|
|
138
|
-
"dependencies": ["roles.config_file"],
|
|
139
|
-
"required_files": ["roles.config_file"],
|
|
140
|
-
"optional_files": []
|
|
141
|
-
},
|
|
142
|
-
"proxy_registration": {
|
|
143
|
-
"enabled_key": "proxy_registration.enabled",
|
|
144
|
-
"dependencies": ["proxy_registration.proxy_url"],
|
|
145
|
-
"required_files": [],
|
|
146
|
-
"optional_files": [
|
|
147
|
-
"proxy_registration.certificate.cert_file",
|
|
148
|
-
"proxy_registration.certificate.key_file"
|
|
149
|
-
]
|
|
150
|
-
},
|
|
151
|
-
"ssl": {
|
|
152
|
-
"enabled_key": "ssl.enabled",
|
|
153
|
-
"dependencies": ["ssl.cert_file", "ssl.key_file"],
|
|
154
|
-
"required_files": ["ssl.cert_file", "ssl.key_file"],
|
|
155
|
-
"optional_files": ["ssl.ca_cert"]
|
|
156
|
-
},
|
|
157
|
-
"transport_ssl": {
|
|
158
|
-
"enabled_key": "transport.ssl.enabled",
|
|
159
|
-
"dependencies": ["transport.ssl.cert_file", "transport.ssl.key_file"],
|
|
160
|
-
"required_files": ["transport.ssl.cert_file", "transport.ssl.key_file"],
|
|
161
|
-
"optional_files": ["transport.ssl.ca_cert"]
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
# Protocol-specific requirements
|
|
166
|
-
self.protocol_requirements = {
|
|
167
|
-
"http": {
|
|
168
|
-
"ssl_enabled": False,
|
|
169
|
-
"client_verification": False,
|
|
170
|
-
"required_files": []
|
|
171
|
-
},
|
|
172
|
-
"https": {
|
|
173
|
-
"ssl_enabled": True,
|
|
174
|
-
"client_verification": False,
|
|
175
|
-
"required_files": ["ssl.cert_file", "ssl.key_file"]
|
|
176
|
-
},
|
|
177
|
-
"mtls": {
|
|
178
|
-
"ssl_enabled": True,
|
|
179
|
-
"client_verification": True,
|
|
180
|
-
"required_files": ["ssl.cert_file", "ssl.key_file", "ssl.ca_cert"]
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
42
|
+
|
|
184
43
|
def load_config(self, config_path: Optional[str] = None) -> None:
|
|
185
44
|
"""
|
|
186
45
|
Load configuration from file.
|
|
@@ -190,1094 +49,128 @@ class ConfigValidator:
|
|
|
190
49
|
"""
|
|
191
50
|
if config_path:
|
|
192
51
|
self.config_path = config_path
|
|
193
|
-
|
|
194
|
-
if not self.config_path
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
message=f"Configuration file not found: {self.config_path}",
|
|
198
|
-
section="config_file"
|
|
199
|
-
))
|
|
200
|
-
return
|
|
201
|
-
|
|
52
|
+
|
|
53
|
+
if not self.config_path:
|
|
54
|
+
raise ValueError("No configuration path provided")
|
|
55
|
+
|
|
202
56
|
try:
|
|
203
57
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
204
58
|
self.config_data = json.load(f)
|
|
59
|
+
logger.info(f"Configuration loaded from {self.config_path}")
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
|
|
205
62
|
except json.JSONDecodeError as e:
|
|
206
|
-
|
|
207
|
-
level="error",
|
|
208
|
-
message=f"Invalid JSON in configuration file: {e}",
|
|
209
|
-
section="config_file"
|
|
210
|
-
))
|
|
63
|
+
raise ValueError(f"Invalid JSON in configuration file: {e}")
|
|
211
64
|
except Exception as e:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
message=f"Error loading configuration file: {e}",
|
|
215
|
-
section="config_file"
|
|
216
|
-
))
|
|
217
|
-
|
|
65
|
+
raise RuntimeError(f"Error loading configuration: {e}")
|
|
66
|
+
|
|
218
67
|
def validate_config(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
|
|
219
68
|
"""
|
|
220
69
|
Validate configuration data.
|
|
221
70
|
|
|
222
71
|
Args:
|
|
223
|
-
config_data: Configuration data to validate
|
|
224
|
-
|
|
72
|
+
config_data: Configuration data to validate (optional)
|
|
73
|
+
|
|
225
74
|
Returns:
|
|
226
75
|
List of validation results
|
|
227
76
|
"""
|
|
228
77
|
if config_data is not None:
|
|
229
78
|
self.config_data = config_data
|
|
230
|
-
|
|
79
|
+
|
|
231
80
|
if not self.config_data:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
message="No configuration data to validate",
|
|
235
|
-
section="config_data"
|
|
236
|
-
))
|
|
237
|
-
return self.validation_results
|
|
238
|
-
|
|
239
|
-
# Clear previous results
|
|
81
|
+
raise ValueError("No configuration data to validate")
|
|
82
|
+
|
|
240
83
|
self.validation_results = []
|
|
241
84
|
|
|
242
|
-
#
|
|
243
|
-
self.
|
|
244
|
-
self.
|
|
245
|
-
self.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
self.
|
|
249
|
-
self.
|
|
250
|
-
self.
|
|
251
|
-
self.
|
|
85
|
+
# Initialize validators
|
|
86
|
+
file_validator = FileValidator(self.config_data)
|
|
87
|
+
security_validator = SecurityValidator(self.config_data)
|
|
88
|
+
protocol_validator = ProtocolValidator(self.config_data)
|
|
89
|
+
|
|
90
|
+
# Run all validations
|
|
91
|
+
self.validation_results.extend(protocol_validator.validate_required_sections())
|
|
92
|
+
self.validation_results.extend(protocol_validator.validate_protocol_requirements())
|
|
93
|
+
self.validation_results.extend(file_validator.validate_file_existence())
|
|
94
|
+
self.validation_results.extend(security_validator.validate_security_consistency())
|
|
95
|
+
self.validation_results.extend(security_validator.validate_ssl_configuration())
|
|
96
|
+
self.validation_results.extend(security_validator.validate_roles_configuration())
|
|
97
|
+
self.validation_results.extend(security_validator.validate_proxy_registration())
|
|
98
|
+
|
|
99
|
+
# Additional validations
|
|
252
100
|
self._validate_unknown_fields()
|
|
101
|
+
self._validate_uuid_format()
|
|
253
102
|
|
|
254
103
|
return self.validation_results
|
|
255
|
-
|
|
104
|
+
|
|
256
105
|
def validate_all(self, config_data: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
|
|
257
106
|
"""
|
|
258
|
-
|
|
107
|
+
Validate all aspects of the configuration.
|
|
259
108
|
|
|
260
109
|
Args:
|
|
261
|
-
config_data: Configuration data to validate
|
|
262
|
-
|
|
110
|
+
config_data: Configuration data to validate (optional)
|
|
111
|
+
|
|
263
112
|
Returns:
|
|
264
113
|
List of validation results
|
|
265
114
|
"""
|
|
266
115
|
return self.validate_config(config_data)
|
|
267
|
-
|
|
268
|
-
def
|
|
269
|
-
"""Validate
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
"logging": self.required_sections["logging"],
|
|
274
|
-
"commands": self.required_sections["commands"]
|
|
116
|
+
|
|
117
|
+
def _validate_unknown_fields(self) -> None:
|
|
118
|
+
"""Validate for unknown configuration fields."""
|
|
119
|
+
known_sections = {
|
|
120
|
+
"server", "protocols", "security", "ssl", "auth", "roles",
|
|
121
|
+
"logging", "commands", "proxy_registration", "transport"
|
|
275
122
|
}
|
|
276
123
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if section_name not in self.config_data:
|
|
124
|
+
for section in self.config_data.keys():
|
|
125
|
+
if section not in known_sections:
|
|
280
126
|
self.validation_results.append(ValidationResult(
|
|
281
|
-
level="
|
|
282
|
-
message=f"
|
|
283
|
-
section=
|
|
284
|
-
|
|
285
|
-
continue
|
|
286
|
-
|
|
287
|
-
section_data = self.config_data[section_name]
|
|
288
|
-
for key, expected_type in required_keys.items():
|
|
289
|
-
if key not in section_data:
|
|
290
|
-
self.validation_results.append(ValidationResult(
|
|
291
|
-
level="error",
|
|
292
|
-
message=f"Required key '{key}' is missing in section '{section_name}'",
|
|
293
|
-
section=section_name,
|
|
294
|
-
key=key
|
|
295
|
-
))
|
|
296
|
-
else:
|
|
297
|
-
# Validate type
|
|
298
|
-
value = section_data[key]
|
|
299
|
-
if isinstance(expected_type, tuple):
|
|
300
|
-
if not isinstance(value, expected_type):
|
|
301
|
-
expected_names = [t.__name__ for t in expected_type]
|
|
302
|
-
self.validation_results.append(ValidationResult(
|
|
303
|
-
level="error",
|
|
304
|
-
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {' or '.join(expected_names)}, got {type(value).__name__}",
|
|
305
|
-
section=section_name,
|
|
306
|
-
key=key
|
|
307
|
-
))
|
|
308
|
-
else:
|
|
309
|
-
if not isinstance(value, expected_type):
|
|
310
|
-
self.validation_results.append(ValidationResult(
|
|
311
|
-
level="error",
|
|
312
|
-
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {expected_type.__name__}, got {type(value).__name__}",
|
|
313
|
-
section=section_name,
|
|
314
|
-
key=key
|
|
315
|
-
))
|
|
316
|
-
|
|
317
|
-
# Check conditional sections based on feature flags
|
|
318
|
-
protocol = self._get_nested_value_safe("server.protocol", "http")
|
|
319
|
-
|
|
320
|
-
for feature_name, feature_config in self.feature_flags.items():
|
|
321
|
-
enabled_key = feature_config["enabled_key"]
|
|
322
|
-
|
|
323
|
-
# Skip SSL validation for HTTP protocol
|
|
324
|
-
if feature_name in ["ssl", "transport_ssl"] and protocol not in ["https", "mtls"]:
|
|
325
|
-
continue
|
|
326
|
-
|
|
327
|
-
# Only check if the enabled key exists in the configuration
|
|
328
|
-
if not self._has_nested_key(enabled_key):
|
|
329
|
-
continue
|
|
330
|
-
|
|
331
|
-
is_enabled = self._get_nested_value_safe(enabled_key, False)
|
|
332
|
-
|
|
333
|
-
if is_enabled and feature_name in self.required_sections:
|
|
334
|
-
section_name = feature_name
|
|
335
|
-
required_keys = self.required_sections[section_name]
|
|
336
|
-
|
|
337
|
-
if section_name not in self.config_data:
|
|
338
|
-
self.validation_results.append(ValidationResult(
|
|
339
|
-
level="error",
|
|
340
|
-
message=f"Required section '{section_name}' is missing for enabled feature",
|
|
341
|
-
section=section_name
|
|
342
|
-
))
|
|
343
|
-
continue
|
|
344
|
-
|
|
345
|
-
section_data = self.config_data[section_name]
|
|
346
|
-
for key, expected_type in required_keys.items():
|
|
347
|
-
# Check if key allows None (optional)
|
|
348
|
-
is_optional = isinstance(expected_type, tuple) and type(None) in expected_type
|
|
349
|
-
|
|
350
|
-
if key not in section_data:
|
|
351
|
-
# Only report error if key is not optional
|
|
352
|
-
if not is_optional:
|
|
353
|
-
self.validation_results.append(ValidationResult(
|
|
354
|
-
level="error",
|
|
355
|
-
message=f"Required key '{key}' is missing in section '{section_name}' for enabled feature",
|
|
356
|
-
section=section_name,
|
|
357
|
-
key=key
|
|
358
|
-
))
|
|
359
|
-
else:
|
|
360
|
-
# Validate type
|
|
361
|
-
value = section_data[key]
|
|
362
|
-
if isinstance(expected_type, tuple):
|
|
363
|
-
if not isinstance(value, expected_type):
|
|
364
|
-
expected_names = [t.__name__ for t in expected_type]
|
|
365
|
-
self.validation_results.append(ValidationResult(
|
|
366
|
-
level="error",
|
|
367
|
-
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {' or '.join(expected_names)}, got {type(value).__name__}",
|
|
368
|
-
section=section_name,
|
|
369
|
-
key=key
|
|
370
|
-
))
|
|
371
|
-
else:
|
|
372
|
-
if not isinstance(value, expected_type):
|
|
373
|
-
self.validation_results.append(ValidationResult(
|
|
374
|
-
level="error",
|
|
375
|
-
message=f"Key '{key}' in section '{section_name}' has wrong type. Expected {expected_type.__name__}, got {type(value).__name__}",
|
|
376
|
-
section=section_name,
|
|
377
|
-
key=key
|
|
378
|
-
))
|
|
379
|
-
|
|
380
|
-
def _validate_feature_flags(self) -> None:
|
|
381
|
-
"""Validate feature flags and their dependencies."""
|
|
382
|
-
protocol = self._get_nested_value_safe("server.protocol", "http")
|
|
383
|
-
|
|
384
|
-
for feature_name, feature_config in self.feature_flags.items():
|
|
385
|
-
enabled_key = feature_config["enabled_key"]
|
|
386
|
-
|
|
387
|
-
# Skip SSL validation for HTTP protocol
|
|
388
|
-
if feature_name in ["ssl", "transport_ssl"] and protocol not in ["https", "mtls"]:
|
|
389
|
-
continue
|
|
390
|
-
|
|
391
|
-
# Check if the enabled key exists in the configuration
|
|
392
|
-
if not self._has_nested_key(enabled_key):
|
|
393
|
-
# Skip validation if the feature flag key doesn't exist
|
|
394
|
-
continue
|
|
395
|
-
|
|
396
|
-
is_enabled = self._get_nested_value_safe(enabled_key, False)
|
|
397
|
-
|
|
398
|
-
if is_enabled:
|
|
399
|
-
# Check dependencies
|
|
400
|
-
for dependency in feature_config["dependencies"]:
|
|
401
|
-
if not self._has_nested_key(dependency):
|
|
402
|
-
self.validation_results.append(ValidationResult(
|
|
403
|
-
level="error",
|
|
404
|
-
message=f"Feature '{feature_name}' is enabled but required dependency '{dependency}' is missing",
|
|
405
|
-
section=feature_name,
|
|
406
|
-
key=dependency
|
|
407
|
-
))
|
|
408
|
-
|
|
409
|
-
# Check required files
|
|
410
|
-
for file_key in feature_config["required_files"]:
|
|
411
|
-
if self._has_nested_key(file_key):
|
|
412
|
-
file_path = self._get_nested_value(file_key)
|
|
413
|
-
if file_path and not os.path.exists(file_path):
|
|
414
|
-
self.validation_results.append(ValidationResult(
|
|
415
|
-
level="error",
|
|
416
|
-
message=f"Feature '{feature_name}' is enabled but required file '{file_path}' does not exist",
|
|
417
|
-
section=feature_name,
|
|
418
|
-
key=file_key
|
|
419
|
-
))
|
|
420
|
-
else:
|
|
421
|
-
# Feature is disabled - check that optional files are not required
|
|
422
|
-
for file_key in feature_config["optional_files"]:
|
|
423
|
-
if self._has_nested_key(file_key):
|
|
424
|
-
file_path = self._get_nested_value(file_key)
|
|
425
|
-
if file_path and not os.path.exists(file_path):
|
|
426
|
-
self.validation_results.append(ValidationResult(
|
|
427
|
-
level="warning",
|
|
428
|
-
message=f"Optional file '{file_path}' for disabled feature '{feature_name}' does not exist",
|
|
429
|
-
section=feature_name,
|
|
430
|
-
key=file_key,
|
|
431
|
-
suggestion="This is not an error since the feature is disabled"
|
|
432
|
-
))
|
|
433
|
-
|
|
434
|
-
def _validate_protocol_requirements(self) -> None:
|
|
435
|
-
"""Validate protocol-specific requirements."""
|
|
436
|
-
protocol = self._get_nested_value_safe("server.protocol", "http")
|
|
437
|
-
|
|
438
|
-
# Check mTLS protocol requirements
|
|
439
|
-
if protocol == "mtls":
|
|
440
|
-
# mTLS requires HTTPS protocol
|
|
441
|
-
if not self._has_nested_key("ssl.enabled"):
|
|
442
|
-
self.validation_results.append(ValidationResult(
|
|
443
|
-
level="error",
|
|
444
|
-
message="mTLS protocol requires SSL configuration",
|
|
445
|
-
section="ssl",
|
|
446
|
-
key="enabled"
|
|
127
|
+
level="warning",
|
|
128
|
+
message=f"Unknown configuration section: {section}",
|
|
129
|
+
section=section,
|
|
130
|
+
suggestion="Check if this section is needed or if it's a typo"
|
|
447
131
|
))
|
|
448
|
-
else:
|
|
449
|
-
ssl_enabled = self._get_nested_value_safe("ssl.enabled", False)
|
|
450
|
-
if not ssl_enabled:
|
|
451
|
-
self.validation_results.append(ValidationResult(
|
|
452
|
-
level="error",
|
|
453
|
-
message="mTLS protocol requires SSL to be enabled",
|
|
454
|
-
section="ssl",
|
|
455
|
-
key="enabled"
|
|
456
|
-
))
|
|
457
|
-
|
|
458
|
-
if protocol not in self.protocol_requirements:
|
|
459
|
-
self.validation_results.append(ValidationResult(
|
|
460
|
-
level="error",
|
|
461
|
-
message=f"Unsupported protocol: {protocol}",
|
|
462
|
-
section="server",
|
|
463
|
-
key="protocol"
|
|
464
|
-
))
|
|
465
|
-
return
|
|
466
132
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if requirements["ssl_enabled"]:
|
|
471
|
-
# Only check SSL if ssl section exists
|
|
472
|
-
if self._has_nested_key("ssl.enabled"):
|
|
473
|
-
ssl_enabled = self._get_nested_value_safe("ssl.enabled", False)
|
|
474
|
-
if not ssl_enabled:
|
|
475
|
-
self.validation_results.append(ValidationResult(
|
|
476
|
-
level="error",
|
|
477
|
-
message=f"Protocol '{protocol}' requires SSL to be enabled",
|
|
478
|
-
section="ssl",
|
|
479
|
-
key="enabled"
|
|
480
|
-
))
|
|
481
|
-
|
|
482
|
-
# Check required SSL files
|
|
483
|
-
for file_key in requirements["required_files"]:
|
|
484
|
-
if self._has_nested_key(file_key):
|
|
485
|
-
file_path = self._get_nested_value(file_key)
|
|
486
|
-
if not file_path:
|
|
487
|
-
self.validation_results.append(ValidationResult(
|
|
488
|
-
level="error",
|
|
489
|
-
message=f"Protocol '{protocol}' requires {file_key} to be specified",
|
|
490
|
-
section="ssl",
|
|
491
|
-
key=file_key.split(".")[-1]
|
|
492
|
-
))
|
|
493
|
-
elif not os.path.exists(file_path):
|
|
494
|
-
self.validation_results.append(ValidationResult(
|
|
495
|
-
level="error",
|
|
496
|
-
message=f"Protocol '{protocol}' requires file '{file_path}' to exist",
|
|
497
|
-
section="ssl",
|
|
498
|
-
key=file_key.split(".")[-1]
|
|
499
|
-
))
|
|
500
|
-
|
|
501
|
-
# Check client verification requirements
|
|
502
|
-
if requirements["client_verification"]:
|
|
503
|
-
verify_client = self._get_nested_value_safe("transport.ssl.verify_client", False)
|
|
504
|
-
if not verify_client:
|
|
505
|
-
self.validation_results.append(ValidationResult(
|
|
506
|
-
level="error",
|
|
507
|
-
message=f"Protocol '{protocol}' requires client certificate verification",
|
|
508
|
-
section="transport.ssl",
|
|
509
|
-
key="verify_client"
|
|
510
|
-
))
|
|
511
|
-
|
|
512
|
-
def _validate_file_existence(self) -> None:
|
|
513
|
-
"""Validate that all referenced files exist."""
|
|
514
|
-
protocol = self._get_nested_value_safe("server.protocol", "http")
|
|
515
|
-
|
|
516
|
-
file_keys = [
|
|
517
|
-
"logging.log_dir",
|
|
518
|
-
"commands.commands_directory",
|
|
519
|
-
"commands.catalog_directory",
|
|
520
|
-
"commands.custom_commands_path",
|
|
521
|
-
"security.roles_file",
|
|
522
|
-
"roles.config_file"
|
|
523
|
-
]
|
|
524
|
-
|
|
525
|
-
# Only add SSL-related files if protocol requires SSL
|
|
526
|
-
if protocol in ["https", "mtls"]:
|
|
527
|
-
file_keys.extend([
|
|
528
|
-
"ssl.cert_file",
|
|
529
|
-
"ssl.key_file",
|
|
530
|
-
"ssl.ca_cert",
|
|
531
|
-
"transport.ssl.cert_file",
|
|
532
|
-
"transport.ssl.key_file",
|
|
533
|
-
"transport.ssl.ca_cert"
|
|
534
|
-
])
|
|
535
|
-
|
|
536
|
-
# Only add proxy registration files if proxy registration is enabled
|
|
537
|
-
if self._has_nested_key("proxy_registration.enabled"):
|
|
538
|
-
proxy_enabled = self._get_nested_value_safe("proxy_registration.enabled", False)
|
|
539
|
-
if proxy_enabled:
|
|
540
|
-
file_keys.extend([
|
|
541
|
-
"proxy_registration.certificate.cert_file",
|
|
542
|
-
"proxy_registration.certificate.key_file"
|
|
543
|
-
])
|
|
544
|
-
|
|
545
|
-
for file_key in file_keys:
|
|
546
|
-
# Skip if the key doesn't exist in the configuration
|
|
547
|
-
if not self._has_nested_key(file_key):
|
|
548
|
-
continue
|
|
549
|
-
|
|
550
|
-
file_path = self._get_nested_value_safe(file_key)
|
|
551
|
-
if file_path and not os.path.exists(file_path):
|
|
552
|
-
# Check if this is a required file based on enabled features
|
|
553
|
-
is_required = self._is_file_required_for_enabled_features(file_key)
|
|
554
|
-
level = "error" if is_required else "warning"
|
|
555
|
-
|
|
556
|
-
self.validation_results.append(ValidationResult(
|
|
557
|
-
level=level,
|
|
558
|
-
message=f"Referenced file '{file_path}' does not exist",
|
|
559
|
-
section=file_key.split(".")[0],
|
|
560
|
-
key=file_key.split(".")[-1],
|
|
561
|
-
suggestion="Create the file or update the configuration" if is_required else "This file is optional"
|
|
562
|
-
))
|
|
563
|
-
|
|
564
|
-
def _validate_security_consistency(self) -> None:
|
|
565
|
-
"""Validate security configuration consistency."""
|
|
566
|
-
security_enabled = self._get_nested_value_safe("security.enabled", False)
|
|
133
|
+
def _validate_uuid_format(self) -> None:
|
|
134
|
+
"""Validate UUID format in configuration."""
|
|
135
|
+
uuid_fields = ["server.server_id", "proxy_registration.server_id"]
|
|
567
136
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
roles = self._get_nested_value_safe("security.roles", {})
|
|
572
|
-
roles_file = self._get_nested_value_safe("security.roles_file")
|
|
573
|
-
|
|
574
|
-
has_tokens = bool(tokens and any(tokens.values()))
|
|
575
|
-
has_roles = bool(roles and any(roles.values()))
|
|
576
|
-
has_roles_file = bool(roles_file and os.path.exists(roles_file))
|
|
577
|
-
|
|
578
|
-
if not (has_tokens or has_roles or has_roles_file):
|
|
579
|
-
self.validation_results.append(ValidationResult(
|
|
580
|
-
level="warning",
|
|
581
|
-
message="Security is enabled but no authentication methods are configured",
|
|
582
|
-
section="security",
|
|
583
|
-
suggestion="Configure tokens, roles, or roles_file in the security section"
|
|
584
|
-
))
|
|
585
|
-
|
|
586
|
-
# Check roles consistency
|
|
587
|
-
if has_roles and has_roles_file:
|
|
137
|
+
for field in uuid_fields:
|
|
138
|
+
value = self._get_nested_value_safe(field)
|
|
139
|
+
if value and not self._is_valid_uuid4(str(value)):
|
|
588
140
|
self.validation_results.append(ValidationResult(
|
|
589
141
|
level="warning",
|
|
590
|
-
message="
|
|
591
|
-
section="
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def _validate_proxy_registration(self) -> None:
|
|
596
|
-
"""Validate proxy registration configuration."""
|
|
597
|
-
registration_enabled = self._get_nested_value_safe("proxy_registration.enabled", False)
|
|
598
|
-
|
|
599
|
-
if registration_enabled:
|
|
600
|
-
if not self._has_nested_key("proxy_registration.proxy_url"):
|
|
601
|
-
self.validation_results.append(ValidationResult(
|
|
602
|
-
level="error",
|
|
603
|
-
message="Proxy registration is enabled but proxy_url is not specified",
|
|
604
|
-
section="proxy_registration",
|
|
605
|
-
key="proxy_url"
|
|
142
|
+
message=f"Invalid UUID format in {field}: {value}",
|
|
143
|
+
section=field.split(".")[0],
|
|
144
|
+
key=field.split(".")[1],
|
|
145
|
+
suggestion="Use a valid UUID4 format"
|
|
606
146
|
))
|
|
607
|
-
else:
|
|
608
|
-
proxy_url = self._get_nested_value("proxy_registration.proxy_url")
|
|
609
|
-
if not proxy_url:
|
|
610
|
-
self.validation_results.append(ValidationResult(
|
|
611
|
-
level="error",
|
|
612
|
-
message="Proxy registration is enabled but proxy_url is not specified",
|
|
613
|
-
section="proxy_registration",
|
|
614
|
-
key="proxy_url"
|
|
615
|
-
))
|
|
616
|
-
|
|
617
|
-
# Check authentication method consistency
|
|
618
|
-
auth_method = self._get_nested_value_safe("proxy_registration.auth_method", "none")
|
|
619
|
-
if auth_method != "none":
|
|
620
|
-
if auth_method == "certificate":
|
|
621
|
-
if not self._has_nested_key("proxy_registration.certificate.cert_file"):
|
|
622
|
-
self.validation_results.append(ValidationResult(
|
|
623
|
-
level="error",
|
|
624
|
-
message="Certificate authentication requires cert_file",
|
|
625
|
-
section="proxy_registration.certificate",
|
|
626
|
-
key="cert_file"
|
|
627
|
-
))
|
|
628
|
-
else:
|
|
629
|
-
cert_file = self._get_nested_value("proxy_registration.certificate.cert_file")
|
|
630
|
-
|
|
631
|
-
if not self._has_nested_key("proxy_registration.certificate.key_file"):
|
|
632
|
-
self.validation_results.append(ValidationResult(
|
|
633
|
-
level="error",
|
|
634
|
-
message="Certificate authentication requires key_file",
|
|
635
|
-
section="proxy_registration.certificate",
|
|
636
|
-
key="key_file"
|
|
637
|
-
))
|
|
638
|
-
else:
|
|
639
|
-
key_file = self._get_nested_value("proxy_registration.certificate.key_file")
|
|
640
|
-
|
|
641
|
-
if not cert_file or not key_file:
|
|
642
|
-
self.validation_results.append(ValidationResult(
|
|
643
|
-
level="error",
|
|
644
|
-
message="Certificate authentication is enabled but certificate files are not specified",
|
|
645
|
-
section="proxy_registration",
|
|
646
|
-
key="certificate"
|
|
647
|
-
))
|
|
648
|
-
elif auth_method == "token":
|
|
649
|
-
if not self._has_nested_key("proxy_registration.token.token"):
|
|
650
|
-
self.validation_results.append(ValidationResult(
|
|
651
|
-
level="error",
|
|
652
|
-
message="Token authentication requires token",
|
|
653
|
-
section="proxy_registration.token",
|
|
654
|
-
key="token"
|
|
655
|
-
))
|
|
656
|
-
else:
|
|
657
|
-
token = self._get_nested_value("proxy_registration.token.token")
|
|
658
|
-
if not token:
|
|
659
|
-
self.validation_results.append(ValidationResult(
|
|
660
|
-
level="error",
|
|
661
|
-
message="Token authentication is enabled but token is not specified",
|
|
662
|
-
section="proxy_registration",
|
|
663
|
-
key="token"
|
|
664
|
-
))
|
|
665
|
-
|
|
666
|
-
def _validate_ssl_configuration(self) -> None:
|
|
667
|
-
"""Validate SSL configuration with detailed certificate validation."""
|
|
668
|
-
# Only validate SSL if the protocol requires it
|
|
669
|
-
protocol = self._get_nested_value_safe("server.protocol", "http")
|
|
670
|
-
if protocol not in ["https", "mtls"]:
|
|
671
|
-
return
|
|
672
147
|
|
|
673
|
-
|
|
674
|
-
if
|
|
675
|
-
|
|
148
|
+
def _is_valid_uuid4(self, uuid_str: str) -> bool:
|
|
149
|
+
"""Check if string is a valid UUID4."""
|
|
150
|
+
import re
|
|
151
|
+
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
|
152
|
+
return bool(re.match(uuid_pattern, uuid_str, re.IGNORECASE))
|
|
676
153
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
# Initialize variables
|
|
681
|
-
cert_file = None
|
|
682
|
-
key_file = None
|
|
683
|
-
ca_cert = None
|
|
684
|
-
|
|
685
|
-
if not self._has_nested_key("ssl.cert_file"):
|
|
686
|
-
self.validation_results.append(ValidationResult(
|
|
687
|
-
level="error",
|
|
688
|
-
message="SSL is enabled but cert_file is not specified",
|
|
689
|
-
section="ssl",
|
|
690
|
-
key="cert_file"
|
|
691
|
-
))
|
|
692
|
-
else:
|
|
693
|
-
cert_file = self._get_nested_value("ssl.cert_file")
|
|
694
|
-
|
|
695
|
-
if not self._has_nested_key("ssl.key_file"):
|
|
696
|
-
self.validation_results.append(ValidationResult(
|
|
697
|
-
level="error",
|
|
698
|
-
message="SSL is enabled but key_file is not specified",
|
|
699
|
-
section="ssl",
|
|
700
|
-
key="key_file"
|
|
701
|
-
))
|
|
702
|
-
else:
|
|
703
|
-
key_file = self._get_nested_value("ssl.key_file")
|
|
704
|
-
|
|
705
|
-
# CA cert is optional for HTTPS but may be required for mTLS with client verification
|
|
706
|
-
if self._has_nested_key("ssl.ca_cert"):
|
|
707
|
-
ca_cert = self._get_nested_value_safe("ssl.ca_cert")
|
|
708
|
-
|
|
709
|
-
# Check certificate file
|
|
710
|
-
if not cert_file:
|
|
711
|
-
self.validation_results.append(ValidationResult(
|
|
712
|
-
level="error",
|
|
713
|
-
message="SSL is enabled but cert_file is not specified",
|
|
714
|
-
section="ssl",
|
|
715
|
-
key="cert_file"
|
|
716
|
-
))
|
|
717
|
-
elif not os.path.exists(cert_file):
|
|
718
|
-
self.validation_results.append(ValidationResult(
|
|
719
|
-
level="error",
|
|
720
|
-
message=f"SSL certificate file '{cert_file}' does not exist",
|
|
721
|
-
section="ssl",
|
|
722
|
-
key="cert_file"
|
|
723
|
-
))
|
|
724
|
-
else:
|
|
725
|
-
# Validate certificate file
|
|
726
|
-
self._validate_certificate_file(cert_file, "ssl", "cert_file")
|
|
727
|
-
|
|
728
|
-
# Check key file
|
|
729
|
-
if not key_file:
|
|
730
|
-
self.validation_results.append(ValidationResult(
|
|
731
|
-
level="error",
|
|
732
|
-
message="SSL is enabled but key_file is not specified",
|
|
733
|
-
section="ssl",
|
|
734
|
-
key="key_file"
|
|
735
|
-
))
|
|
736
|
-
elif not os.path.exists(key_file):
|
|
737
|
-
self.validation_results.append(ValidationResult(
|
|
738
|
-
level="error",
|
|
739
|
-
message=f"SSL key file '{key_file}' does not exist",
|
|
740
|
-
section="ssl",
|
|
741
|
-
key="key_file"
|
|
742
|
-
))
|
|
743
|
-
else:
|
|
744
|
-
# Validate key file
|
|
745
|
-
self._validate_key_file(key_file, "ssl", "key_file")
|
|
746
|
-
|
|
747
|
-
# Check CA certificate if specified
|
|
748
|
-
if ca_cert:
|
|
749
|
-
if not os.path.exists(ca_cert):
|
|
750
|
-
self.validation_results.append(ValidationResult(
|
|
751
|
-
level="error",
|
|
752
|
-
message=f"SSL CA certificate file '{ca_cert}' does not exist",
|
|
753
|
-
section="ssl",
|
|
754
|
-
key="ca_cert"
|
|
755
|
-
))
|
|
756
|
-
else:
|
|
757
|
-
# Validate CA certificate
|
|
758
|
-
self._validate_ca_certificate_file(ca_cert, "ssl", "ca_cert")
|
|
759
|
-
|
|
760
|
-
# Validate certificate-key pair if both exist
|
|
761
|
-
if cert_file and key_file and os.path.exists(cert_file) and os.path.exists(key_file):
|
|
762
|
-
self._validate_certificate_key_pair(cert_file, key_file, ca_cert, "ssl")
|
|
763
|
-
|
|
764
|
-
def _validate_roles_configuration(self) -> None:
|
|
765
|
-
"""Validate roles configuration."""
|
|
766
|
-
roles_enabled = self._get_nested_value_safe("roles.enabled", False)
|
|
767
|
-
|
|
768
|
-
if roles_enabled:
|
|
769
|
-
if not self._has_nested_key("roles.config_file"):
|
|
770
|
-
self.validation_results.append(ValidationResult(
|
|
771
|
-
level="error",
|
|
772
|
-
message="Roles are enabled but config_file is not specified",
|
|
773
|
-
section="roles",
|
|
774
|
-
key="config_file"
|
|
775
|
-
))
|
|
776
|
-
else:
|
|
777
|
-
config_file = self._get_nested_value("roles.config_file")
|
|
778
|
-
if not config_file:
|
|
779
|
-
self.validation_results.append(ValidationResult(
|
|
780
|
-
level="error",
|
|
781
|
-
message="Roles are enabled but config_file is not specified",
|
|
782
|
-
section="roles",
|
|
783
|
-
key="config_file"
|
|
784
|
-
))
|
|
785
|
-
elif not os.path.exists(config_file):
|
|
786
|
-
self.validation_results.append(ValidationResult(
|
|
787
|
-
level="error",
|
|
788
|
-
message=f"Roles config file '{config_file}' does not exist",
|
|
789
|
-
section="roles",
|
|
790
|
-
key="config_file"
|
|
791
|
-
))
|
|
792
|
-
|
|
793
|
-
def _get_nested_value(self, key: str) -> Any:
|
|
794
|
-
"""Get value from nested dictionary using dot notation. Raises exception if key not found."""
|
|
795
|
-
keys = key.split(".")
|
|
154
|
+
def _get_nested_value_safe(self, key: str, default: Any = None) -> Any:
|
|
155
|
+
"""Safely get a nested value from configuration."""
|
|
156
|
+
keys = key.split('.')
|
|
796
157
|
value = self.config_data
|
|
797
158
|
|
|
798
159
|
for k in keys:
|
|
799
160
|
if isinstance(value, dict) and k in value:
|
|
800
161
|
value = value[k]
|
|
801
162
|
else:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
return value
|
|
805
|
-
|
|
806
|
-
def _has_nested_key(self, key: str) -> bool:
|
|
807
|
-
"""Check if nested key exists in configuration."""
|
|
808
|
-
keys = key.split(".")
|
|
809
|
-
value = self.config_data
|
|
163
|
+
return default
|
|
810
164
|
|
|
811
|
-
|
|
812
|
-
if isinstance(value, dict) and k in value:
|
|
813
|
-
value = value[k]
|
|
814
|
-
else:
|
|
815
|
-
return False
|
|
816
|
-
|
|
817
|
-
return True
|
|
818
|
-
|
|
819
|
-
def _get_nested_value_safe(self, key: str, default: Any = None) -> Any:
|
|
820
|
-
"""Get value from nested dictionary using dot notation with fallback."""
|
|
821
|
-
try:
|
|
822
|
-
return self._get_nested_value(key)
|
|
823
|
-
except MissingConfigKeyError:
|
|
824
|
-
return default
|
|
825
|
-
|
|
826
|
-
def _is_file_required_for_enabled_features(self, file_key: str) -> bool:
|
|
827
|
-
"""Check if file is required based on enabled features."""
|
|
828
|
-
for feature_name, feature_config in self.feature_flags.items():
|
|
829
|
-
enabled_key = feature_config["enabled_key"]
|
|
830
|
-
is_enabled = self._get_nested_value_safe(enabled_key, False)
|
|
831
|
-
|
|
832
|
-
if is_enabled and file_key in feature_config["required_files"]:
|
|
833
|
-
return True
|
|
834
|
-
|
|
835
|
-
return False
|
|
836
|
-
|
|
837
|
-
def _validate_certificate_file(self, cert_file: str, section: str, key: str) -> None:
|
|
838
|
-
"""Validate certificate file format and content."""
|
|
839
|
-
try:
|
|
840
|
-
import cryptography
|
|
841
|
-
from cryptography import x509
|
|
842
|
-
from cryptography.hazmat.primitives import serialization
|
|
843
|
-
|
|
844
|
-
with open(cert_file, 'rb') as f:
|
|
845
|
-
cert_data = f.read()
|
|
846
|
-
|
|
847
|
-
# Try to parse as PEM
|
|
848
|
-
try:
|
|
849
|
-
cert = x509.load_pem_x509_certificate(cert_data)
|
|
850
|
-
except Exception:
|
|
851
|
-
# Try to parse as DER
|
|
852
|
-
try:
|
|
853
|
-
cert = x509.load_der_x509_certificate(cert_data)
|
|
854
|
-
except Exception as e:
|
|
855
|
-
self.validation_results.append(ValidationResult(
|
|
856
|
-
level="error",
|
|
857
|
-
message=f"Certificate file '{cert_file}' is not a valid PEM or DER certificate: {e}",
|
|
858
|
-
section=section,
|
|
859
|
-
key=key
|
|
860
|
-
))
|
|
861
|
-
return
|
|
862
|
-
|
|
863
|
-
# Check certificate expiration
|
|
864
|
-
now = datetime.now(timezone.utc)
|
|
865
|
-
not_after = cert.not_valid_after_utc
|
|
866
|
-
|
|
867
|
-
if now > not_after:
|
|
868
|
-
self.validation_results.append(ValidationResult(
|
|
869
|
-
level="error",
|
|
870
|
-
message=f"Certificate '{cert_file}' has expired on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
871
|
-
section=section,
|
|
872
|
-
key=key
|
|
873
|
-
))
|
|
874
|
-
elif (not_after - now).days < 30:
|
|
875
|
-
self.validation_results.append(ValidationResult(
|
|
876
|
-
level="warning",
|
|
877
|
-
message=f"Certificate '{cert_file}' expires in {(not_after - now).days} days on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
878
|
-
section=section,
|
|
879
|
-
key=key,
|
|
880
|
-
suggestion="Consider renewing the certificate"
|
|
881
|
-
))
|
|
882
|
-
|
|
883
|
-
# Check if certificate is self-signed
|
|
884
|
-
issuer = cert.issuer
|
|
885
|
-
subject = cert.subject
|
|
886
|
-
|
|
887
|
-
if issuer == subject:
|
|
888
|
-
self.validation_results.append(ValidationResult(
|
|
889
|
-
level="warning",
|
|
890
|
-
message=f"Certificate '{cert_file}' is self-signed",
|
|
891
|
-
section=section,
|
|
892
|
-
key=key,
|
|
893
|
-
suggestion="Consider using a certificate from a trusted CA for production"
|
|
894
|
-
))
|
|
895
|
-
|
|
896
|
-
# Check certificate key usage
|
|
897
|
-
try:
|
|
898
|
-
key_usage = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.KEY_USAGE)
|
|
899
|
-
if not key_usage.value.digital_signature:
|
|
900
|
-
self.validation_results.append(ValidationResult(
|
|
901
|
-
level="warning",
|
|
902
|
-
message=f"Certificate '{cert_file}' does not have digital signature key usage",
|
|
903
|
-
section=section,
|
|
904
|
-
key=key,
|
|
905
|
-
suggestion="Ensure the certificate supports digital signature for SSL/TLS"
|
|
906
|
-
))
|
|
907
|
-
except x509.ExtensionNotFound:
|
|
908
|
-
pass # Key usage extension not present, which is sometimes OK
|
|
909
|
-
|
|
910
|
-
except ImportError:
|
|
911
|
-
# cryptography library not available, do basic validation
|
|
912
|
-
self.validation_results.append(ValidationResult(
|
|
913
|
-
level="warning",
|
|
914
|
-
message=f"Cannot validate certificate '{cert_file}' - cryptography library not available",
|
|
915
|
-
section=section,
|
|
916
|
-
key=key,
|
|
917
|
-
suggestion="Install cryptography library for detailed certificate validation"
|
|
918
|
-
))
|
|
919
|
-
except Exception as e:
|
|
920
|
-
self.validation_results.append(ValidationResult(
|
|
921
|
-
level="error",
|
|
922
|
-
message=f"Error validating certificate '{cert_file}': {e}",
|
|
923
|
-
section=section,
|
|
924
|
-
key=key
|
|
925
|
-
))
|
|
926
|
-
|
|
927
|
-
def _validate_key_file(self, key_file: str, section: str, key: str) -> None:
|
|
928
|
-
"""Validate private key file format and content."""
|
|
929
|
-
try:
|
|
930
|
-
import cryptography
|
|
931
|
-
from cryptography.hazmat.primitives import serialization
|
|
932
|
-
|
|
933
|
-
with open(key_file, 'rb') as f:
|
|
934
|
-
key_data = f.read()
|
|
935
|
-
|
|
936
|
-
# Try to parse as PEM
|
|
937
|
-
try:
|
|
938
|
-
private_key = serialization.load_pem_private_key(
|
|
939
|
-
key_data,
|
|
940
|
-
password=None
|
|
941
|
-
)
|
|
942
|
-
except Exception:
|
|
943
|
-
# Try to parse as DER
|
|
944
|
-
try:
|
|
945
|
-
private_key = serialization.load_der_private_key(
|
|
946
|
-
key_data,
|
|
947
|
-
password=None
|
|
948
|
-
)
|
|
949
|
-
except Exception as e:
|
|
950
|
-
self.validation_results.append(ValidationResult(
|
|
951
|
-
level="error",
|
|
952
|
-
message=f"Key file '{key_file}' is not a valid PEM or DER private key: {e}",
|
|
953
|
-
section=section,
|
|
954
|
-
key=key
|
|
955
|
-
))
|
|
956
|
-
return
|
|
957
|
-
|
|
958
|
-
# Check key size
|
|
959
|
-
if hasattr(private_key, 'key_size'):
|
|
960
|
-
if private_key.key_size < 2048:
|
|
961
|
-
self.validation_results.append(ValidationResult(
|
|
962
|
-
level="warning",
|
|
963
|
-
message=f"Private key '{key_file}' has key size {private_key.key_size} bits, which is below recommended 2048 bits",
|
|
964
|
-
section=section,
|
|
965
|
-
key=key,
|
|
966
|
-
suggestion="Consider using a key with at least 2048 bits for better security"
|
|
967
|
-
))
|
|
968
|
-
|
|
969
|
-
except ImportError:
|
|
970
|
-
# cryptography library not available, do basic validation
|
|
971
|
-
self.validation_results.append(ValidationResult(
|
|
972
|
-
level="warning",
|
|
973
|
-
message=f"Cannot validate private key '{key_file}' - cryptography library not available",
|
|
974
|
-
section=section,
|
|
975
|
-
key=key,
|
|
976
|
-
suggestion="Install cryptography library for detailed key validation"
|
|
977
|
-
))
|
|
978
|
-
except Exception as e:
|
|
979
|
-
self.validation_results.append(ValidationResult(
|
|
980
|
-
level="error",
|
|
981
|
-
message=f"Error validating private key '{key_file}': {e}",
|
|
982
|
-
section=section,
|
|
983
|
-
key=key
|
|
984
|
-
))
|
|
985
|
-
|
|
986
|
-
def _validate_ca_certificate_file(self, ca_cert_file: str, section: str, key: str) -> None:
|
|
987
|
-
"""Validate CA certificate file."""
|
|
988
|
-
try:
|
|
989
|
-
import cryptography
|
|
990
|
-
from cryptography import x509
|
|
991
|
-
|
|
992
|
-
with open(ca_cert_file, 'rb') as f:
|
|
993
|
-
ca_data = f.read()
|
|
994
|
-
|
|
995
|
-
# Try to parse as PEM
|
|
996
|
-
try:
|
|
997
|
-
ca_cert = x509.load_pem_x509_certificate(ca_data)
|
|
998
|
-
except Exception:
|
|
999
|
-
# Try to parse as DER
|
|
1000
|
-
try:
|
|
1001
|
-
ca_cert = x509.load_der_x509_certificate(ca_data)
|
|
1002
|
-
except Exception as e:
|
|
1003
|
-
self.validation_results.append(ValidationResult(
|
|
1004
|
-
level="error",
|
|
1005
|
-
message=f"CA certificate file '{ca_cert_file}' is not a valid PEM or DER certificate: {e}",
|
|
1006
|
-
section=section,
|
|
1007
|
-
key=key
|
|
1008
|
-
))
|
|
1009
|
-
return
|
|
1010
|
-
|
|
1011
|
-
# Check CA certificate expiration
|
|
1012
|
-
now = datetime.now(timezone.utc)
|
|
1013
|
-
not_after = ca_cert.not_valid_after.replace(tzinfo=timezone.utc)
|
|
1014
|
-
|
|
1015
|
-
if now > not_after:
|
|
1016
|
-
self.validation_results.append(ValidationResult(
|
|
1017
|
-
level="error",
|
|
1018
|
-
message=f"CA certificate '{ca_cert_file}' has expired on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
1019
|
-
section=section,
|
|
1020
|
-
key=key
|
|
1021
|
-
))
|
|
1022
|
-
elif (not_after - now).days < 30:
|
|
1023
|
-
self.validation_results.append(ValidationResult(
|
|
1024
|
-
level="warning",
|
|
1025
|
-
message=f"CA certificate '{ca_cert_file}' expires in {(not_after - now).days} days on {not_after.strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
1026
|
-
section=section,
|
|
1027
|
-
key=key,
|
|
1028
|
-
suggestion="Consider renewing the CA certificate"
|
|
1029
|
-
))
|
|
1030
|
-
|
|
1031
|
-
# Check if CA certificate has CA basic constraint
|
|
1032
|
-
try:
|
|
1033
|
-
basic_constraints = ca_cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.BASIC_CONSTRAINTS)
|
|
1034
|
-
if not basic_constraints.value.ca:
|
|
1035
|
-
self.validation_results.append(ValidationResult(
|
|
1036
|
-
level="warning",
|
|
1037
|
-
message=f"CA certificate '{ca_cert_file}' does not have CA basic constraint set",
|
|
1038
|
-
section=section,
|
|
1039
|
-
key=key,
|
|
1040
|
-
suggestion="Ensure the certificate is marked as a CA certificate"
|
|
1041
|
-
))
|
|
1042
|
-
except x509.ExtensionNotFound:
|
|
1043
|
-
self.validation_results.append(ValidationResult(
|
|
1044
|
-
level="warning",
|
|
1045
|
-
message=f"CA certificate '{ca_cert_file}' does not have basic constraints extension",
|
|
1046
|
-
section=section,
|
|
1047
|
-
key=key,
|
|
1048
|
-
suggestion="Consider using a proper CA certificate with basic constraints"
|
|
1049
|
-
))
|
|
1050
|
-
|
|
1051
|
-
except ImportError:
|
|
1052
|
-
# cryptography library not available
|
|
1053
|
-
self.validation_results.append(ValidationResult(
|
|
1054
|
-
level="warning",
|
|
1055
|
-
message=f"Cannot validate CA certificate '{ca_cert_file}' - cryptography library not available",
|
|
1056
|
-
section=section,
|
|
1057
|
-
key=key,
|
|
1058
|
-
suggestion="Install cryptography library for detailed CA certificate validation"
|
|
1059
|
-
))
|
|
1060
|
-
except Exception as e:
|
|
1061
|
-
self.validation_results.append(ValidationResult(
|
|
1062
|
-
level="error",
|
|
1063
|
-
message=f"Error validating CA certificate '{ca_cert_file}': {e}",
|
|
1064
|
-
section=section,
|
|
1065
|
-
key=key
|
|
1066
|
-
))
|
|
1067
|
-
|
|
1068
|
-
def _validate_certificate_key_pair(self, cert_file: str, key_file: str, ca_cert_file: Optional[str], section: str) -> None:
|
|
1069
|
-
"""Validate that certificate and key are a matching pair."""
|
|
1070
|
-
try:
|
|
1071
|
-
import cryptography
|
|
1072
|
-
from cryptography import x509
|
|
1073
|
-
from cryptography.hazmat.primitives import serialization, hashes
|
|
1074
|
-
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
|
1075
|
-
|
|
1076
|
-
# Load certificate
|
|
1077
|
-
with open(cert_file, 'rb') as f:
|
|
1078
|
-
cert_data = f.read()
|
|
1079
|
-
|
|
1080
|
-
try:
|
|
1081
|
-
cert = x509.load_pem_x509_certificate(cert_data)
|
|
1082
|
-
except Exception:
|
|
1083
|
-
cert = x509.load_der_x509_certificate(cert_data)
|
|
1084
|
-
|
|
1085
|
-
# Load private key
|
|
1086
|
-
with open(key_file, 'rb') as f:
|
|
1087
|
-
key_data = f.read()
|
|
1088
|
-
|
|
1089
|
-
try:
|
|
1090
|
-
private_key = serialization.load_pem_private_key(key_data, password=None)
|
|
1091
|
-
except Exception:
|
|
1092
|
-
private_key = serialization.load_der_private_key(key_data, password=None)
|
|
1093
|
-
|
|
1094
|
-
# Check if certificate public key matches private key
|
|
1095
|
-
cert_public_key = cert.public_key()
|
|
1096
|
-
|
|
1097
|
-
# For RSA keys, compare modulus
|
|
1098
|
-
if isinstance(cert_public_key, rsa.RSAPublicKey) and isinstance(private_key, rsa.RSAPrivateKey):
|
|
1099
|
-
if cert_public_key.public_numbers().n != private_key.public_key().public_numbers().n:
|
|
1100
|
-
self.validation_results.append(ValidationResult(
|
|
1101
|
-
level="error",
|
|
1102
|
-
message=f"Certificate '{cert_file}' and private key '{key_file}' do not match",
|
|
1103
|
-
section=section,
|
|
1104
|
-
key="cert_file",
|
|
1105
|
-
suggestion="Ensure the certificate and private key are from the same key pair"
|
|
1106
|
-
))
|
|
1107
|
-
return
|
|
165
|
+
return value
|
|
1108
166
|
|
|
1109
|
-
# If CA certificate is provided, validate certificate chain
|
|
1110
|
-
if ca_cert_file and os.path.exists(ca_cert_file):
|
|
1111
|
-
self._validate_certificate_chain(cert_file, ca_cert_file, section)
|
|
1112
|
-
|
|
1113
|
-
except ImportError:
|
|
1114
|
-
# cryptography library not available
|
|
1115
|
-
self.validation_results.append(ValidationResult(
|
|
1116
|
-
level="warning",
|
|
1117
|
-
message=f"Cannot validate certificate-key pair - cryptography library not available",
|
|
1118
|
-
section=section,
|
|
1119
|
-
key="cert_file",
|
|
1120
|
-
suggestion="Install cryptography library for detailed certificate validation"
|
|
1121
|
-
))
|
|
1122
|
-
except Exception as e:
|
|
1123
|
-
self.validation_results.append(ValidationResult(
|
|
1124
|
-
level="error",
|
|
1125
|
-
message=f"Error validating certificate-key pair: {e}",
|
|
1126
|
-
section=section,
|
|
1127
|
-
key="cert_file"
|
|
1128
|
-
))
|
|
1129
|
-
|
|
1130
|
-
def _validate_certificate_chain(self, cert_file: str, ca_cert_file: str, section: str) -> None:
|
|
1131
|
-
"""Validate certificate chain against CA certificate."""
|
|
1132
|
-
try:
|
|
1133
|
-
import cryptography
|
|
1134
|
-
from cryptography import x509
|
|
1135
|
-
from cryptography.hazmat.primitives import hashes
|
|
1136
|
-
from cryptography.hazmat.primitives.asymmetric import padding
|
|
1137
|
-
|
|
1138
|
-
# Load certificate
|
|
1139
|
-
with open(cert_file, 'rb') as f:
|
|
1140
|
-
cert_data = f.read()
|
|
1141
|
-
|
|
1142
|
-
try:
|
|
1143
|
-
cert = x509.load_pem_x509_certificate(cert_data)
|
|
1144
|
-
except Exception:
|
|
1145
|
-
cert = x509.load_der_x509_certificate(cert_data)
|
|
1146
|
-
|
|
1147
|
-
# Load CA certificate
|
|
1148
|
-
with open(ca_cert_file, 'rb') as f:
|
|
1149
|
-
ca_data = f.read()
|
|
1150
|
-
|
|
1151
|
-
try:
|
|
1152
|
-
ca_cert = x509.load_pem_x509_certificate(ca_data)
|
|
1153
|
-
except Exception:
|
|
1154
|
-
ca_cert = x509.load_der_x509_certificate(ca_data)
|
|
1155
|
-
|
|
1156
|
-
# Verify certificate signature with CA
|
|
1157
|
-
try:
|
|
1158
|
-
ca_cert.public_key().verify(
|
|
1159
|
-
cert.signature,
|
|
1160
|
-
cert.tbs_certificate_bytes,
|
|
1161
|
-
padding.PKCS1v15(),
|
|
1162
|
-
cert.signature_algorithm_oid._name
|
|
1163
|
-
)
|
|
1164
|
-
except Exception as e:
|
|
1165
|
-
self.validation_results.append(ValidationResult(
|
|
1166
|
-
level="error",
|
|
1167
|
-
message=f"Certificate '{cert_file}' is not signed by CA certificate '{ca_cert_file}': {e}",
|
|
1168
|
-
section=section,
|
|
1169
|
-
key="cert_file",
|
|
1170
|
-
suggestion="Ensure the certificate is properly signed by the CA"
|
|
1171
|
-
))
|
|
1172
|
-
return
|
|
1173
|
-
|
|
1174
|
-
# Check if certificate issuer matches CA subject
|
|
1175
|
-
if cert.issuer != ca_cert.subject:
|
|
1176
|
-
self.validation_results.append(ValidationResult(
|
|
1177
|
-
level="warning",
|
|
1178
|
-
message=f"Certificate issuer '{cert.issuer}' does not match CA subject '{ca_cert.subject}'",
|
|
1179
|
-
section=section,
|
|
1180
|
-
key="cert_file",
|
|
1181
|
-
suggestion="Verify that the certificate is issued by the correct CA"
|
|
1182
|
-
))
|
|
1183
|
-
|
|
1184
|
-
except ImportError:
|
|
1185
|
-
# cryptography library not available
|
|
1186
|
-
self.validation_results.append(ValidationResult(
|
|
1187
|
-
level="warning",
|
|
1188
|
-
message=f"Cannot validate certificate chain - cryptography library not available",
|
|
1189
|
-
section=section,
|
|
1190
|
-
key="cert_file",
|
|
1191
|
-
suggestion="Install cryptography library for detailed certificate chain validation"
|
|
1192
|
-
))
|
|
1193
|
-
except Exception as e:
|
|
1194
|
-
self.validation_results.append(ValidationResult(
|
|
1195
|
-
level="error",
|
|
1196
|
-
message=f"Error validating certificate chain: {e}",
|
|
1197
|
-
section=section,
|
|
1198
|
-
key="cert_file"
|
|
1199
|
-
))
|
|
1200
|
-
|
|
1201
|
-
def _validate_uuid_format(self) -> None:
|
|
1202
|
-
"""Validate UUID4 format in configuration."""
|
|
1203
|
-
|
|
1204
|
-
# Check root level UUID
|
|
1205
|
-
if "uuid" in self.config_data:
|
|
1206
|
-
uuid_value = self.config_data["uuid"]
|
|
1207
|
-
if not self._is_valid_uuid4(uuid_value):
|
|
1208
|
-
self.validation_results.append(ValidationResult(
|
|
1209
|
-
level="error",
|
|
1210
|
-
message=f"Invalid UUID4 format: '{uuid_value}'. Expected format: xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx",
|
|
1211
|
-
section="uuid",
|
|
1212
|
-
key="uuid"
|
|
1213
|
-
))
|
|
1214
|
-
|
|
1215
|
-
# Check proxy_registration UUID if it exists
|
|
1216
|
-
if self._has_nested_key("proxy_registration.uuid"):
|
|
1217
|
-
uuid_value = self._get_nested_value("proxy_registration.uuid")
|
|
1218
|
-
if not self._is_valid_uuid4(uuid_value):
|
|
1219
|
-
self.validation_results.append(ValidationResult(
|
|
1220
|
-
level="error",
|
|
1221
|
-
message=f"Invalid UUID4 format in proxy_registration: '{uuid_value}'. Expected format: xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx",
|
|
1222
|
-
section="proxy_registration",
|
|
1223
|
-
key="uuid"
|
|
1224
|
-
))
|
|
1225
|
-
|
|
1226
|
-
def _is_valid_uuid4(self, uuid_str: str) -> bool:
|
|
1227
|
-
"""Check if string is a valid UUID4."""
|
|
1228
|
-
if not isinstance(uuid_str, str):
|
|
1229
|
-
return False
|
|
1230
|
-
|
|
1231
|
-
uuid4_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
|
1232
|
-
return bool(re.match(uuid4_pattern, uuid_str, re.IGNORECASE))
|
|
1233
|
-
|
|
1234
|
-
def _validate_unknown_fields(self) -> None:
|
|
1235
|
-
"""Validate that no unknown fields are present in configuration."""
|
|
1236
|
-
# Define all known sections and their allowed fields
|
|
1237
|
-
known_sections = {
|
|
1238
|
-
"server": {"host", "port", "protocol", "debug", "log_level"},
|
|
1239
|
-
"logging": {"level", "file", "log_dir", "log_file", "error_log_file", "access_log_file",
|
|
1240
|
-
"max_file_size", "backup_count", "format", "date_format", "console_output", "file_output"},
|
|
1241
|
-
"commands": {"auto_discovery", "commands_directory", "catalog_directory", "plugin_servers",
|
|
1242
|
-
"auto_install_dependencies", "enabled_commands", "disabled_commands", "custom_commands_path"},
|
|
1243
|
-
"transport": {"type", "port", "verify_client", "chk_hostname", "ssl"},
|
|
1244
|
-
"proxy_registration": {"enabled", "proxy_url", "server_id", "server_name", "description", "version",
|
|
1245
|
-
"registration_timeout", "retry_attempts", "retry_delay", "auto_register_on_startup",
|
|
1246
|
-
"auto_unregister_on_shutdown", "uuid", "heartbeat"},
|
|
1247
|
-
"debug": {"enabled", "level"},
|
|
1248
|
-
"security": {"enabled", "tokens", "roles", "roles_file"},
|
|
1249
|
-
"roles": {"enabled", "config_file", "default_policy", "auto_load", "validation_enabled"},
|
|
1250
|
-
"ssl": {"enabled", "cert_file", "key_file", "ca_cert"},
|
|
1251
|
-
"uuid": set() # Root level UUID
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
# Check for unknown root level fields
|
|
1255
|
-
for field in self.config_data:
|
|
1256
|
-
if field not in known_sections:
|
|
1257
|
-
self.validation_results.append(ValidationResult(
|
|
1258
|
-
level="warning",
|
|
1259
|
-
message=f"Unknown field '{field}' at root level",
|
|
1260
|
-
section=field,
|
|
1261
|
-
suggestion="Check if this field is needed or if it's a typo"
|
|
1262
|
-
))
|
|
1263
|
-
|
|
1264
|
-
# Check for unknown fields in known sections
|
|
1265
|
-
for section_name, allowed_fields in known_sections.items():
|
|
1266
|
-
if section_name in self.config_data:
|
|
1267
|
-
section_data = self.config_data[section_name]
|
|
1268
|
-
if isinstance(section_data, dict):
|
|
1269
|
-
for field in section_data:
|
|
1270
|
-
if field not in allowed_fields:
|
|
1271
|
-
self.validation_results.append(ValidationResult(
|
|
1272
|
-
level="warning",
|
|
1273
|
-
message=f"Unknown field '{field}' in section '{section_name}'",
|
|
1274
|
-
section=section_name,
|
|
1275
|
-
key=field,
|
|
1276
|
-
suggestion="Check if this field is needed or if it's a typo"
|
|
1277
|
-
))
|
|
1278
|
-
|
|
1279
167
|
def get_validation_summary(self) -> Dict[str, Any]:
|
|
1280
|
-
"""
|
|
168
|
+
"""
|
|
169
|
+
Get a summary of validation results.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary with validation summary
|
|
173
|
+
"""
|
|
1281
174
|
error_count = sum(1 for r in self.validation_results if r.level == "error")
|
|
1282
175
|
warning_count = sum(1 for r in self.validation_results if r.level == "warning")
|
|
1283
176
|
info_count = sum(1 for r in self.validation_results if r.level == "info")
|
|
@@ -1289,65 +182,30 @@ class ConfigValidator:
|
|
|
1289
182
|
"info": info_count,
|
|
1290
183
|
"is_valid": error_count == 0
|
|
1291
184
|
}
|
|
1292
|
-
|
|
185
|
+
|
|
1293
186
|
def print_validation_report(self) -> None:
|
|
1294
|
-
"""Print
|
|
187
|
+
"""Print a formatted validation report."""
|
|
1295
188
|
summary = self.get_validation_summary()
|
|
1296
189
|
|
|
1297
|
-
print("
|
|
1298
|
-
print("
|
|
1299
|
-
print("=" * 60)
|
|
190
|
+
print(f"\\nš Configuration Validation Report")
|
|
191
|
+
print(f"{'=' * 40}")
|
|
1300
192
|
print(f"Total issues: {summary['total_issues']}")
|
|
1301
193
|
print(f"Errors: {summary['errors']}")
|
|
1302
194
|
print(f"Warnings: {summary['warnings']}")
|
|
1303
195
|
print(f"Info: {summary['info']}")
|
|
1304
|
-
print(f"
|
|
1305
|
-
print("=" * 60)
|
|
196
|
+
print(f"Valid: {'ā
Yes' if summary['is_valid'] else 'ā No'}")
|
|
1306
197
|
|
|
1307
198
|
if self.validation_results:
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
print(f"{level_symbol} [{result.level.value.upper()}] {result.message}")
|
|
1317
|
-
if location:
|
|
1318
|
-
print(f" Location: {location}")
|
|
199
|
+
print(f"\\nš Issues:")
|
|
200
|
+
for i, result in enumerate(self.validation_results, 1):
|
|
201
|
+
level_icon = {"error": "ā", "warning": "ā ļø", "info": "ā¹ļø"}[result.level]
|
|
202
|
+
print(f"{i:2d}. {level_icon} {result.message}")
|
|
203
|
+
if result.section:
|
|
204
|
+
print(f" Section: {result.section}")
|
|
205
|
+
if result.key:
|
|
206
|
+
print(f" Key: {result.key}")
|
|
1319
207
|
if result.suggestion:
|
|
1320
|
-
print(f"
|
|
208
|
+
print(f" Suggestion: {result.suggestion}")
|
|
1321
209
|
print()
|
|
1322
|
-
else:
|
|
1323
|
-
print("ā
No issues found in configuration!")
|
|
1324
210
|
|
|
1325
211
|
|
|
1326
|
-
def validate_config_file(config_path: str) -> bool:
|
|
1327
|
-
"""
|
|
1328
|
-
Validate a configuration file.
|
|
1329
|
-
|
|
1330
|
-
Args:
|
|
1331
|
-
config_path: Path to configuration file
|
|
1332
|
-
|
|
1333
|
-
Returns:
|
|
1334
|
-
True if configuration is valid, False otherwise
|
|
1335
|
-
"""
|
|
1336
|
-
validator = ConfigValidator(config_path)
|
|
1337
|
-
validator.load_config()
|
|
1338
|
-
validator.validate_config()
|
|
1339
|
-
|
|
1340
|
-
validator.print_validation_report()
|
|
1341
|
-
return validator.get_validation_summary()["is_valid"]
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
if __name__ == "__main__":
|
|
1345
|
-
import sys
|
|
1346
|
-
|
|
1347
|
-
if len(sys.argv) != 2:
|
|
1348
|
-
print("Usage: python config_validator.py <config_file>")
|
|
1349
|
-
sys.exit(1)
|
|
1350
|
-
|
|
1351
|
-
config_file = sys.argv[1]
|
|
1352
|
-
is_valid = validate_config_file(config_file)
|
|
1353
|
-
sys.exit(0 if is_valid else 1)
|