Add YARA dependency and implement rule compilation

- Added yara crate v0.28 to ghost-core dependencies
- Implemented real YARA rule compilation from .yar/.yara files
- Added recursive rule file discovery in rules directory
- Implemented memory scanning with compiled YARA rules
- Added proper error handling for rule compilation and scanning
- Cross-platform memory reading support (Windows, Linux, macOS stub)

Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Adir Shitrit
2025-11-21 00:35:37 +02:00
parent 1680095094
commit bcf934fac2
2 changed files with 330 additions and 78 deletions

View File

@@ -15,6 +15,7 @@ serde_json = "1.0"
uuid = { version = "1.0", features = ["v4"] }
toml = "0.8"
chrono = "0.4"
yara = "0.28"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [

View File

@@ -1,33 +1,28 @@
use crate::{GhostError, MemoryRegion, ProcessInfo};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use yara::{Compiler, Rules, Scanner};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DynamicYaraEngine {
rules: Vec<YaraRule>,
sources: Vec<YaraRuleSource>,
rules_path: Option<PathBuf>,
#[serde(skip)]
compiled_rules: Option<Rules>,
rule_metadata: Vec<YaraRuleMetadata>,
scan_cache: HashMap<String, CachedScanResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YaraRule {
pub struct YaraRuleMetadata {
pub name: String,
pub content: String,
pub source: String,
pub file_path: String,
pub threat_level: ThreatLevel,
pub last_updated: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YaraRuleSource {
pub name: String,
pub url: String,
pub enabled: bool,
pub rule_count: usize,
pub last_update: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YaraScanResult {
pub matches: Vec<RuleMatch>,
@@ -38,13 +33,15 @@ pub struct YaraScanResult {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleMatch {
pub rule_name: String,
pub namespace: String,
pub threat_level: ThreatLevel,
pub offset: u64,
pub length: u32,
pub metadata: HashMap<String, String>,
pub matched_strings: Vec<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ThreatLevel {
Info = 1,
Low = 2,
@@ -53,6 +50,22 @@ pub enum ThreatLevel {
Critical = 5,
}
impl ThreatLevel {
pub fn from_metadata(metadata: &HashMap<String, String>) -> Self {
metadata
.get("threat_level")
.and_then(|s| match s.to_lowercase().as_str() {
"info" => Some(ThreatLevel::Info),
"low" => Some(ThreatLevel::Low),
"medium" => Some(ThreatLevel::Medium),
"high" => Some(ThreatLevel::High),
"critical" => Some(ThreatLevel::Critical),
_ => None,
})
.unwrap_or(ThreatLevel::Medium)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CachedScanResult {
result: YaraScanResult,
@@ -60,96 +73,334 @@ struct CachedScanResult {
}
impl DynamicYaraEngine {
pub fn new(_config_path: Option<&str>) -> Result<Self, GhostError> {
let sources = vec![
YaraRuleSource {
name: "Malware Bazaar".to_string(),
url: "https://bazaar.abuse.ch/browse/".to_string(),
enabled: true,
rule_count: 0,
last_update: SystemTime::now(),
},
YaraRuleSource {
name: "VX-Underground".to_string(),
url: "https://vx-underground.org/yara".to_string(),
enabled: true,
rule_count: 0,
last_update: SystemTime::now(),
},
];
/// Create a new YARA engine with optional custom rules directory
pub fn new(rules_path: Option<&str>) -> Result<Self, GhostError> {
let rules_path = rules_path.map(PathBuf::from);
Ok(DynamicYaraEngine {
rules: Vec::new(),
sources,
let mut engine = DynamicYaraEngine {
rules_path,
compiled_rules: None,
rule_metadata: Vec::new(),
scan_cache: HashMap::new(),
})
}
};
pub async fn update_rules(&mut self) -> Result<usize, GhostError> {
let mut updated_count = 0;
for source in &mut self.sources {
if !source.enabled {
continue;
// Attempt to load rules if path is provided
if engine.rules_path.is_some() {
if let Err(e) = engine.compile_rules() {
log::warn!("Failed to compile YARA rules: {:?}", e);
}
// Simulate rule download
let new_rules = vec![YaraRule {
name: format!("generic_malware_{}", updated_count + 1),
content: "rule generic_malware { condition: true }".to_string(),
source: source.name.clone(),
threat_level: ThreatLevel::Medium,
last_updated: SystemTime::now(),
}];
self.rules.extend(new_rules);
source.rule_count = self.rules.len();
source.last_update = SystemTime::now();
updated_count += 1;
}
Ok(updated_count)
Ok(engine)
}
/// Compile all YARA rules from the rules directory
pub fn compile_rules(&mut self) -> Result<usize, GhostError> {
let rules_dir = self.rules_path.as_ref().ok_or_else(|| {
GhostError::ConfigurationError("No rules directory configured".to_string())
})?;
if !rules_dir.exists() {
return Err(GhostError::ConfigurationError(format!(
"Rules directory does not exist: {}",
rules_dir.display()
)));
}
let mut compiler = Compiler::new()
.map_err(|e| GhostError::ConfigurationError(format!("YARA compiler error: {}", e)))?;
let mut rule_count = 0;
self.rule_metadata.clear();
// Recursively find and compile all .yar and .yara files
let rule_files = Self::find_rule_files(rules_dir)?;
for rule_file in &rule_files {
match fs::read_to_string(rule_file) {
Ok(content) => {
let namespace = rule_file
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("default");
match compiler.add_rules_str_with_namespace(&content, namespace) {
Ok(_) => {
log::info!("Compiled YARA rule: {}", rule_file.display());
self.rule_metadata.push(YaraRuleMetadata {
name: namespace.to_string(),
file_path: rule_file.display().to_string(),
threat_level: ThreatLevel::Medium,
last_updated: SystemTime::now(),
});
rule_count += 1;
}
Err(e) => {
log::error!("Failed to compile {}: {}", rule_file.display(), e);
}
}
}
Err(e) => {
log::error!("Failed to read {}: {}", rule_file.display(), e);
}
}
}
if rule_count == 0 {
return Err(GhostError::ConfigurationError(
"No YARA rules were successfully compiled".to_string(),
));
}
self.compiled_rules = Some(
compiler
.compile_rules()
.map_err(|e| GhostError::ConfigurationError(format!("Rule compilation failed: {}", e)))?,
);
log::info!("Successfully compiled {} YARA rules", rule_count);
Ok(rule_count)
}
/// Find all YARA rule files in the given directory
fn find_rule_files(dir: &Path) -> Result<Vec<PathBuf>, GhostError> {
let mut rule_files = Vec::new();
if !dir.is_dir() {
return Ok(rule_files);
}
let entries = fs::read_dir(dir).map_err(|e| {
GhostError::ConfigurationError(format!("Failed to read rules directory: {}", e))
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "yar" || ext == "yara" {
rule_files.push(path);
}
}
} else if path.is_dir() {
// Recursively search subdirectories
rule_files.extend(Self::find_rule_files(&path)?);
}
}
Ok(rule_files)
}
/// Scan process memory regions with compiled YARA rules
pub async fn scan_process(
&self,
_process: &ProcessInfo,
process: &ProcessInfo,
memory_regions: &[MemoryRegion],
) -> Result<YaraScanResult, GhostError> {
let start_time = SystemTime::now();
let mut matches = Vec::new();
let mut bytes_scanned = 0;
// Simulate YARA scanning
let rules = self.compiled_rules.as_ref().ok_or_else(|| {
GhostError::ConfigurationError("YARA rules not compiled".to_string())
})?;
let mut all_matches = Vec::new();
let mut bytes_scanned = 0u64;
// Scan each executable memory region
for region in memory_regions.iter() {
bytes_scanned += region.size;
// Only scan executable regions with reasonable size
if !region.protection.is_executable() {
continue;
}
// Simulate finding suspicious patterns
if region.protection.is_executable() && region.protection.is_writable() {
matches.push(RuleMatch {
rule_name: "suspicious_rwx_memory".to_string(),
threat_level: ThreatLevel::High,
offset: region.base_address as u64,
length: 1024,
metadata: HashMap::new(),
});
if region.size > 100 * 1024 * 1024 {
log::debug!(
"Skipping large region at {:#x} (size: {} MB)",
region.base_address,
region.size / (1024 * 1024)
);
continue;
}
// Read memory content
let memory_content = match Self::read_process_memory(process.pid, region) {
Ok(data) => data,
Err(e) => {
log::debug!(
"Failed to read memory at {:#x}: {:?}",
region.base_address,
e
);
continue;
}
};
bytes_scanned += memory_content.len() as u64;
// Perform YARA scan on this memory region
match Self::scan_memory_with_yara(rules, &memory_content, region.base_address) {
Ok(mut matches) => {
all_matches.append(&mut matches);
}
Err(e) => {
log::debug!("YARA scan error at {:#x}: {:?}", region.base_address, e);
}
}
}
let scan_time_ms = start_time.elapsed().unwrap_or_default().as_millis() as u64;
Ok(YaraScanResult {
matches,
matches: all_matches,
scan_time_ms,
bytes_scanned: bytes_scanned as u64,
bytes_scanned,
})
}
pub fn get_rule_count(&self) -> usize {
self.rules.len()
/// Scan a memory buffer with YARA rules
fn scan_memory_with_yara(
rules: &Rules,
data: &[u8],
base_address: usize,
) -> Result<Vec<RuleMatch>, GhostError> {
let mut scanner = Scanner::new(rules)
.map_err(|e| GhostError::ScanError(format!("Scanner creation failed: {}", e)))?;
let scan_results = scanner
.scan_mem(data)
.map_err(|e| GhostError::ScanError(format!("Scan failed: {}", e)))?;
let mut matches = Vec::new();
for rule in scan_results {
let rule_name = rule.identifier.to_string();
let namespace = rule.namespace.to_string();
// Extract metadata
let mut metadata = HashMap::new();
for meta in rule.metadatas {
let value = match meta.value {
yara::MetadataValue::Integer(i) => i.to_string(),
yara::MetadataValue::String(ref s) => s.clone(),
yara::MetadataValue::Boolean(b) => b.to_string(),
};
metadata.insert(meta.identifier.to_string(), value);
}
let threat_level = ThreatLevel::from_metadata(&metadata);
// Extract matched strings
let mut matched_strings = Vec::new();
for string in rule.strings {
let identifier = string.identifier.to_string();
for m in string.matches {
matched_strings.push(format!(
"{} at offset {:#x}",
identifier,
base_address + m.offset
));
// Create a match entry for each string match
matches.push(RuleMatch {
rule_name: rule_name.clone(),
namespace: namespace.clone(),
threat_level,
offset: (base_address + m.offset) as u64,
length: m.length as u32,
metadata: metadata.clone(),
matched_strings: vec![identifier.clone()],
});
}
}
}
Ok(matches)
}
pub fn get_sources(&self) -> &[YaraRuleSource] {
&self.sources
/// Read memory from a specific process and region
#[cfg(target_os = "windows")]
fn read_process_memory(
pid: u32,
region: &MemoryRegion,
) -> Result<Vec<u8>, GhostError> {
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Threading::{OpenProcess, PROCESS_VM_READ};
unsafe {
let handle = OpenProcess(PROCESS_VM_READ, false, pid)
.map_err(|e| GhostError::MemoryReadError(format!("OpenProcess failed: {}", e)))?;
let mut buffer = vec![0u8; region.size];
let mut bytes_read = 0;
let result = ReadProcessMemory(
handle,
region.base_address as *const _,
buffer.as_mut_ptr() as *mut _,
region.size,
Some(&mut bytes_read),
);
let _ = CloseHandle(handle);
if result.is_ok() && bytes_read > 0 {
buffer.truncate(bytes_read);
Ok(buffer)
} else {
Err(GhostError::MemoryReadError(
"ReadProcessMemory failed".to_string(),
))
}
}
}
/// Read memory from a specific process and region (Linux implementation)
#[cfg(target_os = "linux")]
fn read_process_memory(
pid: u32,
region: &MemoryRegion,
) -> Result<Vec<u8>, GhostError> {
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
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)))?;
file.seek(SeekFrom::Start(region.base_address as u64))
.map_err(|e| GhostError::MemoryReadError(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)))?;
Ok(buffer)
}
/// Read memory from a specific process and region (macOS implementation)
#[cfg(target_os = "macos")]
fn read_process_memory(
_pid: u32,
_region: &MemoryRegion,
) -> Result<Vec<u8>, GhostError> {
Err(GhostError::NotImplemented(
"Memory reading not implemented for macOS".to_string(),
))
}
pub fn get_rule_count(&self) -> usize {
self.rule_metadata.len()
}
pub fn get_rule_metadata(&self) -> &[YaraRuleMetadata] {
&self.rule_metadata
}
pub fn is_compiled(&self) -> bool {
self.compiled_rules.is_some()
}
}