|
11 | 11 | import logging |
12 | 12 | import sys |
13 | 13 |
|
| 14 | +import aiohttp |
14 | 15 | import psutil |
15 | 16 | import rich |
16 | 17 | from rich.logging import RichHandler |
@@ -572,6 +573,11 @@ def main(): |
572 | 573 | add_server_arguments(proxy_parser) |
573 | 574 | add_install_arguments(proxy_parser) |
574 | 575 |
|
| 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 | + |
575 | 581 | # Parse arguments (default to 'scan' if no command provided) |
576 | 582 | if (len(sys.argv) == 1 or sys.argv[1] not in subparsers.choices) and ( |
577 | 583 | not (len(sys.argv) == 2 and sys.argv[1] == "--help") |
@@ -691,12 +697,78 @@ def server(on_exit=None): |
691 | 697 | from mcp_scan.mcp_server import install_mcp_server |
692 | 698 |
|
693 | 699 | sys.exit(install_mcp_server(args)) |
| 700 | + elif args.command == "evo": |
| 701 | + asyncio.run(evo(args)) |
| 702 | + sys.exit(0) |
694 | 703 | else: |
695 | 704 | # This shouldn't happen due to argparse's handling |
696 | 705 | rich.print(f"[bold red]Unknown command: {args.command}[/bold red]") |
697 | 706 | parser.print_help() |
698 | 707 | sys.exit(1) |
699 | 708 |
|
| 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 | + |
700 | 772 |
|
701 | 773 | async def run_scan_inspect(mode="scan", args=None): |
702 | 774 | async with MCPScanner(additional_headers=parse_headers(args.verification_H), **vars(args)) as scanner: |
|
0 commit comments