use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{info, warn}; use crate::security::command_guard::SafeCommand; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ProtectionTool { Lynis, RKHunter, Chkrootkit, Suricata, LMD, ClamAV, } impl std::fmt::Display for ProtectionTool { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Lynis => write!(f, "lynis"), Self::RKHunter => write!(f, "rkhunter"), Self::Chkrootkit => write!(f, "chkrootkit"), Self::Suricata => write!(f, "suricata"), Self::LMD => write!(f, "lmd"), Self::ClamAV => write!(f, "clamav"), } } } impl std::str::FromStr for ProtectionTool { type Err = (); fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "lynis" => Ok(Self::Lynis), "rkhunter" => Ok(Self::RKHunter), "chkrootkit" => Ok(Self::Chkrootkit), "suricata" => Ok(Self::Suricata), "lmd" | "maldet" => Ok(Self::LMD), "clamav" | "clamscan" => Ok(Self::ClamAV), _ => Err(()), } } } impl ProtectionTool { pub fn binary_name(&self) -> &'static str { match self { Self::Lynis => "lynis", Self::RKHunter => "rkhunter", Self::Chkrootkit => "chkrootkit", Self::Suricata => "suricata", Self::LMD => "maldet", Self::ClamAV => "clamscan", } } pub fn service_name(&self) -> Option<&'static str> { match self { Self::Suricata => Some("suricata"), Self::ClamAV => Some("clamav-daemon"), _ => None, } } pub fn package_name(&self) -> &'static str { match self { Self::Lynis => "lynis", Self::RKHunter => "rkhunter", Self::Chkrootkit => "chkrootkit", Self::Suricata => "suricata", Self::LMD => "maldetect", Self::ClamAV => "clamav", } } pub fn all() -> Vec { vec![ Self::Lynis, Self::RKHunter, Self::Chkrootkit, Self::Suricata, Self::LMD, Self::ClamAV, ] } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolStatus { pub tool: ProtectionTool, pub installed: bool, pub version: Option, pub service_running: Option, pub last_scan: Option>, pub last_update: Option>, pub auto_update: bool, pub auto_remediate: bool, pub metrics: ToolMetrics, } impl ToolStatus { pub fn not_installed(tool: ProtectionTool) -> Self { Self { tool, installed: false, version: None, service_running: None, last_scan: None, last_update: None, auto_update: false, auto_remediate: false, metrics: ToolMetrics::default(), } } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ToolMetrics { pub hardening_index: Option, pub warnings: u32, pub suggestions: u32, pub threats_found: u32, pub rules_count: Option, pub alerts_today: u32, pub blocked_today: u32, pub signatures_count: Option, pub quarantined_count: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScanResult { pub scan_id: String, pub tool: ProtectionTool, pub started_at: DateTime, pub completed_at: Option>, pub status: ScanStatus, pub result: ScanResultStatus, pub findings: Vec, pub warnings: u32, pub report_path: Option, pub raw_output: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ScanStatus { Pending, Running, Completed, Failed, Cancelled, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ScanResultStatus { Clean, Warnings, Infected, Unknown, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Finding { pub id: String, pub severity: FindingSeverity, pub category: String, pub title: String, pub description: String, pub file_path: Option, pub remediation: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum FindingSeverity { Info, Low, Medium, High, Critical, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtectionConfig { pub enabled_tools: Vec, pub auto_scan_interval_hours: u32, pub auto_update_interval_hours: u32, pub quarantine_dir: String, pub log_dir: String, } impl Default for ProtectionConfig { fn default() -> Self { Self { enabled_tools: ProtectionTool::all(), auto_scan_interval_hours: 24, auto_update_interval_hours: 6, quarantine_dir: "/var/lib/gb/quarantine".to_string(), log_dir: "/var/log/gb/security".to_string(), } } } pub struct ProtectionManager { config: ProtectionConfig, tool_status: Arc>>, active_scans: Arc>>, scan_history: Arc>>, } impl ProtectionManager { pub fn new(config: ProtectionConfig) -> Self { Self { config, tool_status: Arc::new(RwLock::new(HashMap::new())), active_scans: Arc::new(RwLock::new(HashMap::new())), scan_history: Arc::new(RwLock::new(Vec::new())), } } pub async fn initialize(&self) -> Result<()> { info!("Initializing Protection Manager"); for tool in &self.config.enabled_tools { let status = self.check_tool_status(*tool).await?; self.tool_status.write().await.insert(*tool, status); } Ok(()) } pub async fn check_tool_status(&self, tool: ProtectionTool) -> Result { let installed = self.is_tool_installed(tool).await; if !installed { return Ok(ToolStatus::not_installed(tool)); } let version = self.get_tool_version(tool).await.ok(); let service_running = if tool.service_name().is_some() { Some(self.is_service_running(tool).await) } else { None }; let stored = self.tool_status.read().await; let existing = stored.get(&tool); Ok(ToolStatus { tool, installed: true, version, service_running, last_scan: existing.and_then(|s| s.last_scan), last_update: existing.and_then(|s| s.last_update), auto_update: existing.map(|s| s.auto_update).unwrap_or(false), auto_remediate: existing.map(|s| s.auto_remediate).unwrap_or(false), metrics: existing.map(|s| s.metrics.clone()).unwrap_or_default(), }) } pub async fn is_tool_installed(&self, tool: ProtectionTool) -> bool { let binary = tool.binary_name(); let result = SafeCommand::new("which") .and_then(|cmd| cmd.arg(binary)) .and_then(|cmd| cmd.execute()); match result { Ok(output) => output.status.success(), Err(e) => { warn!("Failed to check if {tool} is installed: {e}"); false } } } pub async fn get_tool_version(&self, tool: ProtectionTool) -> Result { let binary = tool.binary_name(); let version_arg = match tool { ProtectionTool::Lynis => "--version", ProtectionTool::RKHunter => "--version", ProtectionTool::Chkrootkit => "-V", ProtectionTool::Suricata => "--build-info", ProtectionTool::LMD => "--version", ProtectionTool::ClamAV => "--version", }; let output = SafeCommand::new(binary)? .arg(version_arg)? .execute() .context("Failed to get tool version")?; let stdout = String::from_utf8_lossy(&output.stdout); let version = stdout.lines().next().unwrap_or("unknown").trim().to_string(); Ok(version) } pub async fn is_service_running(&self, tool: ProtectionTool) -> bool { let Some(service_name) = tool.service_name() else { return false; }; let result = SafeCommand::new("sudo") .and_then(|cmd| cmd.arg("systemctl")) .and_then(|cmd| cmd.arg("is-active")) .and_then(|cmd| cmd.arg(service_name)); match result { Ok(cmd) => match cmd.execute() { Ok(output) => { let stdout = String::from_utf8_lossy(&output.stdout); stdout.trim() == "active" } Err(_) => false, }, Err(_) => false, } } pub async fn get_all_status(&self) -> HashMap { self.tool_status.read().await.clone() } pub async fn get_tool_status_by_name(&self, name: &str) -> Option { let tool: ProtectionTool = name.parse().ok()?; self.tool_status.read().await.get(&tool).cloned() } pub async fn install_tool(&self, tool: ProtectionTool) -> Result<()> { info!("Installing protection tool: {tool}"); let package = tool.package_name(); SafeCommand::new("apt-get")? .arg("install")? .arg("-y")? .arg(package)? .execute() .context("Failed to install tool")?; let status = self.check_tool_status(tool).await?; self.tool_status.write().await.insert(tool, status); info!("Successfully installed {tool}"); Ok(()) } pub async fn start_service(&self, tool: ProtectionTool) -> Result<()> { let Some(service_name) = tool.service_name() else { return Err(anyhow::anyhow!("{tool} does not have a service")); }; info!("Starting service: {service_name}"); SafeCommand::new("sudo")? .arg("systemctl")? .arg("start")? .arg(service_name)? .execute() .context("Failed to start service")?; if let Some(status) = self.tool_status.write().await.get_mut(&tool) { status.service_running = Some(true); } Ok(()) } pub async fn stop_service(&self, tool: ProtectionTool) -> Result<()> { let Some(service_name) = tool.service_name() else { return Err(anyhow::anyhow!("{tool} does not have a service")); }; info!("Stopping service: {service_name}"); SafeCommand::new("sudo")? .arg("systemctl")? .arg("stop")? .arg(service_name)? .execute() .context("Failed to stop service")?; if let Some(status) = self.tool_status.write().await.get_mut(&tool) { status.service_running = Some(false); } Ok(()) } pub async fn enable_service(&self, tool: ProtectionTool) -> Result<()> { let Some(service_name) = tool.service_name() else { return Err(anyhow::anyhow!("{tool} does not have a service")); }; SafeCommand::new("sudo")? .arg("systemctl")? .arg("enable")? .arg(service_name)? .execute() .context("Failed to enable service")?; Ok(()) } pub async fn disable_service(&self, tool: ProtectionTool) -> Result<()> { let Some(service_name) = tool.service_name() else { return Err(anyhow::anyhow!("{tool} does not have a service")); }; SafeCommand::new("sudo")? .arg("systemctl")? .arg("disable")? .arg(service_name)? .execute() .context("Failed to disable service")?; Ok(()) } pub async fn run_scan(&self, tool: ProtectionTool) -> Result { let scan_id = uuid::Uuid::new_v4().to_string(); let started_at = Utc::now(); info!("Starting {tool} scan with ID: {scan_id}"); let mut result = ScanResult { scan_id: scan_id.clone(), tool, started_at, completed_at: None, status: ScanStatus::Running, result: ScanResultStatus::Unknown, findings: Vec::new(), warnings: 0, report_path: None, raw_output: None, }; self.active_scans.write().await.insert(scan_id.clone(), result.clone()); let scan_output = match tool { ProtectionTool::Lynis => super::lynis::run_scan().await, ProtectionTool::RKHunter => super::rkhunter::run_scan().await, ProtectionTool::Chkrootkit => super::chkrootkit::run_scan().await, ProtectionTool::Suricata => super::suricata::get_alerts().await, ProtectionTool::LMD => super::lmd::run_scan(None).await, ProtectionTool::ClamAV => super::lynis::run_scan().await, }; result.completed_at = Some(Utc::now()); match scan_output { Ok((status, findings, raw)) => { result.status = ScanStatus::Completed; result.result = status; result.warnings = findings.iter().filter(|f| f.severity == FindingSeverity::Medium || f.severity == FindingSeverity::Low).count() as u32; result.findings = findings; result.raw_output = Some(raw); } Err(e) => { warn!("Scan failed for {tool}: {e}"); result.status = ScanStatus::Failed; result.raw_output = Some(e.to_string()); } } self.active_scans.write().await.remove(&scan_id); if let Some(status) = self.tool_status.write().await.get_mut(&tool) { status.last_scan = Some(Utc::now()); status.metrics.warnings = result.warnings; status.metrics.threats_found = result.findings.iter() .filter(|f| f.severity == FindingSeverity::High || f.severity == FindingSeverity::Critical) .count() as u32; } self.scan_history.write().await.push(result.clone()); Ok(result) } pub async fn update_definitions(&self, tool: ProtectionTool) -> Result<()> { info!("Updating definitions for {tool}"); match tool { ProtectionTool::RKHunter => { SafeCommand::new("sudo")? .arg("rkhunter")? .arg("--update")? .execute()?; } ProtectionTool::ClamAV => { SafeCommand::new("sudo")? .arg("freshclam")? .execute()?; } ProtectionTool::Suricata => { SafeCommand::new("sudo")? .arg("suricata-update")? .execute()?; } ProtectionTool::LMD => { SafeCommand::new("sudo")? .arg("maldet")? .arg("--update-sigs")? .execute()?; } _ => { return Err(anyhow::anyhow!("{tool} does not support definition updates")); } } if let Some(status) = self.tool_status.write().await.get_mut(&tool) { status.last_update = Some(Utc::now()); } Ok(()) } pub async fn set_auto_update(&self, tool: ProtectionTool, enabled: bool) -> Result<()> { if let Some(status) = self.tool_status.write().await.get_mut(&tool) { status.auto_update = enabled; } Ok(()) } pub async fn set_auto_remediate(&self, tool: ProtectionTool, enabled: bool) -> Result<()> { if let Some(status) = self.tool_status.write().await.get_mut(&tool) { status.auto_remediate = enabled; } Ok(()) } pub async fn get_scan_history(&self, tool: Option, limit: usize) -> Vec { let history = self.scan_history.read().await; history .iter() .filter(|s| tool.is_none() || Some(s.tool) == tool) .rev() .take(limit) .cloned() .collect() } pub async fn get_active_scans(&self) -> Vec { self.active_scans.read().await.values().cloned().collect() } pub async fn get_report(&self, tool: ProtectionTool) -> Result { let history = self.scan_history.read().await; let latest = history .iter().rfind(|s| s.tool == tool) .ok_or_else(|| anyhow::anyhow!("No scan results found for {tool}"))?; latest.raw_output.clone().ok_or_else(|| anyhow::anyhow!("No report available")) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_protection_tool_from_str() { assert_eq!("lynis".parse::(), Ok(ProtectionTool::Lynis)); assert_eq!("LYNIS".parse::(), Ok(ProtectionTool::Lynis)); assert_eq!("rkhunter".parse::(), Ok(ProtectionTool::RKHunter)); assert_eq!("clamav".parse::(), Ok(ProtectionTool::ClamAV)); assert_eq!("clamscan".parse::(), Ok(ProtectionTool::ClamAV)); assert_eq!("maldet".parse::(), Ok(ProtectionTool::LMD)); assert!("unknown".parse::().is_err()); } #[test] fn test_protection_tool_display() { assert_eq!(format!("{}", ProtectionTool::Lynis), "lynis"); assert_eq!(format!("{}", ProtectionTool::ClamAV), "clamav"); } #[test] fn test_tool_status_not_installed() { let status = ToolStatus::not_installed(ProtectionTool::Lynis); assert!(!status.installed); assert!(status.version.is_none()); assert!(status.service_running.is_none()); } #[test] fn test_protection_config_default() { let config = ProtectionConfig::default(); assert_eq!(config.auto_scan_interval_hours, 24); assert_eq!(config.auto_update_interval_hours, 6); assert_eq!(config.enabled_tools.len(), 6); } #[test] fn test_protection_tool_all() { let all = ProtectionTool::all(); assert_eq!(all.len(), 6); assert!(all.contains(&ProtectionTool::Lynis)); assert!(all.contains(&ProtectionTool::ClamAV)); } #[test] fn test_finding_severity() { let finding = Finding { id: "test".to_string(), severity: FindingSeverity::High, category: "security".to_string(), title: "Test".to_string(), description: "Test finding".to_string(), file_path: None, remediation: None, }; assert_eq!(finding.severity, FindingSeverity::High); } }