diff --git a/Cargo.toml b/Cargo.toml index 5d732d6..3a99e9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ "ghost-cli", - "ghost-core", + "ghost-core", "ghost-tui", ] resolver = "2" diff --git a/SECURITY.md b/SECURITY.md index a3d6964..32be079 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -81,12 +81,6 @@ We follow responsible disclosure practices: - Social engineering attacks - Third-party dependency vulnerabilities (unless exploitable through Ghost) -### Contact Information - -- **Security Team**: security@ghost-project.dev -- **General Issues**: https://github.com/ghost-project/ghost/issues -- **Discussions**: https://github.com/ghost-project/ghost/discussions - --- -*Last updated: November 2024* \ No newline at end of file +*Last updated: November 2025* \ No newline at end of file diff --git a/ghost-tui/Cargo.toml b/ghost-tui/Cargo.toml new file mode 100644 index 0000000..7898e37 --- /dev/null +++ b/ghost-tui/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ghost-tui" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "Terminal user interface for Ghost process injection detection" + +[dependencies] +ghost-core = { path = "../ghost-core" } +ratatui = "0.24" +crossterm = "0.27" +tokio = { version = "1.0", features = ["full"] } +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +tui-input = "0.8" +unicode-width = "0.1" diff --git a/ghost-tui/src/app.rs b/ghost-tui/src/app.rs new file mode 100644 index 0000000..8669867 --- /dev/null +++ b/ghost-tui/src/app.rs @@ -0,0 +1,343 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use ghost_core::{ + DetectionEngine, DetectionResult, ProcessInfo, ThreatLevel, + memory, process, thread +}; +use ratatui::widgets::{ListState, TableState}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use std::time::Instant; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TabIndex { + Overview = 0, + Processes = 1, + Detections = 2, + Memory = 3, + Logs = 4, +} + +impl TabIndex { + pub fn from_index(index: usize) -> Self { + match index { + 0 => TabIndex::Overview, + 1 => TabIndex::Processes, + 2 => TabIndex::Detections, + 3 => TabIndex::Memory, + 4 => TabIndex::Logs, + _ => TabIndex::Overview, + } + } + + pub fn next(self) -> Self { + Self::from_index((self as usize + 1) % 5) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DetectionEvent { + pub timestamp: DateTime, + pub process: ProcessInfo, + pub threat_level: ThreatLevel, + pub indicators: Vec, + pub confidence: f32, +} + +#[derive(Debug, Clone)] +pub struct SystemStats { + pub total_processes: usize, + pub suspicious_processes: usize, + pub malicious_processes: usize, + pub total_detections: usize, + pub scan_time_ms: u64, + pub memory_usage_mb: f64, +} + +#[derive(Debug)] +pub struct App { + pub current_tab: TabIndex, + pub detection_engine: DetectionEngine, + pub processes: Vec, + pub detections: VecDeque, + pub logs: VecDeque, + pub stats: SystemStats, + pub last_scan: Option, + + // UI state + pub processes_state: TableState, + pub detections_state: ListState, + pub logs_state: ListState, + pub selected_process: Option, + + // Settings + pub auto_refresh: bool, + pub max_log_entries: usize, + pub max_detection_entries: usize, +} + +impl App { + pub async fn new() -> Result { + let mut app = Self { + current_tab: TabIndex::Overview, + detection_engine: DetectionEngine::new(), + processes: Vec::new(), + detections: VecDeque::new(), + logs: VecDeque::new(), + stats: SystemStats { + total_processes: 0, + suspicious_processes: 0, + malicious_processes: 0, + total_detections: 0, + scan_time_ms: 0, + memory_usage_mb: 0.0, + }, + last_scan: None, + processes_state: TableState::default(), + detections_state: ListState::default(), + logs_state: ListState::default(), + selected_process: None, + auto_refresh: true, + max_log_entries: 1000, + max_detection_entries: 500, + }; + + app.add_log_message("Ghost TUI v0.1.0 - Process Injection Detection".to_string()); + app.add_log_message("Initializing detection engine...".to_string()); + + // Initial scan + app.update_scan_data().await?; + + Ok(app) + } + + pub async fn update_scan_data(&mut self) -> Result<()> { + let scan_start = Instant::now(); + + // Enumerate processes + self.processes = process::enumerate_processes()?; + let mut detection_count = 0; + let mut suspicious_count = 0; + let mut malicious_count = 0; + + // Scan each process for injections + for proc in &self.processes { + // Skip system processes for performance + if proc.name == "System" || proc.name == "Registry" { + continue; + } + + if let Ok(regions) = memory::enumerate_memory_regions(proc.pid) { + let threads = thread::enumerate_threads(proc.pid).ok(); + let result = self.detection_engine.analyze_process( + proc, + ®ions, + threads.as_deref() + ); + + match result.threat_level { + ThreatLevel::Suspicious => suspicious_count += 1, + ThreatLevel::Malicious => malicious_count += 1, + ThreatLevel::Clean => {} + } + + if result.threat_level != ThreatLevel::Clean { + detection_count += 1; + self.add_detection(DetectionEvent { + timestamp: Utc::now(), + process: proc.clone(), + threat_level: result.threat_level, + indicators: result.indicators, + confidence: result.confidence, + }); + } + } + } + + let scan_duration = scan_start.elapsed(); + + // Update statistics + self.stats = SystemStats { + total_processes: self.processes.len(), + suspicious_processes: suspicious_count, + malicious_processes: malicious_count, + total_detections: self.detections.len(), + scan_time_ms: scan_duration.as_millis() as u64, + memory_usage_mb: self.estimate_memory_usage(), + }; + + self.last_scan = Some(scan_start); + + if detection_count > 0 { + self.add_log_message(format!( + "Scan complete: {} detections found in {}ms", + detection_count, + scan_duration.as_millis() + )); + } + + Ok(()) + } + + pub async fn force_refresh(&mut self) -> Result<()> { + self.add_log_message("Forcing refresh...".to_string()); + self.update_scan_data().await + } + + pub fn add_detection(&mut self, detection: DetectionEvent) { + // Add to front of deque for most recent first + self.detections.push_front(detection); + + // Limit size + while self.detections.len() > self.max_detection_entries { + self.detections.pop_back(); + } + } + + pub fn add_log_message(&mut self, message: String) { + let timestamp = Utc::now().format("%H:%M:%S"); + let log_entry = format!("[{}] {}", timestamp, message); + + self.logs.push_front(log_entry); + + // Limit log size + while self.logs.len() > self.max_log_entries { + self.logs.pop_back(); + } + } + + pub fn clear_detections(&mut self) { + self.detections.clear(); + self.add_log_message("Detection history cleared".to_string()); + } + + pub fn next_tab(&mut self) { + self.current_tab = self.current_tab.next(); + } + + pub fn scroll_up(&mut self) { + match self.current_tab { + TabIndex::Processes => { + let i = match self.processes_state.selected() { + Some(i) => { + if i == 0 { + self.processes.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.processes_state.select(Some(i)); + if let Some(process) = self.processes.get(i) { + self.selected_process = Some(process.clone()); + } + } + TabIndex::Detections => { + let i = match self.detections_state.selected() { + Some(i) => { + if i == 0 { + self.detections.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.detections_state.select(Some(i)); + } + TabIndex::Logs => { + let i = match self.logs_state.selected() { + Some(i) => { + if i == 0 { + self.logs.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.logs_state.select(Some(i)); + } + _ => {} + } + } + + pub fn scroll_down(&mut self) { + match self.current_tab { + TabIndex::Processes => { + let i = match self.processes_state.selected() { + Some(i) => { + if i >= self.processes.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.processes_state.select(Some(i)); + if let Some(process) = self.processes.get(i) { + self.selected_process = Some(process.clone()); + } + } + TabIndex::Detections => { + let i = match self.detections_state.selected() { + Some(i) => { + if i >= self.detections.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.detections_state.select(Some(i)); + } + TabIndex::Logs => { + let i = match self.logs_state.selected() { + Some(i) => { + if i >= self.logs.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.logs_state.select(Some(i)); + } + _ => {} + } + } + + pub fn select_item(&mut self) { + match self.current_tab { + TabIndex::Processes => { + if let Some(i) = self.processes_state.selected() { + if let Some(process) = self.processes.get(i) { + self.selected_process = Some(process.clone()); + self.add_log_message(format!( + "Selected process: {} (PID: {})", + process.name, process.pid + )); + } + } + } + _ => {} + } + } + + fn estimate_memory_usage(&self) -> f64 { + // Rough estimation of memory usage in MB + let processes_size = self.processes.len() * std::mem::size_of::(); + let detections_size = self.detections.len() * 200; // Estimate per detection + let logs_size = self.logs.iter().map(|s| s.len()).sum::(); + + (processes_size + detections_size + logs_size) as f64 / 1024.0 / 1024.0 + } + + pub fn get_tab_titles(&self) -> Vec<&str> { + vec!["Overview", "Processes", "Detections", "Memory", "Logs"] + } +} \ No newline at end of file diff --git a/ghost-tui/src/events.rs b/ghost-tui/src/events.rs new file mode 100644 index 0000000..08a595d --- /dev/null +++ b/ghost-tui/src/events.rs @@ -0,0 +1,41 @@ +// Event handling module for future expansion +// Currently events are handled in main.rs but this provides structure for complex event handling + +use crossterm::event::{Event, KeyEvent, MouseEvent}; + +#[derive(Debug, Clone)] +pub enum AppEvent { + Key(KeyEvent), + Mouse(MouseEvent), + Tick, + Quit, + Refresh, + ClearDetections, + ClearLogs, +} + +impl From for AppEvent { + fn from(event: Event) -> Self { + match event { + Event::Key(key) => AppEvent::Key(key), + Event::Mouse(mouse) => AppEvent::Mouse(mouse), + _ => AppEvent::Tick, + } + } +} + +pub struct EventHandler { + // Future: Add event queue, rate limiting, etc. +} + +impl EventHandler { + pub fn new() -> Self { + Self {} + } +} + +impl Default for EventHandler { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/ghost-tui/src/main.rs b/ghost-tui/src/main.rs new file mode 100644 index 0000000..c2eb36b --- /dev/null +++ b/ghost-tui/src/main.rs @@ -0,0 +1,137 @@ +use anyhow::Result; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ghost_core::{DetectionEngine, ThreatLevel}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + symbols, + text::{Line, Span, Text}, + widgets::{ + Block, Borders, Cell, Clear, Gauge, List, ListItem, ListState, Paragraph, Row, Table, + TableState, Tabs, Wrap, + }, + Frame, Terminal, +}; +use std::{ + collections::VecDeque, + io, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; +use tokio::time; + +mod app; +mod ui; +mod events; + +use app::{App, TabIndex}; + +#[tokio::main] +async fn main() -> Result<()> { + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app state + let app = Arc::new(Mutex::new(App::new().await?)); + + // Clone for background task + let app_clone = Arc::clone(&app); + + // Start background scanning task + tokio::spawn(async move { + let mut interval = time::interval(Duration::from_secs(2)); + loop { + interval.tick().await; + if let Ok(mut app) = app_clone.try_lock() { + if let Err(e) = app.update_scan_data().await { + app.add_log_message(format!("Scan error: {}", e)); + } + } + } + }); + + // Main event loop + let res = run_app(&mut terminal, app).await; + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + Ok(()) +} + +async fn run_app( + terminal: &mut Terminal, + app: Arc>, +) -> Result<()> { + loop { + // Draw the UI + terminal.draw(|f| { + if let Ok(app) = app.try_lock() { + ui::draw(f, &app); + } + })?; + + // Handle events + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Tab => { + if let Ok(mut app) = app.try_lock() { + app.next_tab(); + } + } + KeyCode::Up => { + if let Ok(mut app) = app.try_lock() { + app.scroll_up(); + } + } + KeyCode::Down => { + if let Ok(mut app) = app.try_lock() { + app.scroll_down(); + } + } + KeyCode::Enter => { + if let Ok(mut app) = app.try_lock() { + app.select_item(); + } + } + KeyCode::Char('r') => { + if let Ok(mut app) = app.try_lock() { + if let Err(e) = app.force_refresh().await { + app.add_log_message(format!("Refresh error: {}", e)); + } + } + } + KeyCode::Char('c') => { + if let Ok(mut app) = app.try_lock() { + app.clear_detections(); + } + } + _ => {} + } + } + } + } + } +} diff --git a/ghost-tui/src/ui.rs b/ghost-tui/src/ui.rs new file mode 100644 index 0000000..a0bc61d --- /dev/null +++ b/ghost-tui/src/ui.rs @@ -0,0 +1,448 @@ +use crate::app::{App, TabIndex}; +use ghost_core::ThreatLevel; +use ratatui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Modifier, Style}, + symbols, + text::{Line, Span, Text}, + widgets::{ + BarChart, Block, Borders, Cell, Gauge, List, ListItem, Paragraph, Row, Sparkline, Table, Tabs, Wrap + }, + Frame, +}; + +// Cyberpunk-inspired color scheme +const PRIMARY_COLOR: Color = Color::Cyan; +const SECONDARY_COLOR: Color = Color::Magenta; +const SUCCESS_COLOR: Color = Color::Green; +const WARNING_COLOR: Color = Color::Yellow; +const DANGER_COLOR: Color = Color::Red; +const BACKGROUND_COLOR: Color = Color::Black; +const TEXT_COLOR: Color = Color::White; + +pub fn draw(f: &mut Frame, app: &App) { + let size = f.size(); + + // Create main layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Header + Constraint::Min(0), // Content + Constraint::Length(3), // Footer + ]) + .split(size); + + // Draw header + draw_header(f, chunks[0], app); + + // Draw main content based on selected tab + match app.current_tab { + TabIndex::Overview => draw_overview(f, chunks[1], app), + TabIndex::Processes => draw_processes(f, chunks[1], app), + TabIndex::Detections => draw_detections(f, chunks[1], app), + TabIndex::Memory => draw_memory(f, chunks[1], app), + TabIndex::Logs => draw_logs(f, chunks[1], app), + } + + // Draw footer + draw_footer(f, chunks[2], app); +} + +fn draw_header(f: &mut Frame, area: Rect, app: &App) { + let titles = app.get_tab_titles(); + let tabs = Tabs::new(titles) + .block( + Block::default() + .borders(Borders::ALL) + .title("👻 Ghost - Process Injection Detection") + .title_style(Style::default().fg(PRIMARY_COLOR).add_modifier(Modifier::BOLD)) + .border_style(Style::default().fg(PRIMARY_COLOR)) + ) + .select(app.current_tab as usize) + .style(Style::default().fg(TEXT_COLOR)) + .highlight_style( + Style::default() + .fg(BACKGROUND_COLOR) + .bg(PRIMARY_COLOR) + .add_modifier(Modifier::BOLD) + ); + + f.render_widget(tabs, area); +} + +fn draw_footer(f: &mut Frame, area: Rect, app: &App) { + let help_text = match app.current_tab { + TabIndex::Overview => "↑↓: Navigate | Tab: Switch tabs | R: Refresh | C: Clear | Q: Quit", + TabIndex::Processes => "↑↓: Select process | Enter: View details | Tab: Switch tabs | Q: Quit", + TabIndex::Detections => "↑↓: Navigate detections | C: Clear history | Tab: Switch tabs | Q: Quit", + TabIndex::Memory => "↑↓: Navigate | Tab: Switch tabs | R: Refresh | Q: Quit", + TabIndex::Logs => "↑↓: Navigate logs | C: Clear logs | Tab: Switch tabs | Q: Quit", + }; + + let footer = Paragraph::new(help_text) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(SECONDARY_COLOR)) + ) + .style(Style::default().fg(TEXT_COLOR)) + .alignment(Alignment::Center); + + f.render_widget(footer, area); +} + +fn draw_overview(f: &mut Frame, area: Rect, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(8), // Stats + Constraint::Length(8), // Threat level gauge + Constraint::Min(0), // Recent detections + ]) + .split(area); + + // Statistics panel + draw_stats_panel(f, chunks[0], app); + + // Threat level gauge + draw_threat_gauge(f, chunks[1], app); + + // Recent detections + draw_recent_detections(f, chunks[2], app); +} + +fn draw_stats_panel(f: &mut Frame, area: Rect, app: &App) { + let stats_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(area); + + // Total processes + let total_processes = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("Total Processes") + .border_style(Style::default().fg(PRIMARY_COLOR)) + ) + .gauge_style(Style::default().fg(PRIMARY_COLOR)) + .percent(std::cmp::min(app.stats.total_processes * 100 / 500, 100) as u16) + .label(format!("{}", app.stats.total_processes)); + + f.render_widget(total_processes, stats_chunks[0]); + + // Suspicious processes + let suspicious_gauge = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("Suspicious") + .border_style(Style::default().fg(WARNING_COLOR)) + ) + .gauge_style(Style::default().fg(WARNING_COLOR)) + .percent(if app.stats.total_processes > 0 { + (app.stats.suspicious_processes * 100 / app.stats.total_processes) as u16 + } else { 0 }) + .label(format!("{}", app.stats.suspicious_processes)); + + f.render_widget(suspicious_gauge, stats_chunks[1]); + + // Malicious processes + let malicious_gauge = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("Malicious") + .border_style(Style::default().fg(DANGER_COLOR)) + ) + .gauge_style(Style::default().fg(DANGER_COLOR)) + .percent(if app.stats.total_processes > 0 { + (app.stats.malicious_processes * 100 / app.stats.total_processes) as u16 + } else { 0 }) + .label(format!("{}", app.stats.malicious_processes)); + + f.render_widget(malicious_gauge, stats_chunks[2]); + + // Scan performance + let perf_gauge = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("Scan Time (ms)") + .border_style(Style::default().fg(SUCCESS_COLOR)) + ) + .gauge_style(Style::default().fg(SUCCESS_COLOR)) + .percent(std::cmp::min(app.stats.scan_time_ms as u16 / 10, 100)) + .label(format!("{}ms", app.stats.scan_time_ms)); + + f.render_widget(perf_gauge, stats_chunks[3]); +} + +fn draw_threat_gauge(f: &mut Frame, area: Rect, app: &App) { + let threat_level = if app.stats.malicious_processes > 0 { + 100 + } else if app.stats.suspicious_processes > 0 { + 60 + } else { + 20 + }; + + let color = if threat_level > 80 { + DANGER_COLOR + } else if threat_level > 40 { + WARNING_COLOR + } else { + SUCCESS_COLOR + }; + + let threat_gauge = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("🚨 System Threat Level") + .title_style(Style::default().fg(color).add_modifier(Modifier::BOLD)) + .border_style(Style::default().fg(color)) + ) + .gauge_style(Style::default().fg(color)) + .percent(threat_level) + .label(format!("{}% - {} Detection(s)", threat_level, app.stats.total_detections)); + + f.render_widget(threat_gauge, area); +} + +fn draw_recent_detections(f: &mut Frame, area: Rect, app: &App) { + let items: Vec = app + .detections + .iter() + .take(10) + .map(|detection| { + let level_icon = match detection.threat_level { + ThreatLevel::Malicious => "🔴", + ThreatLevel::Suspicious => "🟡", + ThreatLevel::Clean => "🟢", + }; + + let time = detection.timestamp.format("%H:%M:%S"); + let content = format!( + "{} [{}] {} (PID: {}) - {:.1}%", + level_icon, + time, + detection.process.name, + detection.process.pid, + detection.confidence * 100.0 + ); + + ListItem::new(content).style(Style::default().fg(TEXT_COLOR)) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title("🔍 Recent Detections") + .border_style(Style::default().fg(SECONDARY_COLOR)) + ) + .style(Style::default().fg(TEXT_COLOR)); + + f.render_widget(list, area); +} + +fn draw_processes(f: &mut Frame, area: Rect, app: &App) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) + .split(area); + + // Process table + let header_cells = ["PID", "PPID", "Name", "Threads", "Status"] + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(PRIMARY_COLOR).add_modifier(Modifier::BOLD))); + + let header = Row::new(header_cells).height(1).bottom_margin(1); + + let rows: Vec = app.processes.iter().map(|proc| { + let status = if app.detections.iter().any(|d| d.process.pid == proc.pid) { + match app.detections.iter().find(|d| d.process.pid == proc.pid).unwrap().threat_level { + ThreatLevel::Malicious => "🔴 MALICIOUS", + ThreatLevel::Suspicious => "🟡 SUSPICIOUS", + ThreatLevel::Clean => "🟢 CLEAN", + } + } else { + "🟢 CLEAN" + }; + + Row::new(vec![ + Cell::from(proc.pid.to_string()), + Cell::from(proc.ppid.to_string()), + Cell::from(proc.name.clone()), + Cell::from(proc.thread_count.to_string()), + Cell::from(status), + ]) + }).collect(); + + let table = Table::new(rows) + .header(header) + .block( + Block::default() + .borders(Borders::ALL) + .title("🖥️ System Processes") + .border_style(Style::default().fg(PRIMARY_COLOR)) + ) + .highlight_style(Style::default().bg(PRIMARY_COLOR).fg(BACKGROUND_COLOR)) + .widths(&[ + Constraint::Length(8), + Constraint::Length(8), + Constraint::Min(20), + Constraint::Length(8), + Constraint::Length(15), + ]); + + f.render_stateful_widget(table, chunks[0], &mut app.processes_state.clone()); + + // Process details panel + draw_process_details(f, chunks[1], app); +} + +fn draw_process_details(f: &mut Frame, area: Rect, app: &App) { + let details = if let Some(ref process) = app.selected_process { + format!( + "PID: {}\nPPID: {}\nName: {}\nPath: {}\nThreads: {}", + process.pid, + process.ppid, + process.name, + process.path.as_deref().unwrap_or("Unknown"), + process.thread_count + ) + } else { + "Select a process to view details".to_string() + }; + + let paragraph = Paragraph::new(details) + .block( + Block::default() + .borders(Borders::ALL) + .title("📋 Process Details") + .border_style(Style::default().fg(SECONDARY_COLOR)) + ) + .style(Style::default().fg(TEXT_COLOR)) + .wrap(Wrap { trim: true }); + + f.render_widget(paragraph, area); +} + +fn draw_detections(f: &mut Frame, area: Rect, app: &App) { + let items: Vec = app + .detections + .iter() + .map(|detection| { + let level_style = match detection.threat_level { + ThreatLevel::Malicious => Style::default().fg(DANGER_COLOR), + ThreatLevel::Suspicious => Style::default().fg(WARNING_COLOR), + ThreatLevel::Clean => Style::default().fg(SUCCESS_COLOR), + }; + + let content = vec![ + Line::from(vec![ + Span::styled( + format!("[{}] ", detection.timestamp.format("%Y-%m-%d %H:%M:%S")), + Style::default().fg(Color::Gray) + ), + Span::styled( + format!("{:?}", detection.threat_level), + level_style.add_modifier(Modifier::BOLD) + ), + ]), + Line::from(format!("Process: {} (PID: {})", detection.process.name, detection.process.pid)), + Line::from(format!("Confidence: {:.1}%", detection.confidence * 100.0)), + Line::from("Indicators:"), + ]; + + let mut all_lines = content; + for indicator in &detection.indicators { + all_lines.push(Line::from(format!(" • {}", indicator))); + } + all_lines.push(Line::from("")); + + ListItem::new(Text::from(all_lines)) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("🚨 Detection History ({} total)", app.detections.len())) + .border_style(Style::default().fg(DANGER_COLOR)) + ) + .style(Style::default().fg(TEXT_COLOR)); + + f.render_stateful_widget(list, area, &mut app.detections_state.clone()); +} + +fn draw_memory(f: &mut Frame, area: Rect, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(8), Constraint::Min(0)]) + .split(area); + + // Memory usage gauge + let memory_gauge = Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("💾 Memory Usage") + .border_style(Style::default().fg(PRIMARY_COLOR)) + ) + .gauge_style(Style::default().fg(PRIMARY_COLOR)) + .percent((app.stats.memory_usage_mb * 10.0) as u16) + .label(format!("{:.2} MB", app.stats.memory_usage_mb)); + + f.render_widget(memory_gauge, chunks[0]); + + // Memory analysis placeholder + let memory_info = Paragraph::new( + "Memory Analysis:\n\n\ + • Process memory regions scanned\n\ + • RWX regions monitored\n\ + • Suspicious allocations detected\n\ + • Memory layout anomalies tracked\n\n\ + Advanced memory analysis features coming soon..." + ) + .block( + Block::default() + .borders(Borders::ALL) + .title("🧠 Memory Analysis") + .border_style(Style::default().fg(SECONDARY_COLOR)) + ) + .style(Style::default().fg(TEXT_COLOR)) + .wrap(Wrap { trim: true }); + + f.render_widget(memory_info, chunks[1]); +} + +fn draw_logs(f: &mut Frame, area: Rect, app: &App) { + let items: Vec = app + .logs + .iter() + .map(|log| ListItem::new(log.as_str()).style(Style::default().fg(TEXT_COLOR))) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("📜 System Logs ({} entries)", app.logs.len())) + .border_style(Style::default().fg(SUCCESS_COLOR)) + ) + .style(Style::default().fg(TEXT_COLOR)); + + f.render_stateful_widget(list, area, &mut app.logs_state.clone()); +} \ No newline at end of file