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(crate) use json::resolve_array_index;
17pub use kv::KvEvent;
18pub use map::MapEvent;
19pub use plain::PlainEvent;
20
21use std::borrow::Cow;
22
23use serde_json::Value;
24
25// =============================================================================
26// EventValue
27// =============================================================================
28
29/// A value retrieved from an event field.
30///
31/// Supports zero-copy borrows from JSON-backed events (`Cow::Borrowed`)
32/// and owned values from non-JSON sources (`Cow::Owned`).
33/// Null is distinct from field-absent (`get_field` returns `None`).
34#[derive(Debug, Clone, PartialEq)]
35pub enum EventValue<'a> {
36    Str(Cow<'a, str>),
37    Int(i64),
38    Float(f64),
39    Bool(bool),
40    Null,
41    Array(Vec<EventValue<'a>>),
42    Map(Vec<(Cow<'a, str>, EventValue<'a>)>),
43}
44
45impl<'a> EventValue<'a> {
46    /// Coerce to string. Str as-is, Int/Float decimal, Bool "true"/"false".
47    #[inline]
48    pub fn as_str(&self) -> Option<Cow<'_, str>> {
49        match self {
50            EventValue::Str(s) => Some(Cow::Borrowed(s)),
51            EventValue::Int(n) => Some(Cow::Owned(n.to_string())),
52            EventValue::Float(f) => Some(Cow::Owned(f.to_string())),
53            EventValue::Bool(b) => Some(Cow::Borrowed(if *b { "true" } else { "false" })),
54            _ => None,
55        }
56    }
57
58    /// Coerce to f64. Int lossless, Float as-is, Str parsed.
59    #[inline]
60    pub fn as_f64(&self) -> Option<f64> {
61        match self {
62            EventValue::Float(f) => Some(*f),
63            EventValue::Int(n) => Some(*n as f64),
64            EventValue::Str(s) => s.parse().ok(),
65            _ => None,
66        }
67    }
68
69    /// Coerce to i64. Int as-is, Float truncated if exact, Str parsed.
70    #[inline]
71    pub fn as_i64(&self) -> Option<i64> {
72        match self {
73            EventValue::Int(n) => Some(*n),
74            EventValue::Float(f) => {
75                let truncated = *f as i64;
76                if (truncated as f64 - f).abs() < f64::EPSILON {
77                    Some(truncated)
78                } else {
79                    None
80                }
81            }
82            EventValue::Str(s) => s.parse().ok(),
83            _ => None,
84        }
85    }
86
87    /// Coerce to bool. Bool as-is, Str: true/false/1/0/yes/no.
88    #[inline]
89    pub fn as_bool(&self) -> Option<bool> {
90        match self {
91            EventValue::Bool(b) => Some(*b),
92            EventValue::Str(s) => match s.to_lowercase().as_str() {
93                "true" | "1" | "yes" => Some(true),
94                "false" | "0" | "no" => Some(false),
95                _ => None,
96            },
97            _ => None,
98        }
99    }
100
101    #[inline]
102    pub fn is_null(&self) -> bool {
103        matches!(self, EventValue::Null)
104    }
105
106    /// Convert to `serde_json::Value`.
107    pub fn to_json(&self) -> Value {
108        match self {
109            EventValue::Str(s) => Value::String(s.to_string()),
110            EventValue::Int(n) => Value::Number((*n).into()),
111            EventValue::Float(f) => {
112                serde_json::Number::from_f64(*f).map_or(Value::Null, Value::Number)
113            }
114            EventValue::Bool(b) => Value::Bool(*b),
115            EventValue::Null => Value::Null,
116            EventValue::Array(arr) => Value::Array(arr.iter().map(|v| v.to_json()).collect()),
117            EventValue::Map(entries) => {
118                let map = entries
119                    .iter()
120                    .map(|(k, v)| (k.to_string(), v.to_json()))
121                    .collect();
122                Value::Object(map)
123            }
124        }
125    }
126}
127
128impl<'a> From<&'a Value> for EventValue<'a> {
129    fn from(v: &'a Value) -> Self {
130        match v {
131            Value::String(s) => EventValue::Str(Cow::Borrowed(s.as_str())),
132            Value::Number(n) => {
133                if let Some(i) = n.as_i64() {
134                    EventValue::Int(i)
135                } else {
136                    EventValue::Float(n.as_f64().unwrap_or(f64::NAN))
137                }
138            }
139            Value::Bool(b) => EventValue::Bool(*b),
140            Value::Null => EventValue::Null,
141            Value::Array(arr) => EventValue::Array(arr.iter().map(EventValue::from).collect()),
142            Value::Object(map) => EventValue::Map(
143                map.iter()
144                    .map(|(k, v)| (Cow::Borrowed(k.as_str()), EventValue::from(v)))
145                    .collect(),
146            ),
147        }
148    }
149}
150
151// =============================================================================
152// Event trait
153// =============================================================================
154
155/// Generic interface for accessing event data during Sigma rule evaluation.
156///
157/// Implementations provide field lookup (with dot-notation), keyword search
158/// over all string values, and serialization to JSON for correlation storage.
159pub trait Event {
160    /// Look up a field by name. Supports dot-notation for nested access.
161    ///
162    /// Returns `None` if the field is absent.
163    /// Returns `Some(EventValue::Null)` if the field exists but is null.
164    fn get_field(&self, path: &str) -> Option<EventValue<'_>>;
165
166    /// Check if any string value anywhere in the event satisfies a predicate.
167    /// Used by keyword detection.
168    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool;
169
170    /// Collect all string values in the event.
171    fn all_string_values(&self) -> Vec<Cow<'_, str>>;
172
173    /// Materialize the event as a `serde_json::Value`.
174    fn to_json(&self) -> Value;
175
176    /// Collect the names of every leaf field in the event, with nested
177    /// objects flattened to dot-separated paths (e.g. `actor.id`).
178    /// Intermediate object names are not emitted; only leaves count.
179    /// Arrays contribute their parent path once; per-index suffixes are
180    /// not emitted.
181    ///
182    /// Used by the daemon's opt-in field-observability surface; not on the
183    /// detection hot path. The default implementation walks `to_json()`,
184    /// which clones the event and allocates one `String` per leaf path;
185    /// concrete event types override to skip the `to_json()` clone. The
186    /// per-leaf `String` allocation is unavoidable for nested objects
187    /// (the dot-joined path doesn't exist anywhere in the source) but
188    /// flat formats like `KvEvent` can return `Cow::Borrowed`.
189    fn field_keys(&self) -> Vec<Cow<'_, str>> {
190        let mut paths: Vec<String> = Vec::new();
191        collect_field_keys_json(&self.to_json(), "", &mut paths);
192        paths.into_iter().map(Cow::Owned).collect()
193    }
194}
195
196/// Maximum nesting depth honoured by `field_keys` default + JsonEvent
197/// overrides. Matches the existing 64-level cap used elsewhere in the
198/// crate for recursive JSON traversal.
199pub(crate) const FIELD_KEYS_MAX_DEPTH: usize = 64;
200
201/// Walk a JSON value and push every leaf field path into `out`. The
202/// helper threads owned `String`s rather than `Cow` because every path
203/// is constructed by `format!`-joining (or copying the top-level key),
204/// so there are no borrowed shortcuts to capture.
205pub(crate) fn collect_field_keys_json(value: &Value, prefix: &str, out: &mut Vec<String>) {
206    collect_field_keys_json_depth(value, prefix, out, FIELD_KEYS_MAX_DEPTH);
207}
208
209fn collect_field_keys_json_depth(value: &Value, prefix: &str, out: &mut Vec<String>, depth: usize) {
210    if depth == 0 {
211        return;
212    }
213    if let Value::Object(map) = value {
214        for (k, v) in map {
215            let path = if prefix.is_empty() {
216                k.clone()
217            } else {
218                format!("{prefix}.{k}")
219            };
220            match v {
221                // Recurse into nested objects without emitting the
222                // intermediate path; only leaf descendants count.
223                Value::Object(_) => collect_field_keys_json_depth(v, &path, out, depth - 1),
224                _ => out.push(path),
225            }
226        }
227    }
228}
229
230impl<T: Event + ?Sized> Event for &T {
231    fn get_field(&self, path: &str) -> Option<EventValue<'_>> {
232        (**self).get_field(path)
233    }
234
235    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool {
236        (**self).any_string_value(pred)
237    }
238
239    fn all_string_values(&self) -> Vec<Cow<'_, str>> {
240        (**self).all_string_values()
241    }
242
243    fn to_json(&self) -> Value {
244        (**self).to_json()
245    }
246
247    fn field_keys(&self) -> Vec<Cow<'_, str>> {
248        (**self).field_keys()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use serde_json::json;
256
257    #[test]
258    fn event_value_as_str() {
259        assert_eq!(EventValue::Str(Cow::Borrowed("hi")).as_str().unwrap(), "hi");
260        assert_eq!(EventValue::Int(42).as_str().unwrap(), "42");
261        assert_eq!(EventValue::Float(2.71).as_str().unwrap(), "2.71");
262        assert_eq!(EventValue::Bool(true).as_str().unwrap(), "true");
263        assert!(EventValue::Null.as_str().is_none());
264    }
265
266    #[test]
267    fn event_value_as_f64() {
268        assert_eq!(EventValue::Float(2.71).as_f64(), Some(2.71));
269        assert_eq!(EventValue::Int(42).as_f64(), Some(42.0));
270        assert_eq!(EventValue::Str(Cow::Borrowed("1.5")).as_f64(), Some(1.5));
271        assert!(EventValue::Bool(true).as_f64().is_none());
272    }
273
274    #[test]
275    fn event_value_as_i64() {
276        assert_eq!(EventValue::Int(42).as_i64(), Some(42));
277        assert_eq!(EventValue::Float(42.0).as_i64(), Some(42));
278        assert_eq!(EventValue::Float(42.5).as_i64(), None);
279        assert_eq!(EventValue::Str(Cow::Borrowed("100")).as_i64(), Some(100));
280    }
281
282    #[test]
283    fn event_value_to_json() {
284        assert_eq!(EventValue::Str(Cow::Borrowed("hi")).to_json(), json!("hi"));
285        assert_eq!(EventValue::Int(42).to_json(), json!(42));
286        assert_eq!(EventValue::Bool(true).to_json(), json!(true));
287        assert_eq!(EventValue::Null.to_json(), Value::Null);
288    }
289}