Skip to content

upload scan results to control server #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/mcp_scan/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import psutil
import rich
from invariant.__main__ import add_extra
from mcp_scan_server.server import MCPScanServer
from rich.logging import RichHandler

from mcp_scan.gateway import MCPGatewayConfig, MCPGatewayInstaller
from mcp_scan.upload import upload
from mcp_scan_server.server import MCPScanServer

from .MCPScanner import MCPScanner
from .paths import WELL_KNOWN_MCP_PATHS, client_shorthands_to_paths
Expand Down Expand Up @@ -268,6 +269,21 @@ def main():
action="store_true",
help="Only run verification locally. Does not run all checks, results will be less accurate.",
)
scan_parser.add_argument(
"--control-server",
default=False,
help="Upload the scan results to the provided control server URL (default: Do not upload)",
)
scan_parser.add_argument(
"--org-name",
default=False,
help="When uploading the scan results to the provided control server URL, pass the organization name (default: Do not upload)",
)
scan_parser.add_argument(
"--push-key",
default=False,
help="When uploading the scan results to the provided control server URL, pass the push key (default: Do not upload)",
)

# INSPECT command
inspect_parser = subparsers.add_parser(
Expand Down Expand Up @@ -499,9 +515,15 @@ async def run_scan_inspect(mode="scan", args=None):
result = await scanner.scan()
elif mode == "inspect":
result = await scanner.inspect()

# upload scan result to control server if specified
if args.control_server and args.push_key:
await upload(result, args.control_server, args.push_key)

if args.json:
result = {r.path: r.model_dump() for r in result}
print(json.dumps(result, indent=2))
print(args)
else:
print_scan_result(result)

Expand Down
77 changes: 77 additions & 0 deletions src/mcp_scan/upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import getpass
import logging
import os

import aiohttp

from mcp_scan.models import ScanPathResult
from mcp_scan.paths import get_client_from_path

logger = logging.getLogger(__name__)


async def upload(results: list[ScanPathResult], control_server: str, push_key: str) -> None:
"""
Upload the scan results to the control server.

Args:
results: List of scan path results to upload
control_server: Base URL of the control server
push_key: Push key for authentication
"""
if not results:
logger.info("No scan results to upload")
return

# Normalize control server URL
base_url = control_server.rstrip("/")
upload_url = f"{base_url}/api/scans/push"

# get host name
try:
hostname = os.uname().nodename
except Exception:
hostname = "unknown"

# Get user information
try:
username = getpass.getuser() + "@" + hostname
except Exception:
username = "unknown@" + hostname

# Convert all scan results to server data
for result in results:
try:
# include user and client information in the upload data
payload = {
**(result.model_dump()),
"username": username,
"client": get_client_from_path(result.path) or "result.path",
"push_key": push_key,
}

# print(payload)

async with aiohttp.ClientSession() as session:
headers = {"Content-Type": "application/json", "User-Agent": "mcp-scan/1.0"}

async with session.post(
upload_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=30)
) as response:
if response.status == 200:
response_data = await response.json()
logger.info(
f"Successfully uploaded scan results. Server responded with {len(response_data)} results"
)
print(f"✅ Successfully uploaded scan results to {control_server}")
else:
error_text = await response.text()
logger.error(f"Failed to upload scan results. Status: {response.status}, Error: {error_text}")
print(f"❌ Failed to upload scan results: {response.status} - {error_text}")

except aiohttp.ClientError as e:
logger.error(f"Network error while uploading scan results: {e}")
print(f"❌ Network error while uploading scan results: {e}")
except Exception as e:
logger.error(f"Unexpected error while uploading scan results: {e}")
print(f"❌ Unexpected error while uploading scan results: {e}")