diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1ac015f..58b89ae 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -14,7 +14,17 @@ "Bash(Select-String \"warning\")", "Bash(Select-Object -First 30)", "Bash(cargo fmt:*)", - "Bash(git config:*)" + "Bash(git config:*)", + "Bash(git push:*)", + "Bash(rustc:*)", + "Bash(cargo --version:*)", + "Bash(rustup install:*)", + "Bash(rustup override set:*)", + "Bash(cargo generate-lockfile:*)", + "Bash(rustup override unset:*)", + "Bash(Select-String -Pattern \"Checking|Finished|error\")", + "Bash(Select-Object -First 20)", + "Bash(git restore:*)" ], "deny": [], "ask": [] diff --git a/ghost-core/src/hollowing.rs b/ghost-core/src/hollowing.rs index 5b81069..3127c69 100644 --- a/ghost-core/src/hollowing.rs +++ b/ghost-core/src/hollowing.rs @@ -586,24 +586,24 @@ fn parse_pe_sections(data: &[u8]) -> Result> { use crate::GhostError; if data.len() < 0x40 { - return Err(GhostError::ParseError("Buffer too small".to_string())); + return Err(GhostError::InvalidInput { message: "Buffer too small".to_string() }); } // Check DOS signature if &data[0..2] != b"MZ" { - return Err(GhostError::ParseError("Invalid DOS signature".to_string())); + return Err(GhostError::InvalidInput { message: "Invalid DOS signature".to_string() }); } // Get PE offset let pe_offset = u32::from_le_bytes([data[0x3c], data[0x3d], data[0x3e], data[0x3f]]) as usize; if pe_offset + 0x18 >= data.len() { - return Err(GhostError::ParseError("Invalid PE offset".to_string())); + return Err(GhostError::InvalidInput { message: "Invalid PE offset".to_string() }); } // Check PE signature if &data[pe_offset..pe_offset + 4] != b"PE\0\0" { - return Err(GhostError::ParseError("Invalid PE signature".to_string())); + return Err(GhostError::InvalidInput { message: "Invalid PE signature".to_string() }); } // Parse COFF header diff --git a/ghost-core/src/live_feeds.rs b/ghost-core/src/live_feeds.rs index aa2a39f..055453e 100644 --- a/ghost-core/src/live_feeds.rs +++ b/ghost-core/src/live_feeds.rs @@ -73,12 +73,11 @@ impl LiveThreatFeeds { pub async fn update_feeds(&mut self) -> Result { let mut updated_count = 0; - for feed in &mut self.feeds { - if !feed.enabled { - continue; - } + // Clone feeds to avoid borrow checker issues + let feeds: Vec = self.feeds.iter().filter(|f| f.enabled).cloned().collect(); - match self.fetch_feed_data(feed).await { + for feed in feeds { + match self.fetch_feed_data(&feed).await { Ok(iocs) => { log::info!("Updated {} with {} IOCs", feed.name, iocs.len()); @@ -87,7 +86,10 @@ impl LiveThreatFeeds { self.ioc_cache.insert(ioc.value.clone(), ioc); } - feed.last_updated = SystemTime::now(); + // Update feed's last_updated time + if let Some(f) = self.feeds.iter_mut().find(|f| f.name == feed.name) { + f.last_updated = SystemTime::now(); + } updated_count += 1; } Err(e) => { @@ -114,7 +116,7 @@ impl LiveThreatFeeds { async fn fetch_abuseipdb(&self, feed: &ThreatFeed) -> Result, GhostError> { let api_key = feed.api_key.as_ref().ok_or_else(|| { - GhostError::ConfigurationError("AbuseIPDB requires API key".to_string()) + GhostError::Configuration { message: "AbuseIPDB requires API key".to_string() } })?; let client = reqwest::Client::new(); @@ -127,17 +129,17 @@ impl LiveThreatFeeds { .query(&[("confidenceMinimum", "90")]) .send() .await - .map_err(|e| GhostError::NetworkError(format!("AbuseIPDB request failed: {}", e)))?; + .map_err(|e| GhostError::ThreatIntel { message: format!("AbuseIPDB request failed: {}", e) })?; if !response.status().is_success() { - return Err(GhostError::NetworkError(format!( + return Err(GhostError::ThreatIntel { message: format!( "AbuseIPDB API returned status: {}", response.status() - ))); + ) }); } let data: serde_json::Value = response.json().await.map_err(|e| { - GhostError::ParseError(format!("Failed to parse AbuseIPDB response: {}", e)) + GhostError::Serialization { message: format!("Failed to parse AbuseIPDB response: {}", e) } })?; let mut iocs = Vec::new(); @@ -186,18 +188,18 @@ impl LiveThreatFeeds { .send() .await .map_err(|e| { - GhostError::NetworkError(format!("MalwareBazaar request failed: {}", e)) + GhostError::ThreatIntel { message: format!("MalwareBazaar request failed: {}", e) } })?; if !response.status().is_success() { - return Err(GhostError::NetworkError(format!( + return Err(GhostError::ThreatIntel { message: format!( "MalwareBazaar API returned status: {}", response.status() - ))); + ) }); } let data: serde_json::Value = response.json().await.map_err(|e| { - GhostError::ParseError(format!("Failed to parse MalwareBazaar response: {}", e)) + GhostError::Serialization { message: format!("Failed to parse MalwareBazaar response: {}", e) } })?; let mut iocs = Vec::new(); @@ -235,7 +237,7 @@ impl LiveThreatFeeds { async fn fetch_alienvault(&self, feed: &ThreatFeed) -> Result, GhostError> { let api_key = feed.api_key.as_ref().ok_or_else(|| { - GhostError::ConfigurationError("AlienVault OTX requires API key".to_string()) + GhostError::Configuration { message: "AlienVault OTX requires API key".to_string() } })?; let client = reqwest::Client::new(); @@ -248,17 +250,17 @@ impl LiveThreatFeeds { .query(&[("limit", "50")]) .send() .await - .map_err(|e| GhostError::NetworkError(format!("AlienVault request failed: {}", e)))?; + .map_err(|e| GhostError::ThreatIntel { message: format!("AlienVault request failed: {}", e) })?; if !response.status().is_success() { - return Err(GhostError::NetworkError(format!( + return Err(GhostError::ThreatIntel { message: format!( "AlienVault API returned status: {}", response.status() - ))); + ) }); } let data: serde_json::Value = response.json().await.map_err(|e| { - GhostError::ParseError(format!("Failed to parse AlienVault response: {}", e)) + GhostError::Serialization { message: format!("Failed to parse AlienVault response: {}", e) } })?; let mut iocs = Vec::new(); diff --git a/ghost-core/src/pe_parser.rs b/ghost-core/src/pe_parser.rs index 009abd9..92b95a9 100644 --- a/ghost-core/src/pe_parser.rs +++ b/ghost-core/src/pe_parser.rs @@ -7,7 +7,6 @@ ///! - Function address resolution use crate::{GhostError, Result}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; /// PE data directory indices pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0; @@ -435,9 +434,7 @@ pub fn parse_iat_from_memory( _base_address: usize, _memory_reader: impl Fn(u32, usize, usize) -> Result>, ) -> Result> { - Err(GhostError::NotImplemented( - "IAT parsing not implemented for this platform".to_string(), - )) + Err(GhostError::PlatformNotSupported { feature: "IAT parsing not implemented for this platform".to_string() }) } #[cfg(not(windows))] @@ -447,7 +444,5 @@ pub fn detect_iat_hooks( _disk_path: &str, _memory_reader: impl Fn(u32, usize, usize) -> Result>, ) -> Result { - Err(GhostError::NotImplemented( - "IAT hook detection not implemented for this platform".to_string(), - )) + Err(GhostError::PlatformNotSupported { feature: "IAT hook detection not implemented for this platform".to_string() }) } diff --git a/ghost-core/src/yara_engine.rs b/ghost-core/src/yara_engine.rs index 39af49e..99e11b6 100644 --- a/ghost-core/src/yara_engine.rs +++ b/ghost-core/src/yara_engine.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::time::SystemTime; use yara::{Compiler, Rules, Scanner}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct DynamicYaraEngine { rules_path: Option, #[serde(skip)] @@ -15,6 +15,28 @@ pub struct DynamicYaraEngine { scan_cache: HashMap, } +impl std::fmt::Debug for DynamicYaraEngine { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DynamicYaraEngine") + .field("rules_path", &self.rules_path) + .field("has_compiled_rules", &self.compiled_rules.is_some()) + .field("rule_metadata", &self.rule_metadata) + .field("scan_cache", &self.scan_cache) + .finish() + } +} + +impl Clone for DynamicYaraEngine { + fn clone(&self) -> Self { + DynamicYaraEngine { + rules_path: self.rules_path.clone(), + compiled_rules: None, // Rules cannot be cloned, will need to recompile + rule_metadata: self.rule_metadata.clone(), + scan_cache: self.scan_cache.clone(), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct YaraRuleMetadata { pub name: String, @@ -97,7 +119,7 @@ impl DynamicYaraEngine { /// Compile all YARA rules from the rules directory pub fn compile_rules(&mut self) -> Result { let rules_dir = self.rules_path.as_ref().ok_or_else(|| { - GhostError::ConfigurationError("No rules directory configured".to_string()) + GhostError::Configuration { message: "No rules directory configured".to_string( }) })?; if !rules_dir.exists() { @@ -108,7 +130,7 @@ impl DynamicYaraEngine { } let mut compiler = Compiler::new() - .map_err(|e| GhostError::ConfigurationError(format!("YARA compiler error: {}", e)))?; + .map_err(|e| GhostError::Configuration { message: format!("YARA compiler error: {}", e }))?; let mut rule_count = 0; self.rule_metadata.clear(); @@ -155,7 +177,7 @@ impl DynamicYaraEngine { } self.compiled_rules = Some(compiler.compile_rules().map_err(|e| { - GhostError::ConfigurationError(format!("Rule compilation failed: {}", e)) + GhostError::Configuration { message: format!("Rule compilation failed: {}", e }) })?); log::info!("Successfully compiled {} YARA rules", rule_count); @@ -171,7 +193,7 @@ impl DynamicYaraEngine { } let entries = fs::read_dir(dir).map_err(|e| { - GhostError::ConfigurationError(format!("Failed to read rules directory: {}", e)) + GhostError::Configuration { message: format!("Failed to read rules directory: {}", e }) })?; for entry in entries.flatten() { @@ -203,7 +225,7 @@ impl DynamicYaraEngine { let rules = self .compiled_rules .as_ref() - .ok_or_else(|| GhostError::ConfigurationError("YARA rules not compiled".to_string()))?; + .ok_or_else(|| GhostError::Configuration { message: "YARA rules not compiled".to_string( }))?; let mut all_matches = Vec::new(); let mut bytes_scanned = 0u64; @@ -266,11 +288,11 @@ impl DynamicYaraEngine { base_address: usize, ) -> Result, GhostError> { let mut scanner = Scanner::new(rules) - .map_err(|e| GhostError::ScanError(format!("Scanner creation failed: {}", e)))?; + .map_err(|e| GhostError::Detection { message: format!("Scanner creation failed: {}", e }))?; let scan_results = scanner .scan_mem(data) - .map_err(|e| GhostError::ScanError(format!("Scan failed: {}", e)))?; + .map_err(|e| GhostError::Detection { message: format!("Scan failed: {}", e }))?; let mut matches = Vec::new(); @@ -328,7 +350,7 @@ impl DynamicYaraEngine { unsafe { let handle = OpenProcess(PROCESS_VM_READ, false, pid) - .map_err(|e| GhostError::MemoryReadError(format!("OpenProcess failed: {}", e)))?; + .map_err(|e| GhostError::MemoryEnumeration { reason: format!("OpenProcess failed: {}", e }))?; let mut buffer = vec![0u8; region.size]; let mut bytes_read = 0; @@ -362,15 +384,15 @@ impl DynamicYaraEngine { let mem_path = format!("/proc/{}/mem", pid); let mut file = File::open(&mem_path).map_err(|e| { - GhostError::MemoryReadError(format!("Failed to open {}: {}", mem_path, e)) + GhostError::MemoryEnumeration { reason: format!("Failed to open {}: {}", mem_path, e }) })?; file.seek(SeekFrom::Start(region.base_address as u64)) - .map_err(|e| GhostError::MemoryReadError(format!("Seek failed: {}", e)))?; + .map_err(|e| GhostError::MemoryEnumeration { reason: format!("Seek failed: {}", e }))?; let mut buffer = vec![0u8; region.size]; file.read_exact(&mut buffer) - .map_err(|e| GhostError::MemoryReadError(format!("Read failed: {}", e)))?; + .map_err(|e| GhostError::MemoryEnumeration { reason: format!("Read failed: {}", e }))?; Ok(buffer) }