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