use anyhow::{Context, Result}; use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process::Command; use tracing::{error, info, warn}; use crate::security::command_guard::SafeCommand; const SUDOERS_FILE: &str = "/etc/sudoers.d/gb-protection"; const SUDOERS_CONTENT: &str = r#"# General Bots Security Protection Tools # This file is managed by botserver install protection # DO NOT EDIT MANUALLY # Lynis - security auditing {user} ALL=(ALL) NOPASSWD: /usr/bin/lynis audit system {user} ALL=(ALL) NOPASSWD: /usr/bin/lynis audit system --quick {user} ALL=(ALL) NOPASSWD: /usr/bin/lynis audit system --quick --no-colors {user} ALL=(ALL) NOPASSWD: /usr/bin/lynis audit system --no-colors # RKHunter - rootkit detection {user} ALL=(ALL) NOPASSWD: /usr/bin/rkhunter --check --skip-keypress {user} ALL=(ALL) NOPASSWD: /usr/bin/rkhunter --check --skip-keypress --report-warnings-only {user} ALL=(ALL) NOPASSWD: /usr/bin/rkhunter --update # Chkrootkit - rootkit detection {user} ALL=(ALL) NOPASSWD: /usr/bin/chkrootkit {user} ALL=(ALL) NOPASSWD: /usr/bin/chkrootkit -q # Suricata - IDS/IPS {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start suricata {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop suricata {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart suricata {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable suricata {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable suricata {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active suricata {user} ALL=(ALL) NOPASSWD: /usr/bin/suricata-update # ClamAV - antivirus {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start clamav-daemon {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop clamav-daemon {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart clamav-daemon {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable clamav-daemon {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable clamav-daemon {user} ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active clamav-daemon {user} ALL=(ALL) NOPASSWD: /usr/bin/freshclam # LMD (Linux Malware Detect) {user} ALL=(ALL) NOPASSWD: /usr/local/sbin/maldet -a /home {user} ALL=(ALL) NOPASSWD: /usr/local/sbin/maldet -a /var/www {user} ALL=(ALL) NOPASSWD: /usr/local/sbin/maldet -a /tmp {user} ALL=(ALL) NOPASSWD: /usr/local/sbin/maldet --update-sigs {user} ALL=(ALL) NOPASSWD: /usr/local/sbin/maldet --update-ver "#; const PACKAGES: &[&str] = &[ "lynis", "rkhunter", "chkrootkit", "suricata", "clamav", "clamav-daemon", ]; pub struct ProtectionInstaller { user: String, } impl ProtectionInstaller { pub fn new() -> Result { let user = std::env::var("SUDO_USER") .or_else(|_| std::env::var("USER")) .unwrap_or_else(|_| "root".to_string()); Ok(Self { user }) } pub fn check_root() -> bool { Command::new("id") .arg("-u") .output() .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "0") .unwrap_or(false) } pub fn install(&self) -> Result { if !Self::check_root() { return Err(anyhow::anyhow!( "This command requires root privileges. Run with: sudo botserver install protection" )); } info!("Starting security protection installation for user: {}", self.user); let mut result = InstallResult::default(); match self.install_packages() { Ok(installed) => { result.packages_installed = installed; info!("Packages installed: {:?}", result.packages_installed); } Err(e) => { error!("Failed to install packages: {e}"); result.errors.push(format!("Package installation failed: {e}")); } } match self.create_sudoers() { Ok(()) => { result.sudoers_created = true; info!("Sudoers file created successfully"); } Err(e) => { error!("Failed to create sudoers file: {e}"); result.errors.push(format!("Sudoers creation failed: {e}")); } } match self.install_lmd() { Ok(installed) => { if installed { result.packages_installed.push("maldetect".to_string()); info!("LMD (maldetect) installed successfully"); } } Err(e) => { warn!("LMD installation skipped: {e}"); result.warnings.push(format!("LMD installation skipped: {e}")); } } match self.update_databases() { Ok(()) => { result.databases_updated = true; info!("Security databases updated"); } Err(e) => { warn!("Database update failed: {e}"); result.warnings.push(format!("Database update failed: {e}")); } } result.success = result.errors.is_empty(); Ok(result) } fn install_packages(&self) -> Result> { info!("Updating package lists..."); SafeCommand::new("apt-get")? .arg("update")? .execute() .context("Failed to update package lists")?; let mut installed = Vec::new(); for package in PACKAGES { info!("Installing package: {package}"); let result = SafeCommand::new("apt-get")? .arg("install")? .arg("-y")? .arg(package)? .execute(); match result { Ok(output) => { if output.status.success() { installed.push((*package).to_string()); } else { let stderr = String::from_utf8_lossy(&output.stderr); warn!("Package {package} installation had issues: {stderr}"); } } Err(e) => { warn!("Failed to install {package}: {e}"); } } } Ok(installed) } fn create_sudoers(&self) -> Result<()> { let content = SUDOERS_CONTENT.replace("{user}", &self.user); info!("Creating sudoers file at {SUDOERS_FILE}"); fs::write(SUDOERS_FILE, &content) .context("Failed to write sudoers file")?; let permissions = fs::Permissions::from_mode(0o440); fs::set_permissions(SUDOERS_FILE, permissions) .context("Failed to set sudoers file permissions")?; self.validate_sudoers()?; info!("Sudoers file created and validated"); Ok(()) } fn validate_sudoers(&self) -> Result<()> { let output = std::process::Command::new("visudo") .args(["-c", "-f", SUDOERS_FILE]) .output() .context("Failed to run visudo validation")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); fs::remove_file(SUDOERS_FILE).ok(); return Err(anyhow::anyhow!("Invalid sudoers file syntax: {stderr}")); } Ok(()) } fn install_lmd(&self) -> Result { let maldet_path = Path::new("/usr/local/sbin/maldet"); if maldet_path.exists() { info!("LMD already installed"); return Ok(false); } info!("Installing Linux Malware Detect (LMD)..."); let temp_dir = "/tmp/maldetect_install"; fs::create_dir_all(temp_dir).ok(); let download_result = SafeCommand::new("curl")? .arg("-sL")? .arg("-o")? .arg("/tmp/maldetect-current.tar.gz")? .arg("https://www.rfxn.com/downloads/maldetect-current.tar.gz")? .execute(); if download_result.is_err() { return Err(anyhow::anyhow!("Failed to download LMD")); } SafeCommand::new("tar")? .arg("-xzf")? .arg("/tmp/maldetect-current.tar.gz")? .arg("-C")? .arg(temp_dir)? .execute() .context("Failed to extract LMD archive")?; let entries = fs::read_dir(temp_dir)?; let mut install_dir = None; for entry in entries.flatten() { let path = entry.path(); if path.is_dir() && path.file_name().is_some_and(|n| n.to_string_lossy().starts_with("maldetect")) { install_dir = Some(path); break; } } let install_dir = install_dir.ok_or_else(|| anyhow::anyhow!("LMD install directory not found"))?; let install_script = install_dir.join("install.sh"); if !install_script.exists() { return Err(anyhow::anyhow!("LMD install.sh not found")); } SafeCommand::new("bash")? .arg("-c")? .shell_script_arg(&format!("cd {} && ./install.sh", install_dir.display()))? .execute() .context("Failed to run LMD installer")?; fs::remove_dir_all(temp_dir).ok(); fs::remove_file("/tmp/maldetect-current.tar.gz").ok(); Ok(true) } fn update_databases(&self) -> Result<()> { info!("Updating security tool databases..."); if Path::new("/usr/bin/rkhunter").exists() { info!("Updating RKHunter database..."); let result = SafeCommand::new("rkhunter")? .arg("--update")? .execute(); if let Err(e) = result { warn!("RKHunter update failed: {e}"); } } if Path::new("/usr/bin/freshclam").exists() { info!("Updating ClamAV signatures..."); let result = SafeCommand::new("freshclam")? .execute(); if let Err(e) = result { warn!("ClamAV update failed: {e}"); } } if Path::new("/usr/bin/suricata-update").exists() { info!("Updating Suricata rules..."); let result = SafeCommand::new("suricata-update")? .execute(); if let Err(e) = result { warn!("Suricata update failed: {e}"); } } if Path::new("/usr/local/sbin/maldet").exists() { info!("Updating LMD signatures..."); let result = SafeCommand::new("maldet")? .arg("--update-sigs")? .execute(); if let Err(e) = result { warn!("LMD update failed: {e}"); } } Ok(()) } pub fn uninstall(&self) -> Result { if !Self::check_root() { return Err(anyhow::anyhow!( "This command requires root privileges. Run with: sudo botserver remove protection" )); } info!("Removing security protection components..."); let mut result = UninstallResult::default(); if Path::new(SUDOERS_FILE).exists() { match fs::remove_file(SUDOERS_FILE) { Ok(()) => { result.sudoers_removed = true; info!("Removed sudoers file"); } Err(e) => { result.errors.push(format!("Failed to remove sudoers: {e}")); } } } result.success = result.errors.is_empty(); result.message = "Protection sudoers removed. Packages were NOT uninstalled - remove manually if needed.".to_string(); Ok(result) } pub fn verify(&self) -> VerifyResult { let mut result = VerifyResult::default(); for package in PACKAGES { let binary = match *package { "clamav" | "clamav-daemon" => "clamscan", other => other, }; let check = SafeCommand::new("which") .and_then(|cmd| cmd.arg(binary)) .and_then(|cmd| cmd.execute()); let installed = check.map(|o| o.status.success()).unwrap_or(false); result.tools.push(ToolVerification { name: (*package).to_string(), installed, sudo_configured: false, }); } let maldet_installed = Path::new("/usr/local/sbin/maldet").exists(); result.tools.push(ToolVerification { name: "maldetect".to_string(), installed: maldet_installed, sudo_configured: false, }); result.sudoers_exists = Path::new(SUDOERS_FILE).exists(); if result.sudoers_exists { if let Ok(content) = fs::read_to_string(SUDOERS_FILE) { for tool in &mut result.tools { tool.sudo_configured = content.contains(&tool.name) || (tool.name == "clamav" && content.contains("clamav-daemon")) || (tool.name == "clamav-daemon" && content.contains("clamav-daemon")); } } } result.all_installed = result.tools.iter().filter(|t| t.name != "clamav-daemon").all(|t| t.installed); result.all_configured = result.sudoers_exists && result.tools.iter().all(|t| t.sudo_configured || !t.installed); result } } impl Default for ProtectionInstaller { fn default() -> Self { Self::new().unwrap_or(Self { user: "root".to_string() }) } } #[derive(Debug, Default)] pub struct InstallResult { pub success: bool, pub packages_installed: Vec, pub sudoers_created: bool, pub databases_updated: bool, pub errors: Vec, pub warnings: Vec, } impl InstallResult { pub fn print(&self) { println!(); if self.success { println!("✓ Security Protection installed successfully!"); } else { println!("✗ Security Protection installation completed with errors"); } println!(); if !self.packages_installed.is_empty() { println!("Packages installed:"); for pkg in &self.packages_installed { println!(" ✓ {pkg}"); } println!(); } if self.sudoers_created { println!("✓ Sudoers configuration created at {SUDOERS_FILE}"); } if self.databases_updated { println!("✓ Security databases updated"); } if !self.warnings.is_empty() { println!(); println!("Warnings:"); for warn in &self.warnings { println!(" ⚠ {warn}"); } } if !self.errors.is_empty() { println!(); println!("Errors:"); for err in &self.errors { println!(" ✗ {err}"); } } println!(); println!("The following commands are now available via the UI:"); println!(" - Lynis security audits"); println!(" - RKHunter rootkit scans"); println!(" - Chkrootkit scans"); println!(" - Suricata IDS management"); println!(" - ClamAV antivirus scans"); println!(" - LMD malware detection"); } } #[derive(Debug, Default)] pub struct UninstallResult { pub success: bool, pub sudoers_removed: bool, pub message: String, pub errors: Vec, } impl UninstallResult { pub fn print(&self) { println!(); if self.success { println!("✓ {}", self.message); } else { println!("✗ Uninstall completed with errors"); for err in &self.errors { println!(" ✗ {err}"); } } } } #[derive(Debug, Default)] pub struct VerifyResult { pub all_installed: bool, pub all_configured: bool, pub sudoers_exists: bool, pub tools: Vec, } #[derive(Debug, Default)] pub struct ToolVerification { pub name: String, pub installed: bool, pub sudo_configured: bool, } impl VerifyResult { pub fn print(&self) { println!(); println!("Security Protection Status:"); println!(); println!("Tools:"); for tool in &self.tools { let installed_mark = if tool.installed { "✓" } else { "✗" }; let sudo_mark = if tool.sudo_configured { "✓" } else { "✗" }; println!(" {} {} (installed: {}, sudo: {})", if tool.installed && tool.sudo_configured { "✓" } else { "⚠" }, tool.name, installed_mark, sudo_mark ); } println!(); println!("Sudoers file: {}", if self.sudoers_exists { "✓ exists" } else { "✗ missing" }); println!(); if self.all_installed && self.all_configured { println!("✓ All protection tools are properly configured"); } else if !self.all_installed { println!("⚠ Some tools are not installed. Run: sudo botserver install protection"); } else { println!("⚠ Sudoers not configured. Run: sudo botserver install protection"); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_install_result_default() { let result = InstallResult::default(); assert!(!result.success); assert!(result.packages_installed.is_empty()); assert!(!result.sudoers_created); } #[test] fn test_verify_result_default() { let result = VerifyResult::default(); assert!(!result.all_installed); assert!(!result.all_configured); assert!(result.tools.is_empty()); } #[test] fn test_sudoers_content_has_placeholder() { assert!(SUDOERS_CONTENT.contains("{user}")); } #[test] fn test_sudoers_content_no_wildcards() { assert!(!SUDOERS_CONTENT.contains(" * ")); assert!(!SUDOERS_CONTENT.lines().any(|l| l.trim().ends_with('*'))); } #[test] fn test_packages_list() { assert!(PACKAGES.contains(&"lynis")); assert!(PACKAGES.contains(&"rkhunter")); assert!(PACKAGES.contains(&"chkrootkit")); assert!(PACKAGES.contains(&"suricata")); assert!(PACKAGES.contains(&"clamav")); } #[test] fn test_tool_verification_default() { let tool = ToolVerification::default(); assert!(tool.name.is_empty()); assert!(!tool.installed); assert!(!tool.sudo_configured); } #[test] fn test_uninstall_result_default() { let result = UninstallResult::default(); assert!(!result.success); assert!(!result.sudoers_removed); assert!(result.message.is_empty()); } #[test] fn test_protection_installer_default() { let installer = ProtectionInstaller::default(); assert!(!installer.user.is_empty()); } }