diff --git a/CLAUDE.md b/CLAUDE.md index cf0f788a..65cc12cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,10 +14,14 @@ cp .env.example .env && edit .env # Set ANTHROPIC_API_KEY git clone https://github.com/org/repo.git ./repos/my-repo # or symlink: ln -s /path/to/existing/repo ./repos/my-repo -# Run +# Run (CLI) ./shannon start URL= REPO=my-repo ./shannon start URL= REPO=my-repo CONFIG=./configs/my-config.yaml +# Run (Claude Desktop) +npm run build:mcp # Build the MCP server +# Add to Claude Desktop config (see below) + # Monitor ./shannon logs # Real-time worker logs ./shannon query ID= # Query workflow progress @@ -33,6 +37,28 @@ npm run build **Options:** `CONFIG=` (YAML config), `OUTPUT=` (default: `./audit-logs/`), `PIPELINE_TESTING=true` (minimal prompts, 10s retries), `REBUILD=true` (force Docker rebuild), `ROUTER=true` (multi-model routing via [claude-code-router](https://github.com/musistudio/claude-code-router)) +## Desktop Integration (MCP) + +Control Shannon directly from Claude Desktop to start scans, monitor progress, and read reports. + +1. **Build the Server:** `npm run build:mcp` +2. **Configure Claude Desktop:** Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "shannon": { + "command": "node", + "args": ["/ABSOLUTE/PATH/TO/shannon/desktop-mcp-server/dist/index.js"], + "env": { + "SHANNON_ROOT": "/ABSOLUTE/PATH/TO/shannon", + "ANTHROPIC_API_KEY": "sk-ant-..." + } + } + } +} +``` + ## Architecture ### Core Modules @@ -42,6 +68,7 @@ npm run build - `src/error-handling.ts` — Categorized error types (PentestError, ConfigError, NetworkError) with retry logic - `src/tool-checker.ts` — Validates external security tool availability before execution - `src/queue-validation.ts` — Deliverable validation and agent prerequisites +- `desktop-mcp-server/` — **(New)** Host-based MCP server bridging Claude Desktop to Dockerized infrastructure via Temporal gRPC ### Temporal Orchestration Durable workflow orchestration with crash recovery, queryable progress, intelligent retry, and parallel execution (5 concurrent agents in vuln/exploit phases). @@ -117,6 +144,7 @@ Defensive security tool only. Use only on systems you own or have explicit permi ## Key Files **Entry Points:** `src/temporal/workflows.ts`, `src/temporal/activities.ts`, `src/temporal/worker.ts`, `src/temporal/client.ts` +**MCP:** `desktop-mcp-server/src/index.ts`, `desktop-mcp-server/src/server.ts` **Core Logic:** `src/session-manager.ts`, `src/ai/claude-executor.ts`, `src/config-parser.ts`, `src/audit/` diff --git a/desktop-mcp-server/package-lock.json b/desktop-mcp-server/package-lock.json new file mode 100644 index 00000000..a0242ec0 --- /dev/null +++ b/desktop-mcp-server/package-lock.json @@ -0,0 +1,1656 @@ +{ + "name": "@shannon/desktop-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@shannon/desktop-mcp-server", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@temporalio/client": "^1.11.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "js-yaml": "^4.1.0", + "zod": "^4.3.6" + }, + "bin": { + "shannon-mcp": "dist/index.js" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.0.3", + "typescript": "^5.9.3" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@temporalio/client": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.14.1.tgz", + "integrity": "sha512-AfWSA0LYzBvDLFiFgrPWqTGGq1NGnF3d4xKnxf0PGxSmv5SLb/aqQ9lzHg4DJ5UNkHO4M/NwzdxzzoaR1J5F8Q==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.14.1", + "@temporalio/proto": "1.14.1", + "abort-controller": "^3.0.0", + "long": "^5.2.3", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/common": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.14.1.tgz", + "integrity": "sha512-y49wOm3AIEKZufIQ/QU5JhTSaHJIEkiUt5bGB0/uSzCg8P4g8Cz0XoVPSbDwuCix533O9cOKcliYq7Gzjt/sIA==", + "license": "MIT", + "dependencies": { + "@temporalio/proto": "1.14.1", + "long": "^5.2.3", + "ms": "3.0.0-canary.1", + "nexus-rpc": "^0.0.1", + "proto3-json-serializer": "^2.0.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/proto": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.14.1.tgz", + "integrity": "sha512-mCsUommDPXbXbBu60p1g4jpSqVb+GNR67yR0uKTU8ARb4qVZQo7SQnOUaneoxDERDXuR/yIjVCektMm+7Myb+A==", + "license": "MIT", + "dependencies": { + "long": "^5.2.3", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "license": "MIT", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nexus-rpc": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.1.tgz", + "integrity": "sha512-hAWn8Hh2eewpB5McXR5EW81R3pR/ziuGhKCF3wFyUVCklanPqrIgMNr7jKCbzXeNVad0nUDfWpFRqh2u+zxQtw==", + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/desktop-mcp-server/package.json b/desktop-mcp-server/package.json new file mode 100644 index 00000000..cf2eb866 --- /dev/null +++ b/desktop-mcp-server/package.json @@ -0,0 +1,29 @@ +{ + "name": "@shannon/desktop-mcp-server", + "version": "1.0.0", + "description": "Shannon MCP server for Claude Desktop - start, monitor, and manage pentest workflows", + "type": "module", + "bin": { + "shannon-mcp": "./dist/index.js" + }, + "main": "./dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js", + "clean": "rm -rf dist" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@temporalio/client": "^1.11.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "js-yaml": "^4.1.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.0.3", + "typescript": "^5.9.3" + } +} diff --git a/desktop-mcp-server/src/index.ts b/desktop-mcp-server/src/index.ts new file mode 100644 index 00000000..98baaaac --- /dev/null +++ b/desktop-mcp-server/src/index.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +/** + * Shannon Desktop MCP Server + * + * Entry point for the STDIO-based MCP server that integrates + * Shannon pentest workflows with Claude Desktop. + * + * Usage: + * node dist/index.js + * + * Environment: + * SHANNON_ROOT - Path to Shannon installation (auto-detected if omitted) + * ANTHROPIC_API_KEY - Anthropic API key (passed to Docker containers) + */ + +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createShannonServer } from './server.js'; + +async function main(): Promise { + const server = createShannonServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((error) => { + console.error('Shannon MCP server failed to start:', error); + process.exit(1); +}); diff --git a/desktop-mcp-server/src/infrastructure/docker-bridge.ts b/desktop-mcp-server/src/infrastructure/docker-bridge.ts new file mode 100644 index 00000000..73dfb0c6 --- /dev/null +++ b/desktop-mcp-server/src/infrastructure/docker-bridge.ts @@ -0,0 +1,180 @@ +/** + * Docker Bridge + * + * Checks Docker availability, detects Podman vs Docker, + * and manages container lifecycle for Shannon infrastructure. + */ + +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import type { PathResolver } from './path-resolver.js'; + +const execFileAsync = promisify(execFile); + +export interface DockerStatus { + available: boolean; + isPodman: boolean; + containersRunning: boolean; + temporalHealthy: boolean; +} + +/** + * Check if Docker (or Podman) is available on the system. + */ +export async function isDockerAvailable(): Promise { + try { + await execFileAsync('docker', ['info'], { timeout: 10_000 }); + return true; + } catch { + return false; + } +} + +/** + * Check if Podman is available (for compose file selection). + */ +export async function isPodman(): Promise { + try { + const { stdout } = await execFileAsync('podman', ['--version'], { timeout: 5_000 }); + return stdout.includes('podman'); + } catch { + return false; + } +} + +/** + * Build the docker compose command arguments. + * Mirrors the COMPOSE_FILE / COMPOSE_OVERRIDE logic from the shannon CLI. + */ +export async function getComposeArgs(paths: PathResolver): Promise { + const args = ['-f', paths.composeFile]; + + // Only add the Docker override if NOT using Podman + const podman = await isPodman(); + if (!podman) { + const dockerOverride = paths.composeFile.replace( + 'docker-compose.yml', + 'docker-compose.docker.yml' + ); + try { + const { readFile } = await import('fs/promises'); + await readFile(dockerOverride); + args.push('-f', dockerOverride); + } catch { + // Override file doesn't exist, skip + } + } + + return args; +} + +/** + * Check if Shannon containers are running and Temporal is healthy. + */ +export async function getDockerStatus(paths: PathResolver): Promise { + const available = await isDockerAvailable(); + if (!available) { + return { + available: false, + isPodman: false, + containersRunning: false, + temporalHealthy: false, + }; + } + + const podman = await isPodman(); + const composeArgs = await getComposeArgs(paths); + + // Check if containers are running + let containersRunning = false; + try { + const { stdout } = await execFileAsync( + 'docker', + ['compose', ...composeArgs, 'ps', '--format', 'json'], + { timeout: 10_000, cwd: paths.shannonRoot } + ); + containersRunning = stdout.trim().length > 0; + } catch { + containersRunning = false; + } + + // Check Temporal health + let temporalHealthy = false; + if (containersRunning) { + try { + const { stdout } = await execFileAsync( + 'docker', + [ + 'compose', ...composeArgs, + 'exec', '-T', 'temporal', + 'temporal', 'operator', 'cluster', 'health', + '--address', 'localhost:7233', + ], + { timeout: 10_000, cwd: paths.shannonRoot } + ); + temporalHealthy = stdout.includes('SERVING'); + } catch { + temporalHealthy = false; + } + } + + return { + available, + isPodman: podman, + containersRunning, + temporalHealthy, + }; +} + +/** + * Start Shannon containers (docker compose up -d). + * Mirrors ensure_containers() from the shannon CLI. + */ +export async function ensureContainers(paths: PathResolver): Promise { + const composeArgs = await getComposeArgs(paths); + + await execFileAsync( + 'docker', + ['compose', ...composeArgs, 'up', '-d', '--build'], + { timeout: 300_000, cwd: paths.shannonRoot } + ); + + // Wait for Temporal to be healthy (up to 60 seconds) + for (let i = 0; i < 30; i++) { + try { + const { stdout } = await execFileAsync( + 'docker', + [ + 'compose', ...composeArgs, + 'exec', '-T', 'temporal', + 'temporal', 'operator', 'cluster', 'health', + '--address', 'localhost:7233', + ], + { timeout: 10_000, cwd: paths.shannonRoot } + ); + if (stdout.includes('SERVING')) { + return; + } + } catch { + // Not ready yet + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + throw new Error('Timeout waiting for Temporal to become healthy after 60 seconds'); +} + +/** + * Stop Shannon containers. + */ +export async function stopContainers(paths: PathResolver, clean: boolean = false): Promise { + const composeArgs = await getComposeArgs(paths); + const downArgs = clean + ? ['compose', ...composeArgs, '--profile', 'router', 'down', '-v'] + : ['compose', ...composeArgs, '--profile', 'router', 'down']; + + await execFileAsync('docker', downArgs, { + timeout: 60_000, + cwd: paths.shannonRoot, + }); +} diff --git a/desktop-mcp-server/src/infrastructure/path-resolver.ts b/desktop-mcp-server/src/infrastructure/path-resolver.ts new file mode 100644 index 00000000..c23ebf9b --- /dev/null +++ b/desktop-mcp-server/src/infrastructure/path-resolver.ts @@ -0,0 +1,151 @@ +/** + * Path Resolver + * + * Resolves paths relative to the Shannon installation root. + * Handles repos/, configs/, audit-logs/ directories and + * translates host paths to container paths for Docker. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export class PathResolver { + readonly shannonRoot: string; + + constructor(shannonRoot?: string) { + if (shannonRoot) { + this.shannonRoot = path.resolve(shannonRoot); + } else if (process.env['SHANNON_ROOT']) { + this.shannonRoot = path.resolve(process.env['SHANNON_ROOT']); + } else { + // Walk up from dist/infrastructure/ to find the Shannon root + // desktop-mcp-server/dist/infrastructure/path-resolver.js -> shannon/ + this.shannonRoot = path.resolve(__dirname, '..', '..', '..'); + } + } + + get reposDir(): string { + return path.join(this.shannonRoot, 'repos'); + } + + get configsDir(): string { + return path.join(this.shannonRoot, 'configs'); + } + + get auditLogsDir(): string { + return path.join(this.shannonRoot, 'audit-logs'); + } + + get composeFile(): string { + return path.join(this.shannonRoot, 'docker-compose.yml'); + } + + get configSchemaPath(): string { + return path.join(this.shannonRoot, 'configs', 'config-schema.json'); + } + + /** + * Check if a repo exists in repos/ and return its absolute path. + */ + async resolveRepo(repoName: string): Promise { + const repoPath = path.join(this.reposDir, repoName); + try { + const stat = await fs.stat(repoPath); + if (stat.isDirectory()) { + return repoPath; + } + } catch { + // Does not exist + } + return null; + } + + /** + * Resolve a config file path. Accepts absolute paths or names relative to configs/. + */ + async resolveConfig(configName: string): Promise { + // If absolute path, use directly + if (path.isAbsolute(configName)) { + try { + await fs.access(configName); + return configName; + } catch { + return null; + } + } + + // Try relative to configs/ + const configPath = path.join(this.configsDir, configName); + try { + await fs.access(configPath); + return configPath; + } catch { + return null; + } + } + + /** + * Resolve an audit-log directory for a workflow ID. + */ + async resolveAuditLog(workflowId: string): Promise { + const auditPath = path.join(this.auditLogsDir, workflowId); + try { + const stat = await fs.stat(auditPath); + if (stat.isDirectory()) { + return auditPath; + } + } catch { + // Does not exist + } + return null; + } + + /** + * Translate a host repo name to the container path (/repos/). + */ + toContainerRepoPath(repoName: string): string { + return `/repos/${repoName}`; + } + + /** + * List all repos in the repos/ directory. + */ + async listRepos(): Promise { + try { + const entries = await fs.readdir(this.reposDir, { withFileTypes: true }); + return entries.filter((e) => e.isDirectory()).map((e) => e.name); + } catch { + return []; + } + } + + /** + * List all YAML config files in configs/. + */ + async listConfigs(): Promise { + try { + const entries = await fs.readdir(this.configsDir); + return entries.filter( + (e) => e.endsWith('.yaml') || e.endsWith('.yml') + ); + } catch { + return []; + } + } + + /** + * List all audit log directories (workflow IDs). + */ + async listAuditLogs(): Promise { + try { + const entries = await fs.readdir(this.auditLogsDir, { withFileTypes: true }); + return entries.filter((e) => e.isDirectory()).map((e) => e.name); + } catch { + return []; + } + } +} diff --git a/desktop-mcp-server/src/infrastructure/temporal-client.ts b/desktop-mcp-server/src/infrastructure/temporal-client.ts new file mode 100644 index 00000000..9a8fce1a --- /dev/null +++ b/desktop-mcp-server/src/infrastructure/temporal-client.ts @@ -0,0 +1,149 @@ +/** + * Temporal Client Bridge + * + * Manages a connection to the Temporal gRPC server at localhost:7233. + * Provides workflow start, query, cancel, and list operations. + */ + +import { Connection, Client, type WorkflowExecutionInfo } from '@temporalio/client'; +import type { PipelineInput, PipelineState, PipelineProgress } from '../types.js'; + +const TASK_QUEUE = 'shannon-pipeline'; +const WORKFLOW_TYPE = 'pentestPipelineWorkflow'; +const PROGRESS_QUERY = 'getProgress'; + +export class TemporalBridge { + private connection: Connection | null = null; + private client: Client | null = null; + private readonly address: string; + + constructor(address?: string) { + this.address = address ?? process.env['TEMPORAL_ADDRESS'] ?? 'localhost:7233'; + } + + /** + * Lazily connect to the Temporal server. + */ + async connect(): Promise { + if (this.client) { + return this.client; + } + + this.connection = await Connection.connect({ address: this.address }); + this.client = new Client({ connection: this.connection }); + return this.client; + } + + /** + * Check if we can connect to Temporal. + */ + async isHealthy(): Promise { + try { + await this.connect(); + return true; + } catch { + return false; + } + } + + /** + * Start a new pentest pipeline workflow. + */ + async startWorkflow(input: PipelineInput, workflowId: string): Promise { + const client = await this.connect(); + + const handle = await client.workflow.start<(input: PipelineInput) => Promise>( + WORKFLOW_TYPE, + { + taskQueue: TASK_QUEUE, + workflowId, + args: [input], + } + ); + + return handle.workflowId; + } + + /** + * Query workflow progress. + */ + async queryProgress(workflowId: string): Promise { + const client = await this.connect(); + const handle = client.workflow.getHandle(workflowId); + return await handle.query(PROGRESS_QUERY); + } + + /** + * Cancel a running workflow. + */ + async cancelWorkflow(workflowId: string): Promise { + const client = await this.connect(); + const handle = client.workflow.getHandle(workflowId); + await handle.cancel(); + } + + /** + * Terminate a running workflow. + */ + async terminateWorkflow(workflowId: string, reason?: string): Promise { + const client = await this.connect(); + const handle = client.workflow.getHandle(workflowId); + await handle.terminate(reason); + } + + /** + * List workflows with optional status filter. + */ + async listWorkflows( + statusFilter?: 'Running' | 'Completed' | 'Failed' | 'Terminated' | 'Cancelled', + limit: number = 10 + ): Promise { + const client = await this.connect(); + + let query = `WorkflowType = '${WORKFLOW_TYPE}'`; + if (statusFilter) { + query += ` AND ExecutionStatus = '${statusFilter}'`; + } + + const results: WorkflowInfo[] = []; + for await (const workflow of client.workflow.list({ query })) { + results.push(toWorkflowInfo(workflow)); + if (results.length >= limit) break; + } + + return results; + } + + /** + * Close the Temporal connection. + */ + async disconnect(): Promise { + if (this.connection) { + await this.connection.close(); + this.connection = null; + this.client = null; + } + } +} + +// --- Simplified workflow info for MCP responses --- + +export interface WorkflowInfo { + workflowId: string; + runId: string; + status: string; + startTime: string; + closeTime: string | null; + taskQueue: string; +} + +function toWorkflowInfo(execution: WorkflowExecutionInfo): WorkflowInfo { + return { + workflowId: execution.workflowId, + runId: execution.runId, + status: execution.status.name, + startTime: execution.startTime.toISOString(), + closeTime: execution.closeTime?.toISOString() ?? null, + taskQueue: execution.taskQueue, + }; +} diff --git a/desktop-mcp-server/src/prompts/analyze-results.ts b/desktop-mcp-server/src/prompts/analyze-results.ts new file mode 100644 index 00000000..dac57f0d --- /dev/null +++ b/desktop-mcp-server/src/prompts/analyze-results.ts @@ -0,0 +1,33 @@ +/** + * analyze-results Prompt + * + * A prompt template for analyzing completed scan results. + */ + +export const ANALYZE_RESULTS_PROMPT = { + name: 'analyze-results', + description: 'Analyze the results of a completed Shannon scan. Reads the report, metrics, and provides a conversational summary.', + arguments: [ + { + name: 'workflow_id', + description: 'Workflow ID of the completed scan', + required: true, + }, + ], +}; + +export function buildAnalyzeResultsPrompt(workflowId: string): string { + return [ + `Please analyze the results of Shannon scan: ${workflowId}`, + '', + 'Steps:', + '1. Query the workflow progress to confirm it completed', + '2. Read the final report deliverable', + '3. Summarize the key findings:', + ' - Total vulnerabilities found by severity', + ' - Most critical issues requiring immediate attention', + ' - Attack vectors identified', + '4. Provide actionable remediation recommendations', + '5. Note the scan metrics (cost, duration, agents used)', + ].join('\n'); +} diff --git a/desktop-mcp-server/src/prompts/start-pentest.ts b/desktop-mcp-server/src/prompts/start-pentest.ts new file mode 100644 index 00000000..1c56ef4b --- /dev/null +++ b/desktop-mcp-server/src/prompts/start-pentest.ts @@ -0,0 +1,45 @@ +/** + * start-pentest Prompt + * + * A guided prompt template that helps users configure and start a scan. + */ + +export const START_PENTEST_PROMPT = { + name: 'start-pentest', + description: 'Guide through starting a Shannon penetration test. Validates target, repo, and config before launching.', + arguments: [ + { + name: 'url', + description: 'Target URL to scan (e.g., https://example.com)', + required: true, + }, + { + name: 'repo', + description: 'Repository folder name under ./repos/', + required: false, + }, + ], +}; + +export function buildStartPentestPrompt(url: string, repo?: string): string { + const parts = [ + `I want to start a Shannon penetration test against: ${url}`, + '', + 'Please help me:', + '1. Verify the target URL is valid', + ]; + + if (repo) { + parts.push(`2. Confirm the repo "${repo}" exists in repos/`); + } else { + parts.push('2. List available repos and help me choose one'); + } + + parts.push( + '3. Check if there are any relevant configs available', + '4. Start the scan with the confirmed parameters', + '5. Show me how to monitor progress', + ); + + return parts.join('\n'); +} diff --git a/desktop-mcp-server/src/resources/audit-logs.ts b/desktop-mcp-server/src/resources/audit-logs.ts new file mode 100644 index 00000000..2a6485a7 --- /dev/null +++ b/desktop-mcp-server/src/resources/audit-logs.ts @@ -0,0 +1,119 @@ +/** + * Audit Logs Resource Provider + * + * Exposes audit log files as MCP resources. + * URI pattern: shannon://audit-logs/{workflowId}/{path} + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; + +export interface ResourceEntry { + uri: string; + name: string; + mimeType: string; + description: string; +} + +/** + * List all audit log resources for a specific workflow. + */ +export async function listAuditLogResources( + paths: PathResolver, + workflowId?: string +): Promise { + const resources: ResourceEntry[] = []; + + if (workflowId) { + // List files for a specific workflow + const auditPath = await paths.resolveAuditLog(workflowId); + if (!auditPath) return resources; + + await collectFiles(auditPath, workflowId, '', resources); + } else { + // List all workflow directories as top-level resources + const workflows = await paths.listAuditLogs(); + for (const wfId of workflows) { + resources.push({ + uri: `shannon://audit-logs/${wfId}/session.json`, + name: `${wfId} - Session Info`, + mimeType: 'application/json', + description: `Session metadata for workflow ${wfId}`, + }); + } + } + + return resources; +} + +/** + * Read a specific audit log resource. + */ +export async function readAuditLogResource( + paths: PathResolver, + workflowId: string, + filePath: string +): Promise { + const auditDir = await paths.resolveAuditLog(workflowId); + if (!auditDir) return null; + + // Prevent path traversal + const resolved = path.resolve(auditDir, filePath); + if (!resolved.startsWith(auditDir)) return null; + + try { + return await fs.readFile(resolved, 'utf8'); + } catch { + return null; + } +} + +async function collectFiles( + dirPath: string, + workflowId: string, + relativePath: string, + resources: ResourceEntry[] +): Promise { + let entries; + try { + entries = await fs.readdir(dirPath, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name; + + if (entry.isFile()) { + const mimeType = entry.name.endsWith('.json') + ? 'application/json' + : entry.name.endsWith('.md') + ? 'text/markdown' + : 'text/plain'; + + resources.push({ + uri: `shannon://audit-logs/${workflowId}/${entryRelative}`, + name: entryRelative, + mimeType, + description: describeFile(entryRelative), + }); + } else if (entry.isDirectory()) { + await collectFiles( + path.join(dirPath, entry.name), + workflowId, + entryRelative, + resources + ); + } + } +} + +function describeFile(relativePath: string): string { + if (relativePath === 'session.json') return 'Session metadata and metrics'; + if (relativePath === 'workflow.log') return 'Workflow-level event log'; + if (relativePath.startsWith('agents/')) return 'Agent execution log'; + if (relativePath.startsWith('prompts/')) return 'Agent prompt snapshot'; + if (relativePath.startsWith('deliverables/')) return 'Agent deliverable output'; + return 'Audit log file'; +} diff --git a/desktop-mcp-server/src/resources/configs.ts b/desktop-mcp-server/src/resources/configs.ts new file mode 100644 index 00000000..78fd6818 --- /dev/null +++ b/desktop-mcp-server/src/resources/configs.ts @@ -0,0 +1,45 @@ +/** + * Config Resources Provider + * + * Exposes config YAML files as MCP resources. + * URI pattern: shannon://configs/{filename} + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import type { ResourceEntry } from './audit-logs.js'; + +/** + * List all config file resources. + */ +export async function listConfigResources(paths: PathResolver): Promise { + const configNames = await paths.listConfigs(); + + return configNames.map((name) => ({ + uri: `shannon://configs/${name}`, + name, + mimeType: 'text/yaml', + description: `Shannon configuration file: ${name}`, + })); +} + +/** + * Read a specific config resource. + */ +export async function readConfigResource( + paths: PathResolver, + filename: string +): Promise { + const configPath = path.join(paths.configsDir, filename); + + // Prevent path traversal + const resolved = path.resolve(configPath); + if (!resolved.startsWith(paths.configsDir)) return null; + + try { + return await fs.readFile(resolved, 'utf8'); + } catch { + return null; + } +} diff --git a/desktop-mcp-server/src/resources/deliverables.ts b/desktop-mcp-server/src/resources/deliverables.ts new file mode 100644 index 00000000..ed721d92 --- /dev/null +++ b/desktop-mcp-server/src/resources/deliverables.ts @@ -0,0 +1,79 @@ +/** + * Deliverables Resource Provider + * + * Exposes deliverable files from completed scans as MCP resources. + * URI pattern: shannon://deliverables/{workflowId}/{filename} + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import type { ResourceEntry } from './audit-logs.js'; + +/** + * List all deliverable resources for a specific workflow. + */ +export async function listDeliverableResources( + paths: PathResolver, + workflowId: string +): Promise { + const auditPath = await paths.resolveAuditLog(workflowId); + if (!auditPath) return []; + + const deliverablesDir = path.join(auditPath, 'deliverables'); + let entries: string[]; + try { + entries = await fs.readdir(deliverablesDir); + } catch { + return []; + } + + return entries.map((name) => ({ + uri: `shannon://deliverables/${workflowId}/${name}`, + name, + mimeType: name.endsWith('.json') + ? 'application/json' + : name.endsWith('.md') + ? 'text/markdown' + : 'text/plain', + description: describeDeliverable(name), + })); +} + +/** + * Read a specific deliverable resource. + */ +export async function readDeliverableResource( + paths: PathResolver, + workflowId: string, + filename: string +): Promise { + const auditPath = await paths.resolveAuditLog(workflowId); + if (!auditPath) return null; + + const filePath = path.join(auditPath, 'deliverables', filename); + + // Prevent path traversal + const resolved = path.resolve(filePath); + const expectedDir = path.join(auditPath, 'deliverables'); + if (!resolved.startsWith(expectedDir)) return null; + + try { + return await fs.readFile(resolved, 'utf8'); + } catch { + return null; + } +} + +function describeDeliverable(filename: string): string { + if (filename.includes('report')) return 'Executive security report'; + if (filename.includes('injection')) return 'Injection vulnerability analysis'; + if (filename.includes('xss')) return 'XSS vulnerability analysis'; + if (filename.includes('auth') && filename.includes('authz')) return 'Authorization vulnerability analysis'; + if (filename.includes('auth')) return 'Authentication vulnerability analysis'; + if (filename.includes('ssrf')) return 'SSRF vulnerability analysis'; + if (filename.includes('exploit')) return 'Exploitation results'; + if (filename.includes('queue')) return 'Exploitation queue'; + if (filename.includes('recon')) return 'Reconnaissance findings'; + return 'Scan deliverable'; +} diff --git a/desktop-mcp-server/src/server.ts b/desktop-mcp-server/src/server.ts new file mode 100644 index 00000000..346d9942 --- /dev/null +++ b/desktop-mcp-server/src/server.ts @@ -0,0 +1,381 @@ +/** + * Shannon MCP Server + * + * Registers all tools, resources, and prompts with the MCP server. + * This is the core server setup that wires everything together. + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, + ListResourceTemplatesRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { PathResolver } from './infrastructure/path-resolver.js'; +import { TemporalBridge } from './infrastructure/temporal-client.js'; + +// Tools +import { startScan, type StartScanInput } from './tools/start-scan.js'; +import { queryProgress } from './tools/query-progress.js'; +import { stopScan, type StopScanInput } from './tools/stop-scan.js'; +import { listScans, type ListScansInput } from './tools/list-scans.js'; +import { listConfigs } from './tools/list-configs.js'; +import { validateConfig } from './tools/validate-config.js'; +import { getReport, type GetReportInput } from './tools/get-report.js'; + +// Resources +import { listAuditLogResources, readAuditLogResource } from './resources/audit-logs.js'; +import { listConfigResources, readConfigResource } from './resources/configs.js'; +import { listDeliverableResources, readDeliverableResource } from './resources/deliverables.js'; + +// Prompts +import { START_PENTEST_PROMPT, buildStartPentestPrompt } from './prompts/start-pentest.js'; +import { ANALYZE_RESULTS_PROMPT, buildAnalyzeResultsPrompt } from './prompts/analyze-results.js'; + +export function createShannonServer(): Server { + const server = new Server( + { + name: 'shannon', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + prompts: {}, + }, + } + ); + + const paths = new PathResolver(); + const temporal = new TemporalBridge(); + + // === Tool Registration === + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'start_scan', + description: + 'Start a new Shannon pentest pipeline workflow. Validates the target URL, repo, and config, ensures Docker infrastructure is running, then submits the workflow to Temporal.', + inputSchema: { + type: 'object' as const, + properties: { + url: { + type: 'string', + description: 'Target URL to scan (e.g., https://example.com)', + }, + repo: { + type: 'string', + description: 'Repository folder name under ./repos/ directory', + }, + config: { + type: 'string', + description: 'Config file name in configs/ or absolute path (optional)', + }, + output: { + type: 'string', + description: 'Custom output directory for audit logs (optional)', + }, + pipeline_testing: { + type: 'boolean', + description: 'Use minimal prompts for fast iteration (optional)', + }, + workflow_id: { + type: 'string', + description: 'Custom workflow ID (optional, auto-generated if omitted)', + }, + }, + required: ['url', 'repo'], + }, + }, + { + name: 'query_progress', + description: + 'Check the status and progress of a running or completed Shannon workflow. Returns current phase, completed agents, metrics, and cost.', + inputSchema: { + type: 'object' as const, + properties: { + workflow_id: { + type: 'string', + description: 'Workflow ID to query (e.g., example.com_shannon-1234567890)', + }, + }, + required: ['workflow_id'], + }, + }, + { + name: 'stop_scan', + description: + 'Cancel or terminate a running Shannon workflow. Use force=true to immediately terminate instead of graceful cancellation.', + inputSchema: { + type: 'object' as const, + properties: { + workflow_id: { + type: 'string', + description: 'Workflow ID to stop', + }, + reason: { + type: 'string', + description: 'Reason for stopping (logged in Temporal)', + }, + force: { + type: 'boolean', + description: 'Force terminate instead of graceful cancel (default: false)', + }, + }, + required: ['workflow_id'], + }, + }, + { + name: 'list_scans', + description: + 'List recent and active Shannon workflow executions. Combines data from Temporal and audit-logs.', + inputSchema: { + type: 'object' as const, + properties: { + status: { + type: 'string', + enum: ['running', 'completed', 'failed', 'all'], + description: 'Filter by workflow status (default: all)', + }, + limit: { + type: 'number', + description: 'Maximum number of results (default: 10, max: 50)', + }, + }, + }, + }, + { + name: 'list_configs', + description: + 'List available Shannon YAML configuration files with a summary of their contents (auth type, rules count).', + inputSchema: { + type: 'object' as const, + properties: {}, + }, + }, + { + name: 'validate_config', + description: + 'Validate a Shannon YAML configuration file against the JSON Schema. Reports any errors found.', + inputSchema: { + type: 'object' as const, + properties: { + config: { + type: 'string', + description: 'Config file name in configs/ or absolute path', + }, + }, + required: ['config'], + }, + }, + { + name: 'get_report', + description: + 'Retrieve the final pentest report or a specific deliverable from a completed scan.', + inputSchema: { + type: 'object' as const, + properties: { + workflow_id: { + type: 'string', + description: 'Workflow ID of the completed scan', + }, + deliverable: { + type: 'string', + description: 'Specific deliverable filename to read (optional, defaults to main report)', + }, + }, + required: ['workflow_id'], + }, + }, + ], + })); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case 'start_scan': + return await startScan(args as unknown as StartScanInput, paths, temporal); + + case 'query_progress': + return await queryProgress((args as Record)['workflow_id']!, paths, temporal); + + case 'stop_scan': + return await stopScan(args as unknown as StopScanInput, temporal); + + case 'list_scans': + return await listScans(args as unknown as ListScansInput, paths, temporal); + + case 'list_configs': + return await listConfigs(paths); + + case 'validate_config': + return await validateConfig((args as Record)['config']!, paths); + + case 'get_report': + return await getReport(args as unknown as GetReportInput, paths); + + default: + return { + content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }], + isError: true, + }; + } + }); + + // === Resource Registration === + + server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ + resourceTemplates: [ + { + uriTemplate: 'shannon://audit-logs/{workflowId}/{path}', + name: 'Audit Log File', + description: 'Access audit log files for a specific workflow', + mimeType: 'text/plain', + }, + { + uriTemplate: 'shannon://configs/{filename}', + name: 'Config File', + description: 'Access Shannon YAML configuration files', + mimeType: 'text/yaml', + }, + { + uriTemplate: 'shannon://deliverables/{workflowId}/{filename}', + name: 'Deliverable', + description: 'Access deliverable files from completed scans', + mimeType: 'text/plain', + }, + ], + })); + + server.setRequestHandler(ListResourcesRequestSchema, async () => { + const auditResources = await listAuditLogResources(paths); + const configResources = await listConfigResources(paths); + + return { + resources: [...auditResources, ...configResources], + }; + }); + + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + + // Parse the URI + const parsed = new URL(uri); + const pathParts = parsed.pathname.split('/').filter(Boolean); + + if (parsed.protocol === 'shannon:') { + const resourceType = parsed.hostname; + + if (resourceType === 'audit-logs' && pathParts.length >= 1) { + const workflowId = pathParts[0]!; + const filePath = pathParts.slice(1).join('/'); + const content = await readAuditLogResource(paths, workflowId, filePath || 'session.json'); + + if (content === null) { + throw new Error(`Resource not found: ${uri}`); + } + + return { + contents: [{ uri, text: content, mimeType: 'text/plain' }], + }; + } + + if (resourceType === 'configs' && pathParts.length >= 1) { + const filename = pathParts[0]!; + const content = await readConfigResource(paths, filename); + + if (content === null) { + throw new Error(`Resource not found: ${uri}`); + } + + return { + contents: [{ uri, text: content, mimeType: 'text/yaml' }], + }; + } + + if (resourceType === 'deliverables' && pathParts.length >= 2) { + const workflowId = pathParts[0]!; + const filename = pathParts[1]!; + const content = await readDeliverableResource(paths, workflowId, filename); + + if (content === null) { + throw new Error(`Resource not found: ${uri}`); + } + + return { + contents: [{ uri, text: content, mimeType: 'text/plain' }], + }; + } + } + + throw new Error(`Unknown resource URI: ${uri}`); + }); + + // === Prompt Registration === + + server.setRequestHandler(ListPromptsRequestSchema, async () => ({ + prompts: [ + START_PENTEST_PROMPT, + ANALYZE_RESULTS_PROMPT, + ], + })); + + server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case 'start-pentest': { + const url = args?.['url']; + if (!url) { + throw new Error('url argument is required for start-pentest prompt'); + } + return { + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: buildStartPentestPrompt(url, args?.['repo']), + }, + }, + ], + }; + } + + case 'analyze-results': { + const workflowId = args?.['workflow_id']; + if (!workflowId) { + throw new Error('workflow_id argument is required for analyze-results prompt'); + } + return { + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: buildAnalyzeResultsPrompt(workflowId), + }, + }, + ], + }; + } + + default: + throw new Error(`Unknown prompt: ${name}`); + } + }); + + // Cleanup on server close + server.onclose = async () => { + await temporal.disconnect(); + }; + + return server; +} diff --git a/desktop-mcp-server/src/tools/get-report.ts b/desktop-mcp-server/src/tools/get-report.ts new file mode 100644 index 00000000..5f42841e --- /dev/null +++ b/desktop-mcp-server/src/tools/get-report.ts @@ -0,0 +1,106 @@ +/** + * get_report Tool + * + * Retrieves the final pentest report from a completed scan. + * Checks both the audit-logs deliverables and the repo deliverables directory. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import { toolSuccess, toolError, type ToolResult } from '../types.js'; + +export interface GetReportInput { + workflow_id: string; + deliverable?: string; +} + +export async function getReport( + input: GetReportInput, + paths: PathResolver +): Promise { + const auditPath = await paths.resolveAuditLog(input.workflow_id); + if (!auditPath) { + return toolError(`No audit logs found for workflow: ${input.workflow_id}`, { + suggestion: 'Check the workflow ID with list_scans', + }); + } + + // If specific deliverable requested, read it directly + if (input.deliverable) { + return await readDeliverable(auditPath, input.deliverable, input.workflow_id); + } + + // Otherwise, try to find the main report + const deliverablesDir = path.join(auditPath, 'deliverables'); + + // List available deliverables + let deliverables: string[]; + try { + deliverables = await fs.readdir(deliverablesDir); + } catch { + return toolError('No deliverables found for this workflow', { + workflowId: input.workflow_id, + suggestion: 'The scan may not have completed yet. Check with query_progress.', + }); + } + + if (deliverables.length === 0) { + return toolError('Deliverables directory is empty', { + workflowId: input.workflow_id, + }); + } + + // Try to find the main report (usually the largest .md file or one with "report" in the name) + const reportFile = deliverables.find((f) => f.includes('report') && f.endsWith('.md')) + ?? deliverables.find((f) => f.endsWith('.md')) + ?? deliverables[0]; + + if (!reportFile) { + return toolSuccess({ + workflowId: input.workflow_id, + available_deliverables: deliverables, + message: 'No report file found. Use the deliverable parameter to read a specific file.', + }); + } + + const reportPath = path.join(deliverablesDir, reportFile); + try { + const content = await fs.readFile(reportPath, 'utf8'); + return toolSuccess({ + workflowId: input.workflow_id, + deliverable: reportFile, + content, + all_deliverables: deliverables, + }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`Failed to read report: ${errMsg}`); + } +} + +async function readDeliverable( + auditPath: string, + deliverableName: string, + workflowId: string +): Promise { + const filePath = path.join(auditPath, 'deliverables', deliverableName); + try { + const content = await fs.readFile(filePath, 'utf8'); + return toolSuccess({ + workflowId, + deliverable: deliverableName, + content, + }); + } catch { + // List what's available + try { + const available = await fs.readdir(path.join(auditPath, 'deliverables')); + return toolError(`Deliverable not found: ${deliverableName}`, { + available_deliverables: available, + }); + } catch { + return toolError(`Deliverable not found: ${deliverableName}`); + } + } +} diff --git a/desktop-mcp-server/src/tools/list-configs.ts b/desktop-mcp-server/src/tools/list-configs.ts new file mode 100644 index 00000000..09f4f941 --- /dev/null +++ b/desktop-mcp-server/src/tools/list-configs.ts @@ -0,0 +1,74 @@ +/** + * list_configs Tool + * + * Lists available YAML configuration files in the configs/ directory. + * Reads and summarizes each config's contents. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import yaml from 'js-yaml'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import { toolSuccess, type ToolResult, type Config } from '../types.js'; + +interface ConfigSummary { + name: string; + path: string; + hasAuthentication: boolean; + loginType: string | null; + avoidRules: number; + focusRules: number; +} + +export async function listConfigs(paths: PathResolver): Promise { + const configNames = await paths.listConfigs(); + + const configs: ConfigSummary[] = []; + for (const name of configNames) { + const configPath = path.join(paths.configsDir, name); + const summary = await summarizeConfig(name, configPath); + configs.push(summary); + } + + return toolSuccess({ + total: configs.length, + configsDir: paths.configsDir, + configs, + }); +} + +async function summarizeConfig(name: string, configPath: string): Promise { + try { + const content = await fs.readFile(configPath, 'utf8'); + const parsed = yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA }) as Config | null; + + if (!parsed || typeof parsed !== 'object') { + return { + name, + path: configPath, + hasAuthentication: false, + loginType: null, + avoidRules: 0, + focusRules: 0, + }; + } + + return { + name, + path: configPath, + hasAuthentication: !!parsed.authentication, + loginType: parsed.authentication?.login_type ?? null, + avoidRules: parsed.rules?.avoid?.length ?? 0, + focusRules: parsed.rules?.focus?.length ?? 0, + }; + } catch { + return { + name, + path: configPath, + hasAuthentication: false, + loginType: null, + avoidRules: 0, + focusRules: 0, + }; + } +} diff --git a/desktop-mcp-server/src/tools/list-scans.ts b/desktop-mcp-server/src/tools/list-scans.ts new file mode 100644 index 00000000..cb296765 --- /dev/null +++ b/desktop-mcp-server/src/tools/list-scans.ts @@ -0,0 +1,119 @@ +/** + * list_scans Tool + * + * Lists recent and active Shannon workflows from both + * Temporal and the audit-logs directory. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import type { TemporalBridge } from '../infrastructure/temporal-client.js'; +import { toolSuccess, toolError, type ToolResult } from '../types.js'; + +export interface ListScansInput { + status?: 'running' | 'completed' | 'failed' | 'all'; + limit?: number; +} + +interface ScanEntry { + workflowId: string; + status: string; + startTime: string; + closeTime: string | null; + source: 'temporal' | 'audit-logs'; + target?: string | undefined; +} + +export async function listScans( + input: ListScansInput, + paths: PathResolver, + temporal: TemporalBridge +): Promise { + const limit = input.limit ?? 10; + const statusFilter = input.status ?? 'all'; + + const scans: ScanEntry[] = []; + const seenIds = new Set(); + + // 1. Try Temporal listing + try { + const temporalStatus = mapStatusFilter(statusFilter); + const workflows = await temporal.listWorkflows(temporalStatus, limit); + + for (const w of workflows) { + scans.push({ + workflowId: w.workflowId, + status: w.status.toLowerCase(), + startTime: w.startTime, + closeTime: w.closeTime, + source: 'temporal', + }); + seenIds.add(w.workflowId); + } + } catch { + // Temporal might not be running, fall back to audit-logs only + } + + // 2. Supplement from audit-logs + const auditLogDirs = await paths.listAuditLogs(); + for (const workflowId of auditLogDirs) { + if (seenIds.has(workflowId)) continue; + if (scans.length >= limit) break; + + const entry = await readAuditEntry(paths, workflowId); + if (!entry) continue; + + // Apply status filter + if (statusFilter !== 'all' && entry.status !== statusFilter) continue; + + scans.push(entry); + seenIds.add(workflowId); + } + + // Sort by start time (newest first) + scans.sort((a, b) => { + const timeA = new Date(a.startTime).getTime(); + const timeB = new Date(b.startTime).getTime(); + return timeB - timeA; + }); + + return toolSuccess({ + total: scans.length, + scans: scans.slice(0, limit), + }); +} + +async function readAuditEntry(paths: PathResolver, workflowId: string): Promise { + try { + const sessionPath = path.join(paths.auditLogsDir, workflowId, 'session.json'); + const content = await fs.readFile(sessionPath, 'utf8'); + const session = JSON.parse(content) as Record; + + return { + workflowId, + status: (session['status'] as string) ?? 'unknown', + startTime: (session['startTime'] as string) ?? new Date().toISOString(), + closeTime: (session['endTime'] as string) ?? null, + source: 'audit-logs', + target: session['webUrl'] as string | undefined, + }; + } catch { + return null; + } +} + +function mapStatusFilter( + filter: string +): 'Running' | 'Completed' | 'Failed' | 'Terminated' | 'Cancelled' | undefined { + switch (filter) { + case 'running': + return 'Running'; + case 'completed': + return 'Completed'; + case 'failed': + return 'Failed'; + default: + return undefined; + } +} diff --git a/desktop-mcp-server/src/tools/query-progress.ts b/desktop-mcp-server/src/tools/query-progress.ts new file mode 100644 index 00000000..5fc76b49 --- /dev/null +++ b/desktop-mcp-server/src/tools/query-progress.ts @@ -0,0 +1,95 @@ +/** + * query_progress Tool + * + * Queries the status and progress of a running or completed workflow. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import type { TemporalBridge } from '../infrastructure/temporal-client.js'; +import { toolSuccess, toolError, type ToolResult, type PipelineProgress } from '../types.js'; + +export async function queryProgress( + workflowId: string, + paths: PathResolver, + temporal: TemporalBridge +): Promise { + // Try Temporal query first + let progress: PipelineProgress | null = null; + try { + progress = await temporal.queryProgress(workflowId); + } catch (error) { + // Temporal query may fail if workflow is gone or Temporal is down + const errMsg = error instanceof Error ? error.message : String(error); + + // Try reading from audit-logs as fallback + const auditPath = await paths.resolveAuditLog(workflowId); + if (auditPath) { + return await readAuditProgress(auditPath, workflowId); + } + + return toolError(`Failed to query workflow: ${errMsg}`, { + workflowId, + suggestion: 'Ensure the workflow ID is correct and Temporal is running', + }); + } + + const elapsed = formatElapsed(progress.elapsedMs); + const totalAgents = 13; + const completedCount = progress.completedAgents.length; + + // Compute cost summary from agent metrics + let totalCost = 0; + let totalTurns = 0; + for (const metrics of Object.values(progress.agentMetrics)) { + if (metrics.costUsd) totalCost += metrics.costUsd; + if (metrics.numTurns) totalTurns += metrics.numTurns; + } + + return toolSuccess({ + workflowId: progress.workflowId, + status: progress.status, + elapsed, + currentPhase: progress.currentPhase, + currentAgent: progress.currentAgent, + progress: `${completedCount}/${totalAgents} agents completed`, + completedAgents: progress.completedAgents, + failedAgent: progress.failedAgent, + error: progress.error, + metrics: { + totalCostUsd: `$${totalCost.toFixed(4)}`, + totalTurns, + agentDetails: progress.agentMetrics, + }, + summary: progress.summary, + }); +} + +/** + * Read progress from audit-logs when Temporal is unavailable. + */ +async function readAuditProgress(auditPath: string, workflowId: string): Promise { + try { + const sessionJsonPath = path.join(auditPath, 'session.json'); + const content = await fs.readFile(sessionJsonPath, 'utf8'); + const session = JSON.parse(content) as Record; + + return toolSuccess({ + workflowId, + source: 'audit-logs (Temporal unavailable)', + session, + }); + } catch { + return toolError('Workflow not found in Temporal or audit-logs', { workflowId }); + } +} + +function formatElapsed(ms: number): string { + if (ms < 1000) return `${ms}ms`; + const seconds = ms / 1000; + if (seconds < 60) return `${seconds.toFixed(1)}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}m ${remainingSeconds}s`; +} diff --git a/desktop-mcp-server/src/tools/start-scan.ts b/desktop-mcp-server/src/tools/start-scan.ts new file mode 100644 index 00000000..a3e3bf94 --- /dev/null +++ b/desktop-mcp-server/src/tools/start-scan.ts @@ -0,0 +1,130 @@ +/** + * start_scan Tool + * + * Starts a new Shannon pentest pipeline workflow. + * Validates inputs, ensures Docker infrastructure is running, + * then submits the workflow to Temporal. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import type { TemporalBridge } from '../infrastructure/temporal-client.js'; +import { ensureContainers, getDockerStatus } from '../infrastructure/docker-bridge.js'; +import { toolSuccess, toolError, type ToolResult, type PipelineInput } from '../types.js'; + +export interface StartScanInput { + url: string; + repo: string; + config?: string; + output?: string; + pipeline_testing?: boolean; + workflow_id?: string; +} + +export async function startScan( + input: StartScanInput, + paths: PathResolver, + temporal: TemporalBridge +): Promise { + // 1. Validate URL + let parsedUrl: URL; + try { + parsedUrl = new URL(input.url); + } catch { + return toolError('Invalid URL format', { url: input.url }); + } + + // 2. Validate repo exists + const repoPath = await paths.resolveRepo(input.repo); + if (!repoPath) { + const available = await paths.listRepos(); + return toolError(`Repository not found: ${input.repo}`, { + suggestion: 'Repository must be a folder inside ./repos/', + available_repos: available, + }); + } + + // 3. Validate config if provided + let configPath: string | undefined; + if (input.config) { + const resolved = await paths.resolveConfig(input.config); + if (!resolved) { + const available = await paths.listConfigs(); + return toolError(`Config file not found: ${input.config}`, { + suggestion: 'Config must be a file in ./configs/ or an absolute path', + available_configs: available, + }); + } + configPath = resolved; + } + + // 4. Ensure Docker infrastructure is running + const status = await getDockerStatus(paths); + if (!status.available) { + return toolError('Docker is not available. Please start Docker Desktop and try again.'); + } + + if (!status.temporalHealthy) { + try { + await ensureContainers(paths); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`Failed to start Shannon containers: ${errMsg}`); + } + } + + // 5. Ensure deliverables directory is writable + const deliverablesDir = path.join(repoPath, 'deliverables'); + try { + await fs.mkdir(deliverablesDir, { recursive: true }); + } catch { + // Best effort + } + + // 6. Generate workflow ID + const hostname = parsedUrl.hostname.replace(/[^a-zA-Z0-9-]/g, '-'); + const workflowId = input.workflow_id ?? `${hostname}_shannon-${Date.now()}`; + + // 7. Build pipeline input with container paths + const containerRepoPath = paths.toContainerRepoPath(input.repo); + const pipelineInput: PipelineInput = { + webUrl: input.url, + repoPath: containerRepoPath, + workflowId, + }; + + if (configPath) { + // Config is mounted at /app/configs/ in the container + const configName = path.basename(configPath); + pipelineInput.configPath = `/app/configs/${configName}`; + } + + if (input.output) { + pipelineInput.outputPath = '/app/output'; + } + + if (input.pipeline_testing) { + pipelineInput.pipelineTestingMode = true; + } + + // 8. Start the workflow + try { + await temporal.startWorkflow(pipelineInput, workflowId); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`Failed to start workflow: ${errMsg}`); + } + + return toolSuccess({ + workflowId, + status: 'started', + target: input.url, + repo: input.repo, + config: input.config ?? null, + monitor: { + web_ui: `http://localhost:8233/namespaces/default/workflows/${workflowId}`, + query: `Use the query_progress tool with workflowId: "${workflowId}"`, + }, + }); +} diff --git a/desktop-mcp-server/src/tools/stop-scan.ts b/desktop-mcp-server/src/tools/stop-scan.ts new file mode 100644 index 00000000..7cb22927 --- /dev/null +++ b/desktop-mcp-server/src/tools/stop-scan.ts @@ -0,0 +1,42 @@ +/** + * stop_scan Tool + * + * Cancels or terminates a running Shannon workflow. + */ + +import type { TemporalBridge } from '../infrastructure/temporal-client.js'; +import { toolSuccess, toolError, type ToolResult } from '../types.js'; + +export interface StopScanInput { + workflow_id: string; + reason?: string; + force?: boolean; +} + +export async function stopScan( + input: StopScanInput, + temporal: TemporalBridge +): Promise { + try { + if (input.force) { + await temporal.terminateWorkflow(input.workflow_id, input.reason); + return toolSuccess({ + workflowId: input.workflow_id, + action: 'terminated', + reason: input.reason ?? 'User requested termination', + }); + } + + await temporal.cancelWorkflow(input.workflow_id); + return toolSuccess({ + workflowId: input.workflow_id, + action: 'cancelled', + reason: input.reason ?? 'User requested cancellation', + }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`Failed to stop workflow: ${errMsg}`, { + workflowId: input.workflow_id, + }); + } +} diff --git a/desktop-mcp-server/src/tools/validate-config.ts b/desktop-mcp-server/src/tools/validate-config.ts new file mode 100644 index 00000000..9c9dcc53 --- /dev/null +++ b/desktop-mcp-server/src/tools/validate-config.ts @@ -0,0 +1,107 @@ +/** + * validate_config Tool + * + * Validates a YAML configuration file against Shannon's JSON Schema. + * Duplicates the validation logic from src/config-parser.ts to avoid + * pulling in heavy Shannon dependencies. + */ + +import fs from 'fs/promises'; +import { createRequire } from 'module'; +import yaml from 'js-yaml'; +import { Ajv, type ValidateFunction } from 'ajv'; +import type { FormatsPlugin } from 'ajv-formats'; +import type { PathResolver } from '../infrastructure/path-resolver.js'; +import { toolSuccess, toolError, type ToolResult, type Config } from '../types.js'; + +// Handle ESM/CJS interop for ajv-formats +const require = createRequire(import.meta.url); +const addFormats: FormatsPlugin = require('ajv-formats'); + +export async function validateConfig( + configName: string, + paths: PathResolver +): Promise { + // 1. Resolve config path + const configPath = await paths.resolveConfig(configName); + if (!configPath) { + const available = await paths.listConfigs(); + return toolError(`Config file not found: ${configName}`, { + suggestion: 'Provide a filename in configs/ or an absolute path', + available_configs: available, + }); + } + + // 2. Read and size-check the file + let content: string; + try { + const stats = await fs.stat(configPath); + if (stats.size > 1024 * 1024) { + return toolError('Config file too large (max 1MB)', { size: stats.size }); + } + content = await fs.readFile(configPath, 'utf8'); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`Failed to read config file: ${errMsg}`); + } + + if (!content.trim()) { + return toolError('Config file is empty'); + } + + // 3. Parse YAML + let parsed: unknown; + try { + parsed = yaml.load(content, { + schema: yaml.FAILSAFE_SCHEMA, + json: false, + filename: configPath, + }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`YAML parsing failed: ${errMsg}`); + } + + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return toolError('Config must be a YAML object (not null, array, or scalar)'); + } + + // 4. JSON Schema validation + let schemaErrors: string[] = []; + try { + const schemaContent = await fs.readFile(paths.configSchemaPath, 'utf8'); + const schema = JSON.parse(schemaContent) as object; + + const ajv = new Ajv({ allErrors: true, verbose: true }); + addFormats(ajv); + const validate: ValidateFunction = ajv.compile(schema); + + const isValid = validate(parsed); + if (!isValid && validate.errors) { + schemaErrors = validate.errors.map((err) => { + const errorPath = err.instancePath || 'root'; + return `${errorPath}: ${err.message}`; + }); + } + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return toolError(`Failed to load config schema: ${errMsg}`); + } + + if (schemaErrors.length > 0) { + return toolError('Config validation failed', { errors: schemaErrors }); + } + + // 5. Summarize valid config + const config = parsed as Config; + return toolSuccess({ + valid: true, + path: configPath, + summary: { + hasAuthentication: !!config.authentication, + loginType: config.authentication?.login_type ?? null, + avoidRules: config.rules?.avoid?.length ?? 0, + focusRules: config.rules?.focus?.length ?? 0, + }, + }); +} diff --git a/desktop-mcp-server/src/types.ts b/desktop-mcp-server/src/types.ts new file mode 100644 index 00000000..89bab6dd --- /dev/null +++ b/desktop-mcp-server/src/types.ts @@ -0,0 +1,129 @@ +/** + * Shared types for Shannon Desktop MCP Server + * + * Types here mirror the Temporal workflow types from the main Shannon package + * but are defined independently to avoid pulling in heavy dependencies. + */ + +// --- Pipeline types (mirrors src/temporal/shared.ts) --- + +export interface PipelineInput { + webUrl: string; + repoPath: string; + configPath?: string; + outputPath?: string; + pipelineTestingMode?: boolean; + workflowId?: string; +} + +export interface AgentMetrics { + durationMs: number; + inputTokens: number | null; + outputTokens: number | null; + costUsd: number | null; + numTurns: number | null; + model?: string | undefined; +} + +export interface PipelineSummary { + totalCostUsd: number; + totalDurationMs: number; + totalTurns: number; + agentCount: number; +} + +export interface PipelineState { + status: 'running' | 'completed' | 'failed'; + currentPhase: string | null; + currentAgent: string | null; + completedAgents: string[]; + failedAgent: string | null; + error: string | null; + startTime: number; + agentMetrics: Record; + summary: PipelineSummary | null; +} + +export interface PipelineProgress extends PipelineState { + workflowId: string; + elapsedMs: number; +} + +// --- Session metadata (mirrors src/audit/utils.ts) --- + +export interface SessionMetadata { + id: string; + webUrl: string; + repoPath?: string; + outputPath?: string; + [key: string]: unknown; +} + +// --- Config types (mirrors src/types/config.ts) --- + +export type RuleType = + | 'path' + | 'subdomain' + | 'domain' + | 'method' + | 'header' + | 'parameter'; + +export interface Rule { + description: string; + type: RuleType; + url_path: string; +} + +export interface Rules { + avoid?: Rule[]; + focus?: Rule[]; +} + +export type LoginType = 'form' | 'sso' | 'api' | 'basic'; +export type SuccessConditionType = 'url' | 'cookie' | 'element' | 'redirect'; + +export interface Authentication { + login_type: LoginType; + login_url: string; + credentials: { + username: string; + password: string; + totp_secret?: string; + }; + login_flow: string[]; + success_condition: { + type: SuccessConditionType; + value: string; + }; +} + +export interface Config { + rules?: Rules; + authentication?: Authentication; + login?: unknown; +} + +// --- MCP tool result helpers --- +// The MCP SDK CallToolResult expects `[x: string]: unknown` index signature, +// so we use a plain object type instead of strict interfaces. + +export type ToolResult = { + [key: string]: unknown; + content: Array<{ type: 'text'; text: string }>; + isError?: boolean | undefined; +}; + +export function toolSuccess(data: unknown): ToolResult { + return { + content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], + }; +} + +export function toolError(message: string, context?: Record): ToolResult { + const payload = context ? { error: message, ...context } : { error: message }; + return { + content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], + isError: true, + }; +} diff --git a/desktop-mcp-server/tsconfig.json b/desktop-mcp-server/tsconfig.json new file mode 100644 index 00000000..440d999c --- /dev/null +++ b/desktop-mcp-server/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "es2022", + "lib": ["es2022"], + "types": ["node"], + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "noEmitOnError": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "strict": true, + "noUncheckedSideEffectImports": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json index c38b5ddb..9fe62157 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "build": "tsc", + "build:mcp": "cd desktop-mcp-server && npm run build", "temporal:server": "docker compose -f docker/docker-compose.temporal.yml up temporal -d", "temporal:server:stop": "docker compose -f docker/docker-compose.temporal.yml down", "temporal:worker": "node dist/temporal/worker.js", diff --git a/shannon-architecture-playground.html b/shannon-architecture-playground.html new file mode 100644 index 00000000..8383ef0b --- /dev/null +++ b/shannon-architecture-playground.html @@ -0,0 +1,524 @@ + + + + + + Shannon MCP Architecture Explorer + + + + + + +
+
+ +
+ +
+
Data Flow
+
Tool Call
+
Temporal/gRPC
+
+ +
+
+ Select a tool to see how it flows through the system. +
+ +
+
+ + + + diff --git a/tsconfig.json b/tsconfig.json index f56a0268..a2946634 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,6 +51,7 @@ "exclude": [ "node_modules", "dist", - "mcp-server" + "mcp-server", + "desktop-mcp-server" ] }