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