diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 9a4aea2..32518e6 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -224,21 +224,22 @@ jobs: run: | echo "🔍 Scanning MCP server: ${{ steps.meta.outputs.server_name }}" - # Generate config (outputs JSON with command/args) + # Generate config (outputs JSON with command/args/mock_env) config_json=$(python3 scripts/mcp-scan/generate_mcp_config.py \ "${{ matrix.config }}" \ "${{ steps.meta.outputs.protocol }}" \ "${{ steps.meta.outputs.server_name }}") - command=$(echo "$config_json" | jq -r '.command') - args=$(echo "$config_json" | jq -r '.args') + # Write config to file for run_scan.py + scan_config="/tmp/scan-config-${{ steps.meta.outputs.server_name }}.json" + echo "$config_json" > "$scan_config" # Run scan using Cisco AI Defense mcp-scanner # Note: stderr is redirected to a separate file to avoid corrupting JSON output scan_output="/tmp/mcp-scan-${{ steps.meta.outputs.server_name }}.json" scan_stderr="/tmp/mcp-scan-${{ steps.meta.outputs.server_name }}.stderr" - if python3 scripts/mcp-scan/run_scan.py "$command" "$args" \ + if python3 scripts/mcp-scan/run_scan.py --config "$scan_config" \ > "$scan_output" 2> "$scan_stderr"; then echo "scan_passed=true" >> $GITHUB_OUTPUT else diff --git a/docs/adding-servers.md b/docs/adding-servers.md index cfc99b0..d49ebc2 100644 --- a/docs/adding-servers.md +++ b/docs/adding-servers.md @@ -54,11 +54,17 @@ provenance: # Optional but recommended repository: "user/repo" # Publisher repository workflow: "release.yml" # Publishing workflow (optional) -# Optional: Security scan allowlist +# Optional: Security scan configuration security: + # Allowlist for known false positives or acceptable issues allowed_issues: - code: "AITech-1.1" reason: "Explanation of why this issue is acceptable" + # Mock environment variables for servers that require them during scanning + mock_env: + - name: API_URL + value: "https://mock-api.example.com" + description: "Required for server startup - mock value for scanning" ``` ## Protocol-Specific Examples @@ -222,6 +228,27 @@ security: reason: "Destructive flow mitigated by container sandboxing" ``` +### Servers Requiring Environment Variables + +Some MCP servers require environment variables to start (e.g., API URLs, tokens). Since the security scanner needs to start the server to discover its tools, you can provide mock values that allow the server to start without functional credentials: + +```yaml +security: + mock_env: + - name: SEARXNG_URL + value: "https://mock-searxng.example.com" + description: "SearXNG instance URL - mock for scanning" + - name: API_TOKEN + value: "mock-token-for-scanning-00000000" + description: "API token - mock value, not a real credential" +``` + +**Important notes about mock_env:** +- Mock values are **not secrets** - they are committed to the repository +- Values should be obviously fake (use `mock-`, placeholder UUIDs, example.com domains) +- Purpose is to allow server startup for scanning, not functional operation +- Servers still need to pass security scans or allowlist known issues + See [Security Overview](security.md) for more details on what we scan for. ### After Merge diff --git a/npx/agentql-mcp/spec.yaml b/npx/agentql-mcp/spec.yaml index 8bb5739..f6ecbcb 100644 --- a/npx/agentql-mcp/spec.yaml +++ b/npx/agentql-mcp/spec.yaml @@ -19,5 +19,9 @@ provenance: # Security configuration security: - # Server requires AGENTQL_API_KEY to start - cannot be scanned in CI - insecure_ignore: true + # Mock env vars allow security scanning without real credentials + mock_env: + - name: AGENTQL_API_KEY + value: "mock-agentql-api-key-for-scanning" + description: "AgentQL API key - mock value for security scanning" + allowed_issues: [] diff --git a/scripts/mcp-scan/README.md b/scripts/mcp-scan/README.md index 7462d14..ca21a04 100644 --- a/scripts/mcp-scan/README.md +++ b/scripts/mcp-scan/README.md @@ -19,12 +19,25 @@ python3 generate_mcp_config.py npx/context7/spec.yaml npx context7 ``` **Output:** -Outputs a JSON configuration with command/args for mcp-scanner: +Outputs a JSON configuration with command/args/mock_env for mcp-scanner: ```json { "command": "npx", "args": "@upstash/context7-mcp@2.1.0", - "server_name": "context7" + "server_name": "context7", + "mock_env": [] +} +``` + +For servers with `security.mock_env` defined in spec.yaml: +```json +{ + "command": "npx", + "args": "mcp-searxng@0.8.0", + "server_name": "mcp-searxng", + "mock_env": [ + {"name": "SEARXNG_URL", "value": "https://mock-searxng.example.com", "description": "..."} + ] } ``` @@ -34,14 +47,35 @@ Wrapper script to run Cisco AI Defense mcp-scanner with proper configuration. **Usage:** ```bash +# Recommended: config file mode (supports mock_env) +python3 run_scan.py --config + +# Legacy: positional arguments (no mock_env support) python3 run_scan.py ``` **Example:** ```bash +# Using config file (recommended) +python3 run_scan.py --config /tmp/scan-config.json + +# Legacy mode python3 run_scan.py npx "@upstash/context7-mcp@2.1.0" ``` +**Config file format:** +```json +{ + "command": "npx", + "args": "mcp-searxng@0.8.0", + "mock_env": [ + {"name": "SEARXNG_URL", "value": "https://mock.example.com"} + ] +} +``` + +When `mock_env` is provided, the script passes `--stdio-env KEY=VALUE` arguments to mcp-scanner for each entry, allowing servers that require environment variables to start and be scanned. + **Environment Variables:** - `MCP_SCANNER_ENABLE_LLM`: Set to `true` to enable LLM analyzer (optional) - `MCP_SCANNER_LLM_API_KEY`: API key for LLM provider (required if LLM enabled) @@ -123,18 +157,31 @@ To test the scanning process locally: uv tool install cisco-ai-mcp-scanner pip install pyyaml -# Generate config -config_json=$(python3 scripts/mcp-scan/generate_mcp_config.py npx/context7/spec.yaml npx context7) -command=$(echo "$config_json" | jq -r '.command') -args=$(echo "$config_json" | jq -r '.args') +# Generate config and save to file +python3 scripts/mcp-scan/generate_mcp_config.py npx/context7/spec.yaml npx context7 > /tmp/scan-config.json -# Run scan -python3 scripts/mcp-scan/run_scan.py "$command" "$args" > /tmp/scan-output.json +# Run scan using config file +python3 scripts/mcp-scan/run_scan.py --config /tmp/scan-config.json > /tmp/scan-output.json # Process results python3 scripts/mcp-scan/process_scan_results.py /tmp/scan-output.json context7 npx/context7/spec.yaml ``` +### Testing with Mock Environment Variables + +For servers that require environment variables: + +```bash +# Generate config (will include mock_env if defined in spec.yaml) +python3 scripts/mcp-scan/generate_mcp_config.py npx/mcp-searxng/spec.yaml npx mcp-searxng > /tmp/scan-config.json + +# Verify mock_env is in the config +cat /tmp/scan-config.json | jq '.mock_env' + +# Run scan - mock_env values will be passed to mcp-scanner via --stdio-env +python3 scripts/mcp-scan/run_scan.py --config /tmp/scan-config.json > /tmp/scan-output.json +``` + ## Analyzers By default, only the YARA analyzer is used (free, offline). To enable additional analysis: diff --git a/scripts/mcp-scan/generate_mcp_config.py b/scripts/mcp-scan/generate_mcp_config.py index 34911c8..a9b8591 100644 --- a/scripts/mcp-scan/generate_mcp_config.py +++ b/scripts/mcp-scan/generate_mcp_config.py @@ -23,6 +23,9 @@ def main(): package = data['spec']['package'] version = data['spec'].get('version', 'latest') + # Extract mock_env from security section (for MCP servers requiring env vars) + mock_env = data.get('security', {}).get('mock_env', []) + if protocol in ['npx', 'uvx']: command = protocol args = f"{package}@{version}" @@ -33,8 +36,14 @@ def main(): print(f"Error: Unknown protocol {protocol}", file=sys.stderr) sys.exit(1) - # Output JSON with command info - print(json.dumps({"command": command, "args": args, "server_name": server_name})) + # Output JSON with command info and mock_env for security scanning + output = { + "command": command, + "args": args, + "server_name": server_name, + "mock_env": mock_env + } + print(json.dumps(output)) except FileNotFoundError: print(f"Error: File {config_file} not found", file=sys.stderr) diff --git a/scripts/mcp-scan/run_scan.py b/scripts/mcp-scan/run_scan.py index 67e6f76..afb4945 100644 --- a/scripts/mcp-scan/run_scan.py +++ b/scripts/mcp-scan/run_scan.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Wrapper script to run Cisco AI Defense mcp-scanner.""" +import argparse +import json import shutil import subprocess import sys @@ -13,12 +15,33 @@ def is_scanner_installed(): def main(): - if len(sys.argv) < 3: - print("Usage: run_scan.py ", file=sys.stderr) - sys.exit(1) + parser = argparse.ArgumentParser(description="Run Cisco AI Defense mcp-scanner") + parser.add_argument("--config", type=str, help="Path to JSON config file") + # Legacy positional arguments for backwards compatibility + parser.add_argument("command", nargs="?", help="Command to run (e.g., 'npx')") + parser.add_argument("package_arg", nargs="?", help="Package argument (e.g., '@playwright/mcp@0.0.55')") + args = parser.parse_args() - command = sys.argv[1] # e.g., "npx" - package_arg = sys.argv[2] # e.g., "@playwright/mcp@0.0.55" + # Load config from file or use legacy positional arguments + if args.config: + try: + with open(args.config, 'r') as f: + config = json.load(f) + command = config.get("command") + package_arg = config.get("args") + mock_env = config.get("mock_env", []) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error reading config file: {e}", file=sys.stderr) + sys.exit(1) + elif args.command and args.package_arg: + # Legacy mode: positional arguments + command = args.command + package_arg = args.package_arg + mock_env = [] + else: + print("Usage: run_scan.py --config ", file=sys.stderr) + print(" or: run_scan.py ", file=sys.stderr) + sys.exit(1) # Determine analyzers based on environment analyzers = ["yara"] # Always use yara (free, offline) @@ -41,6 +64,14 @@ def main(): "--stdio-arg", package_arg ] + # Add mock environment variables for servers that require them + # mcp-scanner supports --stdio-env KEY=VALUE (can be repeated) + for env_var in mock_env: + name = env_var.get("name") + value = env_var.get("value") + if name and value: + scanner_args.extend(["--stdio-env", f"{name}={value}"]) + # Use installed mcp-scanner if available (faster), otherwise use uv run --with # CI installs with: uv tool install cisco-ai-mcp-scanner # Local without setup can use: uv run --with cisco-ai-mcp-scanner