1use crate::scanner::regex_rule::access_regex_caches;
2use crate::scanner::regex_rule::compiled::RegexCompiledRule;
3use crate::scanner::scope::ScanArea;
4use crate::{
5 CreateScannerError, Event, MatchAction, ProximityKeywordsConfig, RegexRuleConfig,
6 RootRuleConfig, RuleConfig, RuleMatch, Scanner, ScannerError, Scope,
7};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use thiserror::Error;
11
12#[derive(Debug, Serialize, Deserialize)]
13pub struct DebugRuleMatch {
14 pub rule_match: RuleMatch,
15 pub statuses: Vec<DebugRuleMatchStatus>,
16}
17
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19#[serde(tag = "status")]
20pub enum DebugRuleMatchStatus {
21 Matched(MatchedInfo),
22 MissingIncludedKeyword,
23 IncludedKeywordTooFar,
24 ExcludedKeyword(ExcludedInfo),
25 NotInIncludedScope,
26 InExcludedScope,
27 Suppressed,
28 ChecksumFailed,
29}
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
32pub struct MatchedInfo {
33 included_keyword: Option<String>,
34 included_keyword_start_index: Option<usize>,
35 included_keyword_end_exclusive: Option<usize>,
36}
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39pub struct ExcludedInfo {
40 pub excluded_keyword: Option<String>,
41 pub excluded_keyword_start_index: Option<usize>,
42 pub excluded_keyword_end_exclusive: Option<usize>,
43}
44
45#[derive(Debug, PartialEq, Eq, Error)]
46pub enum DebugScanError {
47 #[error(transparent)]
48 InvalidConfig(CreateScannerError),
49 #[error(transparent)]
50 ScanError(ScannerError),
51}
52
53impl From<CreateScannerError> for DebugScanError {
54 fn from(value: CreateScannerError) -> Self {
55 Self::InvalidConfig(value)
56 }
57}
58
59impl From<ScannerError> for DebugScanError {
60 fn from(value: ScannerError) -> Self {
61 Self::ScanError(value)
62 }
63}
64
65pub fn debug_scan<E: Event>(
74 event: &mut E,
75 mut rule: RootRuleConfig<Arc<dyn RuleConfig>>,
76) -> Result<Vec<DebugRuleMatch>, DebugScanError> {
77 rule.match_action = MatchAction::None;
79
80 let full_scanner = single_rule_scanner(rule.clone())?;
81 let full_matches = full_scanner.scan(event)?;
82 let mut output =
83 matched_debug_matches(event, full_matches, full_scanner.rules[0].as_regex_rule());
84
85 let relaxed_rule = relaxed_rule_from(&rule);
86 let probes = condition_probes(event, &rule, &relaxed_rule)?;
87 if !probes.is_empty() {
88 let candidates = single_rule_scanner(relaxed_rule)?.scan(event)?;
89 let partials = partial_debug_matches(
90 event,
91 candidates,
92 &probes,
93 full_scanner.rules[0].as_regex_rule(),
94 &output,
95 );
96 output.extend(partials);
97 }
98
99 output.sort_by(|left, right| {
100 left.rule_match.path.cmp(&right.rule_match.path).then(
101 left.rule_match
102 .start_index
103 .cmp(&right.rule_match.start_index),
104 )
105 });
106
107 Ok(output)
108}
109
110fn single_rule_scanner(
111 rule: RootRuleConfig<Arc<dyn RuleConfig>>,
112) -> Result<Scanner, DebugScanError> {
113 Scanner::builder(&[rule])
114 .build()
115 .map_err(DebugScanError::InvalidConfig)
116}
117
118fn relaxed_rule_from(
121 rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
122) -> RootRuleConfig<Arc<dyn RuleConfig>> {
123 let mut relaxed_rule = rule.clone();
124 relaxed_rule.scope = Scope::all();
125 relaxed_rule.suppressions = None;
126 if let Some(regex_rule) = rule.inner.as_regex_rule() {
127 let mut relaxed_regex_rule = regex_rule.clone();
128 relaxed_regex_rule.proximity_keywords = None;
129 relaxed_regex_rule.validator = None;
130 relaxed_rule = relaxed_rule.map_inner(|_| relaxed_regex_rule.build());
131 }
132 relaxed_rule
133}
134
135fn cloned_relaxed_regex_rule(
136 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
137) -> RegexRuleConfig {
138 relaxed_rule
139 .inner
140 .as_regex_rule()
141 .expect("relaxing a regex rule must preserve its rule type")
142 .clone()
143}
144
145fn matched_debug_matches<E: Event>(
146 event: &mut E,
147 full_matches: Vec<RuleMatch>,
148 regex_compiled_rule: Option<&RegexCompiledRule>,
149) -> Vec<DebugRuleMatch> {
150 full_matches
151 .into_iter()
152 .map(|rule_match| {
153 let matched_info = included_keyword_info(event, &rule_match, regex_compiled_rule);
154 DebugRuleMatch {
155 rule_match,
156 statuses: vec![DebugRuleMatchStatus::Matched(matched_info)],
157 }
158 })
159 .collect()
160}
161
162fn partial_debug_matches<E: Event>(
165 event: &mut E,
166 candidates: Vec<RuleMatch>,
167 probes: &[ConditionProbe],
168 regex_compiled_rule: Option<&RegexCompiledRule>,
169 full_matches: &[DebugRuleMatch],
170) -> Vec<DebugRuleMatch> {
171 let mut partials = Vec::new();
172 for candidate in candidates {
173 if full_matches
174 .iter()
175 .any(|full_match| full_match.rule_match == candidate)
176 {
177 continue;
178 }
179
180 let statuses = failing_statuses(event, &candidate, probes, regex_compiled_rule);
181
182 if !statuses.is_empty() {
189 partials.push(DebugRuleMatch {
190 rule_match: candidate,
191 statuses,
192 });
193 }
194 }
195 partials
196}
197
198fn failing_statuses<E: Event>(
199 event: &mut E,
200 candidate: &RuleMatch,
201 probes: &[ConditionProbe],
202 regex_compiled_rule: Option<&RegexCompiledRule>,
203) -> Vec<DebugRuleMatchStatus> {
204 probes
205 .iter()
206 .filter(|probe| {
207 !probe
208 .matches
209 .iter()
210 .any(|probe_match| probe_match == candidate)
211 })
212 .map(|probe| {
213 probe
214 .condition
215 .status(event, candidate, regex_compiled_rule)
216 })
217 .collect()
218}
219
220#[derive(Clone, Copy)]
221enum DebugCondition {
222 IncludedKeyword,
223 ExcludedKeyword,
224 Checksum,
225 IncludedScope,
226 ExcludedScope,
227 Suppression,
228}
229
230impl DebugCondition {
231 fn status<E: Event>(
232 self,
233 event: &mut E,
234 rule_match: &RuleMatch,
235 regex_compiled_rule: Option<&RegexCompiledRule>,
236 ) -> DebugRuleMatchStatus {
237 match self {
238 Self::IncludedKeyword => DebugRuleMatchStatus::MissingIncludedKeyword,
239 Self::ExcludedKeyword => DebugRuleMatchStatus::ExcludedKeyword(excluded_keyword_info(
240 event,
241 rule_match,
242 regex_compiled_rule,
243 )),
244 Self::Checksum => DebugRuleMatchStatus::ChecksumFailed,
245 Self::IncludedScope => DebugRuleMatchStatus::NotInIncludedScope,
246 Self::ExcludedScope => DebugRuleMatchStatus::InExcludedScope,
247 Self::Suppression => DebugRuleMatchStatus::Suppressed,
248 }
249 }
250}
251
252struct ConditionProbe {
253 condition: DebugCondition,
254 matches: Vec<RuleMatch>,
255}
256
257fn condition_probes<E: Event>(
261 event: &mut E,
262 root_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
263 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
264) -> Result<Vec<ConditionProbe>, DebugScanError> {
265 let mut probes = Vec::new();
266
267 if let Some(regex_rule) = root_rule.inner.as_regex_rule() {
268 if let Some(probe) = included_keyword_probe(event, relaxed_rule, regex_rule)? {
269 probes.push(probe);
270 }
271 if let Some(probe) = excluded_keyword_probe(event, relaxed_rule, regex_rule)? {
272 probes.push(probe);
273 }
274 if let Some(probe) = checksum_probe(event, relaxed_rule, regex_rule)? {
275 probes.push(probe);
276 }
277 }
278
279 if let Some(probe) = included_scope_probe(event, root_rule, relaxed_rule)? {
280 probes.push(probe);
281 }
282 if let Some(probe) = excluded_scope_probe(event, root_rule, relaxed_rule)? {
283 probes.push(probe);
284 }
285 if let Some(probe) = suppression_probe(event, root_rule, relaxed_rule)? {
286 probes.push(probe);
287 }
288
289 Ok(probes)
290}
291
292fn included_keyword_probe<E: Event>(
293 event: &mut E,
294 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
295 regex_rule: &RegexRuleConfig,
296) -> Result<Option<ConditionProbe>, DebugScanError> {
297 let Some(proximity_keywords) = ®ex_rule.proximity_keywords else {
298 return Ok(None);
299 };
300 if proximity_keywords.included_keywords.is_empty() {
301 return Ok(None);
302 }
303
304 let mut condition_regex_rule = cloned_relaxed_regex_rule(relaxed_rule);
305 condition_regex_rule.proximity_keywords = Some(ProximityKeywordsConfig {
306 excluded_keywords: vec![],
307 ..proximity_keywords.clone()
308 });
309
310 scan_probe(
311 event,
312 relaxed_rule
313 .clone()
314 .map_inner(|_| condition_regex_rule.build()),
315 DebugCondition::IncludedKeyword,
316 )
317 .map(Some)
318}
319
320fn excluded_keyword_probe<E: Event>(
321 event: &mut E,
322 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
323 regex_rule: &RegexRuleConfig,
324) -> Result<Option<ConditionProbe>, DebugScanError> {
325 let Some(proximity_keywords) = ®ex_rule.proximity_keywords else {
326 return Ok(None);
327 };
328 if proximity_keywords.excluded_keywords.is_empty() {
329 return Ok(None);
330 }
331
332 let mut condition_regex_rule = cloned_relaxed_regex_rule(relaxed_rule);
333 condition_regex_rule.proximity_keywords = Some(ProximityKeywordsConfig {
334 included_keywords: vec![],
335 ..proximity_keywords.clone()
336 });
337
338 scan_probe(
339 event,
340 relaxed_rule
341 .clone()
342 .map_inner(|_| condition_regex_rule.build()),
343 DebugCondition::ExcludedKeyword,
344 )
345 .map(Some)
346}
347
348fn checksum_probe<E: Event>(
349 event: &mut E,
350 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
351 regex_rule: &RegexRuleConfig,
352) -> Result<Option<ConditionProbe>, DebugScanError> {
353 if regex_rule.validator.is_none() {
354 return Ok(None);
355 }
356
357 let mut condition_regex_rule = cloned_relaxed_regex_rule(relaxed_rule);
358 condition_regex_rule.validator = regex_rule.validator.clone();
359
360 scan_probe(
361 event,
362 relaxed_rule
363 .clone()
364 .map_inner(|_| condition_regex_rule.build()),
365 DebugCondition::Checksum,
366 )
367 .map(Some)
368}
369
370fn included_scope_probe<E: Event>(
371 event: &mut E,
372 root_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
373 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
374) -> Result<Option<ConditionProbe>, DebugScanError> {
375 let ScanArea::Only(include) = &root_rule.scope.include else {
376 return Ok(None);
377 };
378
379 let mut condition_rule = relaxed_rule.clone();
380 condition_rule.scope = Scope::include(include.clone());
381
382 scan_probe(event, condition_rule, DebugCondition::IncludedScope).map(Some)
383}
384
385fn excluded_scope_probe<E: Event>(
386 event: &mut E,
387 root_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
388 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
389) -> Result<Option<ConditionProbe>, DebugScanError> {
390 let exclude = &root_rule.scope.exclude;
391 if exclude.is_empty() {
392 return Ok(None);
393 }
394
395 let mut condition_rule = relaxed_rule.clone();
396 condition_rule.scope = Scope::exclude(exclude.clone());
397
398 scan_probe(event, condition_rule, DebugCondition::ExcludedScope).map(Some)
399}
400
401fn suppression_probe<E: Event>(
402 event: &mut E,
403 root_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
404 relaxed_rule: &RootRuleConfig<Arc<dyn RuleConfig>>,
405) -> Result<Option<ConditionProbe>, DebugScanError> {
406 let Some(suppressions) = &root_rule.suppressions else {
407 return Ok(None);
408 };
409 if suppressions.starts_with.is_empty()
410 && suppressions.ends_with.is_empty()
411 && suppressions.exact_match.is_empty()
412 {
413 return Ok(None);
414 }
415
416 let mut condition_rule = relaxed_rule.clone();
417 condition_rule.suppressions = Some(suppressions.clone());
418
419 scan_probe(event, condition_rule, DebugCondition::Suppression).map(Some)
420}
421
422fn scan_probe<E: Event>(
423 event: &mut E,
424 rule: RootRuleConfig<Arc<dyn RuleConfig>>,
425 condition: DebugCondition,
426) -> Result<ConditionProbe, DebugScanError> {
427 let scanner = single_rule_scanner(rule)?;
428 Ok(ConditionProbe {
429 condition,
430 matches: scanner.scan(event)?,
431 })
432}
433
434fn included_keyword_info<E: Event>(
435 event: &mut E,
436 rule_match: &RuleMatch,
437 regex_compiled_rule: Option<&RegexCompiledRule>,
438) -> MatchedInfo {
439 let mut included_info = MatchedInfo {
440 included_keyword: None,
441 included_keyword_start_index: None,
442 included_keyword_end_exclusive: None,
443 };
444
445 if let Some(compiled_included_keywords) =
446 regex_compiled_rule.and_then(|rule| rule.included_keywords.as_ref())
447 {
448 event.visit_string_mut(&rule_match.path, |content| {
449 access_regex_caches(|caches| {
450 if let Some(info) = compiled_included_keywords.find_keyword_before_match(
451 rule_match.start_index,
452 caches,
453 content,
454 ) {
455 included_info.included_keyword = Some(info.keyword);
456 included_info.included_keyword_start_index = Some(info.keyword_start_index);
457 included_info.included_keyword_end_exclusive =
458 Some(info.keyword_end_index_exclusive);
459 }
460 });
461 false
462 });
463 }
464
465 included_info
466}
467
468fn excluded_keyword_info<E: Event>(
469 event: &mut E,
470 rule_match: &RuleMatch,
471 regex_compiled_rule: Option<&RegexCompiledRule>,
472) -> ExcludedInfo {
473 let mut excluded_info = ExcludedInfo {
474 excluded_keyword: None,
475 excluded_keyword_start_index: None,
476 excluded_keyword_end_exclusive: None,
477 };
478
479 if let Some(compiled_excluded_keywords) =
480 regex_compiled_rule.and_then(|rule| rule.excluded_keywords.as_ref())
481 {
482 event.visit_string_mut(&rule_match.path, |content| {
483 if let Some(info) =
484 compiled_excluded_keywords.get_false_positive_match(content, rule_match.start_index)
485 {
486 excluded_info.excluded_keyword_start_index = Some(info.start());
487 excluded_info.excluded_keyword_end_exclusive = Some(info.end());
488 excluded_info.excluded_keyword =
489 Some(content[info.start()..info.end()].to_string());
490 }
491 false
492 });
493 }
494
495 excluded_info
496}
497
498#[cfg(test)]
499mod test {
500 use super::*;
501 use crate::{
502 EventVisitor, MatchAction, Path, PathSegment, RegexRuleConfig, RootRuleConfig,
503 SecondaryValidator, SimpleEvent, Suppressions, Utf8Encoding,
504 };
505 use std::collections::BTreeMap;
506
507 struct CountingEvent {
508 content: String,
509 visit_count: usize,
510 }
511
512 impl Event for CountingEvent {
513 type Encoding = Utf8Encoding;
514
515 fn visit_event<'path>(
516 &'path mut self,
517 visitor: &mut impl EventVisitor<'path>,
518 ) -> Result<(), ScannerError> {
519 self.visit_count += 1;
520 visitor.visit_string(&self.content).map(|_| ())
521 }
522
523 fn visit_string_mut(&mut self, _path: &Path, visit: impl FnOnce(&mut String) -> bool) {
524 (visit)(&mut self.content);
525 }
526 }
527
528 #[test]
529 fn test_full_match() {
530 let rule_config = RootRuleConfig::new(RegexRuleConfig::new("secret").build());
531
532 let mut msg = "This is a secret".to_string();
533 let matches = debug_scan(&mut msg, rule_config).unwrap();
534
535 assert_eq!(matches.len(), 1);
537 assert_eq!(
538 matches[0].statuses,
539 vec![DebugRuleMatchStatus::Matched(MatchedInfo {
540 included_keyword: None,
541 included_keyword_start_index: None,
542 included_keyword_end_exclusive: None,
543 })]
544 );
545 assert_eq!(matches[0].rule_match.start_index, 10);
546 }
547
548 #[test]
549 fn test_rule_without_conditions_scans_event_once() {
550 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build());
551 let mut event = CountingEvent {
552 content: "secret".to_string(),
553 visit_count: 0,
554 };
555
556 let matches = debug_scan(&mut event, rule).unwrap();
557
558 assert_eq!(matches.len(), 1);
559 assert_eq!(event.visit_count, 1);
560 }
561
562 #[test]
563 fn test_rule_with_empty_suppressions_scans_event_once() {
564 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build())
565 .suppressions(Suppressions::default());
566 let mut event = CountingEvent {
567 content: "secret".to_string(),
568 visit_count: 0,
569 };
570
571 let matches = debug_scan(&mut event, rule).unwrap();
572
573 assert_eq!(matches.len(), 1);
574 assert_eq!(event.visit_count, 1);
575 }
576
577 #[test]
578 fn test_full_match_with_included_keyword() {
579 let rule_config = RootRuleConfig::new(
580 RegexRuleConfig::new("secret")
581 .with_included_keywords(&["a"])
582 .build(),
583 );
584
585 let mut msg = "This is a secret".to_string();
586 let matches = debug_scan(&mut msg, rule_config).unwrap();
587
588 assert_eq!(matches.len(), 1);
589 assert_eq!(
590 matches[0].statuses,
591 vec![DebugRuleMatchStatus::Matched(MatchedInfo {
592 included_keyword: Some("a".to_string()),
593 included_keyword_start_index: Some(8),
594 included_keyword_end_exclusive: Some(9),
595 })]
596 );
597 assert_eq!(matches[0].rule_match.start_index, 10);
598 }
599
600 #[test]
601 fn test_missing_included_keyword() {
602 let rule = RootRuleConfig::new(
603 RegexRuleConfig::new("secret")
604 .with_included_keywords(&["value"])
605 .build(),
606 )
607 .match_action(MatchAction::redact("[REDACTED]"));
608
609 let mut msg = "This is a secret".to_string();
610 let matches = debug_scan(&mut msg, rule).unwrap();
611
612 assert_eq!(matches.len(), 1);
613 assert_eq!(
614 matches[0].statuses,
615 vec![DebugRuleMatchStatus::MissingIncludedKeyword]
616 );
617 assert_eq!(matches[0].rule_match.start_index, 10);
618 }
619
620 #[test]
621 fn test_with_excluded_keyword() {
622 let rule = RootRuleConfig::new(
623 RegexRuleConfig::new("secret")
624 .with_excluded_keywords(&["a"])
625 .build(),
626 )
627 .match_action(MatchAction::redact("[REDACTED]"));
628
629 let mut msg = "This is a secret".to_string();
630 let matches = debug_scan(&mut msg, rule).unwrap();
631
632 assert_eq!(matches.len(), 1);
633 assert_eq!(
634 matches[0].statuses,
635 vec![DebugRuleMatchStatus::ExcludedKeyword(ExcludedInfo {
636 excluded_keyword: Some("a".to_string()),
637 excluded_keyword_start_index: Some(8),
638 excluded_keyword_end_exclusive: Some(9),
639 })]
640 );
641 }
642
643 #[test]
644 fn test_suppressions() {
645 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build())
646 .match_action(MatchAction::redact("[REDACTED]"))
647 .suppressions(Suppressions {
648 starts_with: vec![],
649 ends_with: vec![],
650 exact_match: vec!["secret".to_string()],
651 });
652
653 let mut msg = "This is a secret".to_string();
654 let matches = debug_scan(&mut msg, rule).unwrap();
655
656 assert_eq!(matches.len(), 1);
657 assert_eq!(matches[0].statuses, vec![DebugRuleMatchStatus::Suppressed]);
658 assert_eq!(matches[0].rule_match.start_index, 10);
659 }
660
661 #[test]
662 fn test_included_scope() {
663 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build())
664 .match_action(MatchAction::redact("[REDACTED]"))
665 .scope(Scope::include(vec![Path::from(vec![PathSegment::from(
666 "tag",
667 )])]));
668
669 let mut map = BTreeMap::new();
670 map.insert(
671 "tag".to_string(),
672 SimpleEvent::String("Not a match".to_string()),
673 );
674 map.insert(
675 "tag2".to_string(),
676 SimpleEvent::String("This is a secret".to_string()),
677 );
678
679 let mut event = SimpleEvent::Map(map);
680 let matches = debug_scan(&mut event, rule).unwrap();
681
682 assert_eq!(matches.len(), 1);
683 assert_eq!(
684 matches[0].statuses,
685 vec![DebugRuleMatchStatus::NotInIncludedScope]
686 );
687 assert_eq!(matches[0].rule_match.start_index, 10);
688 }
689
690 #[test]
691 fn test_excluded_scope() {
692 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build())
693 .match_action(MatchAction::redact("[REDACTED]"))
694 .scope(Scope::exclude(vec![Path::from(vec![PathSegment::from(
695 "tag",
696 )])]));
697
698 let mut map = BTreeMap::new();
699 map.insert(
700 "tag".to_string(),
701 SimpleEvent::String("Contains a secret".to_string()),
702 );
703
704 let mut event = SimpleEvent::Map(map);
705 let matches = debug_scan(&mut event, rule).unwrap();
706
707 assert_eq!(matches.len(), 1);
708 assert_eq!(
709 matches[0].statuses,
710 vec![DebugRuleMatchStatus::InExcludedScope]
711 );
712 }
713
714 #[test]
715 fn test_checksum() {
716 let rule = RootRuleConfig::new(
717 RegexRuleConfig::new("[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}")
718 .with_validator(Some(SecondaryValidator::LuhnChecksum))
719 .build(),
720 );
721
722 let mut event = "1234-1234-1234-1235".to_string();
723 let matches = debug_scan(&mut event, rule).unwrap();
724
725 assert_eq!(matches.len(), 1);
726 assert_eq!(
727 matches[0].statuses,
728 vec![DebugRuleMatchStatus::ChecksumFailed]
729 );
730 }
731
732 #[test]
733 fn test_missing_included_keyword_and_checksum_failure() {
734 let rule = RootRuleConfig::new(
735 RegexRuleConfig::new("[0-9]+")
736 .with_included_keywords(&["value"])
737 .with_validator(Some(SecondaryValidator::LuhnChecksum))
738 .build(),
739 );
740
741 let mut event = "1234".to_string();
742 let matches = debug_scan(&mut event, rule).unwrap();
743
744 assert_eq!(matches.len(), 1);
745 assert_eq!(
746 matches[0].statuses,
747 vec![
748 DebugRuleMatchStatus::MissingIncludedKeyword,
749 DebugRuleMatchStatus::ChecksumFailed,
750 ]
751 );
752 }
753
754 #[test]
755 fn test_multi_status_serialization() {
756 let rule = RootRuleConfig::new(
757 RegexRuleConfig::new("[0-9]+")
758 .with_included_keywords(&["value"])
759 .with_validator(Some(SecondaryValidator::LuhnChecksum))
760 .build(),
761 );
762 let mut event = "1234".to_string();
763
764 let matches = debug_scan(&mut event, rule).unwrap();
765 let serialized_match = serde_json::to_value(&matches[0]).unwrap();
766
767 assert_eq!(
768 serialized_match["statuses"],
769 serde_json::json!([
770 { "status": "MissingIncludedKeyword" },
771 { "status": "ChecksumFailed" }
772 ])
773 );
774 assert!(serialized_match.get("status").is_none());
775 }
776
777 #[test]
778 fn test_included_keyword_too_far_deserialization() {
779 let status: DebugRuleMatchStatus =
780 serde_json::from_str(r#"{"status":"IncludedKeywordTooFar"}"#).unwrap();
781
782 assert_eq!(status, DebugRuleMatchStatus::IncludedKeywordTooFar);
783 }
784
785 #[test]
786 fn test_missing_included_keyword_and_suppression() {
787 let rule = RootRuleConfig::new(
788 RegexRuleConfig::new("secret")
789 .with_included_keywords(&["value"])
790 .build(),
791 )
792 .suppressions(Suppressions {
793 starts_with: vec![],
794 ends_with: vec![],
795 exact_match: vec!["secret".to_string()],
796 });
797
798 let mut event = "secret".to_string();
799 let matches = debug_scan(&mut event, rule).unwrap();
800
801 assert_eq!(matches.len(), 1);
802 assert_eq!(
803 matches[0].statuses,
804 vec![
805 DebugRuleMatchStatus::MissingIncludedKeyword,
806 DebugRuleMatchStatus::Suppressed,
807 ]
808 );
809 }
810
811 #[test]
812 fn test_three_simultaneous_failures() {
813 let rule = RootRuleConfig::new(
814 RegexRuleConfig::new("[0-9]+")
815 .with_included_keywords(&["value"])
816 .with_validator(Some(SecondaryValidator::LuhnChecksum))
817 .build(),
818 )
819 .suppressions(Suppressions {
820 starts_with: vec![],
821 ends_with: vec![],
822 exact_match: vec!["1234".to_string()],
823 });
824
825 let mut event = "1234".to_string();
826 let matches = debug_scan(&mut event, rule).unwrap();
827
828 assert_eq!(matches.len(), 1);
829 assert_eq!(
830 matches[0].statuses,
831 vec![
832 DebugRuleMatchStatus::MissingIncludedKeyword,
833 DebugRuleMatchStatus::ChecksumFailed,
834 DebugRuleMatchStatus::Suppressed,
835 ]
836 );
837 }
838
839 #[test]
840 fn test_outside_included_scope_and_inside_excluded_scope() {
841 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build()).scope(
842 Scope::include_and_exclude(
843 vec![Path::from(vec![PathSegment::from("allowed")])],
844 vec![Path::from(vec![PathSegment::from("blocked")])],
845 ),
846 );
847
848 let mut map = BTreeMap::new();
849 map.insert(
850 "blocked".to_string(),
851 SimpleEvent::String("secret".to_string()),
852 );
853 let mut event = SimpleEvent::Map(map);
854 let matches = debug_scan(&mut event, rule).unwrap();
855
856 assert_eq!(matches.len(), 1);
857 assert_eq!(
858 matches[0].statuses,
859 vec![
860 DebugRuleMatchStatus::NotInIncludedScope,
861 DebugRuleMatchStatus::InExcludedScope,
862 ]
863 );
864 }
865
866 #[test]
867 fn test_multipass_rejection_reports_excluded_scope() {
868 let rule = RootRuleConfig::new(RegexRuleConfig::new("secret").build()).scope(
869 Scope::exclude(vec![Path::from(vec![PathSegment::from("outside")])]),
870 );
871
872 let mut map = BTreeMap::new();
873 map.insert(
874 "allowed".to_string(),
875 SimpleEvent::String("secret".to_string()),
876 );
877 map.insert(
878 "outside".to_string(),
879 SimpleEvent::String("secret".to_string()),
880 );
881 let mut event = SimpleEvent::Map(map);
882
883 let matches = debug_scan(&mut event, rule).unwrap();
884 let allowed_match = matches
885 .iter()
886 .find(|debug_match| {
887 debug_match.rule_match.path == Path::from(vec![PathSegment::from("allowed")])
888 })
889 .unwrap();
890
891 assert_eq!(
892 allowed_match.statuses,
893 vec![DebugRuleMatchStatus::InExcludedScope]
894 );
895 }
896
897 #[test]
898 fn test_excluded_keyword_and_checksum_failure_preserve_keyword_info() {
899 let rule = RootRuleConfig::new(
900 RegexRuleConfig::new("[0-9]+")
901 .with_excluded_keywords(&["blocked"])
902 .with_validator(Some(SecondaryValidator::LuhnChecksum))
903 .build(),
904 );
905
906 let mut event = "blocked 1234".to_string();
907 let matches = debug_scan(&mut event, rule).unwrap();
908
909 assert_eq!(matches.len(), 1);
910 assert_eq!(
911 matches[0].statuses,
912 vec![
913 DebugRuleMatchStatus::ExcludedKeyword(ExcludedInfo {
914 excluded_keyword: Some("blocked".to_string()),
915 excluded_keyword_start_index: Some(0),
916 excluded_keyword_end_exclusive: Some(7),
917 }),
918 DebugRuleMatchStatus::ChecksumFailed,
919 ]
920 );
921 }
922
923 #[test]
924 fn test_missing_included_keyword_and_excluded_keyword() {
925 let rule = RootRuleConfig::new(
926 RegexRuleConfig::new("secret")
927 .with_included_keywords(&["value"])
928 .with_excluded_keywords(&["blocked"])
929 .build(),
930 );
931
932 let mut event = "blocked secret".to_string();
933 let matches = debug_scan(&mut event, rule).unwrap();
934
935 assert_eq!(matches.len(), 1);
936 assert_eq!(
937 matches[0].statuses,
938 vec![
939 DebugRuleMatchStatus::MissingIncludedKeyword,
940 DebugRuleMatchStatus::ExcludedKeyword(ExcludedInfo {
941 excluded_keyword: Some("blocked".to_string()),
942 excluded_keyword_start_index: Some(0),
943 excluded_keyword_end_exclusive: Some(7),
944 }),
945 ]
946 );
947 }
948
949 #[test]
950 fn test_missing_included_keyword_and_excluded_scope() {
951 let rule = RootRuleConfig::new(
952 RegexRuleConfig::new("secret")
953 .with_included_keywords(&["value"])
954 .build(),
955 )
956 .scope(Scope::exclude(vec![Path::from(vec![PathSegment::from(
957 "tag",
958 )])]));
959
960 let mut map = BTreeMap::new();
961 map.insert("tag".to_string(), SimpleEvent::String("secret".to_string()));
962
963 let mut event = SimpleEvent::Map(map);
964 let matches = debug_scan(&mut event, rule).unwrap();
965
966 assert_eq!(matches.len(), 1);
967 assert_eq!(
968 matches[0].statuses,
969 vec![
970 DebugRuleMatchStatus::MissingIncludedKeyword,
971 DebugRuleMatchStatus::InExcludedScope,
972 ]
973 );
974 }
975
976 #[test]
977 fn test_each_candidate_reports_its_own_statuses() {
978 let rule = RootRuleConfig::new(
979 RegexRuleConfig::new("[0-9]+")
980 .with_excluded_keywords(&["blocked"])
981 .build(),
982 )
983 .suppressions(Suppressions {
984 starts_with: vec![],
985 ends_with: vec![],
986 exact_match: vec!["4242".to_string()],
987 });
988
989 let mut map = BTreeMap::new();
990 map.insert(
991 "excluded".to_string(),
992 SimpleEvent::String("blocked 1234".to_string()),
993 );
994 map.insert(
995 "suppressed".to_string(),
996 SimpleEvent::String("4242".to_string()),
997 );
998
999 let mut event = SimpleEvent::Map(map);
1000 let matches = debug_scan(&mut event, rule).unwrap();
1001
1002 assert_eq!(matches.len(), 2);
1003 assert_eq!(
1004 matches[0].rule_match.path,
1005 Path::from(vec![PathSegment::from("excluded")])
1006 );
1007 assert_eq!(
1008 matches[0].statuses,
1009 vec![DebugRuleMatchStatus::ExcludedKeyword(ExcludedInfo {
1010 excluded_keyword: Some("blocked".to_string()),
1011 excluded_keyword_start_index: Some(0),
1012 excluded_keyword_end_exclusive: Some(7),
1013 })]
1014 );
1015 assert_eq!(
1016 matches[1].rule_match.path,
1017 Path::from(vec![PathSegment::from("suppressed")])
1018 );
1019 assert_eq!(matches[1].statuses, vec![DebugRuleMatchStatus::Suppressed]);
1020 }
1021
1022 #[test]
1023 fn test_full_match_and_partial_match_in_same_event() {
1024 let rule = RootRuleConfig::new(
1025 RegexRuleConfig::new("secret")
1026 .with_excluded_keywords(&["blocked"])
1027 .build(),
1028 );
1029
1030 let mut map = BTreeMap::new();
1031 map.insert(
1032 "allowed".to_string(),
1033 SimpleEvent::String("secret".to_string()),
1034 );
1035 map.insert(
1036 "blocked".to_string(),
1037 SimpleEvent::String("blocked secret".to_string()),
1038 );
1039
1040 let mut event = SimpleEvent::Map(map);
1041 let matches = debug_scan(&mut event, rule).unwrap();
1042
1043 assert_eq!(matches.len(), 2);
1044 assert_eq!(
1045 matches[0].rule_match.path,
1046 Path::from(vec![PathSegment::from("allowed")])
1047 );
1048 assert_eq!(
1049 matches[0].statuses,
1050 vec![DebugRuleMatchStatus::Matched(MatchedInfo {
1051 included_keyword: None,
1052 included_keyword_start_index: None,
1053 included_keyword_end_exclusive: None,
1054 })]
1055 );
1056 assert_eq!(
1057 matches[1].rule_match.path,
1058 Path::from(vec![PathSegment::from("blocked")])
1059 );
1060 assert_eq!(
1061 matches[1].statuses,
1062 vec![DebugRuleMatchStatus::ExcludedKeyword(ExcludedInfo {
1063 excluded_keyword: Some("blocked".to_string()),
1064 excluded_keyword_start_index: Some(0),
1065 excluded_keyword_end_exclusive: Some(7),
1066 })]
1067 );
1068 }
1069
1070 #[test]
1071 fn test_no_regex_candidate_returns_no_matches() {
1072 let rule = RootRuleConfig::new(
1073 RegexRuleConfig::new("secret")
1074 .with_included_keywords(&["value"])
1075 .with_validator(Some(SecondaryValidator::LuhnChecksum))
1076 .build(),
1077 );
1078
1079 let mut event = "nothing to find".to_string();
1080 let matches = debug_scan(&mut event, rule).unwrap();
1081
1082 assert!(matches.is_empty());
1083 }
1084}