Skip to main content

rsigma_eval/event/
mapped.rs

1//! A field-name-remapping [`Event`] view.
2//!
3//! Schema routing runs detection in a per-schema engine (the schema's pipeline
4//! is applied to the rules) but feeds every detection into one shared,
5//! Sigma-native correlation store. The correlation layer extracts group-by and
6//! value fields from the event by their Sigma-native names (for example
7//! `User`), but a routed event carries the schema's field names (for example
8//! ECS `user.name`). [`MappedEvent`] bridges that gap: it rewrites a configured
9//! set of field names on read so the shared correlation layer reads the right
10//! values regardless of the event's schema.
11//!
12//! Only [`Event::get_field`] is remapped. The keyword-search and serialization
13//! methods delegate to the inner event unchanged: detection already ran in the
14//! routed engine, so correlation only needs field lookups.
15
16use std::borrow::Cow;
17use std::collections::HashMap;
18
19use serde_json::Value;
20
21use super::{Event, EventValue};
22
23/// An [`Event`] that rewrites field names via a `Sigma -> [event field]` map
24/// before reading from the inner event.
25pub struct MappedEvent<'a, E: Event + ?Sized> {
26    inner: &'a E,
27    /// Logical (Sigma) field name -> one or more event field names to try in
28    /// order. A one-to-many pipeline mapping yields several candidates; the
29    /// first present value wins.
30    mapping: &'a HashMap<String, Vec<String>>,
31}
32
33impl<'a, E: Event + ?Sized> MappedEvent<'a, E> {
34    /// Wrap `inner`, remapping field names via `mapping`. An empty mapping
35    /// makes this a transparent pass-through.
36    pub fn new(inner: &'a E, mapping: &'a HashMap<String, Vec<String>>) -> Self {
37        Self { inner, mapping }
38    }
39}
40
41impl<E: Event + ?Sized> Event for MappedEvent<'_, E> {
42    fn get_field(&self, path: &str) -> Option<EventValue<'_>> {
43        if let Some(targets) = self.mapping.get(path) {
44            for target in targets {
45                if let Some(value) = self.inner.get_field(target) {
46                    return Some(value);
47                }
48            }
49            // Mapped but no target present: fall back to the original name so
50            // a field the pipeline did not rename still resolves.
51            return self.inner.get_field(path);
52        }
53        self.inner.get_field(path)
54    }
55
56    fn any_string_value(&self, pred: &dyn Fn(&str) -> bool) -> bool {
57        self.inner.any_string_value(pred)
58    }
59
60    fn all_string_values(&self) -> Vec<Cow<'_, str>> {
61        self.inner.all_string_values()
62    }
63
64    fn to_json(&self) -> Value {
65        self.inner.to_json()
66    }
67
68    fn field_keys(&self) -> Vec<Cow<'_, str>> {
69        self.inner.field_keys()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::event::JsonEvent;
77    use serde_json::json;
78
79    fn map(pairs: &[(&str, &[&str])]) -> HashMap<String, Vec<String>> {
80        pairs
81            .iter()
82            .map(|(k, vs)| {
83                (
84                    (*k).to_string(),
85                    vs.iter().map(|s| (*s).to_string()).collect(),
86                )
87            })
88            .collect()
89    }
90
91    #[test]
92    fn remaps_field_to_schema_name() {
93        let v = json!({"user": {"name": "alice"}});
94        let inner = JsonEvent::borrow(&v);
95        let m = map(&[("User", &["user.name"])]);
96        let mapped = MappedEvent::new(&inner, &m);
97        assert_eq!(
98            mapped
99                .get_field("User")
100                .and_then(|x| x.as_str().map(|s| s.to_string())),
101            Some("alice".to_string())
102        );
103    }
104
105    #[test]
106    fn falls_back_to_original_name_when_unmapped() {
107        let v = json!({"User": "bob"});
108        let inner = JsonEvent::borrow(&v);
109        // Sysmon-style event with no field rename: empty mapping passes through.
110        let m = HashMap::new();
111        let mapped = MappedEvent::new(&inner, &m);
112        assert_eq!(
113            mapped
114                .get_field("User")
115                .and_then(|x| x.as_str().map(|s| s.to_string())),
116            Some("bob".to_string())
117        );
118    }
119
120    #[test]
121    fn one_to_many_picks_first_present() {
122        let v = json!({"source": {"user": {"name": "carol"}}});
123        let inner = JsonEvent::borrow(&v);
124        let m = map(&[("User", &["user.name", "source.user.name"])]);
125        let mapped = MappedEvent::new(&inner, &m);
126        assert_eq!(
127            mapped
128                .get_field("User")
129                .and_then(|x| x.as_str().map(|s| s.to_string())),
130            Some("carol".to_string())
131        );
132    }
133}