1pub mod allowlist;
14pub mod container_scanner;
15pub mod dns_filter;
16pub mod file_quarantine;
17pub mod fim;
18pub mod heuristics;
19pub mod memory_scanner;
20pub mod network_monitor;
21pub mod process_monitor;
22pub mod rootkit_detector;
23pub mod signatures;
24pub mod supply_chain;
25pub mod threat_intel;
26pub mod usb_monitor;
27pub mod watcher;
28pub mod yara_engine;
29
30use chrono::{DateTime, Utc};
31use parking_lot::RwLock;
32use serde::{Deserialize, Serialize};
33use std::collections::VecDeque;
34use std::path::{Path, PathBuf};
35use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
36use std::sync::Arc;
37
38use crate::audit_chain::{AuditChain, SecurityEventType};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
46pub enum Severity {
47 Info,
48 Low,
49 Medium,
50 High,
51 Critical,
52}
53
54impl std::fmt::Display for Severity {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 Self::Info => write!(f, "info"),
58 Self::Low => write!(f, "low"),
59 Self::Medium => write!(f, "medium"),
60 Self::High => write!(f, "high"),
61 Self::Critical => write!(f, "critical"),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum DetectionCategory {
73 MalwareSignature { name: String, family: String },
74 HeuristicAnomaly { rule: String },
75 SuspiciousProcess { pid: u32, name: String },
76 NetworkAnomaly { connection: String },
77 MemoryAnomaly { pid: u32, region: String },
78 RootkitIndicator { technique: String },
79 YaraMatch { rule_name: String, tags: Vec<String> },
80 FilelessMalware { technique: String },
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
89pub enum RecommendedAction {
90 LogOnly,
91 Alert,
92 Quarantine { source_path: PathBuf },
93 KillProcess { pid: u32 },
94 BlockConnection { addr: String },
95 Multi(Vec<RecommendedAction>),
96}
97
98impl std::fmt::Display for RecommendedAction {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Self::LogOnly => write!(f, "log_only"),
102 Self::Alert => write!(f, "alert"),
103 Self::Quarantine { source_path } => {
104 write!(f, "quarantine({})", source_path.display())
105 }
106 Self::KillProcess { pid } => write!(f, "kill({})", pid),
107 Self::BlockConnection { addr } => write!(f, "block({})", addr),
108 Self::Multi(actions) => {
109 let names: Vec<String> = actions.iter().map(|a| a.to_string()).collect();
110 write!(f, "multi[{}]", names.join(", "))
111 }
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ScanResult {
123 pub id: String,
125 pub timestamp: DateTime<Utc>,
127 pub scanner: String,
129 pub target: String,
131 pub severity: Severity,
133 pub category: DetectionCategory,
135 pub description: String,
137 pub confidence: f64,
139 pub action: RecommendedAction,
141 pub artifact_hash: Option<String>,
143}
144
145impl ScanResult {
146 pub fn new(
148 scanner: impl Into<String>,
149 target: impl Into<String>,
150 severity: Severity,
151 category: DetectionCategory,
152 description: impl Into<String>,
153 confidence: f64,
154 action: RecommendedAction,
155 ) -> Self {
156 Self {
157 id: uuid::Uuid::new_v4().to_string(),
158 timestamp: Utc::now(),
159 scanner: scanner.into(),
160 target: target.into(),
161 severity,
162 category,
163 description: description.into(),
164 confidence: confidence.clamp(0.0, 1.0),
165 action,
166 artifact_hash: None,
167 }
168 }
169
170 pub fn with_hash(mut self, hash: String) -> Self {
172 self.artifact_hash = Some(hash);
173 self
174 }
175}
176
177#[async_trait::async_trait]
183pub trait Scanner: Send + Sync {
184 fn name(&self) -> &str;
186
187 fn is_active(&self) -> bool;
189
190 async fn scan_file(&self, path: &Path) -> Vec<ScanResult>;
192
193 async fn scan_bytes(&self, _data: &[u8], _label: &str) -> Vec<ScanResult> {
195 Vec::new()
196 }
197
198 async fn scan_process(&self, _pid: u32) -> Vec<ScanResult> {
200 Vec::new()
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct EndpointConfig {
211 pub enabled: bool,
212 pub enable_watcher: bool,
213 pub enable_process_monitor: bool,
214 pub enable_network_monitor: bool,
215 pub enable_memory_scanner: bool,
216 pub enable_rootkit_detector: bool,
217 pub enable_dns_filter: bool,
218 pub enable_usb_monitor: bool,
219 pub enable_fim: bool,
220 pub data_dir: PathBuf,
221 pub watcher: watcher::WatcherConfig,
222 pub process_monitor: process_monitor::ProcessMonitorConfig,
223 pub network_monitor: network_monitor::NetworkMonitorConfig,
224 pub memory_scanner: memory_scanner::MemoryScanConfig,
225 pub rootkit_detector: rootkit_detector::RootkitConfig,
226 pub heuristics: heuristics::HeuristicConfig,
227 pub quarantine: file_quarantine::QuarantineVaultConfig,
228 pub allowlist: allowlist::AllowlistConfig,
229 pub threat_intel: threat_intel::ThreatIntelConfig,
230 pub signatures: signatures::SignatureConfig,
231 pub dns_filter: dns_filter::DnsFilterConfig,
232 pub usb_monitor: usb_monitor::UsbMonitorConfig,
233 pub fim: fim::FimConfig,
234}
235
236impl Default for EndpointConfig {
237 fn default() -> Self {
238 let data_dir = dirs_or_default();
239 Self {
240 enabled: true,
241 enable_watcher: true,
242 enable_process_monitor: true,
243 enable_network_monitor: true,
244 enable_memory_scanner: false, enable_rootkit_detector: false, enable_dns_filter: false, enable_usb_monitor: true, enable_fim: false, data_dir: data_dir.clone(),
250 watcher: watcher::WatcherConfig::default(),
251 process_monitor: process_monitor::ProcessMonitorConfig::default(),
252 network_monitor: network_monitor::NetworkMonitorConfig::default(),
253 memory_scanner: memory_scanner::MemoryScanConfig::default(),
254 rootkit_detector: rootkit_detector::RootkitConfig::new(data_dir.clone()),
255 heuristics: heuristics::HeuristicConfig::default(),
256 quarantine: file_quarantine::QuarantineVaultConfig::new(data_dir.join("quarantine")),
257 allowlist: allowlist::AllowlistConfig::default(),
258 threat_intel: threat_intel::ThreatIntelConfig::new(data_dir.join("threat-intel")),
259 signatures: signatures::SignatureConfig::new(data_dir.join("signatures.ndjson")),
260 dns_filter: dns_filter::DnsFilterConfig::default(),
261 usb_monitor: usb_monitor::UsbMonitorConfig::default(),
262 fim: fim::FimConfig::default(),
263 }
264 }
265}
266
267fn dirs_or_default() -> PathBuf {
268 std::env::var("HOME")
269 .map(|h| PathBuf::from(h).join(".nexus-shield"))
270 .unwrap_or_else(|_| PathBuf::from("/tmp/nexus-shield"))
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct EndpointStats {
280 pub total_files_scanned: u64,
281 pub total_threats_detected: u64,
282 pub active_monitors: Vec<String>,
283 pub quarantined_files: usize,
284 pub last_scan_time: Option<DateTime<Utc>>,
285 pub scanners_active: Vec<String>,
286}
287
288pub struct EndpointEngine {
294 scanners: Vec<Arc<dyn Scanner>>,
296 pub allowlist: Arc<allowlist::DeveloperAllowlist>,
298 pub threat_intel: Arc<threat_intel::ThreatIntelDB>,
300 pub quarantine: Arc<file_quarantine::QuarantineVault>,
302 pub dns_filter: Option<Arc<dns_filter::DnsFilter>>,
304 pub container_scanner: container_scanner::ContainerScanner,
306 pub supply_chain_scanner: supply_chain::SupplyChainScanner,
308 result_tx: tokio::sync::broadcast::Sender<ScanResult>,
310 history: Arc<RwLock<VecDeque<ScanResult>>>,
312 config: EndpointConfig,
314 files_scanned: Arc<AtomicU64>,
316 threats_detected: Arc<AtomicU64>,
317 running: AtomicBool,
319}
320
321impl EndpointEngine {
322 pub fn new(config: EndpointConfig) -> Self {
324 let (result_tx, _) = tokio::sync::broadcast::channel(1024);
325
326 let allowlist = Arc::new(allowlist::DeveloperAllowlist::new(config.allowlist.clone()));
328 let threat_intel = Arc::new(threat_intel::ThreatIntelDB::new(config.threat_intel.clone()));
329 let quarantine = Arc::new(file_quarantine::QuarantineVault::new(config.quarantine.clone()));
330
331 let dns_filter = if config.enable_dns_filter {
333 Some(Arc::new(dns_filter::DnsFilter::new(
334 config.dns_filter.clone(),
335 Arc::clone(&threat_intel),
336 )))
337 } else {
338 None
339 };
340
341 let mut scanners: Vec<Arc<dyn Scanner>> = Vec::new();
343
344 let sig_engine = signatures::SignatureEngine::new(config.signatures.clone());
346 scanners.push(Arc::new(sig_engine));
347
348 let heur_engine = heuristics::HeuristicEngine::new(config.heuristics.clone());
350 scanners.push(Arc::new(heur_engine));
351
352 let yara = yara_engine::YaraEngine::new(None);
354 scanners.push(Arc::new(yara));
355
356 let container_scanner = container_scanner::ContainerScanner::new(
358 container_scanner::ContainerScanConfig::default(),
359 );
360 let supply_chain_scanner = supply_chain::SupplyChainScanner::new(
361 supply_chain::SupplyChainConfig::default(),
362 );
363
364 Self {
365 scanners,
366 allowlist,
367 threat_intel,
368 quarantine,
369 dns_filter,
370 container_scanner,
371 supply_chain_scanner,
372 result_tx,
373 history: Arc::new(RwLock::new(VecDeque::with_capacity(10000))),
374 config,
375 files_scanned: Arc::new(AtomicU64::new(0)),
376 threats_detected: Arc::new(AtomicU64::new(0)),
377 running: AtomicBool::new(false),
378 }
379 }
380
381 pub async fn start(&self, audit: Arc<AuditChain>) -> Vec<tokio::task::JoinHandle<()>> {
383 self.running.store(true, Ordering::SeqCst);
384 let mut handles = Vec::new();
385
386 audit.record(
388 SecurityEventType::EndpointScanStarted,
389 "system",
390 "Endpoint protection engine started",
391 0.0,
392 );
393
394 if self.config.enable_watcher {
396 let (scan_tx, mut scan_rx) = tokio::sync::mpsc::unbounded_channel::<PathBuf>();
397 let watcher_handle = watcher::FileWatcher::new(
398 self.config.watcher.clone(),
399 scan_tx,
400 );
401
402 let allowlist = Arc::clone(&self.allowlist);
403 let _watcher_task = watcher_handle.start(allowlist);
404
405 let scanners = self.scanners.clone();
407 let result_tx = self.result_tx.clone();
408 let history = Arc::clone(&self.history);
409 let quarantine = Arc::clone(&self.quarantine);
410 let audit2 = Arc::clone(&audit);
411 let files_scanned = Arc::clone(&self.files_scanned);
412 let threats_detected = Arc::clone(&self.threats_detected);
413
414 let handle = tokio::spawn(async move {
415 while let Some(path) = scan_rx.recv().await {
416 let mut all_results = Vec::new();
418 for scanner in &scanners {
419 if scanner.is_active() {
420 let results = scanner.scan_file(&path).await;
421 all_results.extend(results);
422 }
423 }
424
425 files_scanned.fetch_add(1, Ordering::Relaxed);
426
427 for result in all_results {
429 threats_detected.fetch_add(1, Ordering::Relaxed);
430
431 if let RecommendedAction::Quarantine { ref source_path } = result.action {
433 let _ = quarantine.quarantine_file(
434 source_path,
435 &result.description,
436 &result.scanner,
437 result.severity,
438 );
439 }
440
441 audit2.record(
443 SecurityEventType::MalwareDetected,
444 &result.target,
445 &result.description,
446 result.confidence,
447 );
448
449 let _ = result_tx.send(result.clone());
451 let mut hist = history.write();
452 if hist.len() >= 10000 {
453 hist.pop_front();
454 }
455 hist.push_back(result);
456 }
457 }
458 });
459 handles.push(handle);
460 }
461
462 if self.config.enable_dns_filter {
464 if let Some(ref dns) = self.dns_filter {
465 let (dns_tx, mut dns_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
466 let dns_handle = Arc::clone(dns).start(dns_tx);
467 handles.push(dns_handle);
468
469 let history = Arc::clone(&self.history);
471 let audit3 = Arc::clone(&audit);
472 let result_tx = self.result_tx.clone();
473 let threats_detected = Arc::clone(&self.threats_detected);
474 let dns_consumer = tokio::spawn(async move {
475 while let Some(result) = dns_rx.recv().await {
476 threats_detected.fetch_add(1, Ordering::Relaxed);
477 audit3.record(
478 SecurityEventType::MalwareDetected,
479 &result.target,
480 &result.description,
481 result.confidence,
482 );
483 let _ = result_tx.send(result.clone());
484 let mut hist = history.write();
485 if hist.len() >= 10000 {
486 hist.pop_front();
487 }
488 hist.push_back(result);
489 }
490 });
491 handles.push(dns_consumer);
492 }
493 }
494
495 if self.config.enable_usb_monitor {
497 let (usb_tx, mut usb_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
498 let usb_mon = Arc::new(usb_monitor::UsbMonitor::new(self.config.usb_monitor.clone()));
499 let usb_handle = Arc::clone(&usb_mon).start(usb_tx);
500 handles.push(usb_handle);
501
502 let history = Arc::clone(&self.history);
504 let audit4 = Arc::clone(&audit);
505 let result_tx = self.result_tx.clone();
506 let threats_detected = Arc::clone(&self.threats_detected);
507 let usb_consumer = tokio::spawn(async move {
508 while let Some(result) = usb_rx.recv().await {
509 threats_detected.fetch_add(1, Ordering::Relaxed);
510 audit4.record(
511 SecurityEventType::MalwareDetected,
512 &result.target,
513 &result.description,
514 result.confidence,
515 );
516 let _ = result_tx.send(result.clone());
517 let mut hist = history.write();
518 if hist.len() >= 10000 {
519 hist.pop_front();
520 }
521 hist.push_back(result);
522 }
523 });
524 handles.push(usb_consumer);
525 }
526
527 if self.config.enable_process_monitor {
529 let (pm_tx, mut pm_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
530 let proc_mon = Arc::new(process_monitor::ProcessMonitor::new(self.config.process_monitor.clone()));
531 let pm_handle = Arc::clone(&proc_mon).start(pm_tx);
532 handles.push(pm_handle);
533
534 let history = Arc::clone(&self.history);
535 let audit_pm = Arc::clone(&audit);
536 let result_tx = self.result_tx.clone();
537 let threats_detected = Arc::clone(&self.threats_detected);
538 let pm_consumer = tokio::spawn(async move {
539 while let Some(result) = pm_rx.recv().await {
540 threats_detected.fetch_add(1, Ordering::Relaxed);
541 audit_pm.record(SecurityEventType::SuspiciousProcess, &result.target, &result.description, result.confidence);
542 let _ = result_tx.send(result.clone());
543 let mut hist = history.write();
544 if hist.len() >= 10000 { hist.pop_front(); }
545 hist.push_back(result);
546 }
547 });
548 handles.push(pm_consumer);
549 }
550
551 if self.config.enable_network_monitor {
553 let (nm_tx, mut nm_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
554 let net_mon = Arc::new(network_monitor::NetworkMonitor::new(
555 self.config.network_monitor.clone(),
556 Arc::clone(&self.threat_intel),
557 ));
558 let nm_handle = Arc::clone(&net_mon).start(nm_tx);
559 handles.push(nm_handle);
560
561 let history = Arc::clone(&self.history);
562 let audit_nm = Arc::clone(&audit);
563 let result_tx = self.result_tx.clone();
564 let threats_detected = Arc::clone(&self.threats_detected);
565 let nm_consumer = tokio::spawn(async move {
566 while let Some(result) = nm_rx.recv().await {
567 threats_detected.fetch_add(1, Ordering::Relaxed);
568 audit_nm.record(SecurityEventType::SuspiciousNetwork, &result.target, &result.description, result.confidence);
569 let _ = result_tx.send(result.clone());
570 let mut hist = history.write();
571 if hist.len() >= 10000 { hist.pop_front(); }
572 hist.push_back(result);
573 }
574 });
575 handles.push(nm_consumer);
576 }
577
578 if self.config.enable_memory_scanner {
580 let (ms_tx, mut ms_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
581 let mem_scan = Arc::new(memory_scanner::MemoryScanner::new(self.config.memory_scanner.clone()));
582 let ms_handle = Arc::clone(&mem_scan).start(ms_tx);
583 handles.push(ms_handle);
584
585 let history = Arc::clone(&self.history);
586 let audit_ms = Arc::clone(&audit);
587 let result_tx = self.result_tx.clone();
588 let threats_detected = Arc::clone(&self.threats_detected);
589 let ms_consumer = tokio::spawn(async move {
590 while let Some(result) = ms_rx.recv().await {
591 threats_detected.fetch_add(1, Ordering::Relaxed);
592 audit_ms.record(SecurityEventType::MemoryAnomaly, &result.target, &result.description, result.confidence);
593 let _ = result_tx.send(result.clone());
594 let mut hist = history.write();
595 if hist.len() >= 10000 { hist.pop_front(); }
596 hist.push_back(result);
597 }
598 });
599 handles.push(ms_consumer);
600 }
601
602 if self.config.enable_rootkit_detector {
604 let (rk_tx, mut rk_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
605 let rk_det = Arc::new(rootkit_detector::RootkitDetector::new(self.config.rootkit_detector.clone()));
606 let rk_handle = Arc::clone(&rk_det).start(rk_tx);
607 handles.push(rk_handle);
608
609 let history = Arc::clone(&self.history);
610 let audit_rk = Arc::clone(&audit);
611 let result_tx = self.result_tx.clone();
612 let threats_detected = Arc::clone(&self.threats_detected);
613 let rk_consumer = tokio::spawn(async move {
614 while let Some(result) = rk_rx.recv().await {
615 threats_detected.fetch_add(1, Ordering::Relaxed);
616 audit_rk.record(SecurityEventType::RootkitIndicator, &result.target, &result.description, result.confidence);
617 let _ = result_tx.send(result.clone());
618 let mut hist = history.write();
619 if hist.len() >= 10000 { hist.pop_front(); }
620 hist.push_back(result);
621 }
622 });
623 handles.push(rk_consumer);
624 }
625
626 if self.config.enable_fim {
628 let (fim_tx, mut fim_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
629 let fim_mon = Arc::new(fim::FimMonitor::new(self.config.fim.clone()));
630 let fim_handle = Arc::clone(&fim_mon).start(fim_tx);
631 handles.push(fim_handle);
632
633 let history = Arc::clone(&self.history);
635 let audit5 = Arc::clone(&audit);
636 let result_tx = self.result_tx.clone();
637 let threats_detected = Arc::clone(&self.threats_detected);
638 let fim_consumer = tokio::spawn(async move {
639 while let Some(result) = fim_rx.recv().await {
640 threats_detected.fetch_add(1, Ordering::Relaxed);
641 audit5.record(
642 SecurityEventType::MalwareDetected,
643 &result.target,
644 &result.description,
645 result.confidence,
646 );
647 let _ = result_tx.send(result.clone());
648 let mut hist = history.write();
649 if hist.len() >= 10000 {
650 hist.pop_front();
651 }
652 hist.push_back(result);
653 }
654 });
655 handles.push(fim_consumer);
656 }
657
658 handles
659 }
660
661 pub async fn scan_file(&self, path: &Path) -> Vec<ScanResult> {
663 if self.allowlist.should_skip_path(path) {
664 return Vec::new();
665 }
666
667 self.files_scanned.fetch_add(1, Ordering::Relaxed);
668 let mut results = Vec::new();
669
670 if supply_chain::SupplyChainScanner::detect_ecosystem(path).is_some() {
672 let mut sc_results = self.supply_chain_scanner.scan_file(path);
673 results.append(&mut sc_results);
674 }
675
676 for scanner in &self.scanners {
677 if scanner.is_active() {
678 let mut r = scanner.scan_file(path).await;
679 results.append(&mut r);
680 }
681 }
682
683 if !results.is_empty() {
684 self.threats_detected
685 .fetch_add(results.len() as u64, Ordering::Relaxed);
686 let mut hist = self.history.write();
687 for r in &results {
688 if hist.len() >= 10000 {
689 hist.pop_front();
690 }
691 hist.push_back(r.clone());
692 }
693 }
694
695 results
696 }
697
698 pub async fn scan_dir(&self, dir: &Path) -> Vec<ScanResult> {
700 let mut results = Vec::new();
701 if let Ok(entries) = std::fs::read_dir(dir) {
702 for entry in entries.flatten() {
703 let path = entry.path();
704 if path.is_dir() {
705 if !self.allowlist.should_skip_path(&path) {
706 let mut r = Box::pin(self.scan_dir(&path)).await;
707 results.append(&mut r);
708 }
709 } else if path.is_file() {
710 let mut r = self.scan_file(&path).await;
711 results.append(&mut r);
712 }
713 }
714 }
715 results
716 }
717
718 pub fn scan_container_image(&self, image: &str) -> Vec<ScanResult> {
720 self.container_scanner.scan_image(image)
721 }
722
723 pub fn scan_dependencies(&self, path: &Path) -> Vec<ScanResult> {
725 self.supply_chain_scanner.scan_file(path)
726 }
727
728 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ScanResult> {
730 self.result_tx.subscribe()
731 }
732
733 pub fn recent_detections(&self, count: usize) -> Vec<ScanResult> {
735 let hist = self.history.read();
736 hist.iter().rev().take(count).cloned().collect()
737 }
738
739 pub fn stats(&self) -> EndpointStats {
741 let mut active = Vec::new();
742 if self.config.enable_watcher {
743 active.push("file_watcher".to_string());
744 }
745 if self.config.enable_process_monitor {
746 active.push("process_monitor".to_string());
747 }
748 if self.config.enable_network_monitor {
749 active.push("network_monitor".to_string());
750 }
751 if self.config.enable_memory_scanner {
752 active.push("memory_scanner".to_string());
753 }
754 if self.config.enable_rootkit_detector {
755 active.push("rootkit_detector".to_string());
756 }
757 if self.config.enable_dns_filter {
758 active.push("dns_filter".to_string());
759 }
760 if self.config.enable_usb_monitor {
761 active.push("usb_monitor".to_string());
762 }
763 if self.config.enable_fim {
764 active.push("fim".to_string());
765 }
766
767 let scanner_names: Vec<String> = self
768 .scanners
769 .iter()
770 .filter(|s| s.is_active())
771 .map(|s| s.name().to_string())
772 .collect();
773
774 EndpointStats {
775 total_files_scanned: self.files_scanned.load(Ordering::Relaxed),
776 total_threats_detected: self.threats_detected.load(Ordering::Relaxed),
777 active_monitors: active,
778 quarantined_files: self.quarantine.list_entries().len(),
779 last_scan_time: self.history.read().back().map(|r| r.timestamp),
780 scanners_active: scanner_names,
781 }
782 }
783
784 pub fn is_running(&self) -> bool {
786 self.running.load(Ordering::SeqCst)
787 }
788}
789
790#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
799 fn test_severity_ordering() {
800 assert!(Severity::Info < Severity::Low);
801 assert!(Severity::Low < Severity::Medium);
802 assert!(Severity::Medium < Severity::High);
803 assert!(Severity::High < Severity::Critical);
804 }
805
806 #[test]
807 fn test_severity_display() {
808 assert_eq!(Severity::Critical.to_string(), "critical");
809 assert_eq!(Severity::Info.to_string(), "info");
810 }
811
812 #[test]
813 fn test_scan_result_creation() {
814 let result = ScanResult::new(
815 "test_scanner",
816 "/tmp/malware.exe",
817 Severity::High,
818 DetectionCategory::MalwareSignature {
819 name: "EICAR".to_string(),
820 family: "Test".to_string(),
821 },
822 "EICAR test file detected",
823 0.99,
824 RecommendedAction::Quarantine {
825 source_path: PathBuf::from("/tmp/malware.exe"),
826 },
827 );
828 assert!(!result.id.is_empty());
829 assert_eq!(result.scanner, "test_scanner");
830 assert_eq!(result.severity, Severity::High);
831 assert_eq!(result.confidence, 0.99);
832 }
833
834 #[test]
835 fn test_scan_result_with_hash() {
836 let result = ScanResult::new(
837 "sig",
838 "/tmp/test",
839 Severity::Low,
840 DetectionCategory::HeuristicAnomaly {
841 rule: "test".to_string(),
842 },
843 "test",
844 0.5,
845 RecommendedAction::LogOnly,
846 )
847 .with_hash("abc123".to_string());
848 assert_eq!(result.artifact_hash, Some("abc123".to_string()));
849 }
850
851 #[test]
852 fn test_confidence_clamping() {
853 let r1 = ScanResult::new(
854 "s", "t", Severity::Low,
855 DetectionCategory::HeuristicAnomaly { rule: "x".into() },
856 "d", 1.5, RecommendedAction::LogOnly,
857 );
858 assert_eq!(r1.confidence, 1.0);
859
860 let r2 = ScanResult::new(
861 "s", "t", Severity::Low,
862 DetectionCategory::HeuristicAnomaly { rule: "x".into() },
863 "d", -0.5, RecommendedAction::LogOnly,
864 );
865 assert_eq!(r2.confidence, 0.0);
866 }
867
868 #[test]
869 fn test_recommended_action_display() {
870 assert_eq!(RecommendedAction::LogOnly.to_string(), "log_only");
871 assert_eq!(RecommendedAction::Alert.to_string(), "alert");
872 assert_eq!(
873 RecommendedAction::KillProcess { pid: 1234 }.to_string(),
874 "kill(1234)"
875 );
876 }
877
878 #[test]
879 fn test_endpoint_config_default() {
880 let config = EndpointConfig::default();
881 assert!(config.enabled);
882 assert!(config.enable_watcher);
883 assert!(config.enable_process_monitor);
884 assert!(!config.enable_memory_scanner); assert!(!config.enable_rootkit_detector); assert!(!config.enable_dns_filter); assert!(config.enable_usb_monitor); assert!(!config.enable_fim); }
890}