rsigma_runtime/enrichment/
template.rs1use std::sync::LazyLock;
37
38use async_trait::async_trait;
39use regex::Regex;
40use rsigma_eval::{EvaluationResult, ResultBody};
41use rsigma_parser::Level;
42
43use super::{
44 EnrichError, EnrichErrorKind, Enricher, EnricherKind, OnError, Scope, inject_enrichment,
45};
46
47static TEMPLATE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]+)\}").unwrap());
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56enum VarRef {
57 Detection(String),
59 Correlation(String),
61 Env(String),
63 Invalid(String),
66}
67
68fn classify_ref(name: &str) -> VarRef {
69 if let Some(rest) = name.strip_prefix("detection.") {
70 VarRef::Detection(rest.to_string())
71 } else if let Some(rest) = name.strip_prefix("correlation.") {
72 VarRef::Correlation(rest.to_string())
73 } else if name.contains('.') || name.is_empty() {
74 VarRef::Invalid(name.to_string())
75 } else {
76 VarRef::Env(name.to_string())
77 }
78}
79
80#[derive(Debug, Clone)]
82pub enum TemplateError {
83 CrossNamespace {
86 enricher_id: String,
87 enricher_kind: EnricherKind,
88 reference: String,
89 field: &'static str,
90 },
91 Malformed {
94 enricher_id: String,
95 reference: String,
96 field: &'static str,
97 },
98}
99
100impl std::fmt::Display for TemplateError {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 match self {
103 TemplateError::CrossNamespace {
104 enricher_id,
105 enricher_kind,
106 reference,
107 field,
108 } => write!(
109 f,
110 "enricher '{enricher_id}' (kind: {kind}) references '${{{reference}}}' in field '{field}'; this is the wrong namespace for a {kind} enricher",
111 kind = enricher_kind.as_str(),
112 ),
113 TemplateError::Malformed {
114 enricher_id,
115 reference,
116 field,
117 } => write!(
118 f,
119 "enricher '{enricher_id}': malformed template reference '${{{reference}}}' in field '{field}'; expected ${{detection.*}}, ${{correlation.*}}, or ${{ENV_VAR}}",
120 ),
121 }
122 }
123}
124
125impl std::error::Error for TemplateError {}
126
127pub fn validate_template_namespace(
136 text: &str,
137 enricher_kind: EnricherKind,
138 enricher_id: &str,
139 field: &'static str,
140) -> Result<(), TemplateError> {
141 for caps in TEMPLATE_RE.captures_iter(text) {
142 let inner = caps.get(1).unwrap().as_str();
143 match classify_ref(inner) {
144 VarRef::Env(_) => {}
145 VarRef::Detection(_) if enricher_kind == EnricherKind::Detection => {}
146 VarRef::Correlation(_) if enricher_kind == EnricherKind::Correlation => {}
147 VarRef::Detection(_) | VarRef::Correlation(_) => {
148 return Err(TemplateError::CrossNamespace {
149 enricher_id: enricher_id.to_string(),
150 enricher_kind,
151 reference: inner.to_string(),
152 field,
153 });
154 }
155 VarRef::Invalid(_) => {
156 return Err(TemplateError::Malformed {
157 enricher_id: enricher_id.to_string(),
158 reference: inner.to_string(),
159 field,
160 });
161 }
162 }
163 }
164 Ok(())
165}
166
167pub fn render_template(text: &str, result: &EvaluationResult) -> String {
176 TEMPLATE_RE
177 .replace_all(text, |caps: ®ex::Captures| {
178 let inner = caps.get(1).unwrap().as_str();
179 match classify_ref(inner) {
180 VarRef::Env(name) => std::env::var(name).unwrap_or_default(),
181 VarRef::Detection(path) => match &result.body {
182 ResultBody::Detection(_) => render_detection_path(&path, result),
183 ResultBody::Correlation(_) => String::new(),
184 },
185 VarRef::Correlation(path) => match &result.body {
186 ResultBody::Correlation(_) => render_correlation_path(&path, result),
187 ResultBody::Detection(_) => String::new(),
188 },
189 VarRef::Invalid(_) => String::new(),
190 }
191 })
192 .into_owned()
193}
194
195fn render_detection_path(path: &str, result: &EvaluationResult) -> String {
196 let body = match result.as_detection() {
197 Some(b) => b,
198 None => return String::new(),
199 };
200 if let Some(rest) = path.strip_prefix("rule.") {
201 return render_rule_field(rest, result);
202 }
203 if path == "tags" {
204 return result.header.tags.join(",");
205 }
206 if let Some(name) = path.strip_prefix("fields.") {
207 for fm in &body.matched_fields {
208 if fm.field == name {
209 return json_to_string(&fm.value);
210 }
211 }
212 return String::new();
213 }
214 if let Some(rest) = path.strip_prefix("event.") {
215 if let Some(event) = &body.event {
216 return navigate_json(event, rest)
217 .map(json_to_string)
218 .unwrap_or_default();
219 }
220 return String::new();
221 }
222 if path == "event" {
223 return body.event.as_ref().map(json_to_string).unwrap_or_default();
224 }
225 String::new()
226}
227
228fn render_correlation_path(path: &str, result: &EvaluationResult) -> String {
229 let body = match result.as_correlation() {
230 Some(b) => b,
231 None => return String::new(),
232 };
233 if let Some(rest) = path.strip_prefix("rule.") {
234 return render_rule_field(rest, result);
235 }
236 if path == "tags" {
237 return result.header.tags.join(",");
238 }
239 if path == "type" {
240 return body.correlation_type.as_str().to_string();
241 }
242 if path == "aggregated_value" {
243 return format_f64(body.aggregated_value);
244 }
245 if path == "timespan_secs" {
246 return body.timespan_secs.to_string();
247 }
248 if path == "group_key" {
249 return body
250 .group_key
251 .iter()
252 .map(|(k, v)| format!("{k}={v}"))
253 .collect::<Vec<_>>()
254 .join(",");
255 }
256 if let Some(name) = path.strip_prefix("group_key.") {
257 for (k, v) in &body.group_key {
258 if k == name {
259 return v.clone();
260 }
261 }
262 return String::new();
263 }
264 String::new()
265}
266
267fn render_rule_field(rest: &str, result: &EvaluationResult) -> String {
268 match rest {
269 "title" => result.header.rule_title.clone(),
270 "id" => result.header.rule_id.clone().unwrap_or_default(),
271 "level" => result
272 .header
273 .level
274 .map(|l: Level| l.as_str().to_string())
275 .unwrap_or_default(),
276 _ => String::new(),
277 }
278}
279
280fn navigate_json<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
287 let mut current = value;
288 for segment in path.split('.') {
289 match current {
290 serde_json::Value::Object(map) => current = map.get(segment)?,
291 serde_json::Value::Array(arr) => {
292 let idx: usize = segment.parse().ok()?;
293 current = arr.get(idx)?;
294 }
295 _ => return None,
296 }
297 }
298 Some(current)
299}
300
301fn json_to_string(value: &serde_json::Value) -> String {
302 match value {
303 serde_json::Value::String(s) => s.clone(),
304 serde_json::Value::Null => String::new(),
305 serde_json::Value::Bool(b) => b.to_string(),
306 serde_json::Value::Number(n) => n.to_string(),
307 other => other.to_string(),
308 }
309}
310
311fn format_f64(v: f64) -> String {
315 if v.is_finite() && v.fract() == 0.0 && v.abs() < 1e15 {
316 format!("{}", v as i64)
317 } else {
318 v.to_string()
319 }
320}
321
322pub struct TemplateEnricher {
334 id: String,
335 kind: EnricherKind,
336 inject_field: String,
337 template: String,
338 timeout: std::time::Duration,
339 on_error: OnError,
340 scope: Scope,
341}
342
343impl TemplateEnricher {
344 pub fn new(
349 id: String,
350 kind: EnricherKind,
351 inject_field: String,
352 template: String,
353 timeout: std::time::Duration,
354 on_error: OnError,
355 scope: Scope,
356 ) -> Self {
357 Self {
358 id,
359 kind,
360 inject_field,
361 template,
362 timeout,
363 on_error,
364 scope,
365 }
366 }
367}
368
369#[async_trait]
370impl Enricher for TemplateEnricher {
371 fn kind(&self) -> EnricherKind {
372 self.kind
373 }
374
375 fn id(&self) -> &str {
376 &self.id
377 }
378
379 fn inject_field(&self) -> &str {
380 &self.inject_field
381 }
382
383 fn timeout(&self) -> std::time::Duration {
384 self.timeout
385 }
386
387 fn scope(&self) -> &Scope {
388 &self.scope
389 }
390
391 fn on_error(&self) -> OnError {
392 self.on_error
393 }
394
395 async fn enrich(&self, result: &mut EvaluationResult) -> Result<(), EnrichError> {
396 let rendered = render_template(&self.template, result);
397 inject_enrichment(
398 result,
399 &self.inject_field,
400 serde_json::Value::String(rendered),
401 );
402 Ok(())
403 }
404}
405
406#[allow(dead_code)]
410fn _use_err(_e: EnrichError) -> EnrichErrorKind {
411 EnrichErrorKind::TemplateRender(String::new())
412}