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}