Skip to main content

rsigma_eval/
logsource.rs

1//! Event logsource extraction for opt-in, conflict-based logsource pruning.
2//!
3//! A [`LogSourceExtractor`] derives a [`LogSource`] from an event by reading
4//! configurable field names (defaulting to the literals `product`, `service`,
5//! and `category`), falling back to optional static defaults. The result feeds
6//! the engine's conflict-based pruning: an event tagged `product: windows`
7//! skips `product: linux` rules without dropping Windows-category or
8//! logsource-less rules.
9//!
10//! Extraction is fail-open per dimension: a field that is absent, null, or
11//! blank leaves that dimension unset (after the static default is consulted),
12//! so a missing tag never prunes anything.
13
14use rsigma_parser::LogSource;
15
16use crate::event::Event;
17
18/// Derives an event [`LogSource`] from configurable fields plus static
19/// defaults, for conflict-based logsource pruning on the evaluation hot path.
20///
21/// Each dimension is resolved independently in precedence order: the value of
22/// the configured event field, then the static default, then unset (`None`).
23/// A present-but-blank field value is treated as unset.
24///
25/// # Example
26///
27/// ```rust
28/// use rsigma_eval::LogSourceExtractor;
29/// use rsigma_eval::event::JsonEvent;
30/// use serde_json::json;
31///
32/// let extractor = LogSourceExtractor::new();
33/// let ev = json!({"product": "windows"});
34/// let event = JsonEvent::borrow(&ev);
35///
36/// let ls = extractor.extract(&event);
37/// assert_eq!(ls.product.as_deref(), Some("windows"));
38/// assert_eq!(ls.category, None); // absent fields stay unset (fail-open)
39/// ```
40#[derive(Debug, Clone)]
41pub struct LogSourceExtractor {
42    product_field: String,
43    service_field: String,
44    category_field: String,
45    defaults: LogSource,
46}
47
48impl LogSourceExtractor {
49    /// Create an extractor that reads the literal `product`, `service`, and
50    /// `category` fields with no static defaults.
51    pub fn new() -> Self {
52        LogSourceExtractor {
53            product_field: "product".to_string(),
54            service_field: "service".to_string(),
55            category_field: "category".to_string(),
56            defaults: LogSource::default(),
57        }
58    }
59
60    /// Override the event field names read for each dimension.
61    #[must_use]
62    pub fn with_field_names(
63        mut self,
64        product_field: impl Into<String>,
65        service_field: impl Into<String>,
66        category_field: impl Into<String>,
67    ) -> Self {
68        self.product_field = product_field.into();
69        self.service_field = service_field.into();
70        self.category_field = category_field.into();
71        self
72    }
73
74    /// Set the static per-dimension defaults applied when a field is absent.
75    /// Only `product`, `service`, and `category` are consulted.
76    #[must_use]
77    pub fn with_defaults(mut self, defaults: LogSource) -> Self {
78        self.defaults = defaults;
79        self
80    }
81
82    /// Extract the event's logsource. Each dimension resolves to the configured
83    /// field value, then the static default, then `None` (fail-open).
84    pub fn extract<E: Event>(&self, event: &E) -> LogSource {
85        LogSource {
86            product: self.resolve(event, &self.product_field, &self.defaults.product),
87            service: self.resolve(event, &self.service_field, &self.defaults.service),
88            category: self.resolve(event, &self.category_field, &self.defaults.category),
89            ..LogSource::default()
90        }
91    }
92
93    fn resolve<E: Event>(
94        &self,
95        event: &E,
96        field: &str,
97        default: &Option<String>,
98    ) -> Option<String> {
99        if let Some(value) = event.get_field(field)
100            && let Some(s) = value.as_str()
101        {
102            let trimmed = s.trim();
103            if !trimmed.is_empty() {
104                return Some(trimmed.to_string());
105            }
106        }
107        default.clone()
108    }
109}
110
111impl Default for LogSourceExtractor {
112    fn default() -> Self {
113        Self::new()
114    }
115}