Skip to main content

rsigma_eval/
event.rs

1//! Event wrapper with dot-notation field access.
2//!
3//! Provides a thin wrapper around `serde_json::Value` that supports nested
4//! field access via dot notation (e.g., `actor.id`) with flat-key precedence.
5
6use serde_json::Value;
7
8/// A reference to a JSON event for field access during evaluation.
9///
10/// Flat keys are checked first: `"actor.id"` as a single key takes precedence
11/// over `{"actor": {"id": ...}}` nested traversal.
12#[derive(Debug)]
13pub struct Event<'a> {
14    inner: &'a Value,
15}
16
17impl<'a> Event<'a> {
18    /// Wrap a JSON value as an event.
19    pub fn from_value(value: &'a Value) -> Self {
20        Event { inner: value }
21    }
22
23    /// Get a field value by name, supporting dot-notation for nested access.
24    ///
25    /// Checks for a flat key first (exact match), then falls back to
26    /// dot-separated traversal. When a path segment yields an array,
27    /// each element is tried and the first match is returned (OR semantics).
28    pub fn get_field(&self, path: &str) -> Option<&'a Value> {
29        // Flat key check first
30        if let Some(obj) = self.inner.as_object()
31            && let Some(v) = obj.get(path)
32        {
33            return Some(v);
34        }
35
36        // Dot-notation traversal
37        if path.contains('.') {
38            let parts: Vec<&str> = path.split('.').collect();
39            return traverse(self.inner, &parts);
40        }
41
42        None
43    }
44
45    /// Iterate over all string values in the event (for keyword detection).
46    ///
47    /// Recursively walks the entire event object and yields every string
48    /// value found, including inside nested objects and arrays. Traversal
49    /// is capped at 64 levels of nesting to prevent stack overflow.
50    pub fn all_string_values(&self) -> Vec<&'a str> {
51        let mut values = Vec::new();
52        collect_string_values(self.inner, &mut values, MAX_NESTING_DEPTH);
53        values
54    }
55
56    /// Check if any string value in the event satisfies a predicate.
57    ///
58    /// Short-circuits on the first match, avoiding the allocation of
59    /// collecting all string values into a `Vec`.
60    pub fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool {
61        any_string_value_rec(self.inner, pred, MAX_NESTING_DEPTH)
62    }
63
64    /// Access the underlying JSON value.
65    pub fn as_value(&self) -> &'a Value {
66        self.inner
67    }
68}
69
70/// Recursively traverse a JSON value following dot-notation path segments.
71///
72/// When a segment resolves to an array, each element is tried and the first
73/// match for the remaining path is returned.
74fn traverse<'a>(current: &'a Value, parts: &[&str]) -> Option<&'a Value> {
75    if parts.is_empty() {
76        return Some(current);
77    }
78
79    let (head, rest) = (parts[0], &parts[1..]);
80
81    match current {
82        Value::Object(map) => {
83            let next = map.get(head)?;
84            traverse(next, rest)
85        }
86        Value::Array(arr) => {
87            // Try each element — return first that resolves the remaining path
88            for item in arr {
89                if let Some(v) = traverse(item, parts) {
90                    return Some(v);
91                }
92            }
93            None
94        }
95        _ => None,
96    }
97}
98
99/// Maximum nesting depth for recursive JSON traversal.
100const MAX_NESTING_DEPTH: usize = 64;
101
102fn any_string_value_rec(v: &Value, pred: &dyn Fn(&str) -> bool, depth: usize) -> bool {
103    if depth == 0 {
104        return false;
105    }
106    match v {
107        Value::String(s) => pred(s.as_str()),
108        Value::Object(map) => map
109            .values()
110            .any(|val| any_string_value_rec(val, pred, depth - 1)),
111        Value::Array(arr) => arr
112            .iter()
113            .any(|val| any_string_value_rec(val, pred, depth - 1)),
114        _ => false,
115    }
116}
117
118fn collect_string_values<'a>(v: &'a Value, out: &mut Vec<&'a str>, depth: usize) {
119    if depth == 0 {
120        return;
121    }
122    match v {
123        Value::String(s) => out.push(s.as_str()),
124        Value::Object(map) => {
125            for val in map.values() {
126                collect_string_values(val, out, depth - 1);
127            }
128        }
129        Value::Array(arr) => {
130            for val in arr {
131                collect_string_values(val, out, depth - 1);
132            }
133        }
134        _ => {}
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use serde_json::json;
142
143    #[test]
144    fn test_flat_field() {
145        let v = json!({"CommandLine": "whoami", "User": "admin"});
146        let event = Event::from_value(&v);
147        assert_eq!(
148            event.get_field("CommandLine"),
149            Some(&Value::String("whoami".into()))
150        );
151    }
152
153    #[test]
154    fn test_nested_field() {
155        let v = json!({"actor": {"id": "user123", "type": "User"}});
156        let event = Event::from_value(&v);
157        assert_eq!(
158            event.get_field("actor.id"),
159            Some(&Value::String("user123".into()))
160        );
161    }
162
163    #[test]
164    fn test_flat_key_precedence() {
165        // Flat key "actor.id" takes precedence over nested {"actor":{"id":...}}
166        let v = json!({"actor.id": "flat_value", "actor": {"id": "nested_value"}});
167        let event = Event::from_value(&v);
168        assert_eq!(
169            event.get_field("actor.id"),
170            Some(&Value::String("flat_value".into()))
171        );
172    }
173
174    #[test]
175    fn test_missing_field() {
176        let v = json!({"foo": "bar"});
177        let event = Event::from_value(&v);
178        assert_eq!(event.get_field("missing"), None);
179    }
180
181    #[test]
182    fn test_array_traversal() {
183        // a.b is an array of objects; a.b.c should find the first match
184        let v = json!({"a": {"b": [{"c": "found"}, {"c": "other"}]}});
185        let event = Event::from_value(&v);
186        assert_eq!(
187            event.get_field("a.b.c"),
188            Some(&Value::String("found".into()))
189        );
190    }
191
192    #[test]
193    fn test_array_traversal_no_match() {
194        // Array elements don't have the requested key
195        let v = json!({"a": {"b": [{"x": 1}, {"y": 2}]}});
196        let event = Event::from_value(&v);
197        assert_eq!(event.get_field("a.b.c"), None);
198    }
199
200    #[test]
201    fn test_array_traversal_deep() {
202        // Two levels of arrays: events[].actors[].name
203        let v = json!({
204            "events": [
205                {"actors": [{"name": "alice"}, {"name": "bob"}]},
206                {"actors": [{"name": "charlie"}]}
207            ]
208        });
209        let event = Event::from_value(&v);
210        // Should return first match through the nested arrays
211        assert_eq!(
212            event.get_field("events.actors.name"),
213            Some(&Value::String("alice".into()))
214        );
215    }
216
217    #[test]
218    fn test_array_at_root_level() {
219        // Top-level field is an array of objects
220        let v = json!({"process": [{"command_line": "whoami"}, {"command_line": "id"}]});
221        let event = Event::from_value(&v);
222        assert_eq!(
223            event.get_field("process.command_line"),
224            Some(&Value::String("whoami".into()))
225        );
226    }
227
228    #[test]
229    fn test_array_returns_array_value() {
230        // Path resolves to an array (not traversing into it)
231        let v = json!({"a": {"tags": ["t1", "t2"]}});
232        let event = Event::from_value(&v);
233        assert_eq!(event.get_field("a.tags"), Some(&json!(["t1", "t2"])));
234    }
235
236    #[test]
237    fn test_flat_key_still_wins_over_array_traversal() {
238        // Flat key "a.b.c" takes precedence over nested array traversal
239        let v = json!({"a.b.c": "flat", "a": {"b": [{"c": "nested"}]}});
240        let event = Event::from_value(&v);
241        assert_eq!(
242            event.get_field("a.b.c"),
243            Some(&Value::String("flat".into()))
244        );
245    }
246
247    #[test]
248    fn test_all_string_values() {
249        let v = json!({
250            "a": "hello",
251            "b": 42,
252            "c": {"d": "world", "e": true},
253            "f": ["one", "two"]
254        });
255        let event = Event::from_value(&v);
256        let values = event.all_string_values();
257        assert!(values.contains(&"hello"));
258        assert!(values.contains(&"world"));
259        assert!(values.contains(&"one"));
260        assert!(values.contains(&"two"));
261        assert_eq!(values.len(), 4);
262    }
263}