open_detect/signature.rs
1use crate::errors::Result;
2use std::fs;
3use std::path::Path;
4use std::sync::Arc;
5
6/// A set of compiled YARA signatures for malware detection.
7///
8/// `SigSet` wraps compiled YARA rules and can be cheaply cloned due to internal
9/// use of `Arc`. It provides a fluent builder API for constructing signature sets
10/// from individual rules, directories, or recursive directory trees.
11///
12/// # Examples
13///
14/// ```no_run
15/// use open_detect::{SigSet, Signature};
16/// use std::path::Path;
17///
18/// // From a single signature
19/// let sig_set = SigSet::from_signature(
20/// Signature("rule test { condition: true }".to_string())
21/// ).unwrap();
22///
23/// // From a directory
24/// let sig_set = SigSet::new()
25/// .with_sig_dir(Path::new("signatures"))
26/// .unwrap();
27///
28/// // Chain multiple sources
29/// let sig_set = SigSet::from_signature(
30/// Signature("rule manual { condition: true }".to_string())
31/// )
32/// .unwrap()
33/// .with_sig_dir_recursive(Path::new("signatures"))
34/// .unwrap();
35/// ```
36pub struct SigSet {
37 pub(crate) rules: Arc<yara_x::Rules>,
38 signatures: Vec<Signature>,
39}
40
41impl Clone for SigSet {
42 fn clone(&self) -> Self {
43 Self {
44 rules: Arc::clone(&self.rules),
45 signatures: self.signatures.clone(),
46 }
47 }
48}
49
50impl SigSet {
51 /// Create a new empty `SigSet` with no signatures.
52 ///
53 /// This is useful as a starting point for the builder pattern.
54 ///
55 /// # Examples
56 ///
57 /// ```
58 /// use open_detect::SigSet;
59 ///
60 /// let sig_set = SigSet::new();
61 /// assert_eq!(sig_set.count(), 0);
62 /// ```
63 #[must_use]
64 pub fn new() -> Self {
65 Self {
66 rules: Arc::new(yara_x::Compiler::new().build()),
67 signatures: Vec::new(),
68 }
69 }
70
71 /// Create a `SigSet` from a single YARA signature.
72 ///
73 /// # Errors
74 ///
75 /// Returns an error if the signature fails to compile.
76 ///
77 /// # Examples
78 ///
79 /// ```
80 /// use open_detect::{SigSet, Signature};
81 ///
82 /// let sig_set = SigSet::from_signature(
83 /// Signature("rule test { condition: true }".to_string())
84 /// ).unwrap();
85 /// assert_eq!(sig_set.count(), 1);
86 /// ```
87 pub fn from_signature(signature: Signature) -> Result<Self> {
88 let mut compiler = yara_x::Compiler::new();
89 compiler.add_source(signature.0.as_str())?;
90 let rules = compiler.build();
91 Ok(Self {
92 rules: Arc::new(rules),
93 signatures: vec![signature],
94 })
95 }
96
97 /// Create a `SigSet` from multiple YARA signatures.
98 ///
99 /// # Errors
100 ///
101 /// Returns an error if any signature fails to compile.
102 ///
103 /// # Examples
104 ///
105 /// ```
106 /// use open_detect::{SigSet, Signature};
107 ///
108 /// let sig_set = SigSet::from_signatures(vec![
109 /// Signature("rule test1 { condition: true }".to_string()),
110 /// Signature("rule test2 { condition: false }".to_string()),
111 /// ]).unwrap();
112 /// assert_eq!(sig_set.count(), 2);
113 /// ```
114 pub fn from_signatures(signatures: Vec<Signature>) -> Result<Self> {
115 let mut compiler = yara_x::Compiler::new();
116 for signature in &signatures {
117 compiler.add_source(signature.0.as_str())?;
118 }
119 let rules = compiler.build();
120 Ok(Self {
121 rules: Arc::new(rules),
122 signatures,
123 })
124 }
125
126 /// Add a single signature to this `SigSet`, returning a new `SigSet`.
127 ///
128 /// This recompiles all signatures including the new one.
129 ///
130 /// # Errors
131 ///
132 /// Returns an error if signature compilation fails.
133 ///
134 /// # Examples
135 ///
136 /// ```
137 /// use open_detect::{SigSet, Signature};
138 ///
139 /// let sig_set = SigSet::new()
140 /// .with_signature(Signature("rule test { condition: true }".to_string()))
141 /// .unwrap();
142 /// assert_eq!(sig_set.count(), 1);
143 /// ```
144 pub fn with_signature(self, signature: Signature) -> Result<Self> {
145 let mut signatures = self.signatures;
146 signatures.push(signature);
147 Self::from_signatures(signatures)
148 }
149
150 /// Add multiple signatures to this `SigSet`, returning a new `SigSet`.
151 ///
152 /// This recompiles all signatures including the new ones.
153 ///
154 /// # Errors
155 ///
156 /// Returns an error if signature compilation fails.
157 ///
158 /// # Examples
159 ///
160 /// ```
161 /// use open_detect::{SigSet, Signature};
162 ///
163 /// let sig_set = SigSet::new()
164 /// .with_signatures(vec![
165 /// Signature("rule test1 { condition: true }".to_string()),
166 /// Signature("rule test2 { condition: false }".to_string()),
167 /// ])
168 /// .unwrap();
169 /// assert_eq!(sig_set.count(), 2);
170 /// ```
171 pub fn with_signatures(self, new_signatures: Vec<Signature>) -> Result<Self> {
172 let mut signatures = self.signatures;
173 signatures.extend(new_signatures);
174 Self::from_signatures(signatures)
175 }
176
177 /// Add all YARA files from a directory (non-recursive).
178 ///
179 /// Loads files with extensions: `.yar`, `.yara`, `.yrc`
180 ///
181 /// # Errors
182 ///
183 /// Returns an error if:
184 /// - The directory cannot be read
185 /// - Any signature file cannot be read
186 /// - Signature compilation fails
187 ///
188 /// # Examples
189 ///
190 /// ```no_run
191 /// use open_detect::SigSet;
192 /// use std::path::Path;
193 ///
194 /// let sig_set = SigSet::new()
195 /// .with_sig_dir(Path::new("signatures"))
196 /// .unwrap();
197 /// ```
198 pub fn with_sig_dir(self, path: &Path) -> Result<Self> {
199 let mut signatures = self.signatures;
200 Self::load_signatures_from_dir(path, &mut signatures)?;
201 Self::from_signatures(signatures)
202 }
203
204 /// Add all YARA files from a directory recursively.
205 ///
206 /// Recursively traverses subdirectories and loads all files with
207 /// extensions: `.yar`, `.yara`, `.yrc`
208 ///
209 /// # Errors
210 ///
211 /// Returns an error if:
212 /// - The directory cannot be read
213 /// - Any signature file cannot be read
214 /// - Signature compilation fails
215 ///
216 /// # Examples
217 ///
218 /// ```no_run
219 /// use open_detect::SigSet;
220 /// use std::path::Path;
221 ///
222 /// let sig_set = SigSet::new()
223 /// .with_sig_dir_recursive(Path::new("signatures"))
224 /// .unwrap();
225 /// ```
226 pub fn with_sig_dir_recursive(self, path: &Path) -> Result<Self> {
227 let mut signatures = self.signatures;
228 Self::load_signatures_from_dir_recursive(path, &mut signatures)?;
229 Self::from_signatures(signatures)
230 }
231
232 /// Get the number of rules in this signature set.
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use open_detect::{SigSet, Signature};
238 ///
239 /// let sig_set = SigSet::from_signature(
240 /// Signature("rule test { condition: true }".to_string())
241 /// ).unwrap();
242 /// assert_eq!(sig_set.count(), 1);
243 /// ```
244 #[must_use]
245 pub fn count(&self) -> usize {
246 self.rules.iter().count()
247 }
248
249 // Helper methods
250
251 fn load_signatures_from_dir(path: &Path, signatures: &mut Vec<Signature>) -> Result<()> {
252 let entries = fs::read_dir(path)?;
253
254 for entry in entries {
255 let entry = entry?;
256 let path = entry.path();
257
258 // Skip directories
259 if path.is_dir() {
260 continue;
261 }
262
263 // Check for YARA file extensions
264 if let Some(extension) = path.extension() {
265 let ext = extension.to_string_lossy().to_lowercase();
266 if ext == "yar" || ext == "yara" || ext == "yrc" {
267 // Read the file content
268 let content = fs::read_to_string(&path)?;
269 signatures.push(Signature(content));
270 }
271 }
272 }
273
274 Ok(())
275 }
276
277 fn load_signatures_from_dir_recursive(
278 path: &Path,
279 signatures: &mut Vec<Signature>,
280 ) -> Result<()> {
281 let entries = fs::read_dir(path)?;
282
283 for entry in entries {
284 let entry = entry?;
285 let path = entry.path();
286
287 if path.is_dir() {
288 // Recursively process subdirectories
289 Self::load_signatures_from_dir_recursive(&path, signatures)?;
290 } else {
291 // Check for YARA file extensions
292 if let Some(extension) = path.extension() {
293 let ext = extension.to_string_lossy().to_lowercase();
294 if ext == "yar" || ext == "yara" || ext == "yrc" {
295 // Read the file content
296 let content = fs::read_to_string(&path)?;
297 signatures.push(Signature(content));
298 }
299 }
300 }
301 }
302
303 Ok(())
304 }
305}
306
307impl Default for SigSet {
308 fn default() -> Self {
309 Self::new()
310 }
311}
312
313#[derive(Clone)]
314/// A YARA signature rule as a string.
315///
316/// This is a newtype wrapper around `String` containing YARA rule source code.
317///
318/// # Examples
319///
320/// ```
321/// use open_detect::Signature;
322///
323/// let sig = Signature("rule test { condition: true }".to_string());
324/// ```
325pub struct Signature(pub String);
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 #[test]
331 fn from_signature_valid() {
332 let signature_set =
333 SigSet::from_signature(Signature("rule test { condition: true }".to_string())).unwrap();
334
335 assert_eq!(1, signature_set.count());
336 }
337
338 #[test]
339 fn from_signature_invalid() {
340 let result = SigSet::from_signature(Signature("rule test { condition: ".to_string()));
341 assert!(result.is_err());
342 }
343
344 #[test]
345 fn from_signatures_multiple() {
346 let signature_set = SigSet::from_signatures(vec![
347 Signature("rule test1 { condition: true }".to_string()),
348 Signature("rule test2 { condition: true }".to_string()),
349 ])
350 .unwrap();
351
352 assert_eq!(2, signature_set.count());
353 }
354
355 #[test]
356 fn with_signature_chaining() {
357 let signature_set = SigSet::new()
358 .with_signature(Signature("rule test { condition: true }".to_string()))
359 .unwrap();
360
361 assert_eq!(1, signature_set.count());
362 }
363
364 #[test]
365 fn with_sig_dir_loads_yara_files() {
366 use std::path::PathBuf;
367
368 let test_dir = PathBuf::from("tests/test_sigs");
369 let result = SigSet::new().with_sig_dir(&test_dir);
370
371 assert!(result.is_ok());
372 let sig_set = result.unwrap();
373 assert_eq!(sig_set.count(), 1); // We have 1 .yara file in test_sigs
374 }
375
376 #[test]
377 fn with_sig_dir_nonexistent_directory() {
378 use std::path::PathBuf;
379
380 let test_dir = PathBuf::from("tests/nonexistent_dir");
381 let result = SigSet::new().with_sig_dir(&test_dir);
382
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn test_chaining_with_signature_and_dir() {
388 use std::path::PathBuf;
389
390 let test_dir = PathBuf::from("tests/test_sigs");
391 let sig_set =
392 SigSet::from_signature(Signature("rule test { condition: true }".to_string()))
393 .unwrap()
394 .with_sig_dir(&test_dir)
395 .unwrap();
396
397 // Should have 1 manual + 1 from directory
398 assert_eq!(sig_set.count(), 2);
399 }
400}