Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/build-containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion docs/adding-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions npx/agentql-mcp/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
63 changes: 55 additions & 8 deletions scripts/mcp-scan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "..."}
]
}
```

Expand All @@ -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 <config.json>

# Legacy: positional arguments (no mock_env support)
python3 run_scan.py <command> <package_arg>
```

**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)
Expand Down Expand Up @@ -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:
Expand Down
13 changes: 11 additions & 2 deletions scripts/mcp-scan/generate_mcp_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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)
Expand Down
41 changes: 36 additions & 5 deletions scripts/mcp-scan/run_scan.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,12 +15,33 @@ def is_scanner_installed():


def main():
if len(sys.argv) < 3:
print("Usage: run_scan.py <command> <package_arg>", 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 <config.json>", file=sys.stderr)
print(" or: run_scan.py <command> <package_arg>", file=sys.stderr)
sys.exit(1)

# Determine analyzers based on environment
analyzers = ["yara"] # Always use yara (free, offline)
Expand All @@ -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
Expand Down
Loading