1mod helpers;
9mod mod_ctx;
10mod value;
11
12use std::collections::HashMap;
13
14use rsigma_parser::validate::{check_detection_item, check_regex, exists_flag};
15use rsigma_parser::{
16 ConditionExpr, CorrelationRule, Detection, DetectionItem, Detections, FilterRule, Modifier,
17 SigmaParserError, SigmaRule, SigmaValue,
18};
19
20use crate::error::IrError;
21use crate::{
22 IrCondition, IrCorrelation, IrDetection, IrDetectionItem, IrFilter, IrMatcher, IrRule,
23 IrRuleMetadata,
24};
25
26use helpers::{Result, yaml_to_json_map};
27use mod_ctx::ModCtx;
28use value::{lower_value, lower_value_keywords};
29
30#[derive(Debug, Clone, Default)]
32pub struct LowerOptions {
33 pub permissive_placeholders: bool,
37}
38
39pub fn lower_rule(rule: &SigmaRule, opts: &LowerOptions) -> Result<IrRule> {
41 let mut detections = HashMap::new();
42 for (name, detection) in &rule.detection.named {
43 detections.insert(name.clone(), lower_detection(detection, opts)?);
44 }
45
46 let detection_names: Vec<String> = detections.keys().cloned().collect();
47 let mut conditions = Vec::with_capacity(rule.detection.conditions.len());
48 for condition in &rule.detection.conditions {
49 conditions.push(lower_condition(condition, &detection_names)?);
50 }
51
52 Ok(IrRule {
53 metadata: metadata_from_rule(rule),
54 logsource: rule.logsource.clone(),
55 sigma_version: rule.sigma_version,
56 detections,
57 conditions,
58 })
59}
60
61pub fn lower_conditions(section: &Detections) -> Result<Vec<IrCondition>> {
68 let names: Vec<String> = section.named.keys().cloned().collect();
69 section
70 .conditions
71 .iter()
72 .map(|c| lower_condition(c, &names))
73 .collect()
74}
75
76pub fn lower_detection(detection: &Detection, opts: &LowerOptions) -> Result<IrDetection> {
78 match detection {
79 Detection::AllOf(items) => {
80 if items.is_empty() {
81 return Err(IrError::InvalidModifiers(
82 "AllOf detection must not be empty (vacuous truth)".into(),
83 ));
84 }
85 let lowered: Result<Vec<_>> = items
86 .iter()
87 .map(|item| lower_detection_item(item, opts))
88 .collect();
89 Ok(IrDetection::AllOf(lowered?))
90 }
91 Detection::AnyOf(dets) => {
92 if dets.is_empty() {
93 return Err(IrError::InvalidModifiers(
94 "AnyOf detection must not be empty (would never match)".into(),
95 ));
96 }
97 let lowered: Result<Vec<_>> = dets.iter().map(|d| lower_detection(d, opts)).collect();
98 Ok(IrDetection::AnyOf(lowered?))
99 }
100 Detection::ArrayMatch {
101 field,
102 quantifier,
103 body,
104 } => {
105 let compiled_body = lower_detection(body, opts)?;
106 Ok(IrDetection::ArrayMatch {
107 field: field.clone(),
108 quantifier: *quantifier,
109 body: Box::new(compiled_body),
110 })
111 }
112 Detection::And(dets) => {
113 if dets.is_empty() {
114 return Err(IrError::InvalidModifiers(
115 "And detection must not be empty".into(),
116 ));
117 }
118 let lowered: Result<Vec<_>> = dets.iter().map(|d| lower_detection(d, opts)).collect();
119 Ok(IrDetection::And(lowered?))
120 }
121 Detection::Conditional { named, condition } => {
122 if named.is_empty() {
123 return Err(IrError::InvalidModifiers(
124 "Conditional detection must have at least one named sub-selection".into(),
125 ));
126 }
127 let mut lowered_named = HashMap::new();
128 for (k, d) in named {
129 lowered_named.insert(k.clone(), lower_detection(d, opts)?);
130 }
131 let names: Vec<String> = lowered_named.keys().cloned().collect();
132 let lowered_cond = lower_condition(condition, &names)?;
133 Ok(IrDetection::Conditional {
134 named: lowered_named,
135 condition: lowered_cond,
136 })
137 }
138 Detection::Keywords(values) => {
139 let matchers: Result<Vec<IrMatcher>> =
140 values.iter().map(lower_value_keywords).collect();
141 let matchers = matchers?;
142 let matcher = match matchers.len() {
143 0 => {
144 return Err(IrError::InvalidModifiers(
145 "Keywords detection must not be empty".into(),
146 ));
147 }
148 1 => matchers.into_iter().next().unwrap(),
149 _ => IrMatcher::AnyOf(matchers),
150 };
151 Ok(IrDetection::Keywords(matcher))
152 }
153 }
154}
155
156pub fn lower_detection_item(item: &DetectionItem, opts: &LowerOptions) -> Result<IrDetectionItem> {
158 if !opts.permissive_placeholders {
159 for v in &item.values {
160 reject_placeholders(v)?;
161 }
162 }
163
164 let ctx = ModCtx::from_modifiers(&item.field.modifiers);
165 match check_detection_item(item) {
166 Ok(()) => {}
167 Err(SigmaParserError::InvalidModifiers(message)) => {
168 return Err(IrError::InvalidModifiers(message));
169 }
170 Err(SigmaParserError::InvalidValue(_))
173 if item.field.modifiers.contains(&Modifier::Re)
174 && item.values.iter().any(
175 |value| matches!(value, SigmaValue::String(s) if check_regex(&s.original).is_err()),
176 ) => {}
177 Err(SigmaParserError::InvalidValue(message))
178 if item.field.modifiers.iter().any(|modifier| {
179 matches!(
180 modifier,
181 Modifier::Gt | Modifier::Gte | Modifier::Lt | Modifier::Lte
182 )
183 }) =>
184 {
185 return Err(IrError::ExpectedNumeric(message));
186 }
187 Err(other) => return Err(IrError::IncompatibleValue(other.to_string())),
188 }
189
190 if ctx.exists {
191 if item.field.name.is_none() {
192 return Err(IrError::IncompatibleValue(
193 "|exists must be applied to a field".into(),
194 ));
195 }
196 let expect = match item.values.as_slice() {
197 [value] => exists_flag(value),
198 _ => None,
199 }
200 .ok_or_else(|| IrError::IncompatibleValue("|exists takes a single boolean value".into()))?;
201 return Ok(IrDetectionItem {
202 field: item.field.name.clone(),
203 matcher: IrMatcher::Exists(expect),
204 exists: Some(expect),
205 });
206 }
207
208 if item.values.is_empty() {
210 if item.field.name.is_none() {
211 return Err(IrError::IncompatibleValue(
212 "an empty value list must be bound to a field".into(),
213 ));
214 }
215 let matcher = if ctx.has_neq() {
216 IrMatcher::Not(Box::new(IrMatcher::Null))
217 } else {
218 IrMatcher::Null
219 };
220 return Ok(IrDetectionItem {
221 field: item.field.name.clone(),
222 matcher,
223 exists: None,
224 });
225 }
226
227 let mut value_ctx = ctx;
229 value_ctx.neq = false;
230 let matchers: Result<Vec<IrMatcher>> = item
231 .values
232 .iter()
233 .map(|v| lower_value(v, &value_ctx))
234 .collect();
235 let matchers = matchers?;
236
237 let combined = if ctx.all {
238 if matchers.len() == 1 {
239 matchers.into_iter().next().unwrap()
240 } else {
241 IrMatcher::AllOf(matchers)
242 }
243 } else if matchers.len() == 1 {
244 matchers.into_iter().next().unwrap()
245 } else {
246 IrMatcher::AnyOf(matchers)
247 };
248 let combined = if ctx.has_neq() {
249 IrMatcher::Not(Box::new(combined))
250 } else {
251 combined
252 };
253
254 Ok(IrDetectionItem {
255 field: item.field.name.clone(),
256 matcher: combined,
257 exists: None,
258 })
259}
260
261pub fn lower_condition(expr: &ConditionExpr, detection_names: &[String]) -> Result<IrCondition> {
263 match expr {
264 ConditionExpr::Identifier(name) => {
265 if !detection_names.iter().any(|n| n == name) {
266 return Err(IrError::UnknownDetection(name.clone()));
267 }
268 Ok(IrCondition::Detection(name.clone()))
269 }
270 ConditionExpr::And(exprs) => {
271 let lowered: Result<Vec<_>> = exprs
272 .iter()
273 .map(|e| lower_condition(e, detection_names))
274 .collect();
275 Ok(IrCondition::And(lowered?))
276 }
277 ConditionExpr::Or(exprs) => {
278 let lowered: Result<Vec<_>> = exprs
279 .iter()
280 .map(|e| lower_condition(e, detection_names))
281 .collect();
282 Ok(IrCondition::Or(lowered?))
283 }
284 ConditionExpr::Not(inner) => Ok(IrCondition::Not(Box::new(lower_condition(
285 inner,
286 detection_names,
287 )?))),
288 ConditionExpr::Selector {
289 quantifier,
290 pattern,
291 } => {
292 if !detection_names
295 .iter()
296 .any(|n| pattern.matches_detection_name(n))
297 {
298 return Err(IrError::NoSelectorMatches(format!(
299 "{quantifier} of {pattern}"
300 )));
301 }
302 Ok(IrCondition::Selector {
307 quantifier: quantifier.clone(),
308 pattern: pattern.clone(),
309 })
310 }
311 }
312}
313
314pub fn lower_correlation(corr: &CorrelationRule) -> Result<IrCorrelation> {
316 Ok(IrCorrelation {
317 metadata: metadata_from_correlation(corr),
318 sigma_version: corr.sigma_version,
319 correlation_type: corr.correlation_type,
320 rules: corr.rules.clone(),
321 group_by: corr.group_by.clone(),
322 timespan: corr.timespan.clone(),
323 window: corr.window,
324 gap: corr.gap.clone(),
325 condition: corr.condition.clone(),
326 aliases: corr.aliases.clone(),
327 generate: corr.generate,
328 })
329}
330
331pub fn lower_filter(filter: &FilterRule, opts: &LowerOptions) -> Result<IrFilter> {
333 let mut detections = HashMap::new();
334 for (name, detection) in &filter.detection.named {
335 detections.insert(name.clone(), lower_detection(detection, opts)?);
336 }
337 let conditions = lower_conditions(&filter.detection)?;
338 Ok(IrFilter {
339 metadata: metadata_from_filter(filter),
340 sigma_version: filter.sigma_version,
341 rules: filter.rules.clone(),
342 logsource: filter.logsource.clone(),
343 detections,
344 conditions,
345 })
346}
347
348fn reject_placeholders(value: &SigmaValue) -> Result<()> {
349 if let SigmaValue::String(s) = value
350 && s.original.contains("${source.")
351 {
352 return Err(IrError::Lowering(format!(
353 "unresolved source placeholder in detection value: {}",
354 s.original
355 )));
356 }
357 Ok(())
358}
359
360fn metadata_from_rule(rule: &SigmaRule) -> IrRuleMetadata {
361 IrRuleMetadata {
362 title: rule.title.clone(),
363 id: rule.id.clone(),
364 name: rule.name.clone(),
365 level: rule.level,
366 tags: rule.tags.clone(),
367 status: rule.status,
368 description: rule.description.clone(),
369 author: rule.author.clone(),
370 date: rule.date.clone(),
371 modified: rule.modified.clone(),
372 references: rule.references.clone(),
373 falsepositives: rule.falsepositives.clone(),
374 fields: rule.fields.clone(),
375 related: rule.related.clone(),
376 license: rule.license.clone(),
377 taxonomy: rule.taxonomy.clone(),
378 scope: rule.scope.clone(),
379 custom_attributes: yaml_to_json_map(&rule.custom_attributes),
380 schema_affinity: None,
381 }
382}
383
384fn metadata_from_correlation(corr: &CorrelationRule) -> IrRuleMetadata {
385 IrRuleMetadata {
386 title: corr.title.clone(),
387 id: corr.id.clone(),
388 name: corr.name.clone(),
389 level: corr.level,
390 tags: corr.tags.clone(),
391 status: corr.status,
392 description: corr.description.clone(),
393 author: corr.author.clone(),
394 date: corr.date.clone(),
395 modified: corr.modified.clone(),
396 references: corr.references.clone(),
397 falsepositives: corr.falsepositives.clone(),
398 fields: corr.fields.clone(),
399 related: corr.related.clone(),
400 license: corr.license.clone(),
401 taxonomy: corr.taxonomy.clone(),
402 scope: corr.scope.clone(),
403 custom_attributes: yaml_to_json_map(&corr.custom_attributes),
404 schema_affinity: None,
405 }
406}
407
408fn metadata_from_filter(filter: &FilterRule) -> IrRuleMetadata {
409 IrRuleMetadata {
410 title: filter.title.clone(),
411 id: filter.id.clone(),
412 name: filter.name.clone(),
413 level: filter.level,
414 tags: filter.tags.clone(),
415 status: filter.status,
416 description: filter.description.clone(),
417 author: filter.author.clone(),
418 date: filter.date.clone(),
419 modified: filter.modified.clone(),
420 references: filter.references.clone(),
421 falsepositives: filter.falsepositives.clone(),
422 fields: filter.fields.clone(),
423 related: filter.related.clone(),
424 license: filter.license.clone(),
425 taxonomy: filter.taxonomy.clone(),
426 scope: filter.scope.clone(),
427 custom_attributes: yaml_to_json_map(&filter.custom_attributes),
428 schema_affinity: None,
429 }
430}