mcp-proxy-adapter 4.1.1__py3-none-any.whl ā 6.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_proxy_adapter/__main__.py +32 -0
- mcp_proxy_adapter/api/app.py +290 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +38 -32
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +243 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
- mcp_proxy_adapter/commands/__init__.py +19 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +66 -32
- mcp_proxy_adapter/commands/builtin_commands.py +95 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +711 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +8 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +394 -14
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +1045 -0
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +19 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +385 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +522 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +370 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/security_integration.py +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +234 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +93 -0
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-4.1.1.dist-info ā mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter/examples/README.md +0 -124
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -35
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -217
- mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info ā mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info ā mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,782 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Security Test Client for MCP Proxy Adapter
|
4
|
+
This client tests various security configurations including:
|
5
|
+
- Basic HTTP
|
6
|
+
- HTTP + Token authentication
|
7
|
+
- HTTPS
|
8
|
+
- HTTPS + Token authentication
|
9
|
+
- mTLS with certificate authentication
|
10
|
+
Author: Vasiliy Zdanovskiy
|
11
|
+
email: vasilyvz@gmail.com
|
12
|
+
"""
|
13
|
+
import asyncio
|
14
|
+
import json
|
15
|
+
import os
|
16
|
+
import ssl
|
17
|
+
import sys
|
18
|
+
import time
|
19
|
+
from pathlib import Path
|
20
|
+
from typing import Dict, List, Optional, Any
|
21
|
+
from dataclasses import dataclass
|
22
|
+
import aiohttp
|
23
|
+
from aiohttp import ClientSession, ClientTimeout, TCPConnector
|
24
|
+
|
25
|
+
# Add project root to path for imports
|
26
|
+
project_root = Path(__file__).parent.parent.parent
|
27
|
+
current_dir = Path(__file__).parent
|
28
|
+
parent_dir = current_dir.parent
|
29
|
+
sys.path.insert(0, str(project_root))
|
30
|
+
sys.path.insert(0, str(current_dir))
|
31
|
+
sys.path.insert(0, str(parent_dir))
|
32
|
+
|
33
|
+
# Import mcp_security_framework components
|
34
|
+
try:
|
35
|
+
from mcp_security_framework import SSLManager, CertificateManager
|
36
|
+
from mcp_security_framework.schemas.config import SSLConfig
|
37
|
+
_MCP_SECURITY_AVAILABLE = True
|
38
|
+
print("ā
mcp_security_framework available")
|
39
|
+
except ImportError:
|
40
|
+
_MCP_SECURITY_AVAILABLE = False
|
41
|
+
print("ā ļø mcp_security_framework not available, falling back to standard SSL")
|
42
|
+
|
43
|
+
# Import cryptography components
|
44
|
+
try:
|
45
|
+
from cryptography import x509
|
46
|
+
from cryptography.hazmat.primitives import serialization
|
47
|
+
_CRYPTOGRAPHY_AVAILABLE = True
|
48
|
+
print("ā
cryptography available")
|
49
|
+
except ImportError:
|
50
|
+
_CRYPTOGRAPHY_AVAILABLE = False
|
51
|
+
print("ā ļø cryptography not available, SSL validation will be limited")
|
52
|
+
@dataclass
|
53
|
+
class TestResult:
|
54
|
+
"""Test result data class."""
|
55
|
+
test_name: str
|
56
|
+
server_url: str
|
57
|
+
auth_type: str
|
58
|
+
success: bool
|
59
|
+
status_code: Optional[int] = None
|
60
|
+
response_data: Optional[Dict] = None
|
61
|
+
error_message: Optional[str] = None
|
62
|
+
duration: float = 0.0
|
63
|
+
class SecurityTestClient:
|
64
|
+
"""Security test client for comprehensive testing."""
|
65
|
+
def __init__(self, base_url: str = "http://localhost:8000"):
|
66
|
+
"""Initialize security test client."""
|
67
|
+
self.base_url = base_url
|
68
|
+
self.session: Optional[ClientSession] = None
|
69
|
+
|
70
|
+
# Initialize security managers if available
|
71
|
+
self.ssl_manager = None
|
72
|
+
self.cert_manager = None
|
73
|
+
self._security_available = _MCP_SECURITY_AVAILABLE
|
74
|
+
self._crypto_available = _CRYPTOGRAPHY_AVAILABLE
|
75
|
+
|
76
|
+
if self._security_available:
|
77
|
+
try:
|
78
|
+
# Initialize SSL manager with default config
|
79
|
+
ssl_config = SSLConfig(
|
80
|
+
enabled=True,
|
81
|
+
cert_file=None,
|
82
|
+
key_file=None,
|
83
|
+
ca_cert_file=None,
|
84
|
+
verify_mode="CERT_NONE", # For testing
|
85
|
+
min_tls_version="TLSv1.2"
|
86
|
+
)
|
87
|
+
self.ssl_manager = SSLManager(ssl_config)
|
88
|
+
print("ā
SSL Manager initialized with mcp_security_framework")
|
89
|
+
except Exception as e:
|
90
|
+
print(f"ā ļø Failed to initialize SSL Manager: {e}")
|
91
|
+
self._security_available = False
|
92
|
+
|
93
|
+
if not self._security_available:
|
94
|
+
print("ā¹ļø Using standard SSL library for testing")
|
95
|
+
self.ssl_manager = None
|
96
|
+
self.test_results: List[TestResult] = []
|
97
|
+
# Test tokens
|
98
|
+
self.test_tokens = {
|
99
|
+
"admin": "test-token-123",
|
100
|
+
"user": "user-token-456",
|
101
|
+
"readonly": "readonly-token-123",
|
102
|
+
"guest": "guest-token-123",
|
103
|
+
"proxy": "proxy-token-123",
|
104
|
+
"invalid": "invalid-token-999"
|
105
|
+
}
|
106
|
+
# Test certificates
|
107
|
+
self.test_certificates = {
|
108
|
+
"admin": {
|
109
|
+
"cert": "mcp_proxy_adapter/examples/certs/admin_cert.pem",
|
110
|
+
"key": "mcp_proxy_adapter/examples/certs/admin_key.pem"
|
111
|
+
},
|
112
|
+
"user": {
|
113
|
+
"cert": "mcp_proxy_adapter/examples/certs/user_cert.pem",
|
114
|
+
"key": "mcp_proxy_adapter/examples/certs/user_key.pem"
|
115
|
+
},
|
116
|
+
"readonly": {
|
117
|
+
"cert": "mcp_proxy_adapter/examples/certs/readonly_cert.pem",
|
118
|
+
"key": "mcp_proxy_adapter/examples/certs/readonly_key.pem"
|
119
|
+
}
|
120
|
+
}
|
121
|
+
async def __aenter__(self):
|
122
|
+
"""Async context manager entry."""
|
123
|
+
timeout = ClientTimeout(total=30)
|
124
|
+
# Create SSL context for HTTPS connections
|
125
|
+
ssl_context = self.create_ssl_context()
|
126
|
+
connector = TCPConnector(ssl=ssl_context)
|
127
|
+
self.session = ClientSession(timeout=timeout, connector=connector)
|
128
|
+
return self
|
129
|
+
def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
|
130
|
+
"""Create SSL context for mTLS connections."""
|
131
|
+
if self.ssl_manager and self._security_available:
|
132
|
+
try:
|
133
|
+
# Use mcp_security_framework for mTLS
|
134
|
+
cert_file = "./certs/user_cert.pem"
|
135
|
+
key_file = "./certs/user_key.pem"
|
136
|
+
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
137
|
+
|
138
|
+
return self.ssl_manager.create_client_context(
|
139
|
+
ca_cert_file=ca_cert_file if os.path.exists(ca_cert_file) else None,
|
140
|
+
client_cert_file=cert_file if os.path.exists(cert_file) else None,
|
141
|
+
client_key_file=key_file if os.path.exists(key_file) else None,
|
142
|
+
verify_mode="CERT_NONE", # For testing
|
143
|
+
min_version="TLSv1.2"
|
144
|
+
)
|
145
|
+
except Exception as e:
|
146
|
+
print(f"ā ļø Failed to create mTLS context with mcp_security_framework: {e}")
|
147
|
+
print("ā¹ļø Falling back to standard SSL")
|
148
|
+
|
149
|
+
# Fallback to standard SSL
|
150
|
+
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
151
|
+
# For mTLS testing - client needs to present certificate to server
|
152
|
+
ssl_context.check_hostname = False
|
153
|
+
ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
|
154
|
+
# Load client certificate and key for mTLS
|
155
|
+
cert_file = "./certs/user_cert.pem"
|
156
|
+
key_file = "./certs/user_key.pem"
|
157
|
+
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
158
|
+
if os.path.exists(cert_file) and os.path.exists(key_file):
|
159
|
+
ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
160
|
+
if os.path.exists(ca_cert_file):
|
161
|
+
ssl_context.load_verify_locations(cafile=ca_cert_file)
|
162
|
+
return ssl_context
|
163
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
164
|
+
"""Async context manager exit."""
|
165
|
+
if self.session:
|
166
|
+
await self.session.close()
|
167
|
+
def create_ssl_context(self, cert_file: Optional[str] = None,
|
168
|
+
key_file: Optional[str] = None,
|
169
|
+
ca_cert_file: Optional[str] = None) -> ssl.SSLContext:
|
170
|
+
"""Create SSL context for client."""
|
171
|
+
if self.ssl_manager and self._security_available:
|
172
|
+
try:
|
173
|
+
# Use mcp_security_framework for SSL context creation
|
174
|
+
return self.ssl_manager.create_client_context(
|
175
|
+
ca_cert_file=ca_cert_file if ca_cert_file and os.path.exists(ca_cert_file) else None,
|
176
|
+
client_cert_file=cert_file if cert_file and os.path.exists(cert_file) else None,
|
177
|
+
client_key_file=key_file if key_file and os.path.exists(key_file) else None,
|
178
|
+
verify_mode="CERT_NONE", # For testing
|
179
|
+
min_version="TLSv1.2"
|
180
|
+
)
|
181
|
+
except Exception as e:
|
182
|
+
print(f"ā ļø Failed to create SSL context with mcp_security_framework: {e}")
|
183
|
+
print("ā¹ļø Falling back to standard SSL")
|
184
|
+
|
185
|
+
# Fallback to standard SSL
|
186
|
+
ssl_context = ssl.create_default_context()
|
187
|
+
# For testing with self-signed certificates
|
188
|
+
ssl_context.check_hostname = False
|
189
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
190
|
+
if cert_file and key_file:
|
191
|
+
ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
192
|
+
if ca_cert_file:
|
193
|
+
ssl_context.load_verify_locations(cafile=ca_cert_file)
|
194
|
+
# For testing, still don't verify
|
195
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
196
|
+
return ssl_context
|
197
|
+
def create_auth_headers(self, auth_type: str, **kwargs) -> Dict[str, str]:
|
198
|
+
"""Create authentication headers."""
|
199
|
+
headers = {"Content-Type": "application/json"}
|
200
|
+
if auth_type == "api_key":
|
201
|
+
token = kwargs.get("token", "test-token-123")
|
202
|
+
# Provide both common header styles to maximize compatibility
|
203
|
+
headers["X-API-Key"] = token
|
204
|
+
headers["Authorization"] = f"Bearer {token}"
|
205
|
+
elif auth_type == "basic":
|
206
|
+
username = kwargs.get("username", "admin")
|
207
|
+
password = kwargs.get("password", "password")
|
208
|
+
import base64
|
209
|
+
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
210
|
+
headers["Authorization"] = f"Basic {credentials}"
|
211
|
+
elif auth_type == "certificate":
|
212
|
+
# For mTLS, we need to use client certificates
|
213
|
+
# This is handled by SSL context, not headers
|
214
|
+
pass
|
215
|
+
return headers
|
216
|
+
async def test_health_check(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
217
|
+
"""Test health check endpoint."""
|
218
|
+
start_time = time.time()
|
219
|
+
test_name = f"Health Check ({auth_type})"
|
220
|
+
try:
|
221
|
+
headers = self.create_auth_headers(auth_type, **kwargs)
|
222
|
+
async with self.session.get(f"{server_url}/health", headers=headers) as response:
|
223
|
+
duration = time.time() - start_time
|
224
|
+
if response.status == 200:
|
225
|
+
data = await response.json()
|
226
|
+
return TestResult(
|
227
|
+
test_name=test_name,
|
228
|
+
server_url=server_url,
|
229
|
+
auth_type=auth_type,
|
230
|
+
success=True,
|
231
|
+
status_code=response.status,
|
232
|
+
response_data=data,
|
233
|
+
duration=duration
|
234
|
+
)
|
235
|
+
else:
|
236
|
+
error_text = await response.text()
|
237
|
+
return TestResult(
|
238
|
+
test_name=test_name,
|
239
|
+
server_url=server_url,
|
240
|
+
auth_type=auth_type,
|
241
|
+
success=False,
|
242
|
+
status_code=response.status,
|
243
|
+
error_message=f"Health check failed: {error_text}",
|
244
|
+
duration=duration
|
245
|
+
)
|
246
|
+
except Exception as e:
|
247
|
+
duration = time.time() - start_time
|
248
|
+
return TestResult(
|
249
|
+
test_name=test_name,
|
250
|
+
server_url=server_url,
|
251
|
+
auth_type=auth_type,
|
252
|
+
success=False,
|
253
|
+
error_message=f"Health check error: {str(e)}",
|
254
|
+
duration=duration
|
255
|
+
)
|
256
|
+
async def test_echo_command(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
257
|
+
"""Test echo command."""
|
258
|
+
start_time = time.time()
|
259
|
+
test_name = f"Echo Command ({auth_type})"
|
260
|
+
try:
|
261
|
+
headers = self.create_auth_headers(auth_type, **kwargs)
|
262
|
+
data = {
|
263
|
+
"jsonrpc": "2.0",
|
264
|
+
"method": "echo",
|
265
|
+
"params": {
|
266
|
+
"message": "Hello from security test client!"
|
267
|
+
},
|
268
|
+
"id": 1
|
269
|
+
}
|
270
|
+
async with self.session.post(f"{server_url}/cmd",
|
271
|
+
headers=headers,
|
272
|
+
json=data) as response:
|
273
|
+
duration = time.time() - start_time
|
274
|
+
if response.status == 200:
|
275
|
+
data = await response.json()
|
276
|
+
return TestResult(
|
277
|
+
test_name=test_name,
|
278
|
+
server_url=server_url,
|
279
|
+
auth_type=auth_type,
|
280
|
+
success=True,
|
281
|
+
status_code=response.status,
|
282
|
+
response_data=data,
|
283
|
+
duration=duration
|
284
|
+
)
|
285
|
+
else:
|
286
|
+
error_text = await response.text()
|
287
|
+
return TestResult(
|
288
|
+
test_name=test_name,
|
289
|
+
server_url=server_url,
|
290
|
+
auth_type=auth_type,
|
291
|
+
success=False,
|
292
|
+
status_code=response.status,
|
293
|
+
error_message=f"Echo command failed: {error_text}",
|
294
|
+
duration=duration
|
295
|
+
)
|
296
|
+
except Exception as e:
|
297
|
+
duration = time.time() - start_time
|
298
|
+
return TestResult(
|
299
|
+
test_name=test_name,
|
300
|
+
server_url=server_url,
|
301
|
+
auth_type=auth_type,
|
302
|
+
success=False,
|
303
|
+
error_message=f"Echo command error: {str(e)}",
|
304
|
+
duration=duration
|
305
|
+
)
|
306
|
+
async def test_security_command(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
307
|
+
"""Test security command."""
|
308
|
+
start_time = time.time()
|
309
|
+
test_name = f"Security Command ({auth_type})"
|
310
|
+
try:
|
311
|
+
headers = self.create_auth_headers(auth_type, **kwargs)
|
312
|
+
data = {
|
313
|
+
"jsonrpc": "2.0",
|
314
|
+
"method": "health",
|
315
|
+
"params": {},
|
316
|
+
"id": 2
|
317
|
+
}
|
318
|
+
async with self.session.post(f"{server_url}/cmd",
|
319
|
+
headers=headers,
|
320
|
+
json=data) as response:
|
321
|
+
duration = time.time() - start_time
|
322
|
+
if response.status == 200:
|
323
|
+
data = await response.json()
|
324
|
+
return TestResult(
|
325
|
+
test_name=test_name,
|
326
|
+
server_url=server_url,
|
327
|
+
auth_type=auth_type,
|
328
|
+
success=True,
|
329
|
+
status_code=response.status,
|
330
|
+
response_data=data,
|
331
|
+
duration=duration
|
332
|
+
)
|
333
|
+
else:
|
334
|
+
error_text = await response.text()
|
335
|
+
return TestResult(
|
336
|
+
test_name=test_name,
|
337
|
+
server_url=server_url,
|
338
|
+
auth_type=auth_type,
|
339
|
+
success=False,
|
340
|
+
status_code=response.status,
|
341
|
+
error_message=f"Security command failed: {error_text}",
|
342
|
+
duration=duration
|
343
|
+
)
|
344
|
+
except Exception as e:
|
345
|
+
duration = time.time() - start_time
|
346
|
+
return TestResult(
|
347
|
+
test_name=test_name,
|
348
|
+
server_url=server_url,
|
349
|
+
auth_type=auth_type,
|
350
|
+
success=False,
|
351
|
+
error_message=f"Security command error: {str(e)}",
|
352
|
+
duration=duration
|
353
|
+
)
|
354
|
+
async def test_health(self) -> TestResult:
|
355
|
+
"""Test health endpoint."""
|
356
|
+
return await self.test_health_check(self.base_url, "none")
|
357
|
+
async def test_command_execution(self) -> TestResult:
|
358
|
+
"""Test command execution."""
|
359
|
+
return await self.test_echo_command(self.base_url, "none")
|
360
|
+
async def test_authentication(self) -> TestResult:
|
361
|
+
"""Test authentication."""
|
362
|
+
if "api_key" in self.auth_methods:
|
363
|
+
# Use first available API key
|
364
|
+
api_key = next(iter(self.api_keys.keys()), "test-token-123")
|
365
|
+
return await self.test_echo_command(self.base_url, "api_key", token=api_key)
|
366
|
+
elif "certificate" in self.auth_methods:
|
367
|
+
# For certificate auth, test with client certificate
|
368
|
+
return await self.test_echo_command(self.base_url, "certificate")
|
369
|
+
else:
|
370
|
+
return TestResult(
|
371
|
+
test_name="Authentication Test",
|
372
|
+
server_url=self.base_url,
|
373
|
+
auth_type="none",
|
374
|
+
success=False,
|
375
|
+
error_message="No authentication method available"
|
376
|
+
)
|
377
|
+
async def test_negative_authentication(self) -> TestResult:
|
378
|
+
"""Test negative authentication (should fail)."""
|
379
|
+
return await self.test_echo_command(self.base_url, "api_key", token="invalid-token")
|
380
|
+
async def test_no_auth_required(self) -> TestResult:
|
381
|
+
"""Test that no authentication is required."""
|
382
|
+
return await self.test_echo_command(self.base_url, "none")
|
383
|
+
async def test_negative_auth(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
384
|
+
"""Test negative authentication scenarios."""
|
385
|
+
start_time = time.time()
|
386
|
+
test_name = f"Negative Auth ({auth_type})"
|
387
|
+
try:
|
388
|
+
if auth_type == "certificate":
|
389
|
+
# For mTLS, test with invalid/expired certificate or no certificate
|
390
|
+
import aiohttp
|
391
|
+
from aiohttp import ClientTimeout, TCPConnector
|
392
|
+
import ssl
|
393
|
+
|
394
|
+
# Create SSL context with wrong certificate (should be rejected)
|
395
|
+
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
396
|
+
ssl_context.check_hostname = False
|
397
|
+
# Don't load any client certificate - this should cause rejection
|
398
|
+
# Load CA certificate for server verification
|
399
|
+
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
400
|
+
if os.path.exists(ca_cert_file):
|
401
|
+
ssl_context.load_verify_locations(cafile=ca_cert_file)
|
402
|
+
ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
|
403
|
+
|
404
|
+
connector = TCPConnector(ssl=ssl_context)
|
405
|
+
timeout = ClientTimeout(total=10) # Shorter timeout
|
406
|
+
|
407
|
+
try:
|
408
|
+
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as temp_session:
|
409
|
+
data = {
|
410
|
+
"jsonrpc": "2.0",
|
411
|
+
"method": "echo",
|
412
|
+
"params": {"message": "Should fail without certificate"},
|
413
|
+
"id": 3
|
414
|
+
}
|
415
|
+
async with temp_session.post(f"{server_url}/cmd", json=data) as response:
|
416
|
+
duration = time.time() - start_time
|
417
|
+
# If we get here, the server accepted the connection without proper certificate
|
418
|
+
# This is actually a security issue - server should reject
|
419
|
+
return TestResult(
|
420
|
+
test_name=test_name,
|
421
|
+
server_url=server_url,
|
422
|
+
auth_type=auth_type,
|
423
|
+
success=False,
|
424
|
+
status_code=response.status,
|
425
|
+
error_message=f"SECURITY ISSUE: mTLS server accepted connection without client certificate (status: {response.status})",
|
426
|
+
duration=duration
|
427
|
+
)
|
428
|
+
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, asyncio.TimeoutError) as e:
|
429
|
+
# This is expected - server should reject connections without proper certificate
|
430
|
+
duration = time.time() - start_time
|
431
|
+
return TestResult(
|
432
|
+
test_name=test_name,
|
433
|
+
server_url=server_url,
|
434
|
+
auth_type=auth_type,
|
435
|
+
success=True,
|
436
|
+
status_code=0,
|
437
|
+
response_data={"expected": "connection_rejected", "error": str(e)},
|
438
|
+
duration=duration
|
439
|
+
)
|
440
|
+
else:
|
441
|
+
# For other auth types, use invalid token
|
442
|
+
headers = self.create_auth_headers("api_key", token="invalid-token-999")
|
443
|
+
data = {
|
444
|
+
"jsonrpc": "2.0",
|
445
|
+
"method": "echo",
|
446
|
+
"params": {"message": "Should fail"},
|
447
|
+
"id": 3
|
448
|
+
}
|
449
|
+
async with self.session.post(f"{server_url}/cmd",
|
450
|
+
headers=headers,
|
451
|
+
json=data) as response:
|
452
|
+
duration = time.time() - start_time
|
453
|
+
# Expect 401 only when auth is enforced
|
454
|
+
expects_auth = auth_type in ("api_key", "certificate", "basic")
|
455
|
+
if expects_auth and response.status == 401:
|
456
|
+
return TestResult(
|
457
|
+
test_name=test_name,
|
458
|
+
server_url=server_url,
|
459
|
+
auth_type=auth_type,
|
460
|
+
success=True,
|
461
|
+
status_code=response.status,
|
462
|
+
response_data={"expected": "authentication_failure"},
|
463
|
+
duration=duration
|
464
|
+
)
|
465
|
+
elif not expects_auth and response.status == 200:
|
466
|
+
# Security disabled: negative auth should not fail
|
467
|
+
return TestResult(
|
468
|
+
test_name=test_name,
|
469
|
+
server_url=server_url,
|
470
|
+
auth_type=auth_type,
|
471
|
+
success=True,
|
472
|
+
status_code=response.status,
|
473
|
+
response_data={"expected": "no_auth_required"},
|
474
|
+
duration=duration
|
475
|
+
)
|
476
|
+
else:
|
477
|
+
return TestResult(
|
478
|
+
test_name=test_name,
|
479
|
+
server_url=server_url,
|
480
|
+
auth_type=auth_type,
|
481
|
+
success=False,
|
482
|
+
status_code=response.status,
|
483
|
+
error_message=f"Unexpected status for negative auth: {response.status}",
|
484
|
+
duration=duration
|
485
|
+
)
|
486
|
+
except Exception as e:
|
487
|
+
duration = time.time() - start_time
|
488
|
+
return TestResult(
|
489
|
+
test_name=test_name,
|
490
|
+
server_url=server_url,
|
491
|
+
auth_type=auth_type,
|
492
|
+
success=False,
|
493
|
+
error_message=f"Negative auth error: {str(e)}",
|
494
|
+
duration=duration
|
495
|
+
)
|
496
|
+
async def test_role_based_access(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
497
|
+
"""Test role-based access control."""
|
498
|
+
start_time = time.time()
|
499
|
+
test_name = f"Role-Based Access ({auth_type})"
|
500
|
+
try:
|
501
|
+
# Test with different roles
|
502
|
+
role = kwargs.get("role", "user")
|
503
|
+
token = self.test_tokens.get(role, self.test_tokens["user"])
|
504
|
+
headers = self.create_auth_headers("api_key", token=token)
|
505
|
+
data = {
|
506
|
+
"jsonrpc": "2.0",
|
507
|
+
"method": "echo",
|
508
|
+
"params": {"message": f"Testing {role} role"},
|
509
|
+
"id": 4
|
510
|
+
}
|
511
|
+
async with self.session.post(f"{server_url}/cmd",
|
512
|
+
headers=headers,
|
513
|
+
json=data) as response:
|
514
|
+
duration = time.time() - start_time
|
515
|
+
if response.status == 200:
|
516
|
+
data = await response.json()
|
517
|
+
return TestResult(
|
518
|
+
test_name=test_name,
|
519
|
+
server_url=server_url,
|
520
|
+
auth_type=auth_type,
|
521
|
+
success=True,
|
522
|
+
status_code=response.status,
|
523
|
+
response_data=data,
|
524
|
+
duration=duration
|
525
|
+
)
|
526
|
+
else:
|
527
|
+
error_text = await response.text()
|
528
|
+
return TestResult(
|
529
|
+
test_name=test_name,
|
530
|
+
server_url=server_url,
|
531
|
+
auth_type=auth_type,
|
532
|
+
success=False,
|
533
|
+
status_code=response.status,
|
534
|
+
error_message=f"Role-based access failed: {error_text}",
|
535
|
+
duration=duration
|
536
|
+
)
|
537
|
+
except Exception as e:
|
538
|
+
duration = time.time() - start_time
|
539
|
+
return TestResult(
|
540
|
+
test_name=test_name,
|
541
|
+
server_url=server_url,
|
542
|
+
auth_type=auth_type,
|
543
|
+
success=False,
|
544
|
+
error_message=f"Role-based access error: {str(e)}",
|
545
|
+
duration=duration
|
546
|
+
)
|
547
|
+
async def test_role_permissions(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
548
|
+
"""Test role permissions with role_test command."""
|
549
|
+
start_time = time.time()
|
550
|
+
test_name = f"Role Permissions Test ({auth_type})"
|
551
|
+
try:
|
552
|
+
# Test with different roles and actions
|
553
|
+
role = kwargs.get("role", "user")
|
554
|
+
action = kwargs.get("action", "read")
|
555
|
+
token = self.test_tokens.get(role, self.test_tokens["user"])
|
556
|
+
headers = self.create_auth_headers("api_key", token=token)
|
557
|
+
data = {
|
558
|
+
"jsonrpc": "2.0",
|
559
|
+
"method": "role_test",
|
560
|
+
"params": {"action": action},
|
561
|
+
"id": 5
|
562
|
+
}
|
563
|
+
async with self.session.post(f"{server_url}/cmd",
|
564
|
+
headers=headers,
|
565
|
+
json=data) as response:
|
566
|
+
duration = time.time() - start_time
|
567
|
+
if response.status == 200:
|
568
|
+
data = await response.json()
|
569
|
+
return TestResult(
|
570
|
+
test_name=test_name,
|
571
|
+
server_url=server_url,
|
572
|
+
auth_type=auth_type,
|
573
|
+
success=True,
|
574
|
+
status_code=response.status,
|
575
|
+
response_data=data,
|
576
|
+
duration=duration
|
577
|
+
)
|
578
|
+
else:
|
579
|
+
error_text = await response.text()
|
580
|
+
return TestResult(
|
581
|
+
test_name=test_name,
|
582
|
+
server_url=server_url,
|
583
|
+
auth_type=auth_type,
|
584
|
+
success=False,
|
585
|
+
status_code=response.status,
|
586
|
+
error_message=f"Role permissions test failed: {error_text}",
|
587
|
+
duration=duration
|
588
|
+
)
|
589
|
+
except Exception as e:
|
590
|
+
duration = time.time() - start_time
|
591
|
+
return TestResult(
|
592
|
+
test_name=test_name,
|
593
|
+
server_url=server_url,
|
594
|
+
auth_type=auth_type,
|
595
|
+
success=False,
|
596
|
+
error_message=f"Role permissions test error: {str(e)}",
|
597
|
+
duration=duration
|
598
|
+
)
|
599
|
+
async def test_multiple_roles(self, server_url: str, auth_type: str = "none", **kwargs) -> TestResult:
|
600
|
+
"""Test multiple roles with different permissions."""
|
601
|
+
start_time = time.time()
|
602
|
+
test_name = f"Multiple Roles Test ({auth_type})"
|
603
|
+
try:
|
604
|
+
# Test admin role (should have all permissions)
|
605
|
+
admin_token = self.test_tokens.get("admin", "admin-token-123")
|
606
|
+
admin_headers = self.create_auth_headers("api_key", token=admin_token)
|
607
|
+
admin_data = {
|
608
|
+
"jsonrpc": "2.0",
|
609
|
+
"method": "role_test",
|
610
|
+
"params": {"action": "manage"},
|
611
|
+
"id": 6
|
612
|
+
}
|
613
|
+
async with self.session.post(f"{server_url}/cmd",
|
614
|
+
headers=admin_headers,
|
615
|
+
json=admin_data) as response:
|
616
|
+
if response.status != 200:
|
617
|
+
return TestResult(
|
618
|
+
test_name=test_name,
|
619
|
+
server_url=server_url,
|
620
|
+
auth_type=auth_type,
|
621
|
+
success=False,
|
622
|
+
status_code=response.status,
|
623
|
+
error_message="Admin role test failed",
|
624
|
+
duration=time.time() - start_time
|
625
|
+
)
|
626
|
+
# Test readonly role (should only have read permission)
|
627
|
+
readonly_token = self.test_tokens.get("readonly", "readonly-token-123")
|
628
|
+
readonly_headers = self.create_auth_headers("api_key", token=readonly_token)
|
629
|
+
readonly_data = {
|
630
|
+
"jsonrpc": "2.0",
|
631
|
+
"method": "role_test",
|
632
|
+
"params": {"action": "write"},
|
633
|
+
}
|
634
|
+
async with self.session.post(f"{server_url}/cmd",
|
635
|
+
headers=readonly_headers,
|
636
|
+
json=readonly_data) as response:
|
637
|
+
duration = time.time() - start_time
|
638
|
+
# Readonly should be denied write access
|
639
|
+
if response.status == 403:
|
640
|
+
return TestResult(
|
641
|
+
test_name=test_name,
|
642
|
+
server_url=server_url,
|
643
|
+
auth_type=auth_type,
|
644
|
+
success=True,
|
645
|
+
status_code=response.status,
|
646
|
+
response_data={"message": "Correctly denied write access to readonly role"},
|
647
|
+
duration=duration
|
648
|
+
)
|
649
|
+
else:
|
650
|
+
return TestResult(
|
651
|
+
test_name=test_name,
|
652
|
+
server_url=server_url,
|
653
|
+
auth_type=auth_type,
|
654
|
+
success=False,
|
655
|
+
status_code=response.status,
|
656
|
+
error_message="Readonly role incorrectly allowed write access",
|
657
|
+
duration=duration
|
658
|
+
)
|
659
|
+
except Exception as e:
|
660
|
+
duration = time.time() - start_time
|
661
|
+
return TestResult(
|
662
|
+
test_name=test_name,
|
663
|
+
server_url=server_url,
|
664
|
+
auth_type=auth_type,
|
665
|
+
success=False,
|
666
|
+
error_message=f"Multiple roles test error: {str(e)}",
|
667
|
+
duration=duration
|
668
|
+
)
|
669
|
+
async def run_security_tests(self, server_url: str, auth_type: str = "none", **kwargs) -> List[TestResult]:
|
670
|
+
"""Run comprehensive security tests."""
|
671
|
+
print(f"\nš Running security tests for {server_url} ({auth_type})")
|
672
|
+
print("=" * 60)
|
673
|
+
tests = [
|
674
|
+
self.test_health_check(server_url, auth_type, **kwargs),
|
675
|
+
self.test_echo_command(server_url, auth_type, **kwargs),
|
676
|
+
self.test_security_command(server_url, auth_type, **kwargs),
|
677
|
+
self.test_negative_auth(server_url, auth_type, **kwargs),
|
678
|
+
self.test_role_based_access(server_url, auth_type, **kwargs)
|
679
|
+
]
|
680
|
+
results = []
|
681
|
+
for test in tests:
|
682
|
+
result = await test
|
683
|
+
results.append(result)
|
684
|
+
self.test_results.append(result)
|
685
|
+
# Print result
|
686
|
+
status = "ā
PASS" if result.success else "ā FAIL"
|
687
|
+
print(f"{status} {result.test_name}")
|
688
|
+
print(f" Duration: {result.duration:.3f}s")
|
689
|
+
if result.status_code:
|
690
|
+
print(f" Status: {result.status_code}")
|
691
|
+
if result.error_message:
|
692
|
+
print(f" Error: {result.error_message}")
|
693
|
+
print()
|
694
|
+
return results
|
695
|
+
async def test_all_scenarios(self) -> Dict[str, List[TestResult]]:
|
696
|
+
"""Test all security scenarios."""
|
697
|
+
scenarios = {
|
698
|
+
"basic_http": {
|
699
|
+
"url": "http://localhost:8000",
|
700
|
+
"auth": "none"
|
701
|
+
},
|
702
|
+
"http_token": {
|
703
|
+
"url": "http://localhost:8001",
|
704
|
+
"auth": "api_key"
|
705
|
+
},
|
706
|
+
"https": {
|
707
|
+
"url": "https://localhost:8443",
|
708
|
+
"auth": "none"
|
709
|
+
},
|
710
|
+
"https_token": {
|
711
|
+
"url": "https://localhost:8444",
|
712
|
+
"auth": "api_key"
|
713
|
+
},
|
714
|
+
"mtls": {
|
715
|
+
"url": "https://localhost:8445",
|
716
|
+
"auth": "certificate"
|
717
|
+
}
|
718
|
+
}
|
719
|
+
all_results = {}
|
720
|
+
for scenario_name, config in scenarios.items():
|
721
|
+
print(f"\nš Testing scenario: {scenario_name.upper()}")
|
722
|
+
print("=" * 60)
|
723
|
+
try:
|
724
|
+
results = await self.run_security_tests(
|
725
|
+
config["url"],
|
726
|
+
config["auth"]
|
727
|
+
)
|
728
|
+
all_results[scenario_name] = results
|
729
|
+
except Exception as e:
|
730
|
+
print(f"ā Failed to test {scenario_name}: {e}")
|
731
|
+
all_results[scenario_name] = []
|
732
|
+
return all_results
|
733
|
+
def print_summary(self):
|
734
|
+
"""Print test summary."""
|
735
|
+
print("\n" + "=" * 80)
|
736
|
+
print("š SECURITY TEST SUMMARY")
|
737
|
+
print("=" * 80)
|
738
|
+
total_tests = len(self.test_results)
|
739
|
+
passed_tests = sum(1 for r in self.test_results if r.success)
|
740
|
+
failed_tests = total_tests - passed_tests
|
741
|
+
print(f"Total Tests: {total_tests}")
|
742
|
+
print(f"Passed: {passed_tests} ā
")
|
743
|
+
print(f"Failed: {failed_tests} ā")
|
744
|
+
print(f"Success Rate: {(passed_tests/total_tests*100):.1f}%")
|
745
|
+
if failed_tests > 0:
|
746
|
+
print("\nā Failed Tests:")
|
747
|
+
for result in self.test_results:
|
748
|
+
if not result.success:
|
749
|
+
print(f" - {result.test_name} ({result.server_url})")
|
750
|
+
if result.error_message:
|
751
|
+
print(f" Error: {result.error_message}")
|
752
|
+
print("\nā
Passed Tests:")
|
753
|
+
for result in self.test_results:
|
754
|
+
if result.success:
|
755
|
+
print(f" - {result.test_name} ({result.server_url})")
|
756
|
+
async def main():
|
757
|
+
"""Main function."""
|
758
|
+
import argparse
|
759
|
+
parser = argparse.ArgumentParser(description="Security Test Client for MCP Proxy Adapter")
|
760
|
+
parser.add_argument("--server", default="http://localhost:8000",
|
761
|
+
help="Server URL to test")
|
762
|
+
parser.add_argument("--auth", choices=["none", "api_key", "basic", "certificate"],
|
763
|
+
default="none", help="Authentication type")
|
764
|
+
parser.add_argument("--all-scenarios", action="store_true",
|
765
|
+
help="Test all security scenarios")
|
766
|
+
parser.add_argument("--token", help="API token for authentication")
|
767
|
+
parser.add_argument("--cert", help="Client certificate file")
|
768
|
+
parser.add_argument("--key", help="Client private key file")
|
769
|
+
parser.add_argument("--ca-cert", help="CA certificate file")
|
770
|
+
args = parser.parse_args()
|
771
|
+
if args.all_scenarios:
|
772
|
+
# Test all scenarios
|
773
|
+
async with SecurityTestClient() as client:
|
774
|
+
await client.test_all_scenarios()
|
775
|
+
client.print_summary()
|
776
|
+
else:
|
777
|
+
# Test single server
|
778
|
+
async with SecurityTestClient(args.server) as client:
|
779
|
+
await client.run_security_tests(args.server, args.auth, token=args.token)
|
780
|
+
client.print_summary()
|
781
|
+
if __name__ == "__main__":
|
782
|
+
asyncio.run(main())
|