use std::fmt;
use serde::Serialize;
use crate::error::{Result, SigmaParserError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum SpecialChar {
WildcardMulti,
WildcardSingle,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum StringPart {
Plain(String),
Special(SpecialChar),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SigmaString {
pub parts: Vec<StringPart>,
pub original: String,
}
impl SigmaString {
pub fn new(s: &str) -> Self {
let mut parts: Vec<StringPart> = Vec::new();
let mut acc = String::new();
let mut escaped = false;
for c in s.chars() {
if escaped {
if c == '*' || c == '?' || c == '\\' {
acc.push(c);
} else {
acc.push('\\');
acc.push(c);
}
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '*' {
if !acc.is_empty() {
parts.push(StringPart::Plain(std::mem::take(&mut acc)));
}
parts.push(StringPart::Special(SpecialChar::WildcardMulti));
} else if c == '?' {
if !acc.is_empty() {
parts.push(StringPart::Plain(std::mem::take(&mut acc)));
}
parts.push(StringPart::Special(SpecialChar::WildcardSingle));
} else {
acc.push(c);
}
}
if escaped {
acc.push('\\');
}
if !acc.is_empty() {
parts.push(StringPart::Plain(acc));
}
SigmaString {
parts,
original: s.to_string(),
}
}
pub fn from_raw(s: &str) -> Self {
SigmaString {
parts: if s.is_empty() {
Vec::new()
} else {
vec![StringPart::Plain(s.to_string())]
},
original: s.to_string(),
}
}
pub fn is_plain(&self) -> bool {
self.parts.iter().all(|p| matches!(p, StringPart::Plain(_)))
}
pub fn contains_wildcards(&self) -> bool {
self.parts
.iter()
.any(|p| matches!(p, StringPart::Special(_)))
}
pub fn as_plain(&self) -> Option<String> {
if !self.is_plain() {
return None;
}
Some(
self.parts
.iter()
.filter_map(|p| match p {
StringPart::Plain(s) => Some(s.as_str()),
_ => None,
})
.collect(),
)
}
}
impl fmt::Display for SigmaString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.original)
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum SigmaValue {
String(SigmaString),
Integer(i64),
Float(f64),
Bool(bool),
Null,
}
impl SigmaValue {
pub fn from_yaml(v: &serde_yaml::Value) -> Self {
match v {
serde_yaml::Value::String(s) => SigmaValue::String(SigmaString::new(s)),
serde_yaml::Value::Number(n) => {
if let Some(i) = n.as_i64() {
SigmaValue::Integer(i)
} else if let Some(f) = n.as_f64() {
SigmaValue::Float(f)
} else {
SigmaValue::Null
}
}
serde_yaml::Value::Bool(b) => SigmaValue::Bool(*b),
serde_yaml::Value::Null => SigmaValue::Null,
_ => SigmaValue::String(SigmaString::new(&format!("{v:?}"))),
}
}
pub fn from_raw_string(s: &str) -> Self {
SigmaValue::String(SigmaString::from_raw(s))
}
}
impl fmt::Display for SigmaValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SigmaValue::String(s) => write!(f, "{s}"),
SigmaValue::Integer(n) => write!(f, "{n}"),
SigmaValue::Float(n) => write!(f, "{n}"),
SigmaValue::Bool(b) => write!(f, "{b}"),
SigmaValue::Null => write!(f, "null"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum TimespanUnit {
Second,
Minute,
Hour,
Day,
Week,
Month,
Year,
}
impl fmt::Display for TimespanUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let c = match self {
TimespanUnit::Second => "s",
TimespanUnit::Minute => "m",
TimespanUnit::Hour => "h",
TimespanUnit::Day => "d",
TimespanUnit::Week => "w",
TimespanUnit::Month => "M",
TimespanUnit::Year => "y",
};
write!(f, "{c}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Timespan {
pub count: u64,
pub unit: TimespanUnit,
pub seconds: u64,
pub original: String,
}
impl Timespan {
pub fn parse(s: &str) -> Result<Self> {
if s.len() < 2 {
return Err(SigmaParserError::InvalidTimespan(s.to_string()));
}
let (count_str, unit_str) = s.split_at(s.len() - 1);
let count: u64 = count_str
.parse()
.map_err(|_| SigmaParserError::InvalidTimespan(s.to_string()))?;
let (unit, multiplier) = match unit_str {
"s" => (TimespanUnit::Second, 1u64),
"m" => (TimespanUnit::Minute, 60),
"h" => (TimespanUnit::Hour, 3600),
"d" => (TimespanUnit::Day, 86400),
"w" => (TimespanUnit::Week, 604800),
"M" => (TimespanUnit::Month, 2_629_746), "y" => (TimespanUnit::Year, 31_556_952), _ => return Err(SigmaParserError::InvalidTimespan(s.to_string())),
};
Ok(Timespan {
count,
unit,
seconds: count * multiplier,
original: s.to_string(),
})
}
}
impl fmt::Display for Timespan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.original)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sigma_string_plain() {
let s = SigmaString::new("hello world");
assert!(s.is_plain());
assert!(!s.contains_wildcards());
assert_eq!(s.as_plain(), Some("hello world".to_string()));
}
#[test]
fn test_sigma_string_wildcards() {
let s = SigmaString::new("*admin*");
assert!(!s.is_plain());
assert!(s.contains_wildcards());
assert_eq!(s.parts.len(), 3);
assert_eq!(s.parts[0], StringPart::Special(SpecialChar::WildcardMulti));
assert_eq!(s.parts[1], StringPart::Plain("admin".to_string()));
assert_eq!(s.parts[2], StringPart::Special(SpecialChar::WildcardMulti));
}
#[test]
fn test_sigma_string_escaped_wildcard_is_literal() {
let s = SigmaString::new(r"C:\Windows\*");
assert!(!s.contains_wildcards()); assert!(s.is_plain());
assert_eq!(s.as_plain(), Some(r"C:\Windows*".to_string()));
}
#[test]
fn test_sigma_string_unescaped_wildcard_in_path() {
let s = SigmaString::new(r"C:\Windows*");
assert!(s.contains_wildcards());
assert_eq!(s.parts.len(), 2);
assert_eq!(s.parts[0], StringPart::Plain(r"C:\Windows".to_string()));
assert_eq!(s.parts[1], StringPart::Special(SpecialChar::WildcardMulti));
}
#[test]
fn test_sigma_string_leading_wildcard_path() {
let s = SigmaString::new(r"*\cmd.exe");
assert!(s.contains_wildcards());
assert_eq!(s.parts.len(), 2);
assert_eq!(s.parts[0], StringPart::Special(SpecialChar::WildcardMulti));
assert_eq!(s.parts[1], StringPart::Plain(r"\cmd.exe".to_string()));
}
#[test]
fn test_sigma_string_escaped_wildcard() {
let s = SigmaString::new(r"test\*value");
assert!(s.is_plain());
assert_eq!(s.as_plain(), Some("test*value".to_string()));
}
#[test]
fn test_sigma_string_single_wildcard() {
let s = SigmaString::new("user?admin");
assert!(s.contains_wildcards());
assert_eq!(s.parts.len(), 3);
}
#[test]
fn test_timespan_parse() {
let ts = Timespan::parse("1h").unwrap();
assert_eq!(ts.count, 1);
assert_eq!(ts.unit, TimespanUnit::Hour);
assert_eq!(ts.seconds, 3600);
let ts = Timespan::parse("15s").unwrap();
assert_eq!(ts.count, 15);
assert_eq!(ts.unit, TimespanUnit::Second);
assert_eq!(ts.seconds, 15);
let ts = Timespan::parse("30m").unwrap();
assert_eq!(ts.seconds, 1800);
let ts = Timespan::parse("7d").unwrap();
assert_eq!(ts.seconds, 604800);
}
#[test]
fn test_timespan_invalid() {
assert!(Timespan::parse("x").is_err());
assert!(Timespan::parse("1x").is_err());
assert!(Timespan::parse("").is_err());
}
}