Skip to content

Commit 361081f

Browse files
authored
feat: add additional scan details on push (#137)
1 parent afa8338 commit 361081f

File tree

4 files changed

+32
-10
lines changed

4 files changed

+32
-10
lines changed

src/mcp_scan/MCPScanner.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
import re
5+
import time
56
import traceback
67
from collections import defaultdict
78
from collections.abc import Callable
@@ -78,6 +79,7 @@ def __init__(
7879
additional_headers: dict | None = None,
7980
control_servers: list | None = None,
8081
skip_ssl_verify: bool = False,
82+
scan_context: dict | None = None,
8183
**kwargs: Any,
8284
):
8385
logger.info("Initializing MCPScanner")
@@ -97,6 +99,7 @@ def __init__(
9799
self.control_servers = control_servers
98100
self.verbose = verbose
99101
self.skip_ssl_verify = skip_ssl_verify
102+
self.scan_context = scan_context if scan_context is not None else {}
100103
logger.debug(
101104
"MCPScanner initialized with timeout: %d, checks_per_server: %d", server_timeout, checks_per_server
102105
)
@@ -285,6 +288,7 @@ async def check_path(self, path_result: ScanPathResult) -> ScanPathResult:
285288

286289
async def scan(self) -> list[ScanPathResult]:
287290
logger.info("Starting scan of %d paths", len(self.paths))
291+
scan_start_time = time.perf_counter()
288292
if self.context_manager is not None:
289293
self.context_manager.disable()
290294

@@ -309,6 +313,8 @@ async def scan(self) -> list[ScanPathResult]:
309313
verbose=self.verbose,
310314
skip_ssl_verify=self.skip_ssl_verify,
311315
)
316+
self.scan_context["scan_time_milliseconds"] = (time.perf_counter() - scan_start_time) * 1000
317+
312318
logger.debug("Result verified: %s", result_verified)
313319
logger.debug("Saving storage file")
314320
self.storage_file.save()

src/mcp_scan/cli.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from mcp_scan.MCPScanner import MCPScanner
2020
from mcp_scan.printer import print_scan_result
2121
from mcp_scan.Storage import Storage
22-
from mcp_scan.upload import upload
22+
from mcp_scan.upload import get_hostname, upload
2323
from mcp_scan.utils import parse_headers
2424
from mcp_scan.version import version_info
2525
from mcp_scan.well_known_clients import WELL_KNOWN_MCP_PATHS, client_shorthands_to_paths
@@ -712,6 +712,7 @@ def server(on_exit=None):
712712
parser.print_help()
713713
sys.exit(1)
714714

715+
715716
async def evo(args):
716717
"""
717718
Pushes the scan results to the Evo API.
@@ -721,13 +722,15 @@ async def evo(args):
721722
3. Revokes the client_id
722723
"""
723724

724-
rich.print(f"Go to https://app.snyk.io and select the tenant on the left nav bar. Copy the Tenant ID from the URL and paste it here: ")
725+
rich.print(
726+
"Go to https://app.snyk.io and select the tenant on the left nav bar. Copy the Tenant ID from the URL and paste it here: "
727+
)
725728
tenant_id = input().strip()
726-
rich.print(f"Paste the Authorization token from https://app.snyk.io/account (API Token -> KEY -> click to show): ")
729+
rich.print("Paste the Authorization token from https://app.snyk.io/account (API Token -> KEY -> click to show): ")
727730
token = input().strip()
728731

729732
push_key_url = f"https://api.snyk.io/hidden/tenants/{tenant_id}/mcp-scan/push-key?version=2025-08-28"
730-
push_scan_url = f"https://api.snyk.io/hidden/mcp-scan/push?version=2025-08-28"
733+
push_scan_url = "https://api.snyk.io/hidden/mcp-scan/push?version=2025-08-28"
731734

732735
# create a client_id (shared secret)
733736
client_id = None
@@ -743,18 +746,18 @@ async def evo(args):
743746
if not client_id:
744747
rich.print(f"[bold red]Unexpected response[/bold red]: {data}")
745748
return
746-
rich.print(f"Client ID created")
749+
rich.print("Client ID created")
747750
except Exception as e:
748751
rich.print(f"[bold red]Error calling Snyk API[/bold red]: {e}")
749752
return
750753

751754
# Update the default scan args
752-
args.control_servers=[
755+
args.control_servers = [
753756
{
754757
"url": push_scan_url,
755-
"identifier": None,
758+
"identifier": get_hostname() or None,
756759
"opt_out": False,
757-
"headers": [f"x-client-id:{client_id}"]
760+
"headers": [f"x-client-id:{client_id}"],
758761
}
759762
]
760763
await run_scan_inspect(mode="scan", args=args)
@@ -777,7 +780,12 @@ async def evo(args):
777780

778781

779782
async def run_scan_inspect(mode="scan", args=None):
780-
async with MCPScanner(additional_headers=parse_headers(args.verification_H), **vars(args)) as scanner:
783+
# Initialize scan_context dict that can be populated during scanning
784+
scan_context = {"cli_version": version_info}
785+
786+
async with MCPScanner(
787+
additional_headers=parse_headers(args.verification_H), scan_context=scan_context, **vars(args)
788+
) as scanner:
781789
if mode == "scan":
782790
result = await scanner.scan()
783791
elif mode == "inspect":
@@ -796,6 +804,7 @@ async def run_scan_inspect(mode="scan", args=None):
796804
verbose=getattr(args, "verbose", False),
797805
additional_headers=parse_headers(server_config["headers"]),
798806
skip_ssl_verify=getattr(args, "skip_ssl_verify", False),
807+
scan_context=scan_context,
799808
)
800809
return result
801810

src/mcp_scan/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,4 @@ class AnalysisServerResponse(BaseModel):
375375
class ScanPathResultsCreate(BaseModel):
376376
scan_path_results: list[ScanPathResult]
377377
scan_user_info: ScanUserInfo
378+
scan_metadata: dict[str, Any] | None = None

src/mcp_scan/upload.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ async def upload(
6262
additional_headers: dict | None = None,
6363
max_retries: int = 3,
6464
skip_ssl_verify: bool = False,
65+
scan_context: dict | None = None,
6566
) -> None:
6667
"""
6768
Upload the scan results to the control server with retry logic.
@@ -75,6 +76,7 @@ async def upload(
7576
additional_headers: Additional HTTP headers to send
7677
max_retries: Maximum number of retry attempts (default: 3)
7778
skip_ssl_verify: Whether to disable SSL certificate verification (default: False)
79+
scan_context: Optional dict containing scan context metadata to include in upload
7880
"""
7981
if not results:
8082
logger.info("No scan results to upload")
@@ -94,7 +96,11 @@ async def upload(
9496
result.client = get_client_from_path(result.path) or result.client or result.path
9597
results_with_servers.append(result)
9698

97-
payload = ScanPathResultsCreate(scan_path_results=results_with_servers, scan_user_info=user_info)
99+
payload = ScanPathResultsCreate(
100+
scan_path_results=results_with_servers,
101+
scan_user_info=user_info,
102+
scan_metadata=scan_context if scan_context else None,
103+
)
98104

99105
last_exception = None
100106
trace_configs = setup_aiohttp_debug_logging(verbose=verbose)

0 commit comments

Comments
 (0)