Skip to main content
Glama

Adversary MCP Server

by brettbergin
html_dashboard.py34.9 kB
"""Comprehensive HTML dashboard with rich interactive features.""" import time import webbrowser from datetime import UTC, datetime from pathlib import Path from typing import Any from jinja2 import Environment, FileSystemLoader from ..database.models import AdversaryDatabase from ..logger import get_logger from ..telemetry.service import TelemetryService logger = get_logger("dashboard") class ComprehensiveHTMLDashboard: """Rich interactive HTML dashboard with auto-browser launch.""" def __init__(self, db: AdversaryDatabase = None): self.db = db or AdversaryDatabase() self.telemetry = TelemetryService(self.db) # Dashboard configuration self.dashboard_dir = Path(__file__).parent self.templates_dir = self.dashboard_dir / "templates" self.static_dir = self.dashboard_dir / "static" self.output_dir = Path( "~/.local/share/adversary-mcp-server/dashboard" ).expanduser() # Create output directory self.output_dir.mkdir(parents=True, exist_ok=True) # Setup Jinja2 environment self.jinja_env = Environment( loader=FileSystemLoader(str(self.templates_dir)), autoescape=True, trim_blocks=True, lstrip_blocks=True, ) # Add custom filters self.jinja_env.filters["timestamp_to_datetime"] = self._timestamp_to_datetime self.jinja_env.filters["format_duration"] = self._format_duration self.jinja_env.filters["format_size"] = self._format_size self.jinja_env.filters["percentage"] = self._format_percentage self.jinja_env.filters["basename"] = self._basename def generate_and_launch_dashboard( self, hours: int = 24, auto_launch: bool = True ) -> str: """Generate comprehensive HTML dashboard and optionally launch in browser.""" # Get comprehensive data dashboard_data = self.telemetry.get_dashboard_data(hours) # Add metadata db_size_bytes = 0 if self.db.db_path.exists(): db_size_bytes = self.db.db_path.stat().st_size dashboard_data["metadata"] = { "generated_at": time.time(), "hours_covered": hours, "database_path": str(self.db.db_path), "dashboard_version": "1.0.0", "db_size_bytes": db_size_bytes, } # Generate HTML from template template = self.jinja_env.get_template("dashboard.html") # Add security headers to template data dashboard_data["security"] = { "csp_header": "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.jsdelivr.net/npm/chart.js; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" } # Inline CSS and JS content css_file = self.static_dir / "dashboard.css" js_file = self.static_dir / "dashboard.js" dashboard_data["css_content"] = ( css_file.read_text() if css_file.exists() else "" ) dashboard_data["js_content"] = js_file.read_text() if js_file.exists() else "" # Secure template rendering - validate data types and sanitize safe_dashboard_data = self._sanitize_dashboard_data(dashboard_data) html_content = template.render(**safe_dashboard_data) # Write HTML file html_file = self.output_dir / "adversary_dashboard.html" with open(html_file, "w", encoding="utf-8") as f: f.write(html_content) # Copy static assets if they exist self._copy_static_assets() # Auto-launch in browser if auto_launch: self._launch_in_browser(html_file) return str(html_file) def _copy_static_assets(self): """Copy CSS/JS static assets to output directory.""" if self.static_dir.exists(): static_output_dir = self.output_dir / "static" static_output_dir.mkdir(exist_ok=True) for static_file in self.static_dir.glob("*"): if static_file.is_file(): output_file = static_output_dir / static_file.name output_file.write_text(static_file.read_text()) def _launch_in_browser(self, html_file: Path): """Launch dashboard in default browser.""" try: file_url = f"file://{html_file.absolute()}" webbrowser.open(file_url) except Exception as e: logger.warning(f"Could not auto-launch browser: {e}") logger.info(f"Dashboard available at: {html_file}") # === JINJA2 TEMPLATE FILTERS === def _timestamp_to_datetime(self, timestamp: float) -> str: """Convert Unix timestamp to formatted datetime string.""" if not timestamp: return "N/A" dt = datetime.fromtimestamp(timestamp, tz=UTC) return dt.strftime("%Y-%m-%d %H:%M:%S UTC") def _format_duration(self, duration_ms: float) -> str: """Format duration in milliseconds to human readable format.""" if not duration_ms or duration_ms < 0: return "N/A" if duration_ms < 1000: return f"{duration_ms:.1f}ms" elif duration_ms < 60000: return f"{duration_ms/1000:.1f}s" else: minutes = int(duration_ms / 60000) seconds = (duration_ms % 60000) / 1000 return f"{minutes}m {seconds:.1f}s" def _format_size(self, size_bytes: float) -> str: """Format size in bytes to human readable format.""" if not size_bytes or size_bytes < 0: return "N/A" for unit in ["B", "KB", "MB", "GB"]: if size_bytes < 1024: return f"{size_bytes:.1f} {unit}" size_bytes /= 1024 return f"{size_bytes:.1f} TB" def _format_percentage(self, value: float) -> str: """Format decimal as percentage.""" if value is None: return "N/A" return f"{value * 100:.1f}%" def _basename(self, file_path: str) -> str: """Get basename of file path for display.""" if not file_path: return "N/A" from pathlib import Path return Path(file_path).name def _sanitize_dashboard_data(self, data: dict[str, Any]) -> dict[str, Any]: """Sanitize dashboard data to prevent injection attacks.""" import html def sanitize_value(value: Any) -> Any: """Recursively sanitize values.""" if isinstance(value, str): # HTML escape strings to prevent XSS return html.escape(value) elif isinstance(value, dict): return {k: sanitize_value(v) for k, v in value.items()} elif isinstance(value, list): return [sanitize_value(item) for item in value] elif isinstance(value, int | float | bool | type(None)): # Safe primitive types return value else: # Convert other types to safe string representation return html.escape(str(value)) # Create a copy and sanitize sanitized_data = {} for key, value in data.items(): # Only allow safe keys (alphanumeric plus underscore) safe_key = "".join(c for c in key if c.isalnum() or c == "_") if safe_key == key: # Key is already safe sanitized_data[key] = sanitize_value(value) return sanitized_data # === REAL-TIME DATA METHODS === def get_current_system_status(self) -> dict[str, Any]: """Get current system status for real-time updates.""" try: import psutil # Current system metrics cpu_percent = psutil.cpu_percent(interval=0.1) memory = psutil.virtual_memory() disk = psutil.disk_usage(str(Path.home())) # Database metrics db_size_mb = None if self.db.db_path.exists(): db_size_mb = self.db.db_path.stat().st_size / (1024 * 1024) return { "timestamp": time.time(), "cpu_percent": cpu_percent, "memory_percent": memory.percent, "memory_used_mb": memory.used / (1024 * 1024), "disk_usage_percent": disk.percent, "db_size_mb": db_size_mb, "status": ( "healthy" if cpu_percent < 80 and memory.percent < 90 else "warning" ), } except Exception as e: return {"timestamp": time.time(), "status": "error", "error": str(e)} def get_recent_activity(self, limit: int = 10) -> list[dict[str, Any]]: """Get recent activity for live feed.""" with self.telemetry.get_repository() as repo: session = repo.session # Get recent scan executions from ..database.models import MCPToolExecution, ScanEngineExecution recent_scans = ( session.query(ScanEngineExecution) .filter( ScanEngineExecution.execution_start > (time.time() - 3600) ) # Last hour .order_by(ScanEngineExecution.execution_start.desc()) .limit(limit // 2) .all() ) # Get recent MCP tool executions recent_mcp = ( session.query(MCPToolExecution) .filter(MCPToolExecution.execution_start > (time.time() - 3600)) .order_by(MCPToolExecution.execution_start.desc()) .limit(limit // 2) .all() ) activities = [] for scan in recent_scans: activities.append( { "type": "scan", "timestamp": scan.execution_start, "description": f"Scan {scan.scan_type}: {scan.target_path}", "status": "success" if scan.success else "error", "details": { "threats_found": scan.threats_found, "duration_ms": scan.total_duration_ms, }, } ) for mcp in recent_mcp: activities.append( { "type": "mcp_tool", "timestamp": mcp.execution_start, "description": f"MCP Tool: {mcp.tool_name}", "status": "success" if mcp.success else "error", "details": { "findings_count": mcp.findings_count, "duration_ms": ( (mcp.execution_end - mcp.execution_start) * 1000 if mcp.execution_end else None ), }, } ) # Sort by timestamp and limit activities.sort(key=lambda x: x["timestamp"], reverse=True) return activities[:limit] class DashboardAssetManager: """Manages dashboard static assets (CSS, JS, icons).""" def __init__(self, dashboard_dir: Path): self.dashboard_dir = dashboard_dir self.templates_dir = dashboard_dir / "templates" self.static_dir = dashboard_dir / "static" def create_default_assets(self): """Create default dashboard template and static assets.""" # Create directories self.templates_dir.mkdir(parents=True, exist_ok=True) self.static_dir.mkdir(parents=True, exist_ok=True) # Create main HTML template self._create_main_template() # Create CSS styles self._create_css_styles() # Create JavaScript functionality self._create_javascript() def _create_main_template(self): """Create the main dashboard HTML template.""" template_content = """<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Adversary MCP Server Dashboard</title> <link rel="stylesheet" href="static/dashboard.css"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <div class="dashboard-container"> <!-- Header --> <header class="dashboard-header"> <h1>Adversary MCP Server Dashboard</h1> <div class="header-meta"> <span class="generated-time">Generated: {{ metadata.generated_at | timestamp_to_datetime }}</span> <span class="coverage">Coverage: {{ metadata.hours_covered }} hours</span> </div> </header> <!-- System Status --> <section class="status-overview"> <div class="status-card system-health"> <h2>System Health</h2> <div class="health-metrics"> <div class="metric"> <span class="label">Database Size:</span> <span class="value">{{ metadata.db_size_bytes | format_size }}</span> </div> <div class="metric"> <span class="label">Total Scans:</span> <span class="value">{{ scan_engine.total_scans }}</span> </div> <div class="metric"> <span class="label">Cache Hit Rate:</span> <span class="value">{{ scan_engine.cache_hit_rate | percentage }}</span> </div> </div> </div> </section> <!-- MCP Tools Performance --> <section class="mcp-tools-section"> <h2>MCP Tools Performance</h2> <div class="tools-grid"> {% for tool in mcp_tools %} <div class="tool-card"> <h3>{{ tool.tool_name }}</h3> <div class="tool-stats"> <div class="stat"> <span class="label">Executions:</span> <span class="value">{{ tool.executions }}</span> </div> <div class="stat"> <span class="label">Success Rate:</span> <span class="value">{{ tool.success_rate | percentage }}</span> </div> <div class="stat"> <span class="label">Avg Duration:</span> <span class="value">{{ tool.avg_duration_ms | format_duration }}</span> </div> <div class="stat"> <span class="label">Findings:</span> <span class="value">{{ tool.total_findings }}</span> </div> </div> </div> {% endfor %} </div> </section> <!-- CLI Commands Performance --> <section class="cli-commands-section"> <h2>CLI Commands Performance</h2> <div class="commands-grid"> {% for cmd in cli_commands %} <div class="command-card"> <h3>{{ cmd.command_name }}</h3> <div class="command-stats"> <div class="stat"> <span class="label">Executions:</span> <span class="value">{{ cmd.executions }}</span> </div> <div class="stat"> <span class="label">Success Rate:</span> <span class="value">{{ cmd.success_rate | percentage }}</span> </div> <div class="stat"> <span class="label">Avg Duration:</span> <span class="value">{{ cmd.avg_duration_ms | format_duration }}</span> </div> </div> </div> {% endfor %} </div> </section> <!-- Scan Engine Analytics --> <section class="scan-analytics-section"> <h2>Scan Engine Analytics</h2> <div class="analytics-grid"> <div class="analytics-card"> <h3>Performance Breakdown</h3> <canvas id="scanPerformanceChart"></canvas> </div> <div class="analytics-card"> <h3>Threat Categories</h3> <div class="threat-categories"> {% for threat in threat_categories %} <div class="threat-category"> <span class="category">{{ threat.category }}</span> <span class="severity severity-{{ threat.severity }}">{{ threat.severity }}</span> <span class="count">{{ threat.count }}</span> </div> {% endfor %} </div> </div> </div> </section> <!-- Cache Performance --> <section class="cache-section"> <h2>Cache Performance</h2> <div class="cache-grid"> {% for cache in cache_performance %} <div class="cache-card"> <h3>{{ cache.cache_name }}</h3> <div class="cache-stats"> <div class="stat"> <span class="label">Hit Rate:</span> <span class="value">{{ cache.hit_rate | percentage }}</span> </div> <div class="stat"> <span class="label">Size:</span> <span class="value">{{ cache.total_size_mb | format_size }}</span> </div> <div class="stat"> <span class="label">Avg Access:</span> <span class="value">{{ cache.avg_access_time_ms | format_duration }}</span> </div> </div> </div> {% endfor %} </div> </section> <!-- Language Performance --> <section class="language-section"> <h2>Language Performance</h2> <div class="language-grid"> {% for lang in language_performance %} <div class="language-card"> <h3>{{ lang.language }}</h3> <div class="language-stats"> <div class="stat"> <span class="label">Scans:</span> <span class="value">{{ lang.scans }}</span> </div> <div class="stat"> <span class="label">Avg Duration:</span> <span class="value">{{ lang.avg_duration_ms | format_duration }}</span> </div> <div class="stat"> <span class="label">Threats Found:</span> <span class="value">{{ lang.threats_found }}</span> </div> </div> </div> {% endfor %} </div> </section> </div> <script src="static/dashboard.js"></script> <script> // Initialize charts with data const scanData = { semgrep: {{ scan_engine.avg_semgrep_duration_ms }}, llm: {{ scan_engine.avg_llm_duration_ms }}, validation: {{ scan_engine.avg_validation_duration_ms }} }; initializeScanPerformanceChart(scanData); </script> </body> </html>""" template_file = self.templates_dir / "dashboard.html" template_file.write_text(template_content) def _create_css_styles(self): """Create comprehensive CSS styles.""" css_content = """/* Adversary Dashboard Styles */ :root { --primary-color: #2563eb; --secondary-color: #64748b; --success-color: #22c55e; --warning-color: #f59e0b; --error-color: #ef4444; --background-color: #f8fafc; --card-background: #ffffff; --text-primary: #1e293b; --text-secondary: #64748b; --border-color: #e2e8f0; --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: var(--background-color); color: var(--text-primary); line-height: 1.6; } .dashboard-container { max-width: 98%; margin: 0 auto; padding: 20px; width: 100%; display: flex; flex-direction: column; align-items: center; } /* Header */ .dashboard-header { background: var(--card-background); border-radius: 12px; padding: 24px; margin-bottom: 32px; box-shadow: var(--shadow); display: flex; justify-content: space-between; align-items: center; } .dashboard-header h1 { font-size: 2rem; font-weight: 700; color: var(--primary-color); } .header-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; } .header-meta span { font-size: 0.875rem; color: var(--text-secondary); } /* Status Overview */ .status-overview { margin-bottom: 32px; } .status-card { background: var(--card-background); border-radius: 12px; padding: 24px; box-shadow: var(--shadow); } .status-card h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 20px; color: var(--text-primary); } .health-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; } .metric { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--background-color); border-radius: 8px; } .metric .label { font-weight: 500; color: var(--text-secondary); } .metric .value { font-weight: 600; font-size: 1.125rem; color: var(--primary-color); } /* Section Styles */ section { margin-bottom: 32px; } section h2 { font-size: 1.75rem; font-weight: 600; margin-bottom: 24px; color: var(--text-primary); } /* Grid Layouts */ .tools-grid, .commands-grid, .cache-grid, .language-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } .analytics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; } /* Card Styles */ .tool-card, .command-card, .cache-card, .language-card, .analytics-card { background: var(--card-background); border-radius: 12px; padding: 20px; box-shadow: var(--shadow); transition: transform 0.2s, box-shadow 0.2s; } .tool-card:hover, .command-card:hover, .cache-card:hover, .language-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } .tool-card h3, .command-card h3, .cache-card h3, .language-card h3, .analytics-card h3 { font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: var(--text-primary); border-bottom: 2px solid var(--border-color); padding-bottom: 8px; } /* Stats Grid */ .tool-stats, .command-stats, .cache-stats, .language-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .stat { display: flex; flex-direction: column; padding: 8px; background: var(--background-color); border-radius: 6px; } .stat .label { font-size: 0.75rem; font-weight: 500; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.025em; } .stat .value { font-size: 1.125rem; font-weight: 600; color: var(--primary-color); margin-top: 2px; } /* Threat Categories */ .threat-categories { display: flex; flex-direction: column; gap: 8px; } .threat-category { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--background-color); border-radius: 6px; } .threat-category .category { font-weight: 500; flex: 1; } .severity { padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; } .severity-low { background: #dcfce7; color: #166534; } .severity-medium { background: #fef3c7; color: #92400e; } .severity-high { background: #fed7d7; color: #c53030; } .severity-critical { background: #fde8e8; color: #9b2c2c; } .threat-category .count { font-weight: 600; color: var(--primary-color); min-width: 40px; text-align: right; } /* Chart Container */ canvas { max-height: 300px; width: 100% !important; height: auto !important; } /* Responsive Design */ @media (max-width: 768px) { .dashboard-header { flex-direction: column; align-items: flex-start; gap: 16px; } .header-meta { align-items: flex-start; } .tools-grid, .commands-grid, .cache-grid, .language-grid { grid-template-columns: 1fr; } .analytics-grid { grid-template-columns: 1fr; } .tool-stats, .command-stats, .cache-stats, .language-stats { grid-template-columns: 1fr; } } /* Loading and Animation */ .loading { display: flex; justify-content: center; align-items: center; padding: 40px; } .spinner { border: 3px solid var(--border-color); border-top: 3px solid var(--primary-color); border-radius: 50%; width: 32px; height: 32px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Threat Findings Table Styles */ .threat-findings-section { margin-bottom: 32px; width: 100%; display: flex; flex-direction: column; align-items: center; } .threat-findings-section h2 { text-align: center; margin-bottom: 24px; } .threat-findings-table-container { background: var(--card-background); border-radius: 12px; padding: 32px; box-shadow: var(--shadow); overflow: hidden; width: 100%; max-width: none; } .table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; } .threat-findings-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.9rem; background: var(--card-background); table-layout: fixed; } .threat-findings-table th { background: var(--background-color); color: var(--text-primary); font-weight: 600; padding: 18px 16px; text-align: left; border-bottom: 2px solid var(--border-color); border-right: 1px solid var(--border-color); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.025em; white-space: nowrap; } .threat-findings-table th:last-child { border-right: none; } .threat-findings-table td { padding: 18px 16px; border-bottom: 1px solid var(--border-color); border-right: 1px solid var(--border-color); vertical-align: middle; line-height: 1.5; word-wrap: break-word; overflow: hidden; } .threat-findings-table td:last-child { border-right: none; } .threat-row:hover { background: var(--background-color); } /* Column widths - optimized for wider screens */ .col-uuid { width: 12%; } .col-time { width: 15%; } .col-title { width: 28%; } .col-severity { width: 8%; } .col-file { width: 15%; } .col-line { width: 6%; } .col-scanner { width: 8%; } .col-confidence { width: 10%; } .col-status { width: 8%; } /* Title content */ .title-content { font-weight: 500; color: var(--text-primary); line-height: 1.4; } /* Severity badges */ .severity-badge { padding: 4px 8px; border-radius: 6px; font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.025em; } .severity-critical { background: #fde8e8; color: #9b2c2c; } .severity-high { background: #fed7d7; color: #c53030; } .severity-medium { background: #fef3c7; color: #92400e; } .severity-low { background: #dcfce7; color: #166534; } /* Category tags */ .category-tag { padding: 3px 6px; background: var(--background-color); border-radius: 4px; font-size: 0.75rem; color: var(--text-secondary); border: 1px solid var(--border-color); } /* File info */ .file-info { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.75rem; color: var(--text-secondary); background: var(--background-color); padding: 2px 6px; border-radius: 4px; } /* Scanner badges */ .scanner-badge { padding: 3px 6px; border-radius: 4px; font-size: 0.6875rem; font-weight: 500; text-transform: lowercase; } .scanner-semgrep { background: #e0f2fe; color: #0277bd; } .scanner-llm { background: #f3e5f5; color: #7b1fa2; } /* Confidence bar */ .confidence-bar { position: relative; width: 80px; height: 18px; background: var(--background-color); border-radius: 9px; overflow: hidden; border: 1px solid var(--border-color); } .confidence-fill { height: 100%; background: linear-gradient(90deg, #ef4444, #f59e0b, #22c55e); transition: width 0.3s ease; } .confidence-text { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; font-size: 0.625rem; font-weight: 600; color: var(--text-primary); } /* Status badges */ .status-badge { padding: 3px 6px; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; text-align: center; min-width: 24px; display: inline-block; } .status-badge.false-positive { background: #fef3c7; color: #92400e; } .status-badge.validated { background: #dcfce7; color: #166534; } .status-badge.unvalidated { background: var(--background-color); color: var(--text-secondary); border: 1px solid var(--border-color); } /* UUID display */ .uuid-short { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.6875rem; padding: 2px 4px; background: var(--background-color); border: 1px solid var(--border-color); border-radius: 3px; color: var(--text-secondary); } /* Time info */ .time-info { font-size: 0.75rem; color: var(--text-secondary); white-space: nowrap; } /* No findings state */ .no-findings { text-align: center; padding: 40px 20px; color: var(--text-secondary); } .no-findings .hint { font-size: 0.875rem; margin-top: 8px; color: var(--text-secondary); } /* Responsive adjustments */ @media (max-width: 1200px) { .threat-findings-table { font-size: 0.8125rem; } .threat-findings-table th, .threat-findings-table td { padding: 8px 6px; } } @media (max-width: 768px) { .col-title { width: 30%; } .col-category { display: none; } .col-confidence { display: none; } .col-time { display: none; } }""" css_file = self.static_dir / "dashboard.css" css_file.write_text(css_content) def _create_javascript(self): """Create dashboard JavaScript functionality.""" js_content = """// Dashboard JavaScript Functionality document.addEventListener('DOMContentLoaded', function() { console.log('Adversary Dashboard loaded'); // Auto-refresh every 5 minutes setInterval(() => { if (confirm('Refresh dashboard data?')) { location.reload(); } }, 300000); }); function initializeScanPerformanceChart(data) { const ctx = document.getElementById('scanPerformanceChart'); if (!ctx) return; new Chart(ctx, { type: 'doughnut', data: { labels: ['Semgrep Analysis', 'LLM Analysis', 'Validation'], datasets: [{ data: [data.semgrep || 0, data.llm || 0, data.validation || 0], backgroundColor: [ '#2563eb', '#f59e0b', '#22c55e' ], borderWidth: 2, borderColor: '#ffffff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.parsed || 0; return `${label}: ${value.toFixed(1)}ms`; } } } } } }); } // Utility functions for data formatting function formatDuration(ms) { if (!ms || ms < 0) return 'N/A'; if (ms < 1000) return `${ms.toFixed(1)}ms`; if (ms < 60000) return `${(ms/1000).toFixed(1)}s`; const minutes = Math.floor(ms / 60000); const seconds = ((ms % 60000) / 1000).toFixed(1); return `${minutes}m ${seconds}s`; } function formatSize(bytes) { if (!bytes || bytes < 0) return 'N/A'; const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } function formatPercentage(value) { if (value === null || value === undefined) return 'N/A'; return `${(value * 100).toFixed(1)}%`; }""" js_file = self.static_dir / "dashboard.js" js_file.write_text(js_content) # Initialize dashboard assets when module is loaded def ensure_dashboard_assets(): """Ensure dashboard assets exist, create if missing.""" dashboard_dir = Path(__file__).parent asset_manager = DashboardAssetManager(dashboard_dir) if not (dashboard_dir / "templates" / "dashboard.html").exists(): asset_manager.create_default_assets() # Create assets on import ensure_dashboard_assets()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/brettbergin/adversary-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server