Files
ghost/ghost-tui/src/app.rs
2025-11-08 11:49:33 +02:00

378 lines
12 KiB
Rust

use anyhow::Result;
use chrono::{DateTime, Utc};
use ghost_core::{
DetectionEngine, DetectionResult, ProcessInfo, ThreatLevel,
ThreatIntelligence, ThreatContext, IndicatorOfCompromise,
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,
ThreatIntel = 3,
Memory = 4,
Logs = 5,
}
impl TabIndex {
pub fn from_index(index: usize) -> Self {
match index {
0 => TabIndex::Overview,
1 => TabIndex::Processes,
2 => TabIndex::Detections,
3 => TabIndex::ThreatIntel,
4 => TabIndex::Memory,
5 => TabIndex::Logs,
_ => TabIndex::Overview,
}
}
pub fn next(self) -> Self {
Self::from_index((self as usize + 1) % 6)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectionEvent {
pub timestamp: DateTime<Utc>,
pub process: ProcessInfo,
pub threat_level: ThreatLevel,
pub indicators: Vec<String>,
pub confidence: f32,
pub threat_context: Option<ThreatContext>,
}
#[derive(Debug, Clone)]
pub struct ThreatIntelData {
pub total_iocs: usize,
pub recent_iocs: Vec<IndicatorOfCompromise>,
pub active_threats: Vec<String>,
pub threat_feed_status: Vec<FeedStatus>,
}
#[derive(Debug, Clone)]
pub struct FeedStatus {
pub name: String,
pub status: String,
pub last_update: String,
pub ioc_count: usize,
}
#[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 threat_intel: ThreatIntelligence,
pub processes: Vec<ProcessInfo>,
pub detections: VecDeque<DetectionEvent>,
pub logs: VecDeque<String>,
pub stats: SystemStats,
pub threat_intel_data: ThreatIntelData,
pub last_scan: Option<Instant>,
// UI state
pub processes_state: TableState,
pub detections_state: ListState,
pub logs_state: ListState,
pub threat_intel_state: ListState,
pub selected_process: Option<ProcessInfo>,
// Settings
pub auto_refresh: bool,
pub max_log_entries: usize,
pub max_detection_entries: usize,
}
impl App {
pub async fn new() -> Result<Self> {
let mut threat_intel = ThreatIntelligence::new();
threat_intel.initialize_default_feeds().await?;
let mut app = Self {
current_tab: TabIndex::Overview,
detection_engine: DetectionEngine::new(),
threat_intel,
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,
},
threat_intel_data: ThreatIntelData {
total_iocs: 0,
recent_iocs: Vec::new(),
active_threats: Vec::new(),
threat_feed_status: Vec::new(),
},
last_scan: None,
processes_state: TableState::default(),
detections_state: ListState::default(),
logs_state: ListState::default(),
threat_intel_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,
&regions,
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,
threat_context: None, // TODO: Integrate threat intelligence
});
}
}
}
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::<ProcessInfo>();
let detections_size = self.detections.len() * 200; // Estimate per detection
let logs_size = self.logs.iter().map(|s| s.len()).sum::<usize>();
(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"]
}
}