kindly_guard_server/shield/
universal_display.rs1use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19use std::fs;
20use std::io::{self, Write};
21use std::sync::Arc;
22
23use super::Shield;
24use crate::scanner::ThreatType;
25
26#[derive(Debug, Clone)]
28pub struct UniversalDisplayConfig {
29 pub color: bool,
31 pub detailed: bool,
33 pub format: DisplayFormat,
35 pub status_file: Option<String>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum DisplayFormat {
42 Minimal,
44 Compact,
46 Dashboard,
48 Json,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct UniversalShieldStatus {
55 pub active: bool,
56 pub enhanced_mode: bool,
57 pub threats_blocked: u64,
58 pub uptime_seconds: u64,
59 pub recent_threat_rate: f64,
60 pub last_update: DateTime<Utc>,
61 pub threat_breakdown: ThreatBreakdown,
62 pub mode_name: String,
63 pub status_emoji: String,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ThreatBreakdown {
68 pub unicode_attacks: u64,
69 pub injection_attempts: u64,
70 pub path_traversal: u64,
71 pub mcp_threats: u64,
72}
73
74pub struct UniversalDisplay {
76 shield: Arc<Shield>,
77 config: UniversalDisplayConfig,
78}
79
80impl UniversalDisplay {
81 pub const fn new(shield: Arc<Shield>, config: UniversalDisplayConfig) -> Self {
83 Self { shield, config }
84 }
85
86 pub fn get_status(&self) -> UniversalShieldStatus {
88 let info = self.shield.get_info();
89 let stats = self.shield.get_threat_stats();
90 let enhanced = self.shield.is_event_processor_enabled();
91
92 let unicode_count: u64 = stats
94 .iter()
95 .filter(|(k, _)| {
96 matches!(
97 k,
98 ThreatType::UnicodeInvisible
99 | ThreatType::UnicodeBiDi
100 | ThreatType::UnicodeHomograph
101 | ThreatType::UnicodeControl
102 )
103 })
104 .map(|(_, v)| v)
105 .sum();
106
107 let injection_count: u64 = stats
108 .iter()
109 .filter(|(k, _)| {
110 matches!(
111 k,
112 ThreatType::PromptInjection
113 | ThreatType::CommandInjection
114 | ThreatType::SqlInjection
115 )
116 })
117 .map(|(_, v)| v)
118 .sum();
119
120 let traversal_count = stats.get(&ThreatType::PathTraversal).copied().unwrap_or(0);
121
122 let mcp_count: u64 = stats
123 .iter()
124 .filter(|(k, _)| {
125 matches!(
126 k,
127 ThreatType::SessionIdExposure
128 | ThreatType::ToolPoisoning
129 | ThreatType::TokenTheft
130 )
131 })
132 .map(|(_, v)| v)
133 .sum();
134
135 UniversalShieldStatus {
136 active: info.active,
137 enhanced_mode: enhanced,
138 threats_blocked: info.threats_blocked,
139 uptime_seconds: info.uptime.as_secs(),
140 recent_threat_rate: info.recent_threat_rate,
141 last_update: Utc::now(),
142 threat_breakdown: ThreatBreakdown {
143 unicode_attacks: unicode_count,
144 injection_attempts: injection_count,
145 path_traversal: traversal_count,
146 mcp_threats: mcp_count,
147 },
148 mode_name: if enhanced {
149 "Enhanced".to_string()
150 } else {
151 "Standard".to_string()
152 },
153 status_emoji: if info.active {
154 "🛡️".to_string()
155 } else {
156 "🔓".to_string()
157 },
158 }
159 }
160
161 pub fn render(&self) -> String {
163 let status = self.get_status();
164
165 match self.config.format {
166 DisplayFormat::Minimal => self.render_minimal(&status),
167 DisplayFormat::Compact => self.render_compact(&status),
168 DisplayFormat::Dashboard => self.render_dashboard(&status),
169 DisplayFormat::Json => serde_json::to_string_pretty(&status).unwrap_or_default(),
170 }
171 }
172
173 fn render_minimal(&self, status: &UniversalShieldStatus) -> String {
175 let shield_icon = &status.status_emoji;
176 let status_text = if status.active { "Active" } else { "Inactive" };
177 let mode_indicator = if status.enhanced_mode { " ⚡" } else { "" };
178
179 if self.config.color && status.enhanced_mode {
180 format!(
182 "{} KindlyGuard | Status: \x1b[35m{}{}\x1b[0m | Threats: {} | Uptime: {}",
183 shield_icon,
184 status_text,
185 mode_indicator,
186 status.threats_blocked,
187 format_duration(status.uptime_seconds)
188 )
189 } else if self.config.color {
190 format!(
192 "{} KindlyGuard | Status: \x1b[34m{}\x1b[0m | Threats: {} | Uptime: {}",
193 shield_icon,
194 status_text,
195 status.threats_blocked,
196 format_duration(status.uptime_seconds)
197 )
198 } else {
199 format!(
201 "{} KindlyGuard | Status: {}{} | Threats: {} | Uptime: {}",
202 shield_icon,
203 status_text,
204 mode_indicator,
205 status.threats_blocked,
206 format_duration(status.uptime_seconds)
207 )
208 }
209 }
210
211 fn render_compact(&self, status: &UniversalShieldStatus) -> String {
213 let mut output = String::new();
214
215 if status.enhanced_mode {
217 if self.config.color {
218 output.push_str("\x1b[35mKindlyGuard Security Status [Enhanced]\x1b[0m\n");
219 output.push_str("\x1b[35m─────────────────────────────────────\x1b[0m\n");
220 } else {
221 output.push_str("KindlyGuard Security Status [Enhanced]\n");
222 output.push_str("─────────────────────────────────────\n");
223 }
224 } else if self.config.color {
225 output.push_str("\x1b[34mKindlyGuard Security Status\x1b[0m\n");
226 output.push_str("\x1b[34m─────────────────────────\x1b[0m\n");
227 } else {
228 output.push_str("KindlyGuard Security Status\n");
229 output.push_str("─────────────────────────\n");
230 }
231
232 let status_symbol = if status.active { "●" } else { "○" };
234 let status_text = if status.active { "Active" } else { "Inactive" };
235 let mode_indicator = if status.enhanced_mode { " ⚡" } else { "" };
236
237 if self.config.color && status.active {
238 output.push_str(&format!(
239 "{status_symbol} Protection: \x1b[32m{status_text}{mode_indicator}\x1b[0m\n"
240 ));
241 } else if self.config.color {
242 output.push_str(&format!(
243 "{status_symbol} Protection: \x1b[31m{status_text}\x1b[0m\n"
244 ));
245 } else {
246 output.push_str(&format!(
247 "{status_symbol} Protection: {status_text}{mode_indicator}\n"
248 ));
249 }
250
251 output.push_str(&format!("● Threats Blocked: {}\n", status.threats_blocked));
253 output.push_str(&format!(
254 "● Uptime: {}\n",
255 format_duration(status.uptime_seconds)
256 ));
257 output.push_str(&format!("● Mode: {}\n", status.mode_name));
258
259 if status.enhanced_mode {
261 output.push_str("\nRecent Activity:\n");
262 if self.config.color {
263 output.push_str("• \x1b[35mAdvanced analytics enabled\x1b[0m\n");
264 output.push_str("• \x1b[35mCorrelation engine active\x1b[0m\n");
265 output.push_str("• \x1b[35mReal-time threat analysis\x1b[0m\n");
266 } else {
267 output.push_str("• Advanced analytics enabled\n");
268 output.push_str("• Correlation engine active\n");
269 output.push_str("• Real-time threat analysis\n");
270 }
271 } else {
272 output.push_str("\nRecent Activity:\n");
273 output.push_str("• System initialized\n");
274 output.push_str("• Monitoring active\n");
275 }
276
277 output
278 }
279
280 fn render_dashboard(&self, status: &UniversalShieldStatus) -> String {
282 let mut output = String::new();
283
284 if status.enhanced_mode && self.config.color {
286 output.push_str("\x1b[35m╔═══════════════════════════════════════════════╗\x1b[0m\n");
287 output.push_str("\x1b[35m║ 🛡️ KindlyGuard Security Shield ⚡ ║\x1b[0m\n");
288 output.push_str("\x1b[35m╠═══════════════════════════════════════════════╣\x1b[0m\n");
289 } else if self.config.color {
290 output.push_str("\x1b[34m╔═══════════════════════════════════════════════╗\x1b[0m\n");
291 output.push_str("\x1b[34m║ 🛡️ KindlyGuard Security Shield ║\x1b[0m\n");
292 output.push_str("\x1b[34m╠═══════════════════════════════════════════════╣\x1b[0m\n");
293 } else {
294 output.push_str("╔═══════════════════════════════════════════════╗\n");
295 output.push_str("║ 🛡️ KindlyGuard Security Shield ║\n");
296 output.push_str("╠═══════════════════════════════════════════════╣\n");
297 }
298
299 let status_line = format!(
301 "║ Status: {:37} ║",
302 format!(
303 "{} {} {}",
304 if status.active {
305 "✅ ACTIVE"
306 } else {
307 "❌ INACTIVE"
308 },
309 if status.enhanced_mode {
310 "[Enhanced Mode]"
311 } else {
312 ""
313 },
314 format_duration(status.uptime_seconds)
315 )
316 );
317 output.push_str(&status_line);
318 output.push('\n');
319
320 output.push_str(&format!(
321 "║ Threats Blocked: {:28} ║\n",
322 status.threats_blocked
323 ));
324 output.push_str(&format!(
325 "║ Threat Rate: {:32} ║\n",
326 format!("{:.1}/min", status.recent_threat_rate)
327 ));
328
329 if self.config.color && status.enhanced_mode {
331 output.push_str("\x1b[35m╠═══════════════════════════════════════════════╣\x1b[0m\n");
332 } else if self.config.color {
333 output.push_str("\x1b[34m╠═══════════════════════════════════════════════╣\x1b[0m\n");
334 } else {
335 output.push_str("╠═══════════════════════════════════════════════╣\n");
336 }
337
338 output.push_str("║ Threat Breakdown: ║\n");
340 output.push_str(&format!(
341 "║ • Unicode Attacks: {:20} ║\n",
342 status.threat_breakdown.unicode_attacks
343 ));
344 output.push_str(&format!(
345 "║ • Injection Attempts: {:20} ║\n",
346 status.threat_breakdown.injection_attempts
347 ));
348 output.push_str(&format!(
349 "║ • Path Traversal: {:20} ║\n",
350 status.threat_breakdown.path_traversal
351 ));
352 output.push_str(&format!(
353 "║ • MCP Threats: {:20} ║\n",
354 status.threat_breakdown.mcp_threats
355 ));
356
357 if self.config.color && status.enhanced_mode {
359 output.push_str("\x1b[35m╚═══════════════════════════════════════════════╝\x1b[0m\n");
360 } else if self.config.color {
361 output.push_str("\x1b[34m╚═══════════════════════════════════════════════╝\x1b[0m\n");
362 } else {
363 output.push_str("╚═══════════════════════════════════════════════╝\n");
364 }
365
366 output
367 }
368
369 pub fn write_status_file(&self) -> io::Result<()> {
371 if let Some(ref path) = self.config.status_file {
372 let status = self.get_status();
373 let json = serde_json::to_string_pretty(&status)?;
374 fs::write(path, json)?;
375 }
376 Ok(())
377 }
378
379 pub fn print(&self) -> io::Result<()> {
381 let output = self.render();
382 print!("{output}");
383 io::stdout().flush()?;
384
385 self.write_status_file()
387 }
388}
389
390fn format_duration(seconds: u64) -> String {
392 let hours = seconds / 3600;
393 let minutes = (seconds % 3600) / 60;
394 let secs = seconds % 60;
395
396 if hours > 0 {
397 format!("{hours}h{minutes}m")
398 } else if minutes > 0 {
399 format!("{minutes}m{secs}s")
400 } else {
401 format!("{secs}s")
402 }
403}
404
405pub fn create_universal_display(shield: Arc<Shield>) -> UniversalDisplay {
407 let config = UniversalDisplayConfig {
408 color: supports_color(),
409 detailed: false,
410 format: DisplayFormat::Compact,
411 status_file: Some("/tmp/kindlyguard-status.json".to_string()),
412 };
413
414 UniversalDisplay::new(shield, config)
415}
416
417fn supports_color() -> bool {
419 if std::env::var("NO_COLOR").is_ok() {
421 return false;
422 }
423
424 if let Ok(term) = std::env::var("TERM") {
425 if term == "dumb" {
426 return false;
427 }
428 }
429
430 true
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_universal_display_formats() {
440 let shield = Arc::new(Shield::new());
441 let config = UniversalDisplayConfig {
442 color: false,
443 detailed: false,
444 format: DisplayFormat::Minimal,
445 status_file: None,
446 };
447
448 let display = UniversalDisplay::new(shield, config);
449 let output = display.render();
450
451 assert!(output.contains("KindlyGuard"));
452 assert!(output.contains("Status:"));
453 }
454
455 #[test]
456 fn test_json_output() {
457 let shield = Arc::new(Shield::new());
458 let config = UniversalDisplayConfig {
459 color: false,
460 detailed: false,
461 format: DisplayFormat::Json,
462 status_file: None,
463 };
464
465 let display = UniversalDisplay::new(shield, config);
466 let output = display.render();
467
468 let parsed: Result<UniversalShieldStatus, _> = serde_json::from_str(&output);
470 assert!(parsed.is_ok());
471 }
472}