rsigma_runtime/sources/
template.rs1use std::collections::HashMap;
7
8use regex::Regex;
9use rsigma_eval::Pipeline;
10use std::sync::LazyLock;
11
12static SOURCE_TEMPLATE_RE: LazyLock<Regex> =
13 LazyLock::new(|| Regex::new(r"\$\{source\.([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\}").unwrap());
14
15pub struct TemplateExpander;
17
18impl TemplateExpander {
19 pub fn expand(pipeline: &Pipeline, resolved: &HashMap<String, serde_json::Value>) -> Pipeline {
26 let mut expanded = pipeline.clone();
27
28 for (_var_name, values) in expanded.vars.iter_mut() {
30 let mut new_values = Vec::new();
31 for val in values.iter() {
32 if let Some(expanded_vals) = Self::expand_string_value(val, resolved) {
33 new_values.extend(expanded_vals);
34 } else {
35 new_values.push(val.clone());
36 }
37 }
38 *values = new_values;
39 }
40
41 expanded
42 }
43
44 fn expand_string_value(
49 value: &str,
50 resolved: &HashMap<String, serde_json::Value>,
51 ) -> Option<Vec<String>> {
52 if !value.contains("${source.") {
53 return None;
54 }
55
56 if let Some(caps) = SOURCE_TEMPLATE_RE.captures(value)
58 && caps.get(0).unwrap().as_str() == value
59 {
60 let source_id = caps.get(1).unwrap().as_str();
61 let sub_path = caps.get(2).map(|m| m.as_str());
62
63 if let Some(data) = resolved.get(source_id) {
64 let target = if let Some(path) = sub_path {
65 navigate_path(data, path)
66 } else {
67 Some(data)
68 };
69
70 if let Some(val) = target {
71 return Some(json_to_string_vec(val));
72 }
73 }
74
75 return None;
76 }
77
78 let result = SOURCE_TEMPLATE_RE
80 .replace_all(value, |caps: ®ex::Captures| {
81 let source_id = caps.get(1).unwrap().as_str();
82 let sub_path = caps.get(2).map(|m| m.as_str());
83
84 if let Some(data) = resolved.get(source_id) {
85 let target = if let Some(path) = sub_path {
86 navigate_path(data, path)
87 } else {
88 Some(data)
89 };
90
91 if let Some(val) = target {
92 return json_to_single_string(val);
93 }
94 }
95
96 caps.get(0).unwrap().as_str().to_string()
97 })
98 .to_string();
99
100 Some(vec![result])
101 }
102}
103
104fn navigate_path<'a>(data: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
108 let mut current = data;
109 for segment in path.split('.') {
110 match current {
111 serde_json::Value::Object(map) => {
112 current = map.get(segment)?;
113 }
114 serde_json::Value::Array(arr) => {
115 let idx: usize = segment.parse().ok()?;
116 current = arr.get(idx)?;
117 }
118 _ => return None,
119 }
120 }
121 Some(current)
122}
123
124fn json_to_string_vec(val: &serde_json::Value) -> Vec<String> {
130 match val {
131 serde_json::Value::Array(arr) => arr.iter().map(json_to_single_string).collect(),
132 serde_json::Value::Null => vec![],
133 other => vec![json_to_single_string(other)],
134 }
135}
136
137fn json_to_single_string(val: &serde_json::Value) -> String {
139 match val {
140 serde_json::Value::String(s) => s.clone(),
141 serde_json::Value::Null => String::new(),
142 serde_json::Value::Bool(b) => b.to_string(),
143 serde_json::Value::Number(n) => n.to_string(),
144 other => other.to_string(),
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn expand_simple_var() {
154 let mut vars = HashMap::new();
155 vars.insert(
156 "admin_emails".to_string(),
157 vec!["${source.admin_emails}".to_string()],
158 );
159
160 let pipeline = Pipeline {
161 name: "test".to_string(),
162 priority: 0,
163 vars,
164 transformations: vec![],
165 finalizers: vec![],
166 sources: vec![],
167 source_refs: vec![],
168 };
169
170 let mut resolved = HashMap::new();
171 resolved.insert(
172 "admin_emails".to_string(),
173 serde_json::json!(["admin@corp.com", "root@corp.com"]),
174 );
175
176 let expanded = TemplateExpander::expand(&pipeline, &resolved);
177 assert_eq!(
178 expanded.vars.get("admin_emails").unwrap(),
179 &vec!["admin@corp.com".to_string(), "root@corp.com".to_string()]
180 );
181 }
182
183 #[test]
184 fn expand_nested_path() {
185 let mut vars = HashMap::new();
186 vars.insert(
187 "log_index".to_string(),
188 vec!["${source.env_config.log_index}".to_string()],
189 );
190
191 let pipeline = Pipeline {
192 name: "test".to_string(),
193 priority: 0,
194 vars,
195 transformations: vec![],
196 finalizers: vec![],
197 sources: vec![],
198 source_refs: vec![],
199 };
200
201 let mut resolved = HashMap::new();
202 resolved.insert(
203 "env_config".to_string(),
204 serde_json::json!({"log_index": "security-events", "retention": "30d"}),
205 );
206
207 let expanded = TemplateExpander::expand(&pipeline, &resolved);
208 assert_eq!(
209 expanded.vars.get("log_index").unwrap(),
210 &vec!["security-events".to_string()]
211 );
212 }
213
214 #[test]
215 fn expand_inline_template() {
216 let mut vars = HashMap::new();
217 vars.insert(
218 "index_pattern".to_string(),
219 vec!["logs-${source.env_config.env}-*".to_string()],
220 );
221
222 let pipeline = Pipeline {
223 name: "test".to_string(),
224 priority: 0,
225 vars,
226 transformations: vec![],
227 finalizers: vec![],
228 sources: vec![],
229 source_refs: vec![],
230 };
231
232 let mut resolved = HashMap::new();
233 resolved.insert(
234 "env_config".to_string(),
235 serde_json::json!({"env": "production"}),
236 );
237
238 let expanded = TemplateExpander::expand(&pipeline, &resolved);
239 assert_eq!(
240 expanded.vars.get("index_pattern").unwrap(),
241 &vec!["logs-production-*".to_string()]
242 );
243 }
244
245 #[test]
246 fn static_vars_unchanged() {
247 let mut vars = HashMap::new();
248 vars.insert("static".to_string(), vec!["no_template_here".to_string()]);
249
250 let pipeline = Pipeline {
251 name: "test".to_string(),
252 priority: 0,
253 vars,
254 transformations: vec![],
255 finalizers: vec![],
256 sources: vec![],
257 source_refs: vec![],
258 };
259
260 let resolved = HashMap::new();
261 let expanded = TemplateExpander::expand(&pipeline, &resolved);
262 assert_eq!(
263 expanded.vars.get("static").unwrap(),
264 &vec!["no_template_here".to_string()]
265 );
266 }
267
268 #[test]
269 fn unresolved_template_kept_as_is() {
270 let mut vars = HashMap::new();
271 vars.insert(
272 "missing".to_string(),
273 vec!["${source.nonexistent}".to_string()],
274 );
275
276 let pipeline = Pipeline {
277 name: "test".to_string(),
278 priority: 0,
279 vars,
280 transformations: vec![],
281 finalizers: vec![],
282 sources: vec![],
283 source_refs: vec![],
284 };
285
286 let resolved = HashMap::new();
287 let expanded = TemplateExpander::expand(&pipeline, &resolved);
288 assert_eq!(
289 expanded.vars.get("missing").unwrap(),
290 &vec!["${source.nonexistent}".to_string()]
291 );
292 }
293}