1use std::collections::HashMap;
4use std::fmt;
5
6use rsigma_parser::{
7 Exemplar, ExemplarErrorKind, ExemplarPayload, ExemplarRuleKind, ExemplarShapeError, Expect,
8 SigmaCollection, SigmaRule, correlation_exemplars, exemplars, filter_exemplars,
9};
10use serde::Serialize;
11
12use crate::compiler::yaml_to_json;
13use crate::correlation_engine::{CorrelationConfig, CorrelationEngine};
14use crate::engine::Engine;
15use crate::error::EvalError;
16use crate::event::JsonEvent;
17use crate::pipeline::Pipeline;
18use crate::result::EvaluationResult;
19
20const BASE_TIMESTAMP: i64 = 1_700_000_000;
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25pub struct ExemplarResult {
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub rule_id: Option<String>,
29 pub rule_title: String,
31 pub rule_kind: ExemplarRuleKind,
33 pub index: usize,
35 pub name: String,
37 pub expect: Expect,
39 pub actual: Expect,
41 pub passed: bool,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub diagnostic: Option<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50pub struct MissingExemplars {
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub rule_id: Option<String>,
54 pub rule_title: String,
56 pub rule_kind: ExemplarRuleKind,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
62pub struct ExemplarReport {
63 #[serde(skip_serializing_if = "String::is_empty")]
65 pub source: String,
66 pub results: Vec<ExemplarResult>,
68 #[serde(skip_serializing_if = "Vec::is_empty")]
70 pub missing: Vec<MissingExemplars>,
71}
72
73impl ExemplarReport {
74 pub fn all_passed(&self) -> bool {
76 self.results.iter().all(|r| r.passed)
77 }
78
79 pub fn failures(&self) -> impl Iterator<Item = &ExemplarResult> {
81 self.results.iter().filter(|r| !r.passed)
82 }
83}
84
85#[derive(Debug)]
87pub enum ExemplarRunError {
88 Shape {
90 rule: String,
92 errors: Vec<ExemplarShapeError>,
94 },
95 AmbiguousTitle(String),
97 Reference(String),
99 Compile(EvalError),
101}
102
103impl fmt::Display for ExemplarRunError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Shape { rule, errors } => {
107 write!(f, "invalid exemplars on '{rule}': ")?;
108 let messages: Vec<String> = errors.iter().map(ToString::to_string).collect();
109 f.write_str(&messages.join("; "))
110 }
111 Self::AmbiguousTitle(title) => {
112 write!(
113 f,
114 "ambiguous rule title '{title}': add an id or make the title unique"
115 )
116 }
117 Self::Reference(msg) => f.write_str(msg),
118 Self::Compile(err) => write!(f, "{err}"),
119 }
120 }
121}
122
123impl std::error::Error for ExemplarRunError {}
124
125impl From<EvalError> for ExemplarRunError {
126 fn from(err: EvalError) -> Self {
127 match err {
128 EvalError::UnknownRuleRef(r) => Self::Reference(format!("unknown rule reference: {r}")),
129 other => Self::Compile(other),
130 }
131 }
132}
133
134pub fn run_exemplars(
136 collection: &SigmaCollection,
137 pipelines: &[Pipeline],
138) -> Result<ExemplarReport, ExemplarRunError> {
139 let titles = title_counts(collection);
140 let mut results = Vec::new();
141 let mut missing = Vec::new();
142
143 for rule in &collection.rules {
144 let identity = TargetIdentity {
145 id: rule.id.clone(),
146 title: rule.title.clone(),
147 kind: ExemplarRuleKind::Detection,
148 };
149 let list = exemplars(rule).map_err(|errors| ExemplarRunError::Shape {
150 rule: identity.label(),
151 errors,
152 })?;
153 if list.is_empty() {
154 missing.push(identity.to_missing());
155 continue;
156 }
157 identity.require_unique(&titles)?;
158 for exemplar in list {
159 results.push(run_detection(
160 collection, rule, &identity, &exemplar, pipelines,
161 )?);
162 }
163 }
164
165 for rule in &collection.correlations {
166 let identity = TargetIdentity {
167 id: rule.id.clone(),
168 title: rule.title.clone(),
169 kind: ExemplarRuleKind::Correlation,
170 };
171 let list = correlation_exemplars(rule).map_err(|errors| ExemplarRunError::Shape {
172 rule: identity.label(),
173 errors,
174 })?;
175 if list.is_empty() {
176 missing.push(identity.to_missing());
177 continue;
178 }
179 identity.require_unique(&titles)?;
180 for exemplar in list {
181 results.push(run_correlation(
182 collection, &identity, &exemplar, pipelines,
183 )?);
184 }
185 }
186
187 for rule in &collection.filters {
188 if let Err(errors) = filter_exemplars(rule) {
189 return Err(ExemplarRunError::Shape {
190 rule: rule.id.clone().unwrap_or_else(|| rule.title.clone()),
191 errors,
192 });
193 }
194 }
195
196 Ok(ExemplarReport {
197 source: String::new(),
198 results,
199 missing,
200 })
201}
202
203struct TargetIdentity {
204 id: Option<String>,
205 title: String,
206 kind: ExemplarRuleKind,
207}
208
209impl TargetIdentity {
210 fn label(&self) -> String {
211 self.id.clone().unwrap_or_else(|| self.title.clone())
212 }
213
214 fn require_unique(&self, titles: &HashMap<String, usize>) -> Result<(), ExemplarRunError> {
215 if self.id.is_some() {
216 return Ok(());
217 }
218 if titles.get(&self.title).copied().unwrap_or(0) > 1 {
219 return Err(ExemplarRunError::AmbiguousTitle(self.title.clone()));
220 }
221 Ok(())
222 }
223
224 fn to_missing(&self) -> MissingExemplars {
225 MissingExemplars {
226 rule_id: self.id.clone(),
227 rule_title: self.title.clone(),
228 rule_kind: self.kind,
229 }
230 }
231
232 fn matches(&self, result: &EvaluationResult) -> bool {
233 let kind_ok = match self.kind {
234 ExemplarRuleKind::Detection => result.is_detection(),
235 ExemplarRuleKind::Correlation => result.is_correlation(),
236 ExemplarRuleKind::Filter => false,
237 };
238 if !kind_ok {
239 return false;
240 }
241 match &self.id {
242 Some(id) => result.header.rule_id.as_deref() == Some(id.as_str()),
243 None => result.header.rule_title == self.title,
244 }
245 }
246}
247
248fn title_counts(collection: &SigmaCollection) -> HashMap<String, usize> {
249 let mut counts = HashMap::new();
250 for title in collection
251 .rules
252 .iter()
253 .map(|r| r.title.as_str())
254 .chain(collection.correlations.iter().map(|r| r.title.as_str()))
255 {
256 *counts.entry(title.to_string()).or_insert(0) += 1;
257 }
258 counts
259}
260
261fn run_detection(
262 collection: &SigmaCollection,
263 rule: &SigmaRule,
264 identity: &TargetIdentity,
265 exemplar: &Exemplar,
266 pipelines: &[Pipeline],
267) -> Result<ExemplarResult, ExemplarRunError> {
268 let ExemplarPayload::Event(event) = &exemplar.payload else {
269 return Err(ExemplarRunError::Shape {
270 rule: identity.label(),
271 errors: vec![ExemplarShapeError {
272 path: format!("/custom_attributes/rsigma.exemplars/{}", exemplar.index),
273 message: "detection exemplars must use 'event'".to_string(),
274 kind: ExemplarErrorKind::WrongRuleKind,
275 }],
276 });
277 };
278 let synthetic = SigmaCollection {
279 rules: vec![rule.clone()],
280 correlations: Vec::new(),
281 filters: collection.filters.clone(),
282 errors: Vec::new(),
283 };
284 let mut engine = Engine::new();
285 if pipelines.is_empty() {
286 engine.add_collection(&synthetic)?;
287 } else {
288 engine.add_collection_with_pipelines(&synthetic, pipelines)?;
289 }
290 let json = yaml_to_json(event);
291 let je = JsonEvent::borrow(&json);
292 let matches = engine.evaluate(&je);
293 let fired = matches.iter().any(|r| identity.matches(r));
294 let diagnostic = match (fired, exemplar.expect) {
295 (true, Expect::NoMatch) => Some("the rule matched the event".to_string()),
296 (false, Expect::Match) => Some("the rule did not match the event".to_string()),
297 _ => None,
298 };
299 Ok(outcome(identity, exemplar, fired, diagnostic))
300}
301
302fn run_correlation(
303 collection: &SigmaCollection,
304 identity: &TargetIdentity,
305 exemplar: &Exemplar,
306 pipelines: &[Pipeline],
307) -> Result<ExemplarResult, ExemplarRunError> {
308 let ExemplarPayload::Sequence(events) = &exemplar.payload else {
309 return Err(ExemplarRunError::Shape {
310 rule: identity.label(),
311 errors: vec![ExemplarShapeError {
312 path: format!("/custom_attributes/rsigma.exemplars/{}", exemplar.index),
313 message: "correlation exemplars must use 'events'".to_string(),
314 kind: ExemplarErrorKind::WrongRuleKind,
315 }],
316 });
317 };
318 let mut engine = CorrelationEngine::new(CorrelationConfig {
321 emit_detections: true,
322 ..CorrelationConfig::default()
323 });
324 for pipeline in pipelines {
325 engine.add_pipeline(pipeline.clone());
326 }
327 engine.add_collection(collection)?;
328 let owned: Vec<serde_json::Value> = events.iter().map(|e| yaml_to_json(&e.event)).collect();
329 let mut first_fire: Option<(usize, String)> = None;
330 for (index, (timed, json)) in events.iter().zip(owned.iter()).enumerate() {
331 let je = JsonEvent::borrow(json);
332 let ts = BASE_TIMESTAMP.saturating_add(timed.offset.seconds as i64);
333 let results = engine.process_event_at(&je, ts);
334 if first_fire.is_none() && results.iter().any(|r| identity.matches(r)) {
335 first_fire = Some((index, timed.offset.original.clone()));
336 }
337 }
338 let fired = first_fire.is_some();
339 let diagnostic = match (first_fire, exemplar.expect) {
340 (Some((index, offset)), Expect::NoMatch) => Some(format!(
341 "the correlation fired at event index {index} (offset {offset})"
342 )),
343 (None, Expect::Match) => Some(format!(
344 "the correlation never fired across {} events",
345 events.len()
346 )),
347 _ => None,
348 };
349 Ok(outcome(identity, exemplar, fired, diagnostic))
350}
351
352fn outcome(
353 identity: &TargetIdentity,
354 exemplar: &Exemplar,
355 fired: bool,
356 diagnostic: Option<String>,
357) -> ExemplarResult {
358 let actual = if fired {
359 Expect::Match
360 } else {
361 Expect::NoMatch
362 };
363 ExemplarResult {
364 rule_id: identity.id.clone(),
365 rule_title: identity.title.clone(),
366 rule_kind: identity.kind,
367 index: exemplar.index,
368 name: exemplar.name.clone(),
369 expect: exemplar.expect,
370 actual,
371 passed: actual == exemplar.expect,
372 diagnostic,
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use rsigma_parser::parse_sigma_yaml;
380
381 fn collection(yaml: &str) -> SigmaCollection {
382 parse_sigma_yaml(yaml).unwrap()
383 }
384
385 fn run(yaml: &str) -> ExemplarReport {
386 run_exemplars(&collection(yaml), &[]).expect("run")
387 }
388
389 const DETECTION: &str = r#"
390title: Whoami
391id: 11111111-2222-3333-4444-555555555555
392logsource:
393 category: process_creation
394 product: windows
395detection:
396 selection:
397 CommandLine|contains: whoami
398 condition: selection
399custom_attributes:
400 rsigma.exemplars:
401 - name: whoami fires
402 expect: match
403 event:
404 CommandLine: whoami /all
405 - name: benign hostname
406 expect: no-match
407 event:
408 CommandLine: hostname
409"#;
410
411 #[test]
412 fn detection_match_and_no_match() {
413 let report = run(DETECTION);
414 assert!(report.all_passed());
415 assert_eq!(report.results.len(), 2);
416 assert_eq!(report.results[0].actual, Expect::Match);
417 assert_eq!(report.results[1].actual, Expect::NoMatch);
418 assert!(report.missing.is_empty());
419 }
420
421 #[test]
422 fn failed_assertion_is_not_a_run_error() {
423 let yaml = r#"
424title: Whoami
425id: 11111111-2222-3333-4444-555555555555
426logsource:
427 category: process_creation
428detection:
429 selection:
430 CommandLine|contains: whoami
431 condition: selection
432custom_attributes:
433 rsigma.exemplars:
434 - expect: match
435 event:
436 CommandLine: hostname
437"#;
438 let report = run(yaml);
439 assert!(!report.all_passed());
440 assert_eq!(report.results[0].actual, Expect::NoMatch);
441 assert_eq!(
442 report.results[0].diagnostic.as_deref(),
443 Some("the rule did not match the event")
444 );
445 }
446
447 #[test]
448 fn passing_exemplars_carry_no_diagnostic() {
449 let report = run(DETECTION);
450 assert!(report.results.iter().all(|r| r.diagnostic.is_none()));
451 }
452
453 #[test]
454 fn failed_correlation_no_match_names_the_firing_event() {
455 let yaml = r#"
456title: Login
457id: login-rule
458logsource:
459 category: auth
460detection:
461 selection:
462 EventType: login
463 condition: selection
464---
465title: Many Logins
466correlation:
467 type: event_count
468 rules:
469 - login-rule
470 group-by:
471 - User
472 timespan: 60s
473 condition:
474 gte: 2
475custom_attributes:
476 rsigma.exemplars:
477 - expect: no-match
478 events:
479 - offset: 0s
480 event: { EventType: login, User: alice }
481 - offset: 30s
482 event: { EventType: login, User: alice }
483"#;
484 let report = run(yaml);
485 assert!(!report.all_passed());
486 assert_eq!(
487 report.results[0].diagnostic.as_deref(),
488 Some("the correlation fired at event index 1 (offset 30s)")
489 );
490 }
491
492 #[test]
493 fn suppression_and_reset_do_not_mask_the_first_fire() {
494 let yaml = r#"
495title: Login
496id: login-rule
497logsource:
498 category: auth
499detection:
500 selection:
501 EventType: login
502 condition: selection
503---
504title: Many Logins
505correlation:
506 type: event_count
507 rules:
508 - login-rule
509 group-by:
510 - User
511 timespan: 60s
512 condition:
513 gte: 2
514custom_attributes:
515 rsigma.suppress: 5m
516 rsigma.action: reset
517 rsigma.exemplars:
518 - name: fires despite suppression
519 expect: match
520 events:
521 - offset: 0s
522 event: { EventType: login, User: alice }
523 - offset: 1s
524 event: { EventType: login, User: alice }
525 - offset: 2s
526 event: { EventType: login, User: alice }
527 - offset: 3s
528 event: { EventType: login, User: alice }
529 - name: single event stays quiet
530 expect: no-match
531 events:
532 - offset: 0s
533 event: { EventType: login, User: bob }
534"#;
535 let report = run(yaml);
536 assert!(report.all_passed(), "{report:?}");
537 assert_eq!(report.results.len(), 2);
538 }
539
540 #[test]
541 fn filter_excludes_matching_event() {
542 let yaml = r#"
543title: Whoami
544id: 11111111-2222-3333-4444-555555555555
545logsource:
546 category: process_creation
547 product: windows
548detection:
549 selection:
550 CommandLine|contains: whoami
551 condition: selection
552custom_attributes:
553 rsigma.exemplars:
554 - name: alice fires
555 expect: match
556 event:
557 CommandLine: whoami /all
558 User: alice
559 - name: system filtered
560 expect: no-match
561 event:
562 CommandLine: whoami /all
563 User: SYSTEM
564---
565title: Exclude SYSTEM
566logsource:
567 category: process_creation
568 product: windows
569filter:
570 rules:
571 - 11111111-2222-3333-4444-555555555555
572 selection:
573 User: SYSTEM
574 condition: not selection
575"#;
576 let report = run(yaml);
577 assert!(report.all_passed(), "{report:?}");
578 }
579
580 #[test]
581 fn pipeline_rewrites_rule_fields() {
582 let yaml = r#"
583title: Whoami
584id: 11111111-2222-3333-4444-555555555555
585logsource:
586 category: process_creation
587detection:
588 selection:
589 CommandLine|contains: whoami
590 condition: selection
591custom_attributes:
592 rsigma.exemplars:
593 - expect: match
594 event:
595 process.command_line: whoami /all
596"#;
597 let pipeline = crate::parse_pipeline(
598 r#"
599name: map
600priority: 10
601transformations:
602 - type: field_name_mapping
603 mapping:
604 CommandLine: process.command_line
605"#,
606 )
607 .unwrap();
608 let report = run_exemplars(&collection(yaml), &[pipeline]).unwrap();
609 assert!(report.all_passed(), "{report:?}");
610 }
611
612 #[test]
613 fn event_count_correlation() {
614 let yaml = r#"
615title: Login
616id: login-rule
617logsource:
618 category: auth
619detection:
620 selection:
621 EventType: login
622 condition: selection
623---
624title: Many Logins
625id: many-logins
626correlation:
627 type: event_count
628 rules:
629 - login-rule
630 group-by:
631 - User
632 timespan: 60s
633 condition:
634 gte: 3
635custom_attributes:
636 rsigma.exemplars:
637 - name: burst
638 expect: match
639 events:
640 - offset: 0s
641 event: { EventType: login, User: alice }
642 - offset: 1s
643 event: { EventType: login, User: alice }
644 - offset: 2s
645 event: { EventType: login, User: alice }
646 - name: too few
647 expect: no-match
648 events:
649 - offset: 0s
650 event: { EventType: login, User: bob }
651 - offset: 1s
652 event: { EventType: login, User: bob }
653"#;
654 let report = run(yaml);
655 assert!(report.all_passed(), "{report:?}");
656 assert_eq!(report.missing.len(), 1);
657 assert_eq!(report.missing[0].rule_title, "Login");
658 }
659
660 #[test]
661 fn correlation_referenced_by_another_correlation() {
662 let yaml = r#"
663title: Login
664id: login-rule
665logsource:
666 category: auth
667detection:
668 selection:
669 EventType: login
670 condition: selection
671---
672title: Many Logins
673id: many-logins
674correlation:
675 type: event_count
676 rules:
677 - login-rule
678 group-by:
679 - User
680 timespan: 60s
681 condition:
682 gte: 2
683custom_attributes:
684 rsigma.exemplars:
685 - expect: match
686 events:
687 - offset: 0s
688 event: { EventType: login, User: alice }
689 - offset: 1s
690 event: { EventType: login, User: alice }
691---
692title: Repeated Bursts
693id: repeated-bursts
694correlation:
695 type: event_count
696 rules:
697 - many-logins
698 group-by:
699 - User
700 timespan: 5m
701 condition:
702 gte: 2
703"#;
704 let report = run(yaml);
705 assert!(report.all_passed(), "{report:?}");
706 }
707
708 #[test]
709 fn value_count_correlation() {
710 let yaml = r#"
711title: Login
712id: login-rule
713logsource:
714 category: auth
715detection:
716 selection:
717 EventType: login
718 condition: selection
719---
720title: Distinct Hosts
721correlation:
722 type: value_count
723 rules:
724 - login-rule
725 group-by:
726 - User
727 timespan: 60s
728 condition:
729 field: Host
730 gte: 2
731custom_attributes:
732 rsigma.exemplars:
733 - expect: match
734 events:
735 - offset: 0s
736 event: { EventType: login, User: alice, Host: a }
737 - offset: 1s
738 event: { EventType: login, User: alice, Host: b }
739"#;
740 let report = run(yaml);
741 assert!(report.all_passed(), "{report:?}");
742 }
743
744 #[test]
745 fn temporal_correlation() {
746 let yaml = r#"
747title: Failed
748id: failed-login
749logsource:
750 category: auth
751detection:
752 selection:
753 EventType: failed
754 condition: selection
755---
756title: Success
757id: success-login
758logsource:
759 category: auth
760detection:
761 selection:
762 EventType: success
763 condition: selection
764---
765title: Fail then success
766correlation:
767 type: temporal
768 rules:
769 - failed-login
770 - success-login
771 group-by:
772 - User
773 timespan: 60s
774 condition:
775 gte: 2
776custom_attributes:
777 rsigma.exemplars:
778 - expect: match
779 events:
780 - offset: 0s
781 event: { EventType: failed, User: alice }
782 - offset: 10s
783 event: { EventType: success, User: alice }
784 - expect: no-match
785 events:
786 - offset: 0s
787 event: { EventType: failed, User: bob }
788"#;
789 let report = run(yaml);
790 assert!(report.all_passed(), "{report:?}");
791 }
792
793 #[test]
794 fn exemplars_do_not_share_state() {
795 let yaml = r#"
796title: Login
797id: login-rule
798logsource:
799 category: auth
800detection:
801 selection:
802 EventType: login
803 condition: selection
804---
805title: Many Logins
806correlation:
807 type: event_count
808 rules:
809 - login-rule
810 group-by:
811 - User
812 timespan: 60s
813 condition:
814 gte: 3
815custom_attributes:
816 rsigma.exemplars:
817 - name: first burst
818 expect: match
819 events:
820 - offset: 0s
821 event: { EventType: login, User: alice }
822 - offset: 1s
823 event: { EventType: login, User: alice }
824 - offset: 2s
825 event: { EventType: login, User: alice }
826 - name: second burst
827 expect: match
828 events:
829 - offset: 0s
830 event: { EventType: login, User: alice }
831 - offset: 1s
832 event: { EventType: login, User: alice }
833 - offset: 2s
834 event: { EventType: login, User: alice }
835"#;
836 let report = run(yaml);
837 assert!(report.all_passed(), "{report:?}");
838 assert_eq!(report.results.len(), 2);
839 }
840
841 #[test]
842 fn unrelated_detection_matches_do_not_count() {
843 let yaml = r#"
844title: Login
845id: login-rule
846logsource:
847 category: auth
848detection:
849 selection:
850 EventType: login
851 condition: selection
852---
853title: Also login
854id: also-login
855logsource:
856 category: auth
857detection:
858 selection:
859 EventType: login
860 condition: selection
861---
862title: Many Logins
863id: many-logins
864correlation:
865 type: event_count
866 rules:
867 - login-rule
868 group-by:
869 - User
870 timespan: 60s
871 condition:
872 gte: 2
873custom_attributes:
874 rsigma.exemplars:
875 - expect: match
876 events:
877 - offset: 0s
878 event: { EventType: login, User: alice }
879 - offset: 1s
880 event: { EventType: login, User: alice }
881"#;
882 let report = run(yaml);
883 assert!(report.all_passed(), "{report:?}");
884 assert_eq!(report.results[0].rule_id.as_deref(), Some("many-logins"));
885 }
886
887 #[test]
888 fn missing_correlation_ref_is_config_error() {
889 let yaml = r#"
890title: Many Logins
891correlation:
892 type: event_count
893 rules:
894 - missing-rule
895 timespan: 60s
896 condition:
897 gte: 2
898custom_attributes:
899 rsigma.exemplars:
900 - expect: match
901 events:
902 - offset: 0s
903 event: { EventType: login }
904 - offset: 1s
905 event: { EventType: login }
906"#;
907 let err = run_exemplars(&collection(yaml), &[]).unwrap_err();
908 assert!(matches!(err, ExemplarRunError::Reference(_)), "{err}");
909 }
910
911 #[test]
912 fn duplicate_titles_without_id_are_rejected() {
913 let yaml = r#"
914title: Dup
915logsource:
916 category: test
917detection:
918 selection:
919 field: a
920 condition: selection
921custom_attributes:
922 rsigma.exemplars:
923 - expect: match
924 event:
925 field: a
926---
927title: Dup
928logsource:
929 category: test
930detection:
931 selection:
932 field: b
933 condition: selection
934"#;
935 let err = run_exemplars(&collection(yaml), &[]).unwrap_err();
936 assert!(matches!(err, ExemplarRunError::AmbiguousTitle(_)), "{err}");
937 }
938
939 #[test]
940 fn malformed_exemplars_are_rejected() {
941 let yaml = r#"
942title: Whoami
943logsource:
944 category: test
945detection:
946 selection:
947 field: value
948 condition: selection
949custom_attributes:
950 rsigma.exemplars:
951 - expect: banana
952 event:
953 field: value
954"#;
955 let err = run_exemplars(&collection(yaml), &[]).unwrap_err();
956 assert!(matches!(err, ExemplarRunError::Shape { .. }), "{err}");
957 }
958
959 #[test]
960 fn filter_exemplars_are_rejected() {
961 let yaml = r#"
962title: F
963logsource:
964 category: test
965filter:
966 selection:
967 User: SYSTEM
968 condition: selection
969custom_attributes:
970 rsigma.exemplars:
971 - expect: match
972 event:
973 User: SYSTEM
974"#;
975 let err = run_exemplars(&collection(yaml), &[]).unwrap_err();
976 assert!(matches!(err, ExemplarRunError::Shape { .. }), "{err}");
977 }
978}