1use std::fmt;
2
3use serde::Serialize;
4
5use crate::error::{Result, SigmaParserError};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17pub enum SpecialChar {
18 WildcardMulti,
20 WildcardSingle,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub enum StringPart {
27 Plain(String),
28 Special(SpecialChar),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54pub struct SigmaString {
55 pub parts: Vec<StringPart>,
56 pub original: String,
57}
58
59impl SigmaString {
60 pub fn new(s: &str) -> Self {
62 let mut parts: Vec<StringPart> = Vec::new();
63 let mut acc = String::new();
64 let mut escaped = false;
65
66 for c in s.chars() {
67 if escaped {
68 if c == '*' || c == '?' || c == '\\' {
69 acc.push(c);
70 } else {
71 acc.push('\\');
73 acc.push(c);
74 }
75 escaped = false;
76 } else if c == '\\' {
77 escaped = true;
78 } else if c == '*' {
79 if !acc.is_empty() {
80 parts.push(StringPart::Plain(std::mem::take(&mut acc)));
81 }
82 parts.push(StringPart::Special(SpecialChar::WildcardMulti));
83 } else if c == '?' {
84 if !acc.is_empty() {
85 parts.push(StringPart::Plain(std::mem::take(&mut acc)));
86 }
87 parts.push(StringPart::Special(SpecialChar::WildcardSingle));
88 } else {
89 acc.push(c);
90 }
91 }
92
93 if escaped {
94 acc.push('\\');
95 }
96 if !acc.is_empty() {
97 parts.push(StringPart::Plain(acc));
98 }
99
100 SigmaString {
101 parts,
102 original: s.to_string(),
103 }
104 }
105
106 pub fn from_raw(s: &str) -> Self {
108 SigmaString {
109 parts: if s.is_empty() {
110 Vec::new()
111 } else {
112 vec![StringPart::Plain(s.to_string())]
113 },
114 original: s.to_string(),
115 }
116 }
117
118 pub fn is_plain(&self) -> bool {
120 self.parts.iter().all(|p| matches!(p, StringPart::Plain(_)))
121 }
122
123 pub fn contains_wildcards(&self) -> bool {
125 self.parts
126 .iter()
127 .any(|p| matches!(p, StringPart::Special(_)))
128 }
129
130 pub fn as_plain(&self) -> Option<String> {
132 if !self.is_plain() {
133 return None;
134 }
135 Some(
136 self.parts
137 .iter()
138 .filter_map(|p| match p {
139 StringPart::Plain(s) => Some(s.as_str()),
140 _ => None,
141 })
142 .collect(),
143 )
144 }
145}
146
147impl fmt::Display for SigmaString {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}", self.original)
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize)]
162pub enum SigmaValue {
163 String(SigmaString),
165 Integer(i64),
167 Float(f64),
169 Bool(bool),
171 Null,
173}
174
175impl SigmaValue {
176 pub fn from_yaml(v: &serde_yaml::Value) -> Self {
178 match v {
179 serde_yaml::Value::String(s) => SigmaValue::String(SigmaString::new(s)),
180 serde_yaml::Value::Number(n) => {
181 if let Some(i) = n.as_i64() {
182 SigmaValue::Integer(i)
183 } else if let Some(f) = n.as_f64() {
184 SigmaValue::Float(f)
185 } else {
186 SigmaValue::Null
187 }
188 }
189 serde_yaml::Value::Bool(b) => SigmaValue::Bool(*b),
190 serde_yaml::Value::Null => SigmaValue::Null,
191 _ => SigmaValue::String(SigmaString::new(&format!("{v:?}"))),
192 }
193 }
194
195 pub fn from_raw_string(s: &str) -> Self {
197 SigmaValue::String(SigmaString::from_raw(s))
198 }
199}
200
201impl fmt::Display for SigmaValue {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 match self {
204 SigmaValue::String(s) => write!(f, "{s}"),
205 SigmaValue::Integer(n) => write!(f, "{n}"),
206 SigmaValue::Float(n) => write!(f, "{n}"),
207 SigmaValue::Bool(b) => write!(f, "{b}"),
208 SigmaValue::Null => write!(f, "null"),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
219pub enum TimespanUnit {
220 Second,
221 Minute,
222 Hour,
223 Day,
224 Week,
225 Month,
226 Year,
227}
228
229impl fmt::Display for TimespanUnit {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 let c = match self {
232 TimespanUnit::Second => "s",
233 TimespanUnit::Minute => "m",
234 TimespanUnit::Hour => "h",
235 TimespanUnit::Day => "d",
236 TimespanUnit::Week => "w",
237 TimespanUnit::Month => "M",
238 TimespanUnit::Year => "y",
239 };
240 write!(f, "{c}")
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
248pub struct Timespan {
249 pub count: u64,
250 pub unit: TimespanUnit,
251 pub seconds: u64,
253 pub original: String,
255}
256
257impl Timespan {
258 pub fn parse(s: &str) -> Result<Self> {
263 if s.len() < 2 {
264 return Err(SigmaParserError::InvalidTimespan(s.to_string()));
265 }
266 let (count_str, unit_str) = s.split_at(s.len() - 1);
267 let count: u64 = count_str
268 .parse()
269 .map_err(|_| SigmaParserError::InvalidTimespan(s.to_string()))?;
270
271 let (unit, multiplier) = match unit_str {
272 "s" => (TimespanUnit::Second, 1u64),
273 "m" => (TimespanUnit::Minute, 60),
274 "h" => (TimespanUnit::Hour, 3600),
275 "d" => (TimespanUnit::Day, 86400),
276 "w" => (TimespanUnit::Week, 604800),
277 "M" => (TimespanUnit::Month, 2_629_746), "y" => (TimespanUnit::Year, 31_556_952), _ => return Err(SigmaParserError::InvalidTimespan(s.to_string())),
280 };
281
282 Ok(Timespan {
283 count,
284 unit,
285 seconds: count * multiplier,
286 original: s.to_string(),
287 })
288 }
289}
290
291impl fmt::Display for Timespan {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(f, "{}", self.original)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_sigma_string_plain() {
303 let s = SigmaString::new("hello world");
304 assert!(s.is_plain());
305 assert!(!s.contains_wildcards());
306 assert_eq!(s.as_plain(), Some("hello world".to_string()));
307 }
308
309 #[test]
310 fn test_sigma_string_wildcards() {
311 let s = SigmaString::new("*admin*");
312 assert!(!s.is_plain());
313 assert!(s.contains_wildcards());
314 assert_eq!(s.parts.len(), 3);
315 assert_eq!(s.parts[0], StringPart::Special(SpecialChar::WildcardMulti));
316 assert_eq!(s.parts[1], StringPart::Plain("admin".to_string()));
317 assert_eq!(s.parts[2], StringPart::Special(SpecialChar::WildcardMulti));
318 }
319
320 #[test]
321 fn test_sigma_string_escaped_wildcard_is_literal() {
322 let s = SigmaString::new(r"C:\Windows\*");
325 assert!(!s.contains_wildcards()); assert!(s.is_plain());
327 assert_eq!(s.as_plain(), Some(r"C:\Windows*".to_string()));
329 }
330
331 #[test]
332 fn test_sigma_string_unescaped_wildcard_in_path() {
333 let s = SigmaString::new(r"C:\Windows*");
335 assert!(s.contains_wildcards());
336 assert_eq!(s.parts.len(), 2);
337 assert_eq!(s.parts[0], StringPart::Plain(r"C:\Windows".to_string()));
338 assert_eq!(s.parts[1], StringPart::Special(SpecialChar::WildcardMulti));
339 }
340
341 #[test]
342 fn test_sigma_string_leading_wildcard_path() {
343 let s = SigmaString::new(r"*\cmd.exe");
345 assert!(s.contains_wildcards());
346 assert_eq!(s.parts.len(), 2);
347 assert_eq!(s.parts[0], StringPart::Special(SpecialChar::WildcardMulti));
348 assert_eq!(s.parts[1], StringPart::Plain(r"\cmd.exe".to_string()));
349 }
350
351 #[test]
352 fn test_sigma_string_escaped_wildcard() {
353 let s = SigmaString::new(r"test\*value");
354 assert!(s.is_plain());
355 assert_eq!(s.as_plain(), Some("test*value".to_string()));
356 }
357
358 #[test]
359 fn test_sigma_string_single_wildcard() {
360 let s = SigmaString::new("user?admin");
361 assert!(s.contains_wildcards());
362 assert_eq!(s.parts.len(), 3);
363 }
364
365 #[test]
366 fn test_timespan_parse() {
367 let ts = Timespan::parse("1h").unwrap();
368 assert_eq!(ts.count, 1);
369 assert_eq!(ts.unit, TimespanUnit::Hour);
370 assert_eq!(ts.seconds, 3600);
371
372 let ts = Timespan::parse("15s").unwrap();
373 assert_eq!(ts.count, 15);
374 assert_eq!(ts.unit, TimespanUnit::Second);
375 assert_eq!(ts.seconds, 15);
376
377 let ts = Timespan::parse("30m").unwrap();
378 assert_eq!(ts.seconds, 1800);
379
380 let ts = Timespan::parse("7d").unwrap();
381 assert_eq!(ts.seconds, 604800);
382 }
383
384 #[test]
385 fn test_timespan_invalid() {
386 assert!(Timespan::parse("x").is_err());
387 assert!(Timespan::parse("1x").is_err());
388 assert!(Timespan::parse("").is_err());
389 }
390}