Skip to main content

rsigma_eval/event/
json.rs

1use std::borrow::Cow;
2
3use rsigma_parser::fieldpath::{first_unescaped, unescape_brackets};
4use serde_json::Value;
5
6use super::{Event, EventValue};
7
8/// Maximum nesting depth for recursive JSON traversal.
9const MAX_NESTING_DEPTH: usize = 64;
10
11/// Zero-copy event backed by `serde_json::Value`.
12///
13/// Supports both borrowed (`&Value`) and owned (`Value`) backing via `Cow`.
14/// This is the primary implementation for JSON/NDJSON input.
15///
16/// Flat keys are checked first: `"actor.id"` as a single key takes precedence
17/// over `{"actor": {"id": ...}}` nested traversal.
18#[derive(Debug)]
19pub struct JsonEvent<'a> {
20    inner: Cow<'a, Value>,
21}
22
23impl<'a> JsonEvent<'a> {
24    /// Wrap a borrowed JSON value as an event.
25    pub fn borrow(v: &'a Value) -> Self {
26        Self {
27            inner: Cow::Borrowed(v),
28        }
29    }
30
31    /// Wrap an owned JSON value as an event.
32    pub fn owned(v: Value) -> Self {
33        Self {
34            inner: Cow::Owned(v),
35        }
36    }
37}
38
39impl<'a> From<&'a Value> for JsonEvent<'a> {
40    fn from(v: &'a Value) -> Self {
41        Self::borrow(v)
42    }
43}
44
45impl From<Value> for JsonEvent<'static> {
46    fn from(v: Value) -> Self {
47        Self::owned(v)
48    }
49}
50
51impl<'a> Event for JsonEvent<'a> {
52    /// Get a field value by name, supporting dot-notation for nested access.
53    ///
54    /// Checks for a flat key first (exact match), then falls back to
55    /// dot-separated traversal. When a path segment crosses an array, every
56    /// element is followed and all terminal values are collected: a single
57    /// hit is returned as-is, multiple hits are returned as an
58    /// [`EventValue::Array`] so the matcher applies any-member semantics
59    /// (rather than only testing the first element).
60    fn get_field(&self, path: &str) -> Option<EventValue<'_>> {
61        let value: &Value = &self.inner;
62
63        if let Some(obj) = value.as_object() {
64            if let Some(v) = obj.get(path) {
65                return Some(EventValue::from(v));
66            }
67
68            if path.contains('.') || path.contains('[') || path.contains('\\') {
69                // Most candidate-index probes miss. If the first path segment
70                // is absent at the root, skip path parsing and traversal.
71                if let Some(root) = path_root_key(path)
72                    && !obj.contains_key(root.as_ref())
73                {
74                    return None;
75                }
76                let ops = parse_path_ops(path);
77                let mut collected: Vec<EventValue<'_>> = Vec::new();
78                collect_by_ops(value, &ops, &mut collected);
79                return match collected.len() {
80                    0 => None,
81                    1 => collected.pop(),
82                    _ => Some(EventValue::Array(collected)),
83                };
84            }
85
86            return None;
87        }
88
89        if path.contains('.') || path.contains('[') || path.contains('\\') {
90            let ops = parse_path_ops(path);
91            let mut collected: Vec<EventValue<'_>> = Vec::new();
92            collect_by_ops(value, &ops, &mut collected);
93            return match collected.len() {
94                0 => None,
95                1 => collected.pop(),
96                _ => Some(EventValue::Array(collected)),
97            };
98        }
99
100        None
101    }
102
103    fn top_level_keys(&self) -> Option<Vec<Cow<'_, str>>> {
104        match self.inner.as_ref() {
105            Value::Object(map) => Some(map.keys().map(|k| Cow::Borrowed(k.as_str())).collect()),
106            // Non-object roots (arrays, scalars) cannot cheaply describe the
107            // keys the path walker may touch, so callers fall back.
108            _ => None,
109        }
110    }
111
112    fn visit_top_level_keys(&self, visit: &mut dyn FnMut(&str)) -> bool {
113        match self.inner.as_ref() {
114            Value::Object(map) => {
115                for key in map.keys() {
116                    visit(key.as_str());
117                }
118                true
119            }
120            _ => false,
121        }
122    }
123
124    /// Check if any string value in the event satisfies a predicate.
125    ///
126    /// Short-circuits on the first match, avoiding the allocation of
127    /// collecting all string values into a `Vec`.
128    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool {
129        any_string_value_json(&self.inner, pred, MAX_NESTING_DEPTH)
130    }
131
132    /// Iterate over all string values in the event (for keyword detection).
133    ///
134    /// Recursively walks the entire event object and yields every string
135    /// value found, including inside nested objects and arrays. Numbers are
136    /// yielded in their JSON text form, since keywords match them too. Traversal
137    /// is capped at 64 levels of nesting to prevent stack overflow.
138    fn all_string_values(&self) -> Vec<Cow<'_, str>> {
139        let mut values = Vec::new();
140        collect_string_values_json(&self.inner, &mut values, MAX_NESTING_DEPTH);
141        values
142    }
143
144    fn visit_string_values(&self, visit: &mut dyn FnMut(&str)) {
145        visit_string_values_json(&self.inner, visit, MAX_NESTING_DEPTH);
146    }
147
148    fn to_json(&self) -> Value {
149        self.inner.as_ref().clone()
150    }
151
152    /// Walk every leaf field in the event and yield dot-joined paths.
153    /// Intermediate object names (`actor` for `{"actor":{"id":"x"}}`)
154    /// are NOT emitted; only the leaves (`actor.id`) appear. This
155    /// matches typical Sigma rules, which reference nested values via
156    /// dot-notation; emitting the intermediate name would falsely flag
157    /// every parent object as "unknown" in the gap signal even when
158    /// the rule references a child path. Top-level scalar fields
159    /// (`{"actor":"alice"}`) emit `actor` because they ARE leaves.
160    /// Arrays contribute their parent path once; per-index suffixes
161    /// are not emitted.
162    fn field_keys(&self) -> Vec<Cow<'_, str>> {
163        let mut out = Vec::new();
164        collect_field_keys(&self.inner, "", &mut out, MAX_NESTING_DEPTH);
165        out
166    }
167}
168
169/// First object-key segment of a field path, bracket-unescaped.
170///
171/// `actor.id` → `actor`, `name[0].x` → `name`, `CommandLine` → `CommandLine`.
172/// Leading dots or a bare `[index]` yield `None` (no root object key).
173fn path_root_key(path: &str) -> Option<Cow<'_, str>> {
174    let segment = match first_unescaped(path, b'.') {
175        Some(0) => return None,
176        Some(pos) => &path[..pos],
177        None => path,
178    };
179    if segment.is_empty() {
180        return None;
181    }
182    let name = match first_unescaped(segment, b'[') {
183        Some(0) => return None,
184        Some(pos) => &segment[..pos],
185        None => segment,
186    };
187    if name.is_empty() {
188        return None;
189    }
190    Some(unescape_brackets(name))
191}
192
193/// A single field-path navigation step.
194enum PathOp<'a> {
195    /// Object key lookup (bracket-unescaped). Distributes over arrays (implicit
196    /// any-member).
197    Key(Cow<'a, str>),
198    /// Positional array index, possibly negative. Selects one element; never
199    /// fans out.
200    Index(i64),
201}
202
203/// Resolve a positional index against an array length. Negative indices count
204/// from the end (`-1` is the last element); out-of-range yields `None`.
205pub(crate) fn resolve_array_index(index: i64, len: usize) -> Option<usize> {
206    if index >= 0 {
207        usize::try_from(index).ok().filter(|&i| i < len)
208    } else {
209        usize::try_from(index.unsigned_abs())
210            .ok()
211            .and_then(|abs| len.checked_sub(abs))
212    }
213}
214
215/// Parse a dot path into navigation ops, recognizing positional `name[N]`
216/// (and chained `name[N][M]`, with negative indices counting from the end). A
217/// bracket group that is not an integer degrades to a literal object key so it
218/// simply fails to match.
219fn parse_path_ops(path: &str) -> Vec<PathOp<'_>> {
220    let mut ops = Vec::new();
221    for part in path.split('.') {
222        match first_unescaped(part, b'[') {
223            Some(bpos) if parse_index_groups(&part[bpos..]).is_some() => {
224                let name = &part[..bpos];
225                if !name.is_empty() {
226                    ops.push(PathOp::Key(unescape_brackets(name)));
227                }
228                for idx in parse_index_groups(&part[bpos..]).expect("checked") {
229                    ops.push(PathOp::Index(idx));
230                }
231            }
232            // No unescaped index group: the whole segment is a literal key,
233            // with `\[` / `\]` unescaped to match the event's actual key.
234            _ => ops.push(PathOp::Key(unescape_brackets(part))),
235        }
236    }
237    ops
238}
239
240/// Parse `[N]` or `[N][M]...` into the contained indices (negative allowed), or
241/// `None` if any group is malformed or non-numeric.
242fn parse_index_groups(s: &str) -> Option<Vec<i64>> {
243    let mut out = Vec::new();
244    let mut rem = s;
245    while !rem.is_empty() {
246        let rest = rem.strip_prefix('[')?;
247        let close = rest.find(']')?;
248        let idx: i64 = rest[..close].parse().ok()?;
249        out.push(idx);
250        rem = &rest[close + 1..];
251    }
252    Some(out)
253}
254
255/// Follow navigation ops, collecting every terminal value into `out`.
256///
257/// A `Key` op distributes over arrays (implicit any-member): the remaining ops
258/// are applied to every element, so a path crossing an array of objects yields
259/// one value per element. An `Index` op selects a single element and never
260/// fans out, giving deterministic positional access.
261fn collect_by_ops<'a>(current: &'a Value, ops: &[PathOp<'_>], out: &mut Vec<EventValue<'a>>) {
262    let Some((op, rest)) = ops.split_first() else {
263        out.push(EventValue::from(current));
264        return;
265    };
266
267    match op {
268        PathOp::Key(key) => match current {
269            Value::Object(map) => {
270                if let Some(next) = map.get(key.as_ref()) {
271                    collect_by_ops(next, rest, out);
272                }
273            }
274            Value::Array(arr) => {
275                for item in arr {
276                    collect_by_ops(item, ops, out);
277                }
278            }
279            _ => {}
280        },
281        PathOp::Index(i) => {
282            if let Value::Array(arr) = current
283                && let Some(idx) = resolve_array_index(*i, arr.len())
284                && let Some(next) = arr.get(idx)
285            {
286                collect_by_ops(next, rest, out);
287            }
288        }
289    }
290}
291
292fn any_string_value_json(v: &Value, pred: &dyn Fn(&str) -> bool, depth: usize) -> bool {
293    if depth == 0 {
294        return false;
295    }
296    match v {
297        Value::String(s) => pred(s.as_str()),
298        Value::Number(n) => pred(&n.to_string()),
299        Value::Object(map) => map
300            .values()
301            .any(|val| any_string_value_json(val, pred, depth - 1)),
302        Value::Array(arr) => arr
303            .iter()
304            .any(|val| any_string_value_json(val, pred, depth - 1)),
305        _ => false,
306    }
307}
308
309fn collect_field_keys<'a>(v: &'a Value, prefix: &str, out: &mut Vec<Cow<'a, str>>, depth: usize) {
310    if depth == 0 {
311        return;
312    }
313    if let Value::Object(map) = v {
314        for (k, child) in map {
315            let path = if prefix.is_empty() {
316                k.clone()
317            } else {
318                format!("{prefix}.{k}")
319            };
320            match child {
321                // Recurse into nested objects but do NOT emit the
322                // intermediate path; only the leaf descendants count.
323                // Sigma rules normally reference leaves via
324                // dot-notation, so emitting `actor` alongside
325                // `actor.id` would falsely flag the parent as
326                // "unknown" in the gap signal.
327                Value::Object(_) => collect_field_keys(child, &path, out, depth - 1),
328                _ => out.push(Cow::Owned(path)),
329            }
330        }
331    }
332}
333
334fn collect_string_values_json<'a>(v: &'a Value, out: &mut Vec<Cow<'a, str>>, depth: usize) {
335    if depth == 0 {
336        return;
337    }
338    match v {
339        Value::String(s) => out.push(Cow::Borrowed(s.as_str())),
340        Value::Number(n) => out.push(Cow::Owned(n.to_string())),
341        Value::Object(map) => {
342            for val in map.values() {
343                collect_string_values_json(val, out, depth - 1);
344            }
345        }
346        Value::Array(arr) => {
347            for val in arr {
348                collect_string_values_json(val, out, depth - 1);
349            }
350        }
351        _ => {}
352    }
353}
354
355fn visit_string_values_json(v: &Value, visit: &mut dyn FnMut(&str), depth: usize) {
356    if depth == 0 {
357        return;
358    }
359    match v {
360        Value::String(s) => visit(s.as_str()),
361        Value::Number(n) => visit(&n.to_string()),
362        Value::Object(map) => {
363            for val in map.values() {
364                visit_string_values_json(val, visit, depth - 1);
365            }
366        }
367        Value::Array(arr) => {
368            for val in arr {
369                visit_string_values_json(val, visit, depth - 1);
370            }
371        }
372        _ => {}
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use serde_json::json;
380
381    #[test]
382    fn json_flat_field() {
383        let v = json!({"CommandLine": "whoami", "User": "admin"});
384        let event = JsonEvent::borrow(&v);
385        assert_eq!(
386            event.get_field("CommandLine"),
387            Some(EventValue::Str(Cow::Borrowed("whoami")))
388        );
389    }
390
391    #[test]
392    fn json_nested_field() {
393        let v = json!({"actor": {"id": "user123", "type": "User"}});
394        let event = JsonEvent::borrow(&v);
395        assert_eq!(
396            event.get_field("actor.id"),
397            Some(EventValue::Str(Cow::Borrowed("user123")))
398        );
399    }
400
401    #[test]
402    fn json_flat_key_precedence() {
403        let v = json!({"actor.id": "flat_value", "actor": {"id": "nested_value"}});
404        let event = JsonEvent::borrow(&v);
405        assert_eq!(
406            event.get_field("actor.id"),
407            Some(EventValue::Str(Cow::Borrowed("flat_value")))
408        );
409    }
410
411    #[test]
412    fn json_missing_field() {
413        let v = json!({"foo": "bar"});
414        let event = JsonEvent::borrow(&v);
415        assert_eq!(event.get_field("missing"), None);
416    }
417
418    #[test]
419    fn json_dotted_miss_skips_absent_root() {
420        let v = json!({"message": "x", "product": "windows"});
421        let event = JsonEvent::borrow(&v);
422        assert_eq!(event.get_field("Event.EventData.CommandLine"), None);
423        assert_eq!(event.get_field("actor.id"), None);
424        assert_eq!(event.get_field("name[0].x"), None);
425    }
426
427    #[test]
428    fn json_top_level_keys_object() {
429        let v = json!({"message": "x", "product": "windows"});
430        let event = JsonEvent::borrow(&v);
431        let mut keys: Vec<String> = event
432            .top_level_keys()
433            .unwrap()
434            .into_iter()
435            .map(|k| k.into_owned())
436            .collect();
437        keys.sort();
438        assert_eq!(keys, vec!["message", "product"]);
439    }
440
441    #[test]
442    fn json_top_level_keys_non_object_unknown() {
443        let v = json!([{"a": 1}]);
444        let event = JsonEvent::borrow(&v);
445        assert_eq!(event.top_level_keys(), None);
446    }
447
448    #[test]
449    fn path_root_key_segments() {
450        assert_eq!(path_root_key("CommandLine").as_deref(), Some("CommandLine"));
451        assert_eq!(path_root_key("actor.id").as_deref(), Some("actor"));
452        assert_eq!(path_root_key("name[0].x").as_deref(), Some("name"));
453        assert_eq!(path_root_key(".id"), None);
454        assert_eq!(path_root_key("[0].x"), None);
455    }
456
457    #[test]
458    fn json_null_field() {
459        let v = json!({"foo": null});
460        let event = JsonEvent::borrow(&v);
461        assert_eq!(event.get_field("foo"), Some(EventValue::Null));
462    }
463
464    #[test]
465    fn json_array_traversal() {
466        // A path crossing an array of objects now collects every element's
467        // leaf value (any-member semantics), not just the first.
468        let v = json!({"a": {"b": [{"c": "found"}, {"c": "other"}]}});
469        let event = JsonEvent::borrow(&v);
470        assert_eq!(
471            event.get_field("a.b.c"),
472            Some(EventValue::Array(vec![
473                EventValue::Str(Cow::Borrowed("found")),
474                EventValue::Str(Cow::Borrowed("other")),
475            ]))
476        );
477    }
478
479    #[test]
480    fn json_array_traversal_no_match() {
481        let v = json!({"a": {"b": [{"x": 1}, {"y": 2}]}});
482        let event = JsonEvent::borrow(&v);
483        assert_eq!(event.get_field("a.b.c"), None);
484    }
485
486    #[test]
487    fn json_array_traversal_deep() {
488        let v = json!({
489            "events": [
490                {"actors": [{"name": "alice"}, {"name": "bob"}]},
491                {"actors": [{"name": "charlie"}]}
492            ]
493        });
494        let event = JsonEvent::borrow(&v);
495        // Nested arrays flatten to every leaf value.
496        assert_eq!(
497            event.get_field("events.actors.name"),
498            Some(EventValue::Array(vec![
499                EventValue::Str(Cow::Borrowed("alice")),
500                EventValue::Str(Cow::Borrowed("bob")),
501                EventValue::Str(Cow::Borrowed("charlie")),
502            ]))
503        );
504    }
505
506    #[test]
507    fn json_array_at_root_level() {
508        let v = json!({"process": [{"command_line": "whoami"}, {"command_line": "id"}]});
509        let event = JsonEvent::borrow(&v);
510        assert_eq!(
511            event.get_field("process.command_line"),
512            Some(EventValue::Array(vec![
513                EventValue::Str(Cow::Borrowed("whoami")),
514                EventValue::Str(Cow::Borrowed("id")),
515            ]))
516        );
517    }
518
519    #[test]
520    fn json_array_returns_array_value() {
521        let v = json!({"a": {"tags": ["t1", "t2"]}});
522        let event = JsonEvent::borrow(&v);
523        let result = event.get_field("a.tags");
524        assert!(matches!(result, Some(EventValue::Array(_))));
525    }
526
527    #[test]
528    fn json_flat_key_wins_over_array_traversal() {
529        let v = json!({"a.b.c": "flat", "a": {"b": [{"c": "nested"}]}});
530        let event = JsonEvent::borrow(&v);
531        assert_eq!(
532            event.get_field("a.b.c"),
533            Some(EventValue::Str(Cow::Borrowed("flat")))
534        );
535    }
536
537    #[test]
538    fn json_all_string_values() {
539        let v = json!({
540            "a": "hello",
541            "b": 42,
542            "c": {"d": "world", "e": true},
543            "f": ["one", "two"]
544        });
545        let event = JsonEvent::borrow(&v);
546        let values = event.all_string_values();
547        let strs: Vec<&str> = values.iter().map(|c| c.as_ref()).collect();
548        assert!(strs.contains(&"hello"));
549        assert!(strs.contains(&"world"));
550        assert!(strs.contains(&"one"));
551        assert!(strs.contains(&"two"));
552        assert!(strs.contains(&"42"));
553        assert_eq!(values.len(), 5);
554    }
555
556    #[test]
557    fn json_to_json_roundtrip() {
558        let v = json!({"a": 1, "b": "hello", "c": [1, 2]});
559        let event = JsonEvent::borrow(&v);
560        assert_eq!(event.to_json(), v);
561    }
562
563    #[test]
564    fn json_owned_works() {
565        let v = json!({"key": "value"});
566        let event = JsonEvent::owned(v.clone());
567        assert_eq!(
568            event.get_field("key"),
569            Some(EventValue::Str(Cow::Borrowed("value")))
570        );
571        assert_eq!(event.to_json(), v);
572    }
573
574    #[test]
575    fn json_field_keys_flat() {
576        let v = json!({"CommandLine": "x", "User": "y"});
577        let event = JsonEvent::borrow(&v);
578        let mut keys: Vec<String> = event.field_keys().iter().map(|c| c.to_string()).collect();
579        keys.sort();
580        assert_eq!(keys, vec!["CommandLine", "User"]);
581    }
582
583    #[test]
584    fn json_field_keys_nested_leaves_only() {
585        // Intermediate object names like `actor` are NOT emitted; only
586        // leaves (`actor.id`, `actor.type`) and top-level scalars
587        // (`verb`) appear.
588        let v = json!({"actor": {"id": "u1", "type": "User"}, "verb": "login"});
589        let event = JsonEvent::borrow(&v);
590        let mut keys: Vec<String> = event.field_keys().iter().map(|c| c.to_string()).collect();
591        keys.sort();
592        assert_eq!(keys, vec!["actor.id", "actor.type", "verb"]);
593    }
594
595    #[test]
596    fn json_field_keys_deeply_nested_leaves_only() {
597        let v = json!({"a": {"b": {"c": 1}}, "flat": "x"});
598        let event = JsonEvent::borrow(&v);
599        let mut keys: Vec<String> = event.field_keys().iter().map(|c| c.to_string()).collect();
600        keys.sort();
601        assert_eq!(keys, vec!["a.b.c", "flat"]);
602    }
603
604    #[test]
605    fn json_field_keys_array_parent_only() {
606        let v = json!({"events": [{"id": 1}, {"id": 2}]});
607        let event = JsonEvent::borrow(&v);
608        let keys: Vec<String> = event.field_keys().iter().map(|c| c.to_string()).collect();
609        // Arrays contribute their parent key only; array indices are not enumerated.
610        assert_eq!(keys, vec!["events"]);
611    }
612
613    #[test]
614    fn json_field_keys_top_level_non_object_empty() {
615        let v = json!("just a string");
616        let event = JsonEvent::owned(v);
617        assert!(event.field_keys().is_empty());
618    }
619
620    #[test]
621    fn json_traversal_with_consecutive_dots_does_not_panic() {
622        // Pathological input -- a path like `a..b` used to be tokenised
623        // by `split('.')` into `["a", "", "b"]` and then walked head-by-
624        // head; the new `split_once('.')` recursion produces the same
625        // `("a", ".b")` -> `("", "b")` -> ... sequence with no
626        // allocation. Verify the lookup falls back to `None` rather
627        // than panicking or accidentally matching.
628        let v = json!({"a": {"b": "x"}});
629        let event = JsonEvent::borrow(&v);
630        assert_eq!(event.get_field("a..b"), None);
631    }
632
633    #[test]
634    fn json_traversal_with_trailing_dot_does_not_panic() {
635        // A trailing dot used to leave an empty trailing segment in the
636        // `Vec<&str>` path which the object branch tried to look up
637        // against the map; the iterator-based walker preserves that
638        // miss without allocating.
639        let v = json!({"a": {"b": "x"}});
640        let event = JsonEvent::borrow(&v);
641        assert_eq!(event.get_field("a.b."), None);
642    }
643}