2026-01-10 09:41:12 -03:00
|
|
|
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"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:04:47 -03:00
|
|
|
impl std::str::FromStr for ProtectionTool {
|
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
2026-01-10 09:41:12 -03:00
|
|
|
match s.to_lowercase().as_str() {
|
2026-01-24 22:04:47 -03:00
|
|
|
"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(()),
|
2026-01-10 09:41:12 -03:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-24 22:04:47 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProtectionTool {
|
2026-01-10 09:41:12 -03:00
|
|
|
|
|
|
|
|
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<Self> {
|
|
|
|
|
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<String>,
|
|
|
|
|
pub service_running: Option<bool>,
|
|
|
|
|
pub last_scan: Option<DateTime<Utc>>,
|
|
|
|
|
pub last_update: Option<DateTime<Utc>>,
|
|
|
|
|
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<u32>,
|
|
|
|
|
pub warnings: u32,
|
|
|
|
|
pub suggestions: u32,
|
|
|
|
|
pub threats_found: u32,
|
|
|
|
|
pub rules_count: Option<u32>,
|
|
|
|
|
pub alerts_today: u32,
|
|
|
|
|
pub blocked_today: u32,
|
|
|
|
|
pub signatures_count: Option<u64>,
|
|
|
|
|
pub quarantined_count: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ScanResult {
|
|
|
|
|
pub scan_id: String,
|
|
|
|
|
pub tool: ProtectionTool,
|
|
|
|
|
pub started_at: DateTime<Utc>,
|
|
|
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
|
|
|
pub status: ScanStatus,
|
|
|
|
|
pub result: ScanResultStatus,
|
|
|
|
|
pub findings: Vec<Finding>,
|
|
|
|
|
pub warnings: u32,
|
|
|
|
|
pub report_path: Option<String>,
|
|
|
|
|
pub raw_output: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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<String>,
|
|
|
|
|
pub remediation: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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<ProtectionTool>,
|
|
|
|
|
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<RwLock<HashMap<ProtectionTool, ToolStatus>>>,
|
|
|
|
|
active_scans: Arc<RwLock<HashMap<String, ScanResult>>>,
|
|
|
|
|
scan_history: Arc<RwLock<Vec<ScanResult>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<ToolStatus> {
|
|
|
|
|
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<String> {
|
|
|
|
|
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<ProtectionTool, ToolStatus> {
|
|
|
|
|
self.tool_status.read().await.clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_tool_status_by_name(&self, name: &str) -> Option<ToolStatus> {
|
2026-01-24 22:04:47 -03:00
|
|
|
let tool: ProtectionTool = name.parse().ok()?;
|
2026-01-10 09:41:12 -03:00
|
|
|
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<ScanResult> {
|
|
|
|
|
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<ProtectionTool>, limit: usize) -> Vec<ScanResult> {
|
|
|
|
|
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<ScanResult> {
|
|
|
|
|
self.active_scans.read().await.values().cloned().collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_report(&self, tool: ProtectionTool) -> Result<String> {
|
|
|
|
|
let history = self.scan_history.read().await;
|
|
|
|
|
let latest = history
|
2026-01-24 22:04:47 -03:00
|
|
|
.iter().rfind(|s| s.tool == tool)
|
2026-01-10 09:41:12 -03:00
|
|
|
.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() {
|
2026-01-24 22:04:47 -03:00
|
|
|
assert_eq!("lynis".parse::<ProtectionTool>(), Ok(ProtectionTool::Lynis));
|
|
|
|
|
assert_eq!("LYNIS".parse::<ProtectionTool>(), Ok(ProtectionTool::Lynis));
|
|
|
|
|
assert_eq!("rkhunter".parse::<ProtectionTool>(), Ok(ProtectionTool::RKHunter));
|
|
|
|
|
assert_eq!("clamav".parse::<ProtectionTool>(), Ok(ProtectionTool::ClamAV));
|
|
|
|
|
assert_eq!("clamscan".parse::<ProtectionTool>(), Ok(ProtectionTool::ClamAV));
|
|
|
|
|
assert_eq!("maldet".parse::<ProtectionTool>(), Ok(ProtectionTool::LMD));
|
|
|
|
|
assert!("unknown".parse::<ProtectionTool>().is_err());
|
2026-01-10 09:41:12 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
|
|
|
|
}
|