pulseengine_mcp_auth/monitoring/
dashboard_server.rs1use 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#[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#[derive(Debug, Clone)]
35pub struct DashboardConfig {
36 pub bind_address: SocketAddr,
38
39 pub enable_auth: bool,
41
42 pub access_tokens: Vec<String>,
44
45 pub enable_cors: bool,
47
48 pub cors_origins: Vec<String>,
50
51 pub enable_websocket: bool,
53
54 pub websocket_update_interval: chrono::Duration,
56
57 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#[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
107pub struct DashboardServer {
109 config: DashboardConfig,
110 monitor: Arc<SecurityMonitor>,
111 websocket_connections: Arc<tokio::sync::RwLock<Vec<WebSocketConnection>>>,
112}
113
114impl DashboardServer {
115 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 pub fn with_default_config(monitor: Arc<SecurityMonitor>) -> Self {
126 Self::new(DashboardConfig::default(), monitor)
127 }
128
129 pub async fn start(&self) -> Result<(), DashboardError> {
131 info!(
132 "Starting security dashboard server on {}",
133 self.config.bind_address
134 );
135
136 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 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 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 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 let trends = self.generate_trend_data(&metrics).await;
201
202 Ok(MetricsResponse { metrics, trends })
203 }
204
205 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 let resolved_alerts = Vec::new();
216 let alert_rules = Vec::new(); Ok(AlertsResponse {
219 active_alerts,
220 resolved_alerts,
221 alert_rules,
222 })
223 }
224
225 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 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 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 let mut trends = HashMap::new();
521
522 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 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 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 if let Some(token) = header.strip_prefix("Bearer ") {
570 self.authenticate_request(Some(token))
571 } else {
572 Err(DashboardError::AuthenticationFailed)
573 }
574 }
575
576 pub fn authenticate_api_key(&self, api_key: Option<&str>) -> Result<(), DashboardError> {
578 self.authenticate_request(api_key)
581 }
582
583 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 #[allow(dead_code)]
604 fn is_valid_token_format(&self, token: &str) -> bool {
605 token.len() >= 16
607 && token
608 .chars()
609 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
610 }
611}
612
613#[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 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 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 assert!(
686 server
687 .authenticate_request(Some("dashboard-token-123"))
688 .is_ok()
689 );
690
691 assert!(server.authenticate_request(Some("invalid-token")).is_err());
693
694 assert!(server.authenticate_request(None).is_err());
696
697 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 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 assert!(!server.is_valid_token_format("short"));
730 assert!(!server.is_valid_token_format("contains@invalid!chars"));
731 }
732}