yara_forge/
lib.rs

1//! YARA Rule Generator
2//!
3//! A comprehensive Rust library for generating YARA rules.
4//! This library provides a simple and intuitive API to create,
5//! validate, and manage YARA rules programmatically.
6
7pub mod patterns;
8pub mod templates;
9pub mod utils;
10pub mod validation;
11
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum YaraError {
19    #[error("Invalid rule name: {0}")]
20    InvalidRuleName(String),
21    #[error("Invalid string identifier: {0}")]
22    InvalidIdentifier(String),
23    #[error("Missing required field: {0}")]
24    MissingField(String),
25    #[error("Invalid pattern: {0}")]
26    InvalidPattern(String),
27    #[error("IO error: {0}")]
28    IoError(#[from] std::io::Error),
29}
30
31/// A YARA rule string definition
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct StringDefinition {
34    pub identifier: String,
35    pub pattern: String,
36    pub is_hex: bool,
37    pub modifiers: Vec<String>,
38}
39
40/// A complete YARA rule
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Rule {
43    pub name: String,
44    pub tags: Vec<String>,
45    pub metadata: HashMap<String, String>,
46    pub strings: Vec<StringDefinition>,
47    pub condition: String,
48}
49
50/// Builder for creating YARA rules
51#[derive(Default)]
52pub struct RuleBuilder {
53    name: Option<String>,
54    tags: Vec<String>,
55    metadata: HashMap<String, String>,
56    strings: Vec<StringDefinition>,
57    condition: Option<String>,
58}
59
60impl RuleBuilder {
61    /// Create a new RuleBuilder with a given name
62    pub fn new(name: &str) -> Self {
63        RuleBuilder {
64            name: Some(name.to_string()),
65            ..Default::default()
66        }
67    }
68
69    /// Add a tag to the rule
70    pub fn with_tag(mut self, tag: &str) -> Self {
71        self.tags.push(tag.to_string());
72        self
73    }
74
75    /// Add metadata to the rule
76    pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
77        self.metadata.insert(key.to_string(), value.to_string());
78        self
79    }
80
81    /// Add a string pattern to the rule
82    pub fn with_string(mut self, identifier: &str, pattern: &str) -> Result<Self, YaraError> {
83        if !is_valid_identifier(identifier) {
84            return Err(YaraError::InvalidIdentifier(identifier.to_string()));
85        }
86
87        self.strings.push(StringDefinition {
88            identifier: identifier.to_string(),
89            pattern: pattern.to_string(),
90            is_hex: false,
91            modifiers: Vec::new(),
92        });
93
94        Ok(self)
95    }
96
97    /// Add a hex pattern to the rule
98    pub fn with_hex(mut self, identifier: &str, hex: &str) -> Result<Self, YaraError> {
99        if !is_valid_identifier(identifier) {
100            return Err(YaraError::InvalidIdentifier(identifier.to_string()));
101        }
102
103        self.strings.push(StringDefinition {
104            identifier: identifier.to_string(),
105            pattern: hex.to_string(),
106            is_hex: true,
107            modifiers: Vec::new(),
108        });
109
110        Ok(self)
111    }
112
113    /// Set the condition for the rule
114    pub fn with_condition(mut self, condition: &str) -> Self {
115        self.condition = Some(condition.to_string());
116        self
117    }
118
119    /// Build the YARA rule
120    pub fn build(self) -> Result<Rule, YaraError> {
121        let name = self
122            .name
123            .ok_or_else(|| YaraError::MissingField("rule name".to_string()))?;
124        let condition = self
125            .condition
126            .ok_or_else(|| YaraError::MissingField("condition".to_string()))?;
127
128        if !is_valid_identifier(&name) {
129            return Err(YaraError::InvalidRuleName(name));
130        }
131
132        Ok(Rule {
133            name,
134            tags: self.tags,
135            metadata: self.metadata,
136            strings: self.strings,
137            condition,
138        })
139    }
140}
141
142impl std::fmt::Display for Rule {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        let mut output = String::new();
145
146        // Rule name and tags
147        output.push_str(&format!("rule {} {{\n", self.name));
148
149        // Tags section
150        if !self.tags.is_empty() {
151            output.push_str("    tags:\n");
152            for tag in &self.tags {
153                output.push_str(&format!("        {}\n", tag));
154            }
155        }
156
157        // Metadata section
158        if !self.metadata.is_empty() {
159            output.push_str("    metadata:\n");
160            for (key, value) in &self.metadata {
161                output.push_str(&format!("        {} = \"{}\"\n", key, value));
162            }
163        }
164
165        // Strings section
166        if !self.strings.is_empty() {
167            output.push_str("    strings:\n");
168            for string in &self.strings {
169                let mut modifiers = String::new();
170                if !string.modifiers.is_empty() {
171                    modifiers = format!(" {}", string.modifiers.join(" "));
172                }
173
174                if string.is_hex {
175                    output.push_str(&format!(
176                        "        {} = {{ {} }}{}\n",
177                        string.identifier, string.pattern, modifiers
178                    ));
179                } else {
180                    output.push_str(&format!(
181                        "        {} = \"{}\"{}\n",
182                        string.identifier, string.pattern, modifiers
183                    ));
184                }
185            }
186        }
187
188        // Condition section
189        output.push_str("    condition:\n");
190        output.push_str(&format!("        {}\n", self.condition));
191        output.push('}');
192
193        write!(f, "{}", output)
194    }
195}
196
197/// Check if a string is a valid YARA identifier
198fn is_valid_identifier(s: &str) -> bool {
199    lazy_static::lazy_static! {
200        static ref RE: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap();
201    }
202    RE.is_match(s)
203}
204
205// Re-export commonly used items
206pub use patterns::{
207    C2_PATTERNS, ENCRYPTION_APIS, FILE_HEADERS, OBFUSCATION_PATTERNS, RANSOMWARE_EXTENSIONS,
208};
209pub use templates::{
210    backdoor_template, cryptominer_template, filetype_template, malware_template,
211    ransomware_template,
212};
213pub use utils::{
214    export_rule_to_json, import_rule_from_json, load_rule_from_file, save_rule_to_file,
215};
216pub use validation::{scan_with_rule, validate_against_samples, validate_rule, ValidationOptions};
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_basic_rule() {
224        let rule = RuleBuilder::new("test_rule")
225            .with_tag("malware")
226            .with_metadata("author", "Test Author")
227            .with_string("$suspicious_str", "malicious")
228            .unwrap()
229            .with_condition("$suspicious_str")
230            .build()
231            .unwrap();
232
233        assert_eq!(rule.name, "test_rule");
234        assert_eq!(rule.tags, vec!["malware"]);
235        assert_eq!(
236            rule.metadata.get("author"),
237            Some(&"Test Author".to_string())
238        );
239    }
240
241    #[test]
242    fn test_invalid_rule_name() {
243        let result = RuleBuilder::new("invalid-name")
244            .with_condition("true")
245            .build();
246        assert!(matches!(result, Err(YaraError::InvalidRuleName(_))));
247    }
248
249    #[test]
250    fn test_rule_to_string() {
251        let rule = RuleBuilder::new("test_rule")
252            .with_tag("malware")
253            .with_metadata("author", "Test Author")
254            .with_string("$suspicious_str", "malicious")
255            .unwrap()
256            .with_condition("$suspicious_str")
257            .build()
258            .unwrap();
259
260        let rule_str = format!("{}", rule);
261        assert!(rule_str.contains("rule test_rule"));
262        assert!(rule_str.contains("tags:\n        malware"));
263        assert!(rule_str.contains("author = \"Test Author\""));
264    }
265}