pulseengine_mcp_auth/monitoring/
dashboard_server.rs

1//! Security Dashboard HTTP Server
2//!
3//! This module provides an HTTP server for the security dashboard with
4//! REST API endpoints and real-time WebSocket updates.
5
6use crate::monitoring::{SecurityDashboard, SecurityEventType, SecurityMonitor};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::net::SocketAddr;
10use std::sync::Arc;
11use thiserror::Error;
12use tracing::{debug, error, info};
13
14/// Errors that can occur in the dashboard server
15#[derive(Debug, Error)]
16pub enum DashboardError {
17    #[error("Server error: {0}")]
18    ServerError(String),
19
20    #[error("Authentication failed")]
21    AuthenticationFailed,
22
23    #[error("Authorization failed")]
24    AuthorizationFailed,
25
26    #[error("Invalid request: {reason}")]
27    InvalidRequest { reason: String },
28
29    #[error("Monitoring error: {0}")]
30    MonitoringError(String),
31}
32
33/// Configuration for the dashboard server
34#[derive(Debug, Clone)]
35pub struct DashboardConfig {
36    /// Server bind address
37    pub bind_address: SocketAddr,
38
39    /// Enable authentication for dashboard access
40    pub enable_auth: bool,
41
42    /// Dashboard access tokens
43    pub access_tokens: Vec<String>,
44
45    /// Enable CORS
46    pub enable_cors: bool,
47
48    /// CORS allowed origins
49    pub cors_origins: Vec<String>,
50
51    /// Enable real-time WebSocket updates
52    pub enable_websocket: bool,
53
54    /// WebSocket update interval
55    pub websocket_update_interval: chrono::Duration,
56
57    /// Maximum concurrent WebSocket connections
58    pub max_websocket_connections: usize,
59}
60
61impl Default for DashboardConfig {
62    fn default() -> Self {
63        Self {
64            bind_address: "127.0.0.1:8080".parse().unwrap(),
65            enable_auth: true,
66            access_tokens: vec!["dashboard-token-123".to_string()],
67            enable_cors: true,
68            cors_origins: vec!["http://localhost:3000".to_string()],
69            enable_websocket: true,
70            websocket_update_interval: chrono::Duration::seconds(5),
71            max_websocket_connections: 100,
72        }
73    }
74}
75
76/// Dashboard API request/response types
77#[derive(Debug, Serialize, Deserialize)]
78pub struct DashboardRequest {
79    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
80    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
81    pub event_types: Option<Vec<SecurityEventType>>,
82    pub user_id: Option<String>,
83    pub limit: Option<usize>,
84}
85
86#[derive(Debug, Serialize, Deserialize)]
87pub struct EventsResponse {
88    pub events: Vec<crate::monitoring::SecurityEvent>,
89    pub total_count: usize,
90    pub page: usize,
91    pub per_page: usize,
92}
93
94#[derive(Debug, Serialize, Deserialize)]
95pub struct MetricsResponse {
96    pub metrics: crate::monitoring::SecurityMetrics,
97    pub trends: HashMap<String, Vec<f64>>,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101pub struct AlertsResponse {
102    pub active_alerts: Vec<crate::monitoring::SecurityAlert>,
103    pub resolved_alerts: Vec<crate::monitoring::SecurityAlert>,
104    pub alert_rules: Vec<crate::monitoring::AlertRule>,
105}
106
107/// Security dashboard HTTP server
108pub struct DashboardServer {
109    config: DashboardConfig,
110    monitor: Arc<SecurityMonitor>,
111    websocket_connections: Arc<tokio::sync::RwLock<Vec<WebSocketConnection>>>,
112}
113
114impl DashboardServer {
115    /// Create a new dashboard server
116    pub fn new(config: DashboardConfig, monitor: Arc<SecurityMonitor>) -> Self {
117        Self {
118            config,
119            monitor,
120            websocket_connections: Arc::new(tokio::sync::RwLock::new(Vec::new())),
121        }
122    }
123
124    /// Create with default configuration
125    pub fn with_default_config(monitor: Arc<SecurityMonitor>) -> Self {
126        Self::new(DashboardConfig::default(), monitor)
127    }
128
129    /// Start the dashboard server
130    pub async fn start(&self) -> Result<(), DashboardError> {
131        info!(
132            "Starting security dashboard server on {}",
133            self.config.bind_address
134        );
135
136        // In a real implementation, this would start an HTTP server
137        // For now, we'll simulate the server functionality
138
139        if self.config.enable_websocket {
140            self.start_websocket_updates().await;
141        }
142
143        info!("Security dashboard server started successfully");
144        Ok(())
145    }
146
147    /// Handle dashboard data request
148    pub async fn handle_dashboard_request(
149        &self,
150        auth_token: Option<&str>,
151    ) -> Result<SecurityDashboard, DashboardError> {
152        self.authenticate_request(auth_token)?;
153        Ok(self.monitor.get_dashboard_data().await)
154    }
155
156    /// Handle events request
157    pub async fn handle_events_request(
158        &self,
159        request: DashboardRequest,
160        auth_token: Option<&str>,
161    ) -> Result<EventsResponse, DashboardError> {
162        self.authenticate_request(auth_token)?;
163        let events = if let Some(event_type) =
164            request.event_types.and_then(|types| types.first().cloned())
165        {
166            self.monitor
167                .get_events_by_type(event_type, request.start_time, request.limit)
168                .await
169        } else if let Some(user_id) = &request.user_id {
170            self.monitor
171                .get_events_by_user(user_id, request.start_time, request.limit)
172                .await
173        } else {
174            self.monitor.get_recent_events(request.limit).await
175        };
176
177        Ok(EventsResponse {
178            total_count: events.len(),
179            page: 1,
180            per_page: request.limit.unwrap_or(100),
181            events,
182        })
183    }
184
185    /// Handle metrics request
186    pub async fn handle_metrics_request(
187        &self,
188        request: DashboardRequest,
189        auth_token: Option<&str>,
190    ) -> Result<MetricsResponse, DashboardError> {
191        self.authenticate_request(auth_token)?;
192        let end_time = request.end_time.unwrap_or_else(chrono::Utc::now);
193        let start_time = request
194            .start_time
195            .unwrap_or_else(|| end_time - chrono::Duration::hours(24));
196
197        let metrics = self.monitor.generate_metrics(start_time, end_time).await;
198
199        // Generate trend data (simplified)
200        let trends = self.generate_trend_data(&metrics).await;
201
202        Ok(MetricsResponse { metrics, trends })
203    }
204
205    /// Handle alerts request
206    pub async fn handle_alerts_request(
207        &self,
208        auth_token: Option<&str>,
209    ) -> Result<AlertsResponse, DashboardError> {
210        self.authenticate_request(auth_token)?;
211        let active_alerts = self.monitor.get_active_alerts().await;
212
213        // For this implementation, we'll just return active alerts
214        // In a real system, you'd also fetch resolved alerts from storage
215        let resolved_alerts = Vec::new();
216        let alert_rules = Vec::new(); // Would fetch from monitor
217
218        Ok(AlertsResponse {
219            active_alerts,
220            resolved_alerts,
221            alert_rules,
222        })
223    }
224
225    /// Generate HTML dashboard page
226    pub fn generate_dashboard_html(&self) -> String {
227        r#"
228<!DOCTYPE html>
229<html lang="en">
230<head>
231    <meta charset="UTF-8">
232    <meta name="viewport" content="width=device-width, initial-scale=1.0">
233    <title>MCP Security Dashboard</title>
234    <style>
235        body {
236            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
237            margin: 0;
238            padding: 20px;
239            background-color: #f5f5f5;
240        }
241        .container {
242            max-width: 1200px;
243            margin: 0 auto;
244        }
245        .header {
246            background: #fff;
247            padding: 20px;
248            border-radius: 8px;
249            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
250            margin-bottom: 20px;
251        }
252        .header h1 {
253            margin: 0;
254            color: #333;
255        }
256        .dashboard-grid {
257            display: grid;
258            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
259            gap: 20px;
260        }
261        .card {
262            background: #fff;
263            padding: 20px;
264            border-radius: 8px;
265            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
266        }
267        .card h2 {
268            margin-top: 0;
269            color: #333;
270            font-size: 1.2em;
271        }
272        .metric {
273            display: flex;
274            justify-content: space-between;
275            margin: 10px 0;
276            padding: 10px;
277            background: #f8f9fa;
278            border-radius: 4px;
279        }
280        .metric-value {
281            font-weight: bold;
282            color: #007bff;
283        }
284        .alert {
285            padding: 10px;
286            margin: 10px 0;
287            border-radius: 4px;
288            border-left: 4px solid #dc3545;
289            background: #f8d7da;
290        }
291        .alert.warning {
292            border-color: #ffc107;
293            background: #fff3cd;
294        }
295        .alert.info {
296            border-color: #17a2b8;
297            background: #d1ecf1;
298        }
299        .events-list {
300            max-height: 400px;
301            overflow-y: auto;
302        }
303        .event {
304            padding: 8px;
305            margin: 5px 0;
306            border-radius: 4px;
307            background: #f8f9fa;
308            font-size: 0.9em;
309        }
310        .event.high {
311            background: #f8d7da;
312        }
313        .event.medium {
314            background: #fff3cd;
315        }
316        .refresh-btn {
317            background: #007bff;
318            color: white;
319            border: none;
320            padding: 10px 20px;
321            border-radius: 4px;
322            cursor: pointer;
323            margin-left: 10px;
324        }
325        .refresh-btn:hover {
326            background: #0056b3;
327        }
328        .status-indicator {
329            display: inline-block;
330            width: 12px;
331            height: 12px;
332            border-radius: 50%;
333            margin-right: 5px;
334        }
335        .status-ok { background-color: #28a745; }
336        .status-warning { background-color: #ffc107; }
337        .status-error { background-color: #dc3545; }
338    </style>
339</head>
340<body>
341    <div class="container">
342        <div class="header">
343            <h1>🛡️ MCP Security Dashboard</h1>
344            <p>Real-time security monitoring and alerting system</p>
345            <button class="refresh-btn" onclick="refreshDashboard()">Refresh</button>
346            <span id="last-updated"></span>
347        </div>
348
349        <div class="dashboard-grid">
350            <div class="card">
351                <h2>📊 Security Metrics (24h)</h2>
352                <div id="metrics-content">
353                    <div class="metric">
354                        <span>Authentication Success</span>
355                        <span class="metric-value" id="auth-success">--</span>
356                    </div>
357                    <div class="metric">
358                        <span>Authentication Failures</span>
359                        <span class="metric-value" id="auth-failures">--</span>
360                    </div>
361                    <div class="metric">
362                        <span>Security Violations</span>
363                        <span class="metric-value" id="violations">--</span>
364                    </div>
365                    <div class="metric">
366                        <span>Active Sessions</span>
367                        <span class="metric-value" id="active-sessions">--</span>
368                    </div>
369                </div>
370            </div>
371
372            <div class="card">
373                <h2>🚨 Active Alerts</h2>
374                <div id="alerts-content">
375                    <p>No active alerts</p>
376                </div>
377            </div>
378
379            <div class="card">
380                <h2>📈 System Health</h2>
381                <div id="health-content">
382                    <div class="metric">
383                        <span><span class="status-indicator status-ok"></span>Events in Memory</span>
384                        <span class="metric-value" id="events-memory">--</span>
385                    </div>
386                    <div class="metric">
387                        <span><span class="status-indicator status-ok"></span>Memory Usage</span>
388                        <span class="metric-value" id="memory-usage">-- MB</span>
389                    </div>
390                    <div class="metric">
391                        <span><span class="status-indicator status-ok"></span>Last Event</span>
392                        <span class="metric-value" id="last-event">--</span>
393                    </div>
394                </div>
395            </div>
396
397            <div class="card">
398                <h2>📝 Recent Events</h2>
399                <div class="events-list" id="events-content">
400                    <p>Loading events...</p>
401                </div>
402            </div>
403
404            <div class="card">
405                <h2>🌍 Top Source IPs</h2>
406                <div id="top-ips-content">
407                    <p>No data available</p>
408                </div>
409            </div>
410
411            <div class="card">
412                <h2>🔧 Top User Agents</h2>
413                <div id="top-agents-content">
414                    <p>No data available</p>
415                </div>
416            </div>
417        </div>
418    </div>
419
420    <script>
421        async function refreshDashboard() {
422            try {
423                // This would make actual API calls to the dashboard endpoints
424                // For demo purposes, we'll show static data
425
426                document.getElementById('auth-success').textContent = '1,234';
427                document.getElementById('auth-failures').textContent = '12';
428                document.getElementById('violations').textContent = '3';
429                document.getElementById('active-sessions').textContent = '89';
430
431                document.getElementById('events-memory').textContent = '2,150';
432                document.getElementById('memory-usage').textContent = '15.2';
433                document.getElementById('last-event').textContent = 'Just now';
434
435                document.getElementById('last-updated').textContent =
436                    'Last updated: ' + new Date().toLocaleTimeString();
437
438                // Update events list
439                const eventsHtml = `
440                    <div class="event medium">
441                        <strong>Auth Failure</strong> - Invalid API key from 192.168.1.100
442                        <br><small>${new Date().toLocaleString()}</small>
443                    </div>
444                    <div class="event low">
445                        <strong>Session Created</strong> - New session for user admin
446                        <br><small>${new Date().toLocaleString()}</small>
447                    </div>
448                    <div class="event high">
449                        <strong>Injection Attempt</strong> - SQL injection detected in parameters
450                        <br><small>${new Date().toLocaleString()}</small>
451                    </div>
452                `;
453                document.getElementById('events-content').innerHTML = eventsHtml;
454
455                // Update top IPs
456                const topIpsHtml = `
457                    <div class="metric">
458                        <span>192.168.1.100</span>
459                        <span class="metric-value">45 events</span>
460                    </div>
461                    <div class="metric">
462                        <span>10.0.0.15</span>
463                        <span class="metric-value">23 events</span>
464                    </div>
465                    <div class="metric">
466                        <span>172.16.0.5</span>
467                        <span class="metric-value">12 events</span>
468                    </div>
469                `;
470                document.getElementById('top-ips-content').innerHTML = topIpsHtml;
471
472            } catch (error) {
473                console.error('Failed to refresh dashboard:', error);
474            }
475        }
476
477        // Auto-refresh every 30 seconds
478        setInterval(refreshDashboard, 30000);
479
480        // Initial load
481        refreshDashboard();
482    </script>
483</body>
484</html>
485        "#.to_string()
486    }
487
488    // Private helper methods
489
490    async fn start_websocket_updates(&self) {
491        let monitor = Arc::clone(&self.monitor);
492        let connections = Arc::clone(&self.websocket_connections);
493        let interval = self.config.websocket_update_interval;
494
495        tokio::spawn(async move {
496            let mut update_interval = tokio::time::interval(interval.to_std().unwrap());
497
498            loop {
499                update_interval.tick().await;
500
501                let dashboard_data = monitor.get_dashboard_data().await;
502                let connections_guard = connections.read().await;
503
504                // In a real implementation, this would send updates to WebSocket clients
505                debug!(
506                    "Would send WebSocket update to {} connections with {} events, {} alerts",
507                    connections_guard.len(),
508                    dashboard_data.recent_events.len(),
509                    dashboard_data.active_alerts.len()
510                );
511            }
512        });
513    }
514
515    async fn generate_trend_data(
516        &self,
517        _metrics: &crate::monitoring::SecurityMetrics,
518    ) -> HashMap<String, Vec<f64>> {
519        // Generate simplified trend data
520        let mut trends = HashMap::new();
521
522        // Mock trend data for demonstration
523        trends.insert(
524            "auth_success".to_string(),
525            vec![10.0, 15.0, 12.0, 18.0, 20.0],
526        );
527        trends.insert("auth_failures".to_string(), vec![2.0, 3.0, 1.0, 4.0, 2.0]);
528        trends.insert("violations".to_string(), vec![0.0, 1.0, 0.0, 2.0, 1.0]);
529
530        trends
531    }
532
533    fn authenticate_request(&self, token: Option<&str>) -> Result<(), DashboardError> {
534        if !self.config.enable_auth {
535            return Ok(());
536        }
537
538        let provided_token = token.ok_or(DashboardError::AuthenticationFailed)?;
539
540        // Check if the provided token is in our list of valid access tokens
541        if !self
542            .config
543            .access_tokens
544            .contains(&provided_token.to_string())
545        {
546            debug!(
547                "Invalid dashboard access token provided: {}",
548                provided_token
549            );
550            return Err(DashboardError::AuthenticationFailed);
551        }
552
553        debug!("Dashboard authentication successful");
554        Ok(())
555    }
556
557    /// Authenticate request with Bearer token
558    pub fn authenticate_bearer_token(
559        &self,
560        auth_header: Option<&str>,
561    ) -> Result<(), DashboardError> {
562        if !self.config.enable_auth {
563            return Ok(());
564        }
565
566        let header = auth_header.ok_or(DashboardError::AuthenticationFailed)?;
567
568        // Extract token from "Bearer <token>" format
569        if let Some(token) = header.strip_prefix("Bearer ") {
570            self.authenticate_request(Some(token))
571        } else {
572            Err(DashboardError::AuthenticationFailed)
573        }
574    }
575
576    /// Authenticate request with API key
577    pub fn authenticate_api_key(&self, api_key: Option<&str>) -> Result<(), DashboardError> {
578        // For now, treat API keys the same as access tokens
579        // In a production system, you might have separate API key validation
580        self.authenticate_request(api_key)
581    }
582
583    /// Generate a new access token for dashboard access
584    pub fn generate_access_token(&self) -> String {
585        use rand::Rng;
586        let mut rng = rand::thread_rng();
587        let token: String = (0..32)
588            .map(|_| {
589                let idx = rng.gen_range(0..62);
590                match idx {
591                    0..=25 => (b'a' + idx) as char,
592                    26..=51 => (b'A' + (idx - 26)) as char,
593                    52..=61 => (b'0' + (idx - 52)) as char,
594                    _ => unreachable!(),
595                }
596            })
597            .collect();
598
599        format!("dashboard_{}", token)
600    }
601
602    /// Validate token format
603    #[allow(dead_code)]
604    fn is_valid_token_format(&self, token: &str) -> bool {
605        // Basic validation - tokens should be alphanumeric and at least 16 characters
606        token.len() >= 16
607            && token
608                .chars()
609                .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
610    }
611}
612
613/// WebSocket connection information
614#[derive(Debug, Clone)]
615pub struct WebSocketConnection {
616    pub connection_id: String,
617    pub connected_at: chrono::DateTime<chrono::Utc>,
618    pub last_ping: chrono::DateTime<chrono::Utc>,
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use crate::monitoring::{SecurityMonitor, SecurityMonitorConfig};
625
626    #[tokio::test]
627    async fn test_dashboard_server_creation() {
628        let monitor = Arc::new(SecurityMonitor::new(SecurityMonitorConfig::default()));
629        let server = DashboardServer::with_default_config(monitor);
630
631        assert!(server.config.enable_auth);
632        assert!(server.config.enable_websocket);
633    }
634
635    #[tokio::test]
636    async fn test_dashboard_request_handling() {
637        let monitor = Arc::new(SecurityMonitor::new(SecurityMonitorConfig::default()));
638        let server = DashboardServer::with_default_config(monitor);
639
640        // Test with valid token
641        let valid_token = Some("dashboard-token-123");
642        let dashboard_data = server.handle_dashboard_request(valid_token).await;
643        assert!(dashboard_data.is_ok());
644
645        // Test with invalid token should fail
646        let invalid_token = Some("invalid-token");
647        let dashboard_data = server.handle_dashboard_request(invalid_token).await;
648        assert!(dashboard_data.is_err());
649    }
650
651    #[tokio::test]
652    async fn test_events_request_handling() {
653        let monitor = Arc::new(SecurityMonitor::new(SecurityMonitorConfig::default()));
654        let server = DashboardServer::with_default_config(monitor);
655
656        let request = DashboardRequest {
657            start_time: None,
658            end_time: None,
659            event_types: None,
660            user_id: None,
661            limit: Some(10),
662        };
663
664        let valid_token = Some("dashboard-token-123");
665        let response = server.handle_events_request(request, valid_token).await;
666        assert!(response.is_ok());
667    }
668
669    #[test]
670    fn test_html_generation() {
671        let monitor = Arc::new(SecurityMonitor::new(SecurityMonitorConfig::default()));
672        let server = DashboardServer::with_default_config(monitor);
673
674        let html = server.generate_dashboard_html();
675        assert!(html.contains("MCP Security Dashboard"));
676        assert!(html.contains("Security Metrics"));
677    }
678
679    #[test]
680    fn test_authentication() {
681        let monitor = Arc::new(SecurityMonitor::new(SecurityMonitorConfig::default()));
682        let server = DashboardServer::with_default_config(monitor);
683
684        // Test valid token
685        assert!(
686            server
687                .authenticate_request(Some("dashboard-token-123"))
688                .is_ok()
689        );
690
691        // Test invalid token
692        assert!(server.authenticate_request(Some("invalid-token")).is_err());
693
694        // Test missing token
695        assert!(server.authenticate_request(None).is_err());
696
697        // Test Bearer token authentication
698        assert!(
699            server
700                .authenticate_bearer_token(Some("Bearer dashboard-token-123"))
701                .is_ok()
702        );
703        assert!(
704            server
705                .authenticate_bearer_token(Some("Invalid format"))
706                .is_err()
707        );
708
709        // Test API key authentication
710        assert!(
711            server
712                .authenticate_api_key(Some("dashboard-token-123"))
713                .is_ok()
714        );
715        assert!(server.authenticate_api_key(Some("invalid-key")).is_err());
716    }
717
718    #[test]
719    fn test_token_generation() {
720        let monitor = Arc::new(SecurityMonitor::new(SecurityMonitorConfig::default()));
721        let server = DashboardServer::with_default_config(monitor);
722
723        let token = server.generate_access_token();
724        assert!(token.starts_with("dashboard_"));
725        assert!(token.len() > 16);
726        assert!(server.is_valid_token_format(&token));
727
728        // Test invalid token formats
729        assert!(!server.is_valid_token_format("short"));
730        assert!(!server.is_valid_token_format("contains@invalid!chars"));
731    }
732}