botserver/src/security/protection/chkrootkit.rs

294 lines
9.7 KiB
Rust
Raw Normal View History

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<Finding>, 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<Finding>, 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<Finding> {
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<String> {
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<String> {
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<Finding> = 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);
}
}