Skip to main content

nexus_shield/endpoint/
mod.rs

1// ============================================================================
2// File: endpoint/mod.rs
3// Description: Real-time endpoint protection engine — core types and orchestrator
4// Author: Andrew Jewell Sr. - AutomataNexus
5// Updated: March 24, 2026
6// ============================================================================
7//! # Endpoint Protection Engine
8//!
9//! Real-time file, process, network, and memory monitoring with multi-engine
10//! malware detection. Developer-aware allowlisting eliminates false positives
11//! on dev machines.
12
13pub 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// =============================================================================
41// Severity
42// =============================================================================
43
44/// Detection severity levels, ordered from lowest to highest.
45#[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// =============================================================================
67// Detection Category
68// =============================================================================
69
70/// Category of a detection, carrying module-specific metadata.
71#[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// =============================================================================
84// Recommended Action
85// =============================================================================
86
87/// Action the engine recommends after a detection.
88#[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// =============================================================================
117// Scan Result
118// =============================================================================
119
120/// Unified result returned by every scanner engine.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ScanResult {
123    /// Unique detection ID.
124    pub id: String,
125    /// When the detection occurred.
126    pub timestamp: DateTime<Utc>,
127    /// Which scanner produced this result.
128    pub scanner: String,
129    /// What was scanned (file path, PID, connection, etc.).
130    pub target: String,
131    /// Severity of the detection.
132    pub severity: Severity,
133    /// Category with detection-specific metadata.
134    pub category: DetectionCategory,
135    /// Human-readable description.
136    pub description: String,
137    /// Confidence score 0.0–1.0.
138    pub confidence: f64,
139    /// Recommended action.
140    pub action: RecommendedAction,
141    /// SHA-256 of the scanned artifact (if applicable).
142    pub artifact_hash: Option<String>,
143}
144
145impl ScanResult {
146    /// Create a new ScanResult with auto-generated ID and timestamp.
147    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    /// Attach an artifact hash to this result.
171    pub fn with_hash(mut self, hash: String) -> Self {
172        self.artifact_hash = Some(hash);
173        self
174    }
175}
176
177// =============================================================================
178// Scanner Trait
179// =============================================================================
180
181/// Trait that all scanning engines implement.
182#[async_trait::async_trait]
183pub trait Scanner: Send + Sync {
184    /// Human-readable name of this scanner.
185    fn name(&self) -> &str;
186
187    /// Whether this scanner is currently enabled and operational.
188    fn is_active(&self) -> bool;
189
190    /// Scan a file on disk. Returns empty vec if clean.
191    async fn scan_file(&self, path: &Path) -> Vec<ScanResult>;
192
193    /// Scan raw bytes (for in-memory content). Default: no-op.
194    async fn scan_bytes(&self, _data: &[u8], _label: &str) -> Vec<ScanResult> {
195        Vec::new()
196    }
197
198    /// Scan a running process by PID. Default: no-op.
199    async fn scan_process(&self, _pid: u32) -> Vec<ScanResult> {
200        Vec::new()
201    }
202}
203
204// =============================================================================
205// Endpoint Configuration
206// =============================================================================
207
208/// Top-level configuration for the endpoint protection engine.
209#[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, // requires elevated privileges
245            enable_rootkit_detector: false, // requires root
246            enable_dns_filter: false, // opt-in: requires configuring system DNS
247            enable_usb_monitor: true, // on by default: monitors for USB insertions
248            enable_fim: false, // opt-in: baselines system files, alerts on changes
249            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// =============================================================================
274// Endpoint Stats
275// =============================================================================
276
277/// Runtime statistics for the endpoint protection engine.
278#[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
288// =============================================================================
289// Endpoint Engine
290// =============================================================================
291
292/// Orchestrates all endpoint protection subsystems.
293pub struct EndpointEngine {
294    /// All registered scanning engines.
295    scanners: Vec<Arc<dyn Scanner>>,
296    /// Developer-aware allowlist.
297    pub allowlist: Arc<allowlist::DeveloperAllowlist>,
298    /// Threat intelligence database.
299    pub threat_intel: Arc<threat_intel::ThreatIntelDB>,
300    /// File quarantine vault.
301    pub quarantine: Arc<file_quarantine::QuarantineVault>,
302    /// DNS filtering proxy.
303    pub dns_filter: Option<Arc<dns_filter::DnsFilter>>,
304    /// Container image scanner (on-demand).
305    pub container_scanner: container_scanner::ContainerScanner,
306    /// Supply chain dependency scanner (on-demand).
307    pub supply_chain_scanner: supply_chain::SupplyChainScanner,
308    /// Broadcast channel for real-time scan results.
309    result_tx: tokio::sync::broadcast::Sender<ScanResult>,
310    /// Detection history (ring buffer).
311    history: Arc<RwLock<VecDeque<ScanResult>>>,
312    /// Configuration.
313    config: EndpointConfig,
314    /// Counters (Arc-wrapped for safe sharing across spawned tasks).
315    files_scanned: Arc<AtomicU64>,
316    threats_detected: Arc<AtomicU64>,
317    /// Whether the engine is running.
318    running: AtomicBool,
319}
320
321impl EndpointEngine {
322    /// Create a new endpoint engine with the given configuration.
323    pub fn new(config: EndpointConfig) -> Self {
324        let (result_tx, _) = tokio::sync::broadcast::channel(1024);
325
326        // Initialize subsystems
327        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        // DNS filter (if enabled)
332        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        // Build scanner list
342        let mut scanners: Vec<Arc<dyn Scanner>> = Vec::new();
343
344        // Signature engine
345        let sig_engine = signatures::SignatureEngine::new(config.signatures.clone());
346        scanners.push(Arc::new(sig_engine));
347
348        // Heuristic engine
349        let heur_engine = heuristics::HeuristicEngine::new(config.heuristics.clone());
350        scanners.push(Arc::new(heur_engine));
351
352        // YARA engine
353        let yara = yara_engine::YaraEngine::new(None);
354        scanners.push(Arc::new(yara));
355
356        // On-demand scanners
357        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    /// Start all background monitors. Returns JoinHandles for spawned tasks.
382    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        // Record startup event
387        audit.record(
388            SecurityEventType::EndpointScanStarted,
389            "system",
390            "Endpoint protection engine started",
391            0.0,
392        );
393
394        // Start file watcher
395        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            // File scan consumer task
406            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                    // Run all scanners on the file
417                    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                    // Process results
428                    for result in all_results {
429                        threats_detected.fetch_add(1, Ordering::Relaxed);
430
431                        // Quarantine if needed
432                        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                        // Record to audit chain
442                        audit2.record(
443                            SecurityEventType::MalwareDetected,
444                            &result.target,
445                            &result.description,
446                            result.confidence,
447                        );
448
449                        // Broadcast and save to history
450                        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        // Start DNS filter proxy
463        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                // DNS detection consumer
470                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        // Start USB monitor
496        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            // USB detection consumer
503            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        // Start process monitor
528        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        // Start network monitor
552        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        // Start memory scanner
579        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        // Start rootkit detector
603        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        // Start File Integrity Monitor
627        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            // FIM detection consumer
634            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    /// Scan a single file with all engines.
662    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        // Check if it's a dependency lock file (supply chain scan)
671        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    /// Scan a directory recursively.
699    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    /// Scan a Docker image for security issues.
719    pub fn scan_container_image(&self, image: &str) -> Vec<ScanResult> {
720        self.container_scanner.scan_image(image)
721    }
722
723    /// Scan a dependency lock file for supply chain risks.
724    pub fn scan_dependencies(&self, path: &Path) -> Vec<ScanResult> {
725        self.supply_chain_scanner.scan_file(path)
726    }
727
728    /// Subscribe to real-time scan results.
729    pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ScanResult> {
730        self.result_tx.subscribe()
731    }
732
733    /// Get recent detection history.
734    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    /// Get runtime statistics.
740    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    /// Check if the engine is running.
785    pub fn is_running(&self) -> bool {
786        self.running.load(Ordering::SeqCst)
787    }
788}
789
790// =============================================================================
791// Tests
792// =============================================================================
793
794#[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); // requires elevated
885        assert!(!config.enable_rootkit_detector); // requires root
886        assert!(!config.enable_dns_filter); // opt-in
887        assert!(config.enable_usb_monitor); // on by default
888        assert!(!config.enable_fim); // opt-in
889    }
890}