Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| MCP server for detect-secrets - a tool for detecting secrets in code | |
| """ | |
| import gradio as gr | |
| import subprocess | |
| import json | |
| import os | |
| import tempfile | |
| from typing import Dict, List, Optional | |
| from pathlib import Path | |
| def detect_secrets_scan( | |
| code_input: str, | |
| scan_type: str = "code", | |
| base64_limit: float = 3.0, | |
| hex_limit: float = 2.0, | |
| exclude_lines: str = "", | |
| exclude_files: str = "", | |
| exclude_secrets: str = "", | |
| word_list: str = "", | |
| output_format: str = "json" | |
| ) -> Dict: | |
| """ | |
| Scans code for secrets using detect-secrets. | |
| Args: | |
| code_input (str): Code to scan or path to file/directory | |
| scan_type (str): Scan type - 'code' for direct code or 'path' for file/directory | |
| base64_limit (float): Entropy limit for base64 strings (0.0-8.0) | |
| hex_limit (float): Entropy limit for hex strings (0.0-8.0) | |
| exclude_lines (str): Regex pattern for lines to exclude | |
| exclude_files (str): Regex pattern for files to exclude | |
| exclude_secrets (str): Regex pattern for secrets to exclude | |
| word_list (str): Path to word list file | |
| output_format (str): Output format - 'json' or 'txt' | |
| Returns: | |
| Dict: Scan results | |
| """ | |
| try: | |
| print(f"Debug: Input code length: {len(code_input)}") | |
| print(f"Debug: First 100 chars: {code_input[:100]}") | |
| # Build detect-secrets command | |
| cmd = ["detect-secrets", "scan"] | |
| # Add entropy limits | |
| cmd.extend(["--base64-limit", str(base64_limit)]) | |
| cmd.extend(["--hex-limit", str(hex_limit)]) | |
| # Add exclude patterns | |
| if exclude_lines: | |
| cmd.extend(["--exclude-lines", exclude_lines]) | |
| if exclude_files: | |
| cmd.extend(["--exclude-files", exclude_files]) | |
| if exclude_secrets: | |
| cmd.extend(["--exclude-secrets", exclude_secrets]) | |
| if word_list: | |
| cmd.extend(["--word-list", word_list]) | |
| # Добавляем параметры для улучшения обнаружения | |
| cmd.extend(["--force-use-all-plugins"]) # Принудительно используем все плагины | |
| cmd.extend(["--no-verify"]) # Отключаем верификацию | |
| cmd.extend(["--disable-filter", "detect_secrets.filters.gibberish.should_exclude_secret"]) # Отключаем фильтр бессмысленного текста | |
| cmd.extend(["--disable-filter", "detect_secrets.filters.heuristic.is_likely_id_string"]) # Отключаем фильтр ID строк | |
| cmd.extend(["--disable-filter", "detect_secrets.filters.heuristic.is_sequential_string"]) # Отключаем фильтр последовательных строк | |
| # Execute command with pipe | |
| if scan_type == "code": | |
| # Нормализуем строки | |
| lines = code_input.replace('\r\n', '\n').replace('\r', '\n').split('\n') | |
| print(f"Debug: Number of lines: {len(lines)}") | |
| all_results = {} | |
| all_plugins = set() | |
| for idx, line in enumerate(lines): | |
| if not line.strip(): | |
| continue # пропускаем пустые строки | |
| print(f"Debug: Scanning line {idx + 1}: {line}") | |
| cmd_line = cmd.copy() # копия базовой команды | |
| cmd_line.append("--string") | |
| cmd_line.append(line) | |
| print(f"Debug: Command: {' '.join(cmd_line)}") | |
| result = subprocess.run(cmd_line, capture_output=True, text=True) | |
| stdout, stderr = result.stdout, result.stderr | |
| print(f"Debug: Line {idx + 1} stdout: {stdout}") | |
| # Парсим текстовый вывод | |
| for output_line in stdout.split('\n'): | |
| if ':' in output_line: | |
| plugin, result = output_line.split(':', 1) | |
| plugin = plugin.strip() | |
| result = result.strip() | |
| # Добавляем плагин в список использованных | |
| all_plugins.add(plugin) | |
| # Если плагин нашел секрет | |
| if result.lower() == 'true': | |
| # Добавляем результат | |
| if plugin not in all_results: | |
| all_results[plugin] = [] | |
| # Создаем запись о найденном секрете | |
| secret_info = { | |
| "type": plugin, | |
| "line_number": idx + 1, | |
| "line": line, | |
| "hashed_secret": f"hash_{line}", | |
| "is_secret": True, | |
| "is_verified": False | |
| } | |
| # Добавляем энтропию, если она указана | |
| if '(' in result: | |
| entropy = result.split('(')[1].split(')')[0] | |
| try: | |
| secret_info["entropy"] = float(entropy) | |
| except ValueError: | |
| pass | |
| all_results[plugin].append(secret_info) | |
| # Собираем финальный результат | |
| final_output = { | |
| "version": "1.5.0", | |
| "plugins_used": [{"name": plugin} for plugin in sorted(all_plugins)], | |
| "filters_used": [], | |
| "results": all_results, | |
| "generated_at": "" | |
| } | |
| print(f"Debug: Final results: {json.dumps(final_output, indent=2)}") | |
| return { | |
| "success": True, | |
| "results": final_output, | |
| "stderr": "", | |
| "return_code": 0 | |
| } | |
| else: | |
| # Для сканирования файла/директории используем обычный способ | |
| if not os.path.exists(code_input): | |
| return { | |
| "error": f"Path not found: {code_input}", | |
| "success": False | |
| } | |
| cmd.append(code_input) | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| stdout, stderr = result.stdout, result.stderr | |
| return_code = result.returncode | |
| # Process result | |
| if output_format == "json": | |
| try: | |
| output_data = json.loads(stdout) if stdout else {} | |
| return { | |
| "success": True, | |
| "results": output_data, | |
| "stderr": stderr, | |
| "return_code": return_code | |
| } | |
| except json.JSONDecodeError as e: | |
| print(f"Debug: JSON parse error: {e}") | |
| print(f"Debug: Raw stdout: {stdout}") | |
| return { | |
| "success": False, | |
| "error": "JSON parsing error", | |
| "stdout": stdout, | |
| "stderr": stderr, | |
| "return_code": return_code | |
| } | |
| else: | |
| return { | |
| "success": True, | |
| "output": stdout, | |
| "stderr": stderr, | |
| "return_code": return_code | |
| } | |
| except Exception as e: | |
| print(f"Debug: Exception: {str(e)}") | |
| return { | |
| "success": False, | |
| "error": f"Error executing detect-secrets: {str(e)}" | |
| } | |
| def detect_secrets_baseline( | |
| target_path: str, | |
| baseline_file: str, | |
| base64_limit: float = 4.5, | |
| hex_limit: float = 3.0 | |
| ) -> Dict: | |
| """ | |
| Creates or updates a baseline file for detect-secrets. | |
| Args: | |
| target_path (str): Path to code for analysis | |
| baseline_file (str): Path to baseline file | |
| base64_limit (float): Entropy limit for base64 strings | |
| hex_limit (float): Entropy limit for hex strings | |
| Returns: | |
| Dict: Result of baseline creation/update | |
| """ | |
| try: | |
| if not os.path.exists(target_path): | |
| return { | |
| "error": f"Path not found: {target_path}", | |
| "success": False | |
| } | |
| # Build command | |
| cmd = ["detect-secrets", "scan"] | |
| # Add entropy limits | |
| cmd.extend(["--base64-limit", str(base64_limit)]) | |
| cmd.extend(["--hex-limit", str(hex_limit)]) | |
| # Add baseline file if exists | |
| if os.path.exists(baseline_file): | |
| cmd.extend(["--baseline", baseline_file]) | |
| # Add scan target | |
| cmd.append(target_path) | |
| # Execute command | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| # Save output to baseline file | |
| with open(baseline_file, 'w') as f: | |
| f.write(result.stdout) | |
| return { | |
| "success": True, | |
| "action": "created" if not os.path.exists(baseline_file) else "updated", | |
| "message": f"Baseline file {'created' if not os.path.exists(baseline_file) else 'updated'}: {baseline_file}", | |
| "return_code": result.returncode, | |
| "stderr": result.stderr | |
| } | |
| except Exception as e: | |
| return { | |
| "success": False, | |
| "error": f"Error working with baseline: {str(e)}" | |
| } | |
| def detect_secrets_audit( | |
| baseline_file: str, | |
| show_stats: bool = False, | |
| show_report: bool = False, | |
| only_real: bool = False, | |
| only_false: bool = False | |
| ) -> Dict: | |
| """ | |
| Audits a detect-secrets baseline file. | |
| Args: | |
| baseline_file (str): Path to baseline file | |
| show_stats (bool): Show statistics | |
| show_report (bool): Show report | |
| only_real (bool): Only show real secrets | |
| only_false (bool): Only show false positives | |
| Returns: | |
| Dict: Audit results | |
| """ | |
| try: | |
| if not os.path.exists(baseline_file): | |
| return { | |
| "error": f"Baseline file not found: {baseline_file}", | |
| "success": False | |
| } | |
| # Build command | |
| cmd = ["detect-secrets", "audit"] | |
| if show_stats: | |
| cmd.append("--stats") | |
| if show_report: | |
| cmd.append("--report") | |
| if only_real: | |
| cmd.append("--only-real") | |
| if only_false: | |
| cmd.append("--only-false") | |
| cmd.append(baseline_file) | |
| # Execute command | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| return { | |
| "success": True, | |
| "output": result.stdout, | |
| "stderr": result.stderr, | |
| "return_code": result.returncode | |
| } | |
| except Exception as e: | |
| return { | |
| "success": False, | |
| "error": f"Error auditing baseline: {str(e)}" | |
| } | |
| # Create Gradio interface | |
| with gr.Blocks(title="Detect Secrets MCP") as demo: | |
| gr.Markdown("# 🔍 Detect Secrets Scanner") | |
| gr.Markdown("Secret detection tool with MCP support") | |
| with gr.Tab("Basic Scanning"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| scan_type = gr.Radio( | |
| choices=["code", "path"], | |
| value="code", | |
| label="Scan Type" | |
| ) | |
| code_input = gr.Textbox( | |
| lines=10, | |
| placeholder="Enter code or path to scan...", | |
| label="Code or Path" | |
| ) | |
| base64_limit = gr.Slider( | |
| minimum=0.0, | |
| maximum=8.0, | |
| value=4.5, | |
| step=0.1, | |
| label="Base64 Entropy Limit" | |
| ) | |
| hex_limit = gr.Slider( | |
| minimum=0.0, | |
| maximum=8.0, | |
| value=3.0, | |
| step=0.1, | |
| label="Hex Entropy Limit" | |
| ) | |
| exclude_lines = gr.Textbox( | |
| label="Exclude Lines Pattern (regex)" | |
| ) | |
| exclude_files = gr.Textbox( | |
| label="Exclude Files Pattern (regex)" | |
| ) | |
| exclude_secrets = gr.Textbox( | |
| label="Exclude Secrets Pattern (regex)" | |
| ) | |
| word_list = gr.Textbox( | |
| label="Word List File Path" | |
| ) | |
| output_format = gr.Dropdown( | |
| choices=["json", "txt"], | |
| value="json", | |
| label="Output Format" | |
| ) | |
| scan_btn = gr.Button("🔍 Scan", variant="primary") | |
| with gr.Column(): | |
| scan_output = gr.JSON(label="Scan Results") | |
| scan_btn.click( | |
| fn=detect_secrets_scan, | |
| inputs=[ | |
| code_input, scan_type, base64_limit, hex_limit, | |
| exclude_lines, exclude_files, exclude_secrets, | |
| word_list, output_format | |
| ], | |
| outputs=scan_output | |
| ) | |
| with gr.Tab("Baseline Management"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| baseline_path = gr.Textbox( | |
| label="Project Path", | |
| placeholder="/path/to/your/project" | |
| ) | |
| baseline_file = gr.Textbox( | |
| label="Baseline File Path", | |
| placeholder="/path/to/.secrets.baseline" | |
| ) | |
| baseline_base64_limit = gr.Slider( | |
| minimum=0.0, | |
| maximum=8.0, | |
| value=4.5, | |
| step=0.1, | |
| label="Base64 Entropy Limit" | |
| ) | |
| baseline_hex_limit = gr.Slider( | |
| minimum=0.0, | |
| maximum=8.0, | |
| value=3.0, | |
| step=0.1, | |
| label="Hex Entropy Limit" | |
| ) | |
| baseline_btn = gr.Button("📋 Create/Update Baseline", variant="secondary") | |
| with gr.Column(): | |
| baseline_output = gr.JSON(label="Baseline Results") | |
| baseline_btn.click( | |
| fn=detect_secrets_baseline, | |
| inputs=[ | |
| baseline_path, baseline_file, | |
| baseline_base64_limit, baseline_hex_limit | |
| ], | |
| outputs=baseline_output | |
| ) | |
| with gr.Tab("Baseline Audit"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| audit_baseline = gr.Textbox( | |
| label="Baseline File Path", | |
| placeholder="/path/to/.secrets.baseline" | |
| ) | |
| show_stats = gr.Checkbox( | |
| label="Show Statistics", | |
| value=False | |
| ) | |
| show_report = gr.Checkbox( | |
| label="Show Report", | |
| value=False | |
| ) | |
| only_real = gr.Checkbox( | |
| label="Only Real Secrets", | |
| value=False | |
| ) | |
| only_false = gr.Checkbox( | |
| label="Only False Positives", | |
| value=False | |
| ) | |
| audit_btn = gr.Button("🔍 Audit Baseline", variant="secondary") | |
| with gr.Column(): | |
| audit_output = gr.JSON(label="Audit Results") | |
| audit_btn.click( | |
| fn=detect_secrets_audit, | |
| inputs=[ | |
| audit_baseline, show_stats, | |
| show_report, only_real, only_false | |
| ], | |
| outputs=audit_output | |
| ) | |
| with gr.Tab("Examples"): | |
| gr.Markdown(""" | |
| ## 🚨 Examples of secrets that can be detected: | |
| ### 1. API Keys | |
| ```python | |
| API_KEY = "sk_live_51H1h2K3L4M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8" | |
| ``` | |
| ### 2. Passwords | |
| ```python | |
| password = "SuperSecret123!" # High entropy string | |
| ``` | |
| ### 3. Private Keys | |
| ```python | |
| private_key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA..." | |
| ``` | |
| ### 4. OAuth Tokens | |
| ```python | |
| oauth_token = "ya29.a0AfB_byC..." | |
| ``` | |
| """) | |
| if __name__ == "__main__": | |
| # Получаем настройки сервера из переменных окружения | |
| server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0") | |
| server_port = int(os.getenv("GRADIO_SERVER_PORT", "7862")) | |
| demo.launch( | |
| mcp_server=True, | |
| server_name=server_name, | |
| server_port=server_port, | |
| share=False | |
| ) |