use anyhow::{Context, Result}; use tracing::info; use crate::security::command_guard::SafeCommand; use super::manager::{Finding, FindingSeverity, ScanResultStatus}; pub async fn run_scan() -> Result<(ScanResultStatus, Vec, String)> { info!("Running Chkrootkit rootkit scan"); let output = SafeCommand::new("sudo")? .arg("chkrootkit")? .arg("-q")? .execute() .context("Failed to run Chkrootkit scan")?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let raw_output = format!("{stdout}\n{stderr}"); let findings = parse_chkrootkit_output(&stdout); let status = determine_result_status(&findings); Ok((status, findings, raw_output)) } pub async fn run_expert_scan() -> Result<(ScanResultStatus, Vec, String)> { info!("Running Chkrootkit expert mode scan"); let output = SafeCommand::new("sudo")? .arg("chkrootkit")? .arg("-x")? .execute() .context("Failed to run Chkrootkit expert scan")?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let raw_output = format!("{stdout}\n{stderr}"); let findings = parse_chkrootkit_output(&stdout); let status = determine_result_status(&findings); Ok((status, findings, raw_output)) } pub fn parse_chkrootkit_output(output: &str) -> Vec { let mut findings = Vec::new(); let mut current_check = String::new(); for line in output.lines() { let trimmed = line.trim(); if trimmed.is_empty() { continue; } if trimmed.starts_with("Checking") || trimmed.starts_with("Searching") { current_check = trimmed.to_string(); continue; } if trimmed.contains("INFECTED") { let finding = Finding { id: format!("chkrootkit-infected-{}", findings.len()), severity: FindingSeverity::Critical, category: "Rootkit Detection".to_string(), title: "Infected File or Process Detected".to_string(), description: format!("{current_check}: {trimmed}"), file_path: extract_file_path(trimmed), remediation: Some("Immediately investigate and consider system recovery from clean backup".to_string()), }; findings.push(finding); } if trimmed.contains("Vulnerable") || trimmed.contains("VULNERABLE") { let finding = Finding { id: format!("chkrootkit-vuln-{}", findings.len()), severity: FindingSeverity::High, category: "Vulnerability".to_string(), title: "Vulnerable Component Detected".to_string(), description: format!("{current_check}: {trimmed}"), file_path: extract_file_path(trimmed), remediation: Some("Update the affected component to patch the vulnerability".to_string()), }; findings.push(finding); } if trimmed.contains("Possible") && trimmed.contains("rootkit") { let finding = Finding { id: format!("chkrootkit-possible-{}", findings.len()), severity: FindingSeverity::High, category: "Rootkit Detection".to_string(), title: "Possible Rootkit Detected".to_string(), description: format!("{current_check}: {trimmed}"), file_path: extract_file_path(trimmed), remediation: Some("Investigate the suspicious activity and verify system integrity".to_string()), }; findings.push(finding); } if trimmed.contains("Warning") || trimmed.contains("WARNING") { let finding = Finding { id: format!("chkrootkit-warn-{}", findings.len()), severity: FindingSeverity::Medium, category: "Security Warning".to_string(), title: "Security Warning".to_string(), description: trimmed.to_string(), file_path: extract_file_path(trimmed), remediation: None, }; findings.push(finding); } if trimmed.contains("suspicious") { let finding = Finding { id: format!("chkrootkit-susp-{}", findings.len()), severity: FindingSeverity::Medium, category: current_check.clone(), title: "Suspicious Activity Detected".to_string(), description: trimmed.to_string(), file_path: extract_file_path(trimmed), remediation: Some("Review the flagged item for potential threats".to_string()), }; findings.push(finding); } } findings } pub async fn get_version() -> Result { let output = SafeCommand::new("chkrootkit")? .arg("-V")? .execute() .context("Failed to get Chkrootkit version")?; let stdout = String::from_utf8_lossy(&output.stdout); let version = stdout .lines() .next() .unwrap_or("unknown") .trim() .to_string(); Ok(version) } fn extract_file_path(line: &str) -> Option { let words: Vec<&str> = line.split_whitespace().collect(); for word in words { if word.starts_with('/') { return Some(word.trim_matches(|c| c == ':' || c == ',' || c == ';' || c == '`' || c == '\'').to_string()); } } None } fn determine_result_status(findings: &[Finding]) -> ScanResultStatus { let has_critical = findings.iter().any(|f| f.severity == FindingSeverity::Critical); let has_high = findings.iter().any(|f| f.severity == FindingSeverity::High); let has_medium = findings.iter().any(|f| f.severity == FindingSeverity::Medium); if has_critical { ScanResultStatus::Infected } else if has_high { ScanResultStatus::Warnings } else if has_medium { ScanResultStatus::Warnings } else { ScanResultStatus::Clean } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_chkrootkit_output_clean() { let output = r#" Checking `amd'... not found Checking `basename'... not infected Checking `biff'... not found Checking `chfn'... not infected Checking `chsh'... not infected Checking `cron'... not infected "#; let findings = parse_chkrootkit_output(output); assert!(findings.is_empty()); } #[test] fn test_parse_chkrootkit_output_infected() { let output = r#" Checking `amd'... not found Checking `basename'... INFECTED Checking `biff'... not found "#; let findings = parse_chkrootkit_output(output); assert_eq!(findings.len(), 1); assert_eq!(findings[0].severity, FindingSeverity::Critical); assert!(findings[0].description.contains("INFECTED")); } #[test] fn test_parse_chkrootkit_output_vulnerable() { let output = r#" Checking `lkm'... Searching for Suckit rootkit... Vulnerable "#; let findings = parse_chkrootkit_output(output); assert_eq!(findings.len(), 1); assert_eq!(findings[0].severity, FindingSeverity::High); } #[test] fn test_parse_chkrootkit_output_suspicious() { let output = r#" Checking `sniffer'... lo: not promisc and no packet sniffer sockets eth0: suspicious activity detected "#; let findings = parse_chkrootkit_output(output); assert_eq!(findings.len(), 1); assert_eq!(findings[0].severity, FindingSeverity::Medium); } #[test] fn test_extract_file_path() { assert_eq!( extract_file_path("Found suspicious file: /etc/passwd"), Some("/etc/passwd".to_string()) ); assert_eq!( extract_file_path("Checking `/usr/bin/ls'"), Some("/usr/bin/ls".to_string()) ); assert_eq!(extract_file_path("No path in this line"), None); } #[test] fn test_determine_result_status_clean() { let findings: Vec = vec![]; assert_eq!(determine_result_status(&findings), ScanResultStatus::Clean); } #[test] fn test_determine_result_status_infected() { let findings = vec![Finding { id: "test".to_string(), severity: FindingSeverity::Critical, category: "test".to_string(), title: "Test".to_string(), description: "Test".to_string(), file_path: None, remediation: None, }]; assert_eq!(determine_result_status(&findings), ScanResultStatus::Infected); } #[test] fn test_determine_result_status_warnings() { let findings = vec![Finding { id: "test".to_string(), severity: FindingSeverity::High, category: "test".to_string(), title: "Test".to_string(), description: "Test".to_string(), file_path: None, remediation: None, }]; assert_eq!(determine_result_status(&findings), ScanResultStatus::Warnings); } #[test] fn test_parse_chkrootkit_possible_rootkit() { let output = r#" Checking `sniffer'... Possible rootkit activity detected "#; let findings = parse_chkrootkit_output(output); assert_eq!(findings.len(), 1); assert_eq!(findings[0].severity, FindingSeverity::High); assert!(findings[0].title.contains("Possible Rootkit")); } #[test] fn test_parse_chkrootkit_warning() { let output = r#" Warning: some security issue detected "#; let findings = parse_chkrootkit_output(output); assert_eq!(findings.len(), 1); assert_eq!(findings[0].severity, FindingSeverity::Medium); } }