Skip to main content

kindly_guard_server/shield/
universal_display.rs

1// Copyright 2025 Kindly Software Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Universal shield display that works in any environment
15//! No terminal control sequences, just plain ASCII text
16
17use 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/// Universal display options
27#[derive(Debug, Clone)]
28pub struct UniversalDisplayConfig {
29    /// Enable color codes (ANSI)
30    pub color: bool,
31    /// Show detailed statistics
32    pub detailed: bool,
33    /// Output format
34    pub format: DisplayFormat,
35    /// Status file path (optional)
36    pub status_file: Option<String>,
37}
38
39/// Display format for universal output
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum DisplayFormat {
42    /// Single line status
43    Minimal,
44    /// Multi-line compact status
45    Compact,
46    /// Full dashboard
47    Dashboard,
48    /// JSON output
49    Json,
50}
51
52/// Status data for JSON/file output
53#[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
74/// Universal shield display
75pub struct UniversalDisplay {
76    shield: Arc<Shield>,
77    config: UniversalDisplayConfig,
78}
79
80impl UniversalDisplay {
81    /// Create new universal display
82    pub const fn new(shield: Arc<Shield>, config: UniversalDisplayConfig) -> Self {
83        Self { shield, config }
84    }
85
86    /// Get current status as structured data
87    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        // Count threats by category
93        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    /// Render the display
162    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    /// Render minimal single-line format
174    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            // Purple color for enhanced mode
181            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            // Standard blue color
191            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            // No color
200            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    /// Render compact multi-line format
212    fn render_compact(&self, status: &UniversalShieldStatus) -> String {
213        let mut output = String::new();
214
215        // Header with mode indication
216        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        // Status line
233        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        // Stats
252        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        // Recent activity for enhanced mode
260        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    /// Render full dashboard format
281    fn render_dashboard(&self, status: &UniversalShieldStatus) -> String {
282        let mut output = String::new();
283
284        // Title
285        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        // Status section
300        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        // Separator
330        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        // Threat breakdown
339        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        // Footer
358        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    /// Write status to file if configured
370    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    /// Print to stdout
380    pub fn print(&self) -> io::Result<()> {
381        let output = self.render();
382        print!("{output}");
383        io::stdout().flush()?;
384
385        // Also write to status file if configured
386        self.write_status_file()
387    }
388}
389
390/// Format duration as human-readable string
391fn 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
405/// Create a universal display with sensible defaults
406pub 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
417/// Check if terminal supports color
418fn supports_color() -> bool {
419    // Check common environment variables
420    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    // Default to true for most environments
431    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        // Should be valid JSON
469        let parsed: Result<UniversalShieldStatus, _> = serde_json::from_str(&output);
470        assert!(parsed.is_ok());
471    }
472}