1pub 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#[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#[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#[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 pub fn new(name: &str) -> Self {
63 RuleBuilder {
64 name: Some(name.to_string()),
65 ..Default::default()
66 }
67 }
68
69 pub fn with_tag(mut self, tag: &str) -> Self {
71 self.tags.push(tag.to_string());
72 self
73 }
74
75 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 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 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 pub fn with_condition(mut self, condition: &str) -> Self {
115 self.condition = Some(condition.to_string());
116 self
117 }
118
119 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 output.push_str(&format!("rule {} {{\n", self.name));
148
149 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 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 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 output.push_str(" condition:\n");
190 output.push_str(&format!(" {}\n", self.condition));
191 output.push('}');
192
193 write!(f, "{}", output)
194 }
195}
196
197fn 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
205pub 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}