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    /// Collect the names of every leaf field in the event, with nested
176    /// objects flattened to dot-separated paths (e.g. `actor.id`).
177    /// Intermediate object names are not emitted; only leaves count.
178    /// Arrays contribute their parent path once; per-index suffixes are
179    /// not emitted.
180    ///
181    /// Used by the daemon's opt-in field-observability surface; not on the
182    /// detection hot path. The default implementation walks `to_json()`,
183    /// which clones the event and allocates one `String` per leaf path;
184    /// concrete event types override to skip the `to_json()` clone. The
185    /// per-leaf `String` allocation is unavoidable for nested objects
186    /// (the dot-joined path doesn't exist anywhere in the source) but
187    /// flat formats like `KvEvent` can return `Cow::Borrowed`.
188    fn field_keys(&self) -> Vec<Cow<'_, str>> {
189        let mut paths: Vec<String> = Vec::new();
190        collect_field_keys_json(&self.to_json(), "", &mut paths);
191        paths.into_iter().map(Cow::Owned).collect()
192    }
193}
194
195/// Maximum nesting depth honoured by `field_keys` default + JsonEvent
196/// overrides. Matches the existing 64-level cap used elsewhere in the
197/// crate for recursive JSON traversal.
198pub(crate) const FIELD_KEYS_MAX_DEPTH: usize = 64;
199
200/// Walk a JSON value and push every leaf field path into `out`. The
201/// helper threads owned `String`s rather than `Cow` because every path
202/// is constructed by `format!`-joining (or copying the top-level key),
203/// so there are no borrowed shortcuts to capture.
204pub(crate) fn collect_field_keys_json(value: &Value, prefix: &str, out: &mut Vec<String>) {
205    collect_field_keys_json_depth(value, prefix, out, FIELD_KEYS_MAX_DEPTH);
206}
207
208fn collect_field_keys_json_depth(value: &Value, prefix: &str, out: &mut Vec<String>, depth: usize) {
209    if depth == 0 {
210        return;
211    }
212    if let Value::Object(map) = value {
213        for (k, v) in map {
214            let path = if prefix.is_empty() {
215                k.clone()
216            } else {
217                format!("{prefix}.{k}")
218            };
219            match v {
220                // Recurse into nested objects without emitting the
221                // intermediate path; only leaf descendants count.
222                Value::Object(_) => collect_field_keys_json_depth(v, &path, out, depth - 1),
223                _ => out.push(path),
224            }
225        }
226    }
227}
228
229impl<T: Event + ?Sized> Event for &T {
230    fn get_field(&self, path: &str) -> Option<EventValue<'_>> {
231        (**self).get_field(path)
232    }
233
234    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool {
235        (**self).any_string_value(pred)
236    }
237
238    fn all_string_values(&self) -> Vec<Cow<'_, str>> {
239        (**self).all_string_values()
240    }
241
242    fn to_json(&self) -> Value {
243        (**self).to_json()
244    }
245
246    fn field_keys(&self) -> Vec<Cow<'_, str>> {
247        (**self).field_keys()
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use serde_json::json;
255
256    #[test]
257    fn event_value_as_str() {
258        assert_eq!(EventValue::Str(Cow::Borrowed("hi")).as_str().unwrap(), "hi");
259        assert_eq!(EventValue::Int(42).as_str().unwrap(), "42");
260        assert_eq!(EventValue::Float(2.71).as_str().unwrap(), "2.71");
261        assert_eq!(EventValue::Bool(true).as_str().unwrap(), "true");
262        assert!(EventValue::Null.as_str().is_none());
263    }
264
265    #[test]
266    fn event_value_as_f64() {
267        assert_eq!(EventValue::Float(2.71).as_f64(), Some(2.71));
268        assert_eq!(EventValue::Int(42).as_f64(), Some(42.0));
269        assert_eq!(EventValue::Str(Cow::Borrowed("1.5")).as_f64(), Some(1.5));
270        assert!(EventValue::Bool(true).as_f64().is_none());
271    }
272
273    #[test]
274    fn event_value_as_i64() {
275        assert_eq!(EventValue::Int(42).as_i64(), Some(42));
276        assert_eq!(EventValue::Float(42.0).as_i64(), Some(42));
277        assert_eq!(EventValue::Float(42.5).as_i64(), None);
278        assert_eq!(EventValue::Str(Cow::Borrowed("100")).as_i64(), Some(100));
279    }
280
281    #[test]
282    fn event_value_to_json() {
283        assert_eq!(EventValue::Str(Cow::Borrowed("hi")).to_json(), json!("hi"));
284        assert_eq!(EventValue::Int(42).to_json(), json!(42));
285        assert_eq!(EventValue::Bool(true).to_json(), json!(true));
286        assert_eq!(EventValue::Null.to_json(), Value::Null);
287    }
288}