yara_forge/validation/
mod.rs

1//! Validation module for YARA rules
2//! Provides functionality to validate and test YARA rules
3
4use std::io::Write;
5use std::path::Path;
6use std::process::Command;
7use tempfile::NamedTempFile;
8use thiserror::Error;
9use walkdir::WalkDir;
10
11#[derive(Debug, Error)]
12pub enum ValidationError {
13    #[error("Syntax error: {0}")]
14    SyntaxError(String),
15    #[error("Compilation error: {0}")]
16    CompilationError(String),
17    #[error("IO error: {0}")]
18    IoError(#[from] std::io::Error),
19    #[error("Invalid path: {0}")]
20    InvalidPath(String),
21    #[error("YARA command failed: {0}")]
22    CommandFailed(String),
23}
24
25/// Options for rule validation
26#[derive(Debug, Clone)]
27pub struct ValidationOptions {
28    /// Only check syntax without compiling
29    pub syntax_only: bool,
30    /// Test against sample files
31    pub test_against_samples: bool,
32    /// Maximum file size to scan (in bytes)
33    pub max_file_size: usize,
34    /// Timeout in seconds
35    pub timeout: u32,
36}
37
38impl Default for ValidationOptions {
39    fn default() -> Self {
40        ValidationOptions {
41            syntax_only: false,
42            test_against_samples: false,
43            max_file_size: 10 * 1024 * 1024, // 10MB
44            timeout: 60,
45        }
46    }
47}
48
49/// Validate a YARA rule
50pub fn validate_rule(rule: &str, _options: &ValidationOptions) -> Result<(), ValidationError> {
51    // Create a temporary file for the rule
52    let mut temp_file = NamedTempFile::new()?;
53    temp_file.write_all(rule.as_bytes())?;
54
55    // Run yarac for syntax checking
56    let output = Command::new("yarac")
57        .arg(temp_file.path())
58        .arg("/dev/null")
59        .output()?;
60
61    if !output.status.success() {
62        return Err(ValidationError::SyntaxError(
63            String::from_utf8_lossy(&output.stderr).to_string(),
64        ));
65    }
66
67    Ok(())
68}
69
70/// Tests a YARA rule against a directory of samples
71pub fn validate_against_samples(
72    rule: &str,
73    _options: &ValidationOptions,
74) -> Result<(), ValidationError> {
75    let mut temp_rule = NamedTempFile::new()?;
76    temp_rule.write_all(rule.as_bytes())?;
77
78    // Compile the rule
79    let status = Command::new("yarac")
80        .arg(temp_rule.path())
81        .arg("compiled_rule")
82        .status()
83        .map_err(ValidationError::IoError)?;
84
85    if !status.success() {
86        return Err(ValidationError::CompilationError(
87            "Failed to compile rule".to_string(),
88        ));
89    }
90
91    Ok(())
92}
93
94/// Scan a file or directory with a YARA rule
95pub fn scan_with_rule(
96    rule_path: impl AsRef<Path>,
97    target_path: impl AsRef<Path>,
98    options: &ValidationOptions,
99) -> Result<Vec<String>, ValidationError> {
100    let mut matches = Vec::new();
101
102    let output = Command::new("yara")
103        .arg("--timeout")
104        .arg(options.timeout.to_string())
105        .arg("--max-files")
106        .arg("1000")
107        .arg(rule_path.as_ref())
108        .arg(target_path.as_ref())
109        .output()?;
110
111    if output.status.success() {
112        let output_str = String::from_utf8_lossy(&output.stdout);
113        matches.extend(output_str.lines().map(String::from));
114    }
115
116    Ok(matches)
117}
118
119/// Scan files in parallel using multiple threads
120pub fn parallel_scan(
121    rule_path: impl AsRef<Path>,
122    target_dir: impl AsRef<Path>,
123    options: &ValidationOptions,
124) -> Result<Vec<String>, ValidationError> {
125    use rayon::prelude::*;
126
127    let walker = WalkDir::new(target_dir)
128        .min_depth(1)
129        .max_depth(5)
130        .into_iter()
131        .filter_map(Result::ok)
132        .filter(|e| {
133            e.file_type().is_file()
134                && e.metadata()
135                    .map(|m| m.len() as usize <= options.max_file_size)
136                    .unwrap_or(false)
137        })
138        .collect::<Vec<_>>();
139
140    let rule_path = rule_path.as_ref().to_path_buf();
141    let matches: Vec<String> = walker
142        .par_iter()
143        .filter_map(|entry| scan_with_rule(&rule_path, entry.path(), options).ok())
144        .flatten()
145        .collect();
146
147    Ok(matches)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_rule_validation() {
156        let rule = r#"
157            rule test {
158                strings:
159                    $a = "test"
160                condition:
161                    $a
162            }
163        "#;
164
165        let options = ValidationOptions::default();
166        assert!(validate_rule(rule, &options).is_ok());
167    }
168
169    #[test]
170    fn test_invalid_rule() {
171        let rule = r#"
172            rule test {
173                strings:
174                    $a = "test
175                condition:
176                    $a
177            }
178        "#;
179
180        let options = ValidationOptions::default();
181        assert!(validate_rule(rule, &options).is_err());
182    }
183}