Skip to content

Commit 6f0073e

Browse files
authored
Add evo command to push scan results (#132)
1 parent cfe6373 commit 6f0073e

File tree

1 file changed

+72
-0
lines changed

1 file changed

+72
-0
lines changed

src/mcp_scan/cli.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import logging
1212
import sys
1313

14+
import aiohttp
1415
import psutil
1516
import rich
1617
from rich.logging import RichHandler
@@ -572,6 +573,11 @@ def main():
572573
add_server_arguments(proxy_parser)
573574
add_install_arguments(proxy_parser)
574575

576+
# EVO command
577+
evo_parser = subparsers.add_parser("evo", help="Push scan results to Snyk Evo")
578+
# use the same parser as scan
579+
setup_scan_parser(evo_parser)
580+
575581
# Parse arguments (default to 'scan' if no command provided)
576582
if (len(sys.argv) == 1 or sys.argv[1] not in subparsers.choices) and (
577583
not (len(sys.argv) == 2 and sys.argv[1] == "--help")
@@ -691,12 +697,78 @@ def server(on_exit=None):
691697
from mcp_scan.mcp_server import install_mcp_server
692698

693699
sys.exit(install_mcp_server(args))
700+
elif args.command == "evo":
701+
asyncio.run(evo(args))
702+
sys.exit(0)
694703
else:
695704
# This shouldn't happen due to argparse's handling
696705
rich.print(f"[bold red]Unknown command: {args.command}[/bold red]")
697706
parser.print_help()
698707
sys.exit(1)
699708

709+
async def evo(args):
710+
"""
711+
Pushes the scan results to the Evo API.
712+
713+
1. Creates a client_id (shared secret)
714+
2. Pushes scan results to the Evo API
715+
3. Revokes the client_id
716+
"""
717+
718+
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: ")
719+
tenant_id = input().strip()
720+
rich.print(f"Paste the Authorization token from https://app.snyk.io/account (API Token -> KEY -> click to show): ")
721+
token = input().strip()
722+
723+
push_key_url = f"https://api.snyk.io/hidden/tenants/{tenant_id}/mcp-scan/push-key?version=2025-08-28"
724+
push_scan_url = f"https://api.snyk.io/hidden/mcp-scan/push?version=2025-08-28"
725+
726+
# create a client_id (shared secret)
727+
client_id = None
728+
try:
729+
async with aiohttp.ClientSession() as session:
730+
async with session.post(push_key_url, data="", headers={"Authorization": f"token {token}"}) as resp:
731+
if resp.status not in (200, 201):
732+
text = await resp.text()
733+
rich.print(f"[bold red]Request failed[/bold red]: HTTP {resp.status} - {text}")
734+
return
735+
data = await resp.json()
736+
client_id = data.get("client_id")
737+
if not client_id:
738+
rich.print(f"[bold red]Unexpected response[/bold red]: {data}")
739+
return
740+
rich.print(f"Client ID created")
741+
except Exception as e:
742+
rich.print(f"[bold red]Error calling Snyk API[/bold red]: {e}")
743+
return
744+
745+
# Update the default scan args
746+
args.control_servers=[
747+
{
748+
"url": push_scan_url,
749+
"identifier": None,
750+
"opt_out": False,
751+
"headers": [f"x-client-id:{client_id}"]
752+
}
753+
]
754+
await run_scan_inspect(mode="scan", args=args)
755+
756+
# revoke the created client_id
757+
del_headers = {
758+
"Content-Type": "application/json",
759+
"Authorization": f"token {token}",
760+
"x-client-id": client_id,
761+
}
762+
try:
763+
async with aiohttp.ClientSession() as session:
764+
async with session.delete(push_key_url, headers=del_headers) as del_resp:
765+
if del_resp.status not in (200, 204):
766+
text = await del_resp.text()
767+
rich.print(f"[bold red]Failed to revoke client_id[/bold red]: HTTP {del_resp.status} - {text}")
768+
rich.print("Client ID revoked")
769+
except Exception as e:
770+
rich.print(f"[bold red]Error revoking client_id[/bold red]: {e}")
771+
700772

701773
async def run_scan_inspect(mode="scan", args=None):
702774
async with MCPScanner(additional_headers=parse_headers(args.verification_H), **vars(args)) as scanner:

0 commit comments

Comments
 (0)