Skip to main content

rsigma_eval/event/
mod.rs

1//! Event abstraction for Sigma rule evaluation.
2//!
3//! Provides the [`Event`] trait for generic event access, the [`EventValue`]
4//! enum representing field values, and concrete implementations:
5//! - [`JsonEvent`] — zero-copy wrapper around `serde_json::Value`
6//! - [`KvEvent`] — flat key-value pairs (e.g., from logfmt / syslog)
7//! - [`PlainEvent`] — raw log line (keyword matching only)
8//! - [`MapEvent`] — generic `HashMap<K, V>` adapter
9
10mod json;
11mod kv;
12mod map;
13mod plain;
14
15pub use json::JsonEvent;
16pub use kv::KvEvent;
17pub use map::MapEvent;
18pub use plain::PlainEvent;
19
20use std::borrow::Cow;
21
22use serde_json::Value;
23
24// =============================================================================
25// EventValue
26// =============================================================================
27
28/// A value retrieved from an event field.
29///
30/// Supports zero-copy borrows from JSON-backed events (`Cow::Borrowed`)
31/// and owned values from non-JSON sources (`Cow::Owned`).
32/// Null is distinct from field-absent (`get_field` returns `None`).
33#[derive(Debug, Clone, PartialEq)]
34pub enum EventValue<'a> {
35    Str(Cow<'a, str>),
36    Int(i64),
37    Float(f64),
38    Bool(bool),
39    Null,
40    Array(Vec<EventValue<'a>>),
41    Map(Vec<(Cow<'a, str>, EventValue<'a>)>),
42}
43
44impl<'a> EventValue<'a> {
45    /// Coerce to string. Str as-is, Int/Float decimal, Bool "true"/"false".
46    #[inline]
47    pub fn as_str(&self) -> Option<Cow<'_, str>> {
48        match self {
49            EventValue::Str(s) => Some(Cow::Borrowed(s)),
50            EventValue::Int(n) => Some(Cow::Owned(n.to_string())),
51            EventValue::Float(f) => Some(Cow::Owned(f.to_string())),
52            EventValue::Bool(b) => Some(Cow::Borrowed(if *b { "true" } else { "false" })),
53            _ => None,
54        }
55    }
56
57    /// Coerce to f64. Int lossless, Float as-is, Str parsed.
58    #[inline]
59    pub fn as_f64(&self) -> Option<f64> {
60        match self {
61            EventValue::Float(f) => Some(*f),
62            EventValue::Int(n) => Some(*n as f64),
63            EventValue::Str(s) => s.parse().ok(),
64            _ => None,
65        }
66    }
67
68    /// Coerce to i64. Int as-is, Float truncated if exact, Str parsed.
69    #[inline]
70    pub fn as_i64(&self) -> Option<i64> {
71        match self {
72            EventValue::Int(n) => Some(*n),
73            EventValue::Float(f) => {
74                let truncated = *f as i64;
75                if (truncated as f64 - f).abs() < f64::EPSILON {
76                    Some(truncated)
77                } else {
78                    None
79                }
80            }
81            EventValue::Str(s) => s.parse().ok(),
82            _ => None,
83        }
84    }
85
86    /// Coerce to bool. Bool as-is, Str: true/false/1/0/yes/no.
87    #[inline]
88    pub fn as_bool(&self) -> Option<bool> {
89        match self {
90            EventValue::Bool(b) => Some(*b),
91            EventValue::Str(s) => match s.to_lowercase().as_str() {
92                "true" | "1" | "yes" => Some(true),
93                "false" | "0" | "no" => Some(false),
94                _ => None,
95            },
96            _ => None,
97        }
98    }
99
100    #[inline]
101    pub fn is_null(&self) -> bool {
102        matches!(self, EventValue::Null)
103    }
104
105    /// Convert to `serde_json::Value`.
106    pub fn to_json(&self) -> Value {
107        match self {
108            EventValue::Str(s) => Value::String(s.to_string()),
109            EventValue::Int(n) => Value::Number((*n).into()),
110            EventValue::Float(f) => {
111                serde_json::Number::from_f64(*f).map_or(Value::Null, Value::Number)
112            }
113            EventValue::Bool(b) => Value::Bool(*b),
114            EventValue::Null => Value::Null,
115            EventValue::Array(arr) => Value::Array(arr.iter().map(|v| v.to_json()).collect()),
116            EventValue::Map(entries) => {
117                let map = entries
118                    .iter()
119                    .map(|(k, v)| (k.to_string(), v.to_json()))
120                    .collect();
121                Value::Object(map)
122            }
123        }
124    }
125}
126
127impl<'a> From<&'a Value> for EventValue<'a> {
128    fn from(v: &'a Value) -> Self {
129        match v {
130            Value::String(s) => EventValue::Str(Cow::Borrowed(s.as_str())),
131            Value::Number(n) => {
132                if let Some(i) = n.as_i64() {
133                    EventValue::Int(i)
134                } else {
135                    EventValue::Float(n.as_f64().unwrap_or(f64::NAN))
136                }
137            }
138            Value::Bool(b) => EventValue::Bool(*b),
139            Value::Null => EventValue::Null,
140            Value::Array(arr) => EventValue::Array(arr.iter().map(EventValue::from).collect()),
141            Value::Object(map) => EventValue::Map(
142                map.iter()
143                    .map(|(k, v)| (Cow::Borrowed(k.as_str()), EventValue::from(v)))
144                    .collect(),
145            ),
146        }
147    }
148}
149
150// =============================================================================
151// Event trait
152// =============================================================================
153
154/// Generic interface for accessing event data during Sigma rule evaluation.
155///
156/// Implementations provide field lookup (with dot-notation), keyword search
157/// over all string values, and serialization to JSON for correlation storage.
158pub trait Event {
159    /// Look up a field by name. Supports dot-notation for nested access.
160    ///
161    /// Returns `None` if the field is absent.
162    /// Returns `Some(EventValue::Null)` if the field exists but is null.
163    fn get_field(&self, path: &str) -> Option<EventValue<'_>>;
164
165    /// Check if any string value anywhere in the event satisfies a predicate.
166    /// Used by keyword detection.
167    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool;
168
169    /// Collect all string values in the event.
170    fn all_string_values(&self) -> Vec<Cow<'_, str>>;
171
172    /// Materialize the event as a `serde_json::Value`.
173    fn to_json(&self) -> Value;
174}
175
176impl<T: Event + ?Sized> Event for &T {
177    fn get_field(&self, path: &str) -> Option<EventValue<'_>> {
178        (**self).get_field(path)
179    }
180
181    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool {
182        (**self).any_string_value(pred)
183    }
184
185    fn all_string_values(&self) -> Vec<Cow<'_, str>> {
186        (**self).all_string_values()
187    }
188
189    fn to_json(&self) -> Value {
190        (**self).to_json()
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use serde_json::json;
198
199    #[test]
200    fn event_value_as_str() {
201        assert_eq!(EventValue::Str(Cow::Borrowed("hi")).as_str().unwrap(), "hi");
202        assert_eq!(EventValue::Int(42).as_str().unwrap(), "42");
203        assert_eq!(EventValue::Float(2.71).as_str().unwrap(), "2.71");
204        assert_eq!(EventValue::Bool(true).as_str().unwrap(), "true");
205        assert!(EventValue::Null.as_str().is_none());
206    }
207
208    #[test]
209    fn event_value_as_f64() {
210        assert_eq!(EventValue::Float(2.71).as_f64(), Some(2.71));
211        assert_eq!(EventValue::Int(42).as_f64(), Some(42.0));
212        assert_eq!(EventValue::Str(Cow::Borrowed("1.5")).as_f64(), Some(1.5));
213        assert!(EventValue::Bool(true).as_f64().is_none());
214    }
215
216    #[test]
217    fn event_value_as_i64() {
218        assert_eq!(EventValue::Int(42).as_i64(), Some(42));
219        assert_eq!(EventValue::Float(42.0).as_i64(), Some(42));
220        assert_eq!(EventValue::Float(42.5).as_i64(), None);
221        assert_eq!(EventValue::Str(Cow::Borrowed("100")).as_i64(), Some(100));
222    }
223
224    #[test]
225    fn event_value_to_json() {
226        assert_eq!(EventValue::Str(Cow::Borrowed("hi")).to_json(), json!("hi"));
227        assert_eq!(EventValue::Int(42).to_json(), json!(42));
228        assert_eq!(EventValue::Bool(true).to_json(), json!(true));
229        assert_eq!(EventValue::Null.to_json(), Value::Null);
230    }
231}