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 result_tx: tokio::sync::broadcast::Sender<ScanResult>,
306 history: Arc<RwLock<VecDeque<ScanResult>>>,
308 config: EndpointConfig,
310 files_scanned: AtomicU64,
312 threats_detected: AtomicU64,
313 running: AtomicBool,
315}
316
317impl EndpointEngine {
318 pub fn new(config: EndpointConfig) -> Self {
320 let (result_tx, _) = tokio::sync::broadcast::channel(1024);
321
322 let allowlist = Arc::new(allowlist::DeveloperAllowlist::new(config.allowlist.clone()));
324 let threat_intel = Arc::new(threat_intel::ThreatIntelDB::new(config.threat_intel.clone()));
325 let quarantine = Arc::new(file_quarantine::QuarantineVault::new(config.quarantine.clone()));
326
327 let dns_filter = if config.enable_dns_filter {
329 Some(Arc::new(dns_filter::DnsFilter::new(
330 config.dns_filter.clone(),
331 Arc::clone(&threat_intel),
332 )))
333 } else {
334 None
335 };
336
337 let mut scanners: Vec<Arc<dyn Scanner>> = Vec::new();
339
340 let sig_engine = signatures::SignatureEngine::new(config.signatures.clone());
342 scanners.push(Arc::new(sig_engine));
343
344 let heur_engine = heuristics::HeuristicEngine::new(config.heuristics.clone());
346 scanners.push(Arc::new(heur_engine));
347
348 let yara = yara_engine::YaraEngine::new(None);
350 scanners.push(Arc::new(yara));
351
352 Self {
353 scanners,
354 allowlist,
355 threat_intel,
356 quarantine,
357 dns_filter,
358 result_tx,
359 history: Arc::new(RwLock::new(VecDeque::with_capacity(10000))),
360 config,
361 files_scanned: AtomicU64::new(0),
362 threats_detected: AtomicU64::new(0),
363 running: AtomicBool::new(false),
364 }
365 }
366
367 pub async fn start(&self, audit: Arc<AuditChain>) -> Vec<tokio::task::JoinHandle<()>> {
369 self.running.store(true, Ordering::SeqCst);
370 let mut handles = Vec::new();
371
372 audit.record(
374 SecurityEventType::EndpointScanStarted,
375 "system",
376 "Endpoint protection engine started",
377 0.0,
378 );
379
380 if self.config.enable_watcher {
382 let (scan_tx, mut scan_rx) = tokio::sync::mpsc::unbounded_channel::<PathBuf>();
383 let watcher_handle = watcher::FileWatcher::new(
384 self.config.watcher.clone(),
385 scan_tx,
386 );
387
388 let allowlist = Arc::clone(&self.allowlist);
389 let _watcher_task = watcher_handle.start(allowlist);
390
391 let scanners = self.scanners.clone();
393 let result_tx = self.result_tx.clone();
394 let history = Arc::clone(&self.history);
395 let quarantine = Arc::clone(&self.quarantine);
396 let audit2 = Arc::clone(&audit);
397 let files_scanned = &self.files_scanned as *const AtomicU64 as usize;
398 let threats_detected = &self.threats_detected as *const AtomicU64 as usize;
399
400 let handle = tokio::spawn(async move {
401 while let Some(path) = scan_rx.recv().await {
402 let mut all_results = Vec::new();
404 for scanner in &scanners {
405 if scanner.is_active() {
406 let results = scanner.scan_file(&path).await;
407 all_results.extend(results);
408 }
409 }
410
411 unsafe {
413 (*(files_scanned as *const AtomicU64)).fetch_add(1, Ordering::Relaxed);
414 }
415
416 for result in all_results {
418 unsafe {
419 (*(threats_detected as *const AtomicU64)).fetch_add(1, Ordering::Relaxed);
420 }
421
422 if let RecommendedAction::Quarantine { ref source_path } = result.action {
424 let _ = quarantine.quarantine_file(
425 source_path,
426 &result.description,
427 &result.scanner,
428 result.severity,
429 );
430 }
431
432 audit2.record(
434 SecurityEventType::MalwareDetected,
435 &result.target,
436 &result.description,
437 result.confidence,
438 );
439
440 let _ = result_tx.send(result.clone());
442 let mut hist = history.write();
443 if hist.len() >= 10000 {
444 hist.pop_front();
445 }
446 hist.push_back(result);
447 }
448 }
449 });
450 handles.push(handle);
451 }
452
453 if self.config.enable_dns_filter {
455 if let Some(ref dns) = self.dns_filter {
456 let (dns_tx, mut dns_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
457 let dns_handle = Arc::clone(dns).start(dns_tx);
458 handles.push(dns_handle);
459
460 let history = Arc::clone(&self.history);
462 let audit3 = Arc::clone(&audit);
463 let result_tx = self.result_tx.clone();
464 let threats_detected = &self.threats_detected as *const AtomicU64 as usize;
465 let dns_consumer = tokio::spawn(async move {
466 while let Some(result) = dns_rx.recv().await {
467 unsafe {
468 (*(threats_detected as *const AtomicU64)).fetch_add(1, Ordering::Relaxed);
469 }
470 audit3.record(
471 SecurityEventType::MalwareDetected,
472 &result.target,
473 &result.description,
474 result.confidence,
475 );
476 let _ = result_tx.send(result.clone());
477 let mut hist = history.write();
478 if hist.len() >= 10000 {
479 hist.pop_front();
480 }
481 hist.push_back(result);
482 }
483 });
484 handles.push(dns_consumer);
485 }
486 }
487
488 if self.config.enable_usb_monitor {
490 let (usb_tx, mut usb_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
491 let usb_mon = Arc::new(usb_monitor::UsbMonitor::new(self.config.usb_monitor.clone()));
492 let usb_handle = Arc::clone(&usb_mon).start(usb_tx);
493 handles.push(usb_handle);
494
495 let history = Arc::clone(&self.history);
497 let audit4 = Arc::clone(&audit);
498 let result_tx = self.result_tx.clone();
499 let threats_detected = &self.threats_detected as *const AtomicU64 as usize;
500 let usb_consumer = tokio::spawn(async move {
501 while let Some(result) = usb_rx.recv().await {
502 unsafe {
503 (*(threats_detected as *const AtomicU64)).fetch_add(1, Ordering::Relaxed);
504 }
505 audit4.record(
506 SecurityEventType::MalwareDetected,
507 &result.target,
508 &result.description,
509 result.confidence,
510 );
511 let _ = result_tx.send(result.clone());
512 let mut hist = history.write();
513 if hist.len() >= 10000 {
514 hist.pop_front();
515 }
516 hist.push_back(result);
517 }
518 });
519 handles.push(usb_consumer);
520 }
521
522 if self.config.enable_fim {
524 let (fim_tx, mut fim_rx) = tokio::sync::mpsc::unbounded_channel::<ScanResult>();
525 let fim_mon = Arc::new(fim::FimMonitor::new(self.config.fim.clone()));
526 let fim_handle = Arc::clone(&fim_mon).start(fim_tx);
527 handles.push(fim_handle);
528
529 let history = Arc::clone(&self.history);
531 let audit5 = Arc::clone(&audit);
532 let result_tx = self.result_tx.clone();
533 let threats_detected = &self.threats_detected as *const AtomicU64 as usize;
534 let fim_consumer = tokio::spawn(async move {
535 while let Some(result) = fim_rx.recv().await {
536 unsafe {
537 (*(threats_detected as *const AtomicU64)).fetch_add(1, Ordering::Relaxed);
538 }
539 audit5.record(
540 SecurityEventType::MalwareDetected,
541 &result.target,
542 &result.description,
543 result.confidence,
544 );
545 let _ = result_tx.send(result.clone());
546 let mut hist = history.write();
547 if hist.len() >= 10000 {
548 hist.pop_front();
549 }
550 hist.push_back(result);
551 }
552 });
553 handles.push(fim_consumer);
554 }
555
556 handles
557 }
558
559 pub async fn scan_file(&self, path: &Path) -> Vec<ScanResult> {
561 if self.allowlist.should_skip_path(path) {
562 return Vec::new();
563 }
564
565 self.files_scanned.fetch_add(1, Ordering::Relaxed);
566 let mut results = Vec::new();
567
568 for scanner in &self.scanners {
569 if scanner.is_active() {
570 let mut r = scanner.scan_file(path).await;
571 results.append(&mut r);
572 }
573 }
574
575 if !results.is_empty() {
576 self.threats_detected
577 .fetch_add(results.len() as u64, Ordering::Relaxed);
578 let mut hist = self.history.write();
579 for r in &results {
580 if hist.len() >= 10000 {
581 hist.pop_front();
582 }
583 hist.push_back(r.clone());
584 }
585 }
586
587 results
588 }
589
590 pub async fn scan_dir(&self, dir: &Path) -> Vec<ScanResult> {
592 let mut results = Vec::new();
593 if let Ok(entries) = std::fs::read_dir(dir) {
594 for entry in entries.flatten() {
595 let path = entry.path();
596 if path.is_dir() {
597 if !self.allowlist.should_skip_path(&path) {
598 let mut r = Box::pin(self.scan_dir(&path)).await;
599 results.append(&mut r);
600 }
601 } else if path.is_file() {
602 let mut r = self.scan_file(&path).await;
603 results.append(&mut r);
604 }
605 }
606 }
607 results
608 }
609
610 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ScanResult> {
612 self.result_tx.subscribe()
613 }
614
615 pub fn recent_detections(&self, count: usize) -> Vec<ScanResult> {
617 let hist = self.history.read();
618 hist.iter().rev().take(count).cloned().collect()
619 }
620
621 pub fn stats(&self) -> EndpointStats {
623 let mut active = Vec::new();
624 if self.config.enable_watcher {
625 active.push("file_watcher".to_string());
626 }
627 if self.config.enable_process_monitor {
628 active.push("process_monitor".to_string());
629 }
630 if self.config.enable_network_monitor {
631 active.push("network_monitor".to_string());
632 }
633 if self.config.enable_memory_scanner {
634 active.push("memory_scanner".to_string());
635 }
636 if self.config.enable_rootkit_detector {
637 active.push("rootkit_detector".to_string());
638 }
639 if self.config.enable_dns_filter {
640 active.push("dns_filter".to_string());
641 }
642 if self.config.enable_usb_monitor {
643 active.push("usb_monitor".to_string());
644 }
645 if self.config.enable_fim {
646 active.push("fim".to_string());
647 }
648
649 let scanner_names: Vec<String> = self
650 .scanners
651 .iter()
652 .filter(|s| s.is_active())
653 .map(|s| s.name().to_string())
654 .collect();
655
656 EndpointStats {
657 total_files_scanned: self.files_scanned.load(Ordering::Relaxed),
658 total_threats_detected: self.threats_detected.load(Ordering::Relaxed),
659 active_monitors: active,
660 quarantined_files: self.quarantine.list_entries().len(),
661 last_scan_time: self.history.read().back().map(|r| r.timestamp),
662 scanners_active: scanner_names,
663 }
664 }
665
666 pub fn is_running(&self) -> bool {
668 self.running.load(Ordering::SeqCst)
669 }
670}
671
672#[cfg(test)]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn test_severity_ordering() {
682 assert!(Severity::Info < Severity::Low);
683 assert!(Severity::Low < Severity::Medium);
684 assert!(Severity::Medium < Severity::High);
685 assert!(Severity::High < Severity::Critical);
686 }
687
688 #[test]
689 fn test_severity_display() {
690 assert_eq!(Severity::Critical.to_string(), "critical");
691 assert_eq!(Severity::Info.to_string(), "info");
692 }
693
694 #[test]
695 fn test_scan_result_creation() {
696 let result = ScanResult::new(
697 "test_scanner",
698 "/tmp/malware.exe",
699 Severity::High,
700 DetectionCategory::MalwareSignature {
701 name: "EICAR".to_string(),
702 family: "Test".to_string(),
703 },
704 "EICAR test file detected",
705 0.99,
706 RecommendedAction::Quarantine {
707 source_path: PathBuf::from("/tmp/malware.exe"),
708 },
709 );
710 assert!(!result.id.is_empty());
711 assert_eq!(result.scanner, "test_scanner");
712 assert_eq!(result.severity, Severity::High);
713 assert_eq!(result.confidence, 0.99);
714 }
715
716 #[test]
717 fn test_scan_result_with_hash() {
718 let result = ScanResult::new(
719 "sig",
720 "/tmp/test",
721 Severity::Low,
722 DetectionCategory::HeuristicAnomaly {
723 rule: "test".to_string(),
724 },
725 "test",
726 0.5,
727 RecommendedAction::LogOnly,
728 )
729 .with_hash("abc123".to_string());
730 assert_eq!(result.artifact_hash, Some("abc123".to_string()));
731 }
732
733 #[test]
734 fn test_confidence_clamping() {
735 let r1 = ScanResult::new(
736 "s", "t", Severity::Low,
737 DetectionCategory::HeuristicAnomaly { rule: "x".into() },
738 "d", 1.5, RecommendedAction::LogOnly,
739 );
740 assert_eq!(r1.confidence, 1.0);
741
742 let r2 = ScanResult::new(
743 "s", "t", Severity::Low,
744 DetectionCategory::HeuristicAnomaly { rule: "x".into() },
745 "d", -0.5, RecommendedAction::LogOnly,
746 );
747 assert_eq!(r2.confidence, 0.0);
748 }
749
750 #[test]
751 fn test_recommended_action_display() {
752 assert_eq!(RecommendedAction::LogOnly.to_string(), "log_only");
753 assert_eq!(RecommendedAction::Alert.to_string(), "alert");
754 assert_eq!(
755 RecommendedAction::KillProcess { pid: 1234 }.to_string(),
756 "kill(1234)"
757 );
758 }
759
760 #[test]
761 fn test_endpoint_config_default() {
762 let config = EndpointConfig::default();
763 assert!(config.enabled);
764 assert!(config.enable_watcher);
765 assert!(config.enable_process_monitor);
766 assert!(!config.enable_memory_scanner); assert!(!config.enable_rootkit_detector); assert!(!config.enable_dns_filter); assert!(config.enable_usb_monitor); assert!(!config.enable_fim); }
772}