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 visit_string_values(&self, visit: &mut dyn FnMut(&str)) {
65        self.inner.visit_string_values(visit)
66    }
67
68    fn to_json(&self) -> Value {
69        self.inner.to_json()
70    }
71
72    fn field_keys(&self) -> Vec<Cow<'_, str>> {
73        self.inner.field_keys()
74    }
75
76    fn top_level_keys(&self) -> Option<Vec<Cow<'_, str>>> {
77        self.inner.top_level_keys()
78    }
79
80    fn visit_top_level_keys(&self, visit: &mut dyn FnMut(&str)) -> bool {
81        self.inner.visit_top_level_keys(visit)
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::event::JsonEvent;
89    use serde_json::json;
90
91    fn map(pairs: &[(&str, &[&str])]) -> HashMap<String, Vec<String>> {
92        pairs
93            .iter()
94            .map(|(k, vs)| {
95                (
96                    (*k).to_string(),
97                    vs.iter().map(|s| (*s).to_string()).collect(),
98                )
99            })
100            .collect()
101    }
102
103    #[test]
104    fn remaps_field_to_schema_name() {
105        let v = json!({"user": {"name": "alice"}});
106        let inner = JsonEvent::borrow(&v);
107        let m = map(&[("User", &["user.name"])]);
108        let mapped = MappedEvent::new(&inner, &m);
109        assert_eq!(
110            mapped
111                .get_field("User")
112                .and_then(|x| x.as_str().map(|s| s.to_string())),
113            Some("alice".to_string())
114        );
115    }
116
117    #[test]
118    fn falls_back_to_original_name_when_unmapped() {
119        let v = json!({"User": "bob"});
120        let inner = JsonEvent::borrow(&v);
121        // Sysmon-style event with no field rename: empty mapping passes through.
122        let m = HashMap::new();
123        let mapped = MappedEvent::new(&inner, &m);
124        assert_eq!(
125            mapped
126                .get_field("User")
127                .and_then(|x| x.as_str().map(|s| s.to_string())),
128            Some("bob".to_string())
129        );
130    }
131
132    #[test]
133    fn one_to_many_picks_first_present() {
134        let v = json!({"source": {"user": {"name": "carol"}}});
135        let inner = JsonEvent::borrow(&v);
136        let m = map(&[("User", &["user.name", "source.user.name"])]);
137        let mapped = MappedEvent::new(&inner, &m);
138        assert_eq!(
139            mapped
140                .get_field("User")
141                .and_then(|x| x.as_str().map(|s| s.to_string())),
142            Some("carol".to_string())
143        );
144    }
145}