Skip to content

Commit a6a793f

Browse files
authored
Add --insecure flag to skip SSL cert verification. (#133)
* Add --insecure flag to skip SSL cert verification. * Fix the insecure arg passed to upload in run_scan_inspect * fix tests
1 parent 03e60ea commit a6a793f

File tree

5 files changed

+80
-5
lines changed

5 files changed

+80
-5
lines changed

src/mcp_scan/MCPScanner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(
7676
verbose: bool = False,
7777
additional_headers: dict | None = None,
7878
control_servers: list | None = None,
79+
insecure: bool = False,
7980
**kwargs: Any,
8081
):
8182
logger.info("Initializing MCPScanner")
@@ -94,6 +95,7 @@ def __init__(
9495
self.include_built_in = include_built_in
9596
self.control_servers = control_servers
9697
self.verbose = verbose
98+
self.insecure = insecure
9799
logger.debug(
98100
"MCPScanner initialized with timeout: %d, checks_per_server: %d", server_timeout, checks_per_server
99101
)
@@ -304,6 +306,7 @@ async def scan(self) -> list[ScanPathResult]:
304306
opt_out_of_identity=self.opt_out_of_identity,
305307
skip_pushing=bool(self.control_servers),
306308
verbose=self.verbose,
309+
insecure=self.insecure,
307310
)
308311
logger.debug("Result verified: %s", result_verified)
309312
logger.debug("Saving storage file")

src/mcp_scan/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ def add_common_arguments(parser):
212212
default=False,
213213
help="Output results in JSON format instead of rich text",
214214
)
215+
parser.add_argument(
216+
"--insecure",
217+
default=False,
218+
action="store_true",
219+
help="Disable SSL certificate verification",
220+
)
215221

216222

217223
def add_server_arguments(parser):
@@ -787,8 +793,9 @@ async def run_scan_inspect(mode="scan", args=None):
787793
server_config["url"],
788794
server_config["identifier"],
789795
server_config["opt_out"],
790-
verbose=hasattr(args, "verbose") and args.verbose,
796+
verbose=getattr(args, "verbose", False),
791797
additional_headers=parse_headers(server_config["headers"]),
798+
insecure=getattr(args, "insecure", False),
792799
)
793800
return result
794801

src/mcp_scan/upload.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ async def upload(
6161
verbose: bool = False,
6262
additional_headers: dict | None = None,
6363
max_retries: int = 3,
64+
insecure: bool = False,
6465
) -> None:
6566
"""
6667
Upload the scan results to the control server with retry logic.
@@ -73,6 +74,7 @@ async def upload(
7374
verbose: Whether to enable verbose logging
7475
additional_headers: Additional HTTP headers to send
7576
max_retries: Maximum number of retry attempts (default: 3)
77+
insecure: Whether to disable SSL certificate verification (default: False)
7678
"""
7779
if not results:
7880
logger.info("No scan results to upload")
@@ -100,7 +102,10 @@ async def upload(
100102

101103
for attempt in range(max_retries):
102104
try:
103-
async with aiohttp.ClientSession(trace_configs=trace_configs, connector=setup_tcp_connector()) as session:
105+
async with aiohttp.ClientSession(
106+
trace_configs=trace_configs,
107+
connector=setup_tcp_connector(insecure=insecure),
108+
) as session:
104109
headers = {"Content-Type": "application/json"}
105110
headers.update(additional_headers)
106111

src/mcp_scan/verify_api.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,17 @@ async def on_request_redirect(session, trace_config_ctx, params):
103103
return [trace_config]
104104

105105

106-
def setup_tcp_connector() -> aiohttp.TCPConnector:
106+
def setup_tcp_connector(insecure: bool = False) -> aiohttp.TCPConnector:
107107
"""
108-
Setup a TCP connector with a default SSL context and cleanup enabled.
108+
Setup a TCP connector with SSL settings.
109+
110+
When insecure is True, disable SSL verification and hostname checking.
111+
Otherwise, use a secure default SSL context with certifi CA and TLSv1.2+.
109112
"""
113+
if insecure:
114+
# Disable SSL verification at the connector level
115+
return aiohttp.TCPConnector(ssl=False, enable_cleanup_closed=True)
116+
110117
ssl_context = ssl.create_default_context(cafile=certifi.where())
111118
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
112119
connector = aiohttp.TCPConnector(ssl=ssl_context, enable_cleanup_closed=True)
@@ -145,6 +152,7 @@ async def analyze_machine(
145152
verbose: bool = False,
146153
skip_pushing: bool = False,
147154
max_retries: int = 3,
155+
insecure: bool = False,
148156
) -> list[ScanPathResult]:
149157
"""
150158
Analyze the scan paths with the analysis server.
@@ -158,6 +166,7 @@ async def analyze_machine(
158166
verbose: Whether to enable verbose logging
159167
skip_pushing: Whether to skip pushing the scan to the platform
160168
max_retries: Maximum number of retry attempts
169+
insecure: Whether to skip SSL verification
161170
"""
162171
logger.debug(f"Analyzing scan path with URL: {analysis_url}")
163172
user_info = get_user_info(identifier=identifier, opt_out=opt_out_of_identity)
@@ -179,7 +188,10 @@ async def analyze_machine(
179188

180189
for attempt in range(max_retries):
181190
try:
182-
async with aiohttp.ClientSession(trace_configs=trace_configs, connector=setup_tcp_connector()) as session:
191+
async with aiohttp.ClientSession(
192+
trace_configs=trace_configs,
193+
connector=setup_tcp_connector(insecure=insecure),
194+
) as session:
183195
async with session.post(
184196
analysis_url,
185197
data=payload.model_dump_json(),

tests/unit/test_cli_parsing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,3 +407,51 @@ async def test_no_upload_when_no_control_servers(self):
407407

408408
# Verify upload was not called
409409
mock_upload.assert_not_called()
410+
411+
@pytest.mark.asyncio
412+
async def test_upload_with_insecure(self):
413+
"""Test that upload is called with insecure option."""
414+
from argparse import Namespace
415+
416+
from mcp_scan.cli import run_scan_inspect
417+
418+
mock_result = ScanPathResult(path="/test/path")
419+
420+
with patch("mcp_scan.cli.MCPScanner") as MockScanner, patch("mcp_scan.cli.upload") as mock_upload:
421+
# Setup scanner mock
422+
mock_scanner_instance = AsyncMock()
423+
mock_scanner_instance.scan = AsyncMock(return_value=[mock_result])
424+
mock_scanner_instance.__aenter__ = AsyncMock(return_value=mock_scanner_instance)
425+
mock_scanner_instance.__aexit__ = AsyncMock(return_value=None)
426+
MockScanner.return_value = mock_scanner_instance
427+
428+
# Setup upload mock
429+
mock_upload.return_value = None
430+
431+
# Create args with a control server and without the insecure option
432+
args_without_insecure = Namespace(
433+
verification_H=None,
434+
control_servers=[{"url": "https://server1.com", "headers": [], "identifier": None, "opt_out": False}],
435+
)
436+
437+
# Run the scan
438+
await run_scan_inspect(mode="scan", args=args_without_insecure)
439+
440+
# Verify upload was called and insecure was not propagated
441+
_, kwargs = mock_upload.call_args
442+
assert kwargs.get("insecure") is False
443+
444+
# Create args with a control server and insecure option
445+
args_with_insecure = Namespace(
446+
verification_H=None,
447+
control_servers=[{"url": "https://server1.com", "headers": [], "identifier": None, "opt_out": False}],
448+
insecure=True,
449+
)
450+
451+
# Run the scan
452+
await run_scan_inspect(mode="scan", args=args_with_insecure)
453+
454+
# Verify upload was called and insecure was propagated
455+
assert mock_upload.call_count == 2
456+
_, kwargs = mock_upload.call_args
457+
assert kwargs.get("insecure") is True

0 commit comments

Comments
 (0)