garibong's picture
fix: resolve MCP tool name validation error
4da0167
"""
Simple Security Scanner - Gradio App with MCP Server
A security vulnerability scanner that provides beginner-friendly explanations.
"""
import gradio as gr
from src.scanner.pattern_detector import scan_patterns
from src.scanner.sql_injection import scan_sql_injection
from src.formatter import format_results
# Sample vulnerable codes for demo
SAMPLE_CODES = {
"SQL Injection": '''import sqlite3
def get_user(username):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
return cursor.fetchone()
''',
"Hardcoded Secret": '''import requests
API_KEY = "sk-1234567890abcdef"
DATABASE_PASSWORD = "admin123"
def connect():
return requests.get(f"https://api.example.com?key={API_KEY}")
''',
"Path Traversal": '''import os
def read_file(filename):
base_path = "/var/www/uploads/"
file_path = base_path + filename
with open(file_path, 'r') as f:
return f.read()
''',
"Insecure Deserialization": '''import pickle
import base64
def load_user_data(data):
decoded = base64.b64decode(data)
return pickle.loads(decoded)
'''
}
def format_results_markdown(results: dict) -> str:
"""Format scan results as Markdown for Gradio display."""
summary = results.get("summary", {})
vulns = results.get("vulnerabilities", [])
output = []
output.append("## πŸ“Š Scan Summary\n")
output.append(f"**Total Issues Found:** {summary.get('total_issues', 0)}\n")
output.append(f"- πŸ”΄ CRITICAL: {summary.get('critical', 0)}")
output.append(f"- 🟠 HIGH: {summary.get('high', 0)}")
output.append(f"- 🟑 MEDIUM: {summary.get('medium', 0)}")
output.append(f"- 🟒 LOW: {summary.get('low', 0)}\n")
if not vulns:
output.append("βœ… **No security vulnerabilities found!**")
return "\n".join(output)
output.append("---\n")
output.append("## πŸ” Detailed Findings\n")
for i, vuln in enumerate(vulns, 1):
severity = vuln.get("severity", "UNKNOWN")
severity_emoji = {"CRITICAL": "πŸ”΄", "HIGH": "🟠", "MEDIUM": "🟑", "LOW": "🟒"}.get(severity, "βšͺ")
output.append(f"### {severity_emoji} [{i}] {vuln.get('title', 'Unknown Issue')}\n")
output.append(f"**Severity:** {severity} ")
output.append(f"**Line:** {vuln.get('line_number', 'N/A')}\n")
code_snippet = vuln.get("code_snippet", "")
if code_snippet:
output.append(f"```python\n{code_snippet}\n```\n")
explanation = vuln.get("explanation", {})
if explanation:
output.append(f"**What:** {explanation.get('what', 'N/A')}\n")
output.append(f"**Why it's dangerous:** {explanation.get('why', 'N/A')}\n")
output.append(f"**How to fix:** {explanation.get('how_to_fix', 'N/A')}\n")
example = explanation.get("example", "")
if example:
output.append(f"\n**Example:**\n```python\n{example}\n```\n")
output.append("---\n")
return "\n".join(output)
def scan_code(code: str, severity_threshold: str = "MEDIUM") -> str:
"""
Scan Python code for security vulnerabilities.
Args:
code: Python source code to analyze
severity_threshold: Minimum severity level (CRITICAL, HIGH, MEDIUM, LOW)
Returns:
Beginner-friendly explanation of found vulnerabilities
"""
if not code or not code.strip():
return "⚠️ Please enter some code to scan."
all_findings = []
try:
pattern_findings = scan_patterns("input.py", code)
all_findings.extend(pattern_findings)
except Exception:
pass
try:
sql_findings = scan_sql_injection("input.py", code)
all_findings.extend(sql_findings)
except Exception:
pass
severity_order = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1}
threshold_value = severity_order.get(severity_threshold, 2)
filtered_findings = [
f for f in all_findings
if severity_order.get(f.get("severity", "LOW"), 1) >= threshold_value
]
if not filtered_findings:
return "βœ… No security vulnerabilities found at the selected severity level!"
results = format_results(filtered_findings, severity_threshold)
return format_results_markdown(results)
# Sample loader functions - use named functions instead of lambda
def load_sample_sql_injection():
"""Load SQL Injection sample code."""
return SAMPLE_CODES["SQL Injection"]
def load_sample_hardcoded_secret():
"""Load Hardcoded Secret sample code."""
return SAMPLE_CODES["Hardcoded Secret"]
def load_sample_path_traversal():
"""Load Path Traversal sample code."""
return SAMPLE_CODES["Path Traversal"]
def load_sample_insecure_deserialization():
"""Load Insecure Deserialization sample code."""
return SAMPLE_CODES["Insecure Deserialization"]
# Mapping of sample names to loader functions
SAMPLE_LOADERS = {
"SQL Injection": load_sample_sql_injection,
"Hardcoded Secret": load_sample_hardcoded_secret,
"Path Traversal": load_sample_path_traversal,
"Insecure Deserialization": load_sample_insecure_deserialization,
}
# Full UI with Blocks (not exposed to MCP)
def create_blocks_ui():
"""Create the Blocks UI - separated to avoid MCP exposure."""
with gr.Blocks(
title="πŸ”’ Simple Security Scanner",
theme=gr.themes.Soft()
) as blocks_demo:
gr.Markdown("""
# πŸ”’ Simple Security Scanner
**Scans Python code for security vulnerabilities and provides beginner-friendly explanations.**
Also available as an MCP (Model Context Protocol) server.
""")
with gr.Row():
with gr.Column(scale=2):
code_input = gr.Code(
label="Python Code Input",
language="python",
lines=15,
)
with gr.Row():
severity_dropdown = gr.Dropdown(
choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"],
value="MEDIUM",
label="Minimum Severity",
scale=1
)
scan_btn = gr.Button("πŸ” Scan", variant="primary", scale=2)
with gr.Column(scale=1):
gr.Markdown("### πŸ“ Sample Code")
for name, loader_fn in SAMPLE_LOADERS.items():
sample_btn = gr.Button(name, size="sm")
sample_btn.click(
fn=loader_fn,
outputs=code_input,
api_name=False # Do not expose to MCP
)
output = gr.Markdown(label="Scan Results")
scan_btn.click(
fn=scan_code,
inputs=[code_input, severity_dropdown],
outputs=output,
api_name=False # Do not expose to MCP
)
gr.Markdown("""
---
### πŸ› οΈ Use as MCP Server
Connect with Claude Desktop or other MCP clients:
```json
{
"mcpServers": {
"security-scanner": {
"command": "npx",
"args": ["mcp-remote", "https://mcp-1st-birthday-simple-security-scanner.hf.space/gradio_api/mcp/sse"]
}
}
}
```
""")
return blocks_demo
# MCP Tool - gr.Interface for MCP server exposure
# Tool name must match pattern: ^[a-zA-Z0-9_-]{1,64}$
mcp_interface = gr.Interface(
fn=scan_code,
inputs=[
gr.Textbox(label="code", lines=10, placeholder="Paste Python code here..."),
gr.Dropdown(
choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"],
value="MEDIUM",
label="severity_threshold"
)
],
outputs=gr.Textbox(label="results"),
title="Security Scanner",
description="Scan Python code for security vulnerabilities and get beginner-friendly explanations.",
api_name="scan_code" # MCP tool name: only alphanumeric, underscore, hyphen allowed
)
# Create UI
blocks_demo = create_blocks_ui()
# Combine: TabbedInterface for both UI and MCP
demo = gr.TabbedInterface(
[blocks_demo, mcp_interface],
["πŸ”’ Scanner UI", "πŸ› οΈ MCP Tool"],
title="Simple Security Scanner"
)
if __name__ == "__main__":
demo.launch(mcp_server=True)