yara_forge/validation/
mod.rs1use 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#[derive(Debug, Clone)]
27pub struct ValidationOptions {
28 pub syntax_only: bool,
30 pub test_against_samples: bool,
32 pub max_file_size: usize,
34 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, timeout: 60,
45 }
46 }
47}
48
49pub fn validate_rule(rule: &str, _options: &ValidationOptions) -> Result<(), ValidationError> {
51 let mut temp_file = NamedTempFile::new()?;
53 temp_file.write_all(rule.as_bytes())?;
54
55 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
70pub 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 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
94pub 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
119pub 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}