From 041c5542a628eeee59a34569334abad8aa1f1f27 Mon Sep 17 00:00:00 2001 From: Taiki Date: Fri, 28 Aug 2026 15:56:15 +0200 Subject: [PATCH 1/6] Deserialize the rule tags into the temporary RootRuleConfig structure --- sds/src/scanner/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sds/src/scanner/mod.rs b/sds/src/scanner/mod.rs index 13ecce21..8e7c0b59 100644 --- a/sds/src/scanner/mod.rs +++ b/sds/src/scanner/mod.rs @@ -31,6 +31,7 @@ use ahash::AHashMap; use futures::executor::block_on; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use std::collections::BTreeMap; use std::ops::Deref; use std::pin::Pin; use std::sync::Arc; @@ -107,6 +108,8 @@ pub struct RootRuleConfig { precedence: Precedence, #[serde(default)] pub is_supporting_rule: bool, + #[serde(default)] + pub tags: BTreeMap, #[serde(flatten)] pub inner: T, } @@ -135,6 +138,7 @@ impl RootRuleConfig { suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + tags: BTreeMap::new(), inner, } } @@ -149,6 +153,7 @@ impl RootRuleConfig { suppressions: self.suppressions, precedence: self.precedence, is_supporting_rule: self.is_supporting_rule, + tags: self.tags, inner: func(self.inner), } } @@ -186,6 +191,11 @@ impl RootRuleConfig { self } + pub fn tags(mut self, tags: BTreeMap) -> Self { + self.tags = tags; + self + } + fn get_third_party_active_checker(&self) -> Option<&MatchValidationType> { #[allow(deprecated)] self.third_party_active_checker From 5343dadfe1f29ed3bc01d7a59f92fe1fa26a7513 Mon Sep 17 00:00:00 2001 From: Taiki Date: Fri, 28 Aug 2026 15:57:19 +0200 Subject: [PATCH 2/6] Introduce the new `sds_rule_name` field in RootCompiledRule --- sds/src/match_validation/http_validator_v2.rs | 7 +++++++ sds/src/scanner/mod.rs | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/sds/src/match_validation/http_validator_v2.rs b/sds/src/match_validation/http_validator_v2.rs index ae936d68..f4e4b7dd 100644 --- a/sds/src/match_validation/http_validator_v2.rs +++ b/sds/src/match_validation/http_validator_v2.rs @@ -556,6 +556,7 @@ mod tests { suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, } } @@ -1369,6 +1370,7 @@ calls: suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, }, RootCompiledRule { inner: Box::new(MockCompiledRule), @@ -1387,6 +1389,7 @@ calls: suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, }, ]; @@ -1496,6 +1499,7 @@ match_pairing: suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, }, RootCompiledRule { inner: Box::new(MockCompiledRule), @@ -1514,6 +1518,7 @@ match_pairing: suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, }, ]; @@ -1710,6 +1715,7 @@ match_pairing: suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, }, RootCompiledRule { inner: Box::new(MockCompiledRule), @@ -1728,6 +1734,7 @@ match_pairing: suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, + sds_rule_name: None, }, ]; diff --git a/sds/src/scanner/mod.rs b/sds/src/scanner/mod.rs index 8e7c0b59..7b816e48 100644 --- a/sds/src/scanner/mod.rs +++ b/sds/src/scanner/mod.rs @@ -219,6 +219,10 @@ pub struct RootCompiledRule { pub suppressions: Option, pub precedence: Precedence, pub is_supporting_rule: bool, + /// Precomputed `"{sensitive_data_category}/{sensitive_data}"` tag value derived from + /// the rule's `tags`, used to tag `scanning.match_count`. `None` when the rule has no + /// `sensitive_data` tag. + pub sds_rule_name: Option, } impl RootCompiledRule { From 8981bd714af74e45e3674b65c82d9d13cfe10e9d Mon Sep 17 00:00:00 2001 From: Taiki Date: Fri, 28 Aug 2026 15:57:41 +0200 Subject: [PATCH 3/6] Populate sds_rule_name based on the tag values --- sds/src/scanner/metrics.rs | 63 ++++++++++++++++++++++++++++++++++++++ sds/src/scanner/mod.rs | 1 + 2 files changed, 64 insertions(+) diff --git a/sds/src/scanner/metrics.rs b/sds/src/scanner/metrics.rs index a50d1528..706532e0 100644 --- a/sds/src/scanner/metrics.rs +++ b/sds/src/scanner/metrics.rs @@ -1,5 +1,18 @@ use crate::Labels; use metrics::{Counter, counter}; +use std::collections::BTreeMap; + +/// Computes the `"{sensitive_data_category}/{sensitive_data}"` tag value for a rule from its +/// `tags`. Missing `sensitive_data_category` falls back to `"missing_category"`. Missing +/// `sensitive_data` yields `None`, since there's nothing meaningful to tag with. +pub fn compute_sds_rule_name(tags: &BTreeMap) -> Option { + let sensitive_data = tags.get("sensitive_data")?; + let sensitive_data_category = tags + .get("sensitive_data_category") + .map(String::as_str) + .unwrap_or("missing_category"); + Some(format!("{sensitive_data_category}/{sensitive_data}")) +} #[derive(Clone)] pub struct RuleMetrics { @@ -49,3 +62,53 @@ impl ScannerMetrics { } } } + +#[cfg(test)] +mod test { + use super::compute_sds_rule_name; + use std::collections::BTreeMap; + + #[test] + fn compute_sds_rule_name_returns_none_for_empty_tags() { + assert_eq!(compute_sds_rule_name(&BTreeMap::new()), None); + } + + #[test] + fn compute_sds_rule_name_concatenates_category_and_data() { + let tags = BTreeMap::from([ + ( + "sensitive_data_category".to_string(), + "credentials".to_string(), + ), + ( + "sensitive_data".to_string(), + "travis_ci_access_token".to_string(), + ), + ]); + assert_eq!( + compute_sds_rule_name(&tags), + Some("credentials/travis_ci_access_token".to_string()) + ); + } + + #[test] + fn compute_sds_rule_name_returns_none_when_category_present_but_data_missing() { + let tags = BTreeMap::from([( + "sensitive_data_category".to_string(), + "credentials".to_string(), + )]); + assert_eq!(compute_sds_rule_name(&tags), None); + } + + #[test] + fn compute_sds_rule_name_falls_back_to_missing_category_when_category_absent() { + let tags = BTreeMap::from([( + "sensitive_data".to_string(), + "travis_ci_access_token".to_string(), + )]); + assert_eq!( + compute_sds_rule_name(&tags), + Some("missing_category/travis_ci_access_token".to_string()) + ); + } +} diff --git a/sds/src/scanner/mod.rs b/sds/src/scanner/mod.rs index 7b816e48..84c17c3f 100644 --- a/sds/src/scanner/mod.rs +++ b/sds/src/scanner/mod.rs @@ -1147,6 +1147,7 @@ impl ScannerBuilder<'_> { suppressions: compiled_suppressions, precedence: config.precedence, is_supporting_rule: config.is_supporting_rule, + sds_rule_name: metrics::compute_sds_rule_name(&config.tags), }) }) .collect::, CreateScannerError>>()?; From ebb324c136a365b274cd210c4812fbb94ba57419 Mon Sep 17 00:00:00 2001 From: Taiki Date: Fri, 28 Aug 2026 16:12:33 +0200 Subject: [PATCH 4/6] Add the metric generation code, deduplicating when a rule triggers multiple times --- sds/src/scanner/mod.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/sds/src/scanner/mod.rs b/sds/src/scanner/mod.rs index 84c17c3f..2537eaeb 100644 --- a/sds/src/scanner/mod.rs +++ b/sds/src/scanner/mod.rs @@ -27,6 +27,7 @@ pub use crate::secondary_validation::Validator; use crate::stats::GLOBAL_STATS; use crate::tokio::TOKIO_RUNTIME; use crate::{CreateScannerError, EncodeIndices, MatchAction, Path, ScannerError}; +use ::metrics::counter; use ahash::AHashMap; use futures::executor::block_on; use serde::{Deserialize, Serialize}; @@ -599,10 +600,26 @@ impl Scanner { ) { // Add number of scanned events self.metrics.num_scanned_events.increment(1); - // Add number of matches - self.metrics - .match_count - .increment(output_rule_matches.len() as u64); + // Add number of matches, tagged per rule so `sds_rule_name` can break down match counts + // by the rule's sensitive_data_category/sensitive_data tags. + let mut match_counts_by_rule: AHashMap = AHashMap::new(); + for rule_match in output_rule_matches { + *match_counts_by_rule + .entry(rule_match.rule_index) + .or_default() += 1; + } + for (rule_index, count) in match_counts_by_rule { + match self.rules[rule_index].sds_rule_name.as_deref() { + Some(sds_rule_name) => { + let labels = self.labels.clone_with_labels(Labels::new(&[( + "sds_rule_name", + sds_rule_name.to_string(), + )])); + counter!("scanning.match_count", labels).increment(count); + } + None => self.metrics.match_count.increment(count), + } + } if let Some(io_duration) = io_duration { let total_duration = start.elapsed(); From 6d9133ffcfca57a6b7705c9b9684518f0d610470 Mon Sep 17 00:00:00 2001 From: Taiki Date: Fri, 28 Aug 2026 16:14:00 +0200 Subject: [PATCH 5/6] Add a few E2E metric generation tests --- sds/src/scanner/test/metrics.rs | 179 ++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/sds/src/scanner/test/metrics.rs b/sds/src/scanner/test/metrics.rs index 55f33c2e..0cd8d25d 100644 --- a/sds/src/scanner/test/metrics.rs +++ b/sds/src/scanner/test/metrics.rs @@ -61,6 +61,185 @@ fn should_submit_scanning_metrics() { } } +#[test] +fn should_submit_match_count_metric_tagged_with_sds_rule_name() { + let recorder = DebuggingRecorder::new(); + let snapshotter = recorder.snapshotter(); + + metrics::with_local_recorder(&recorder, || { + let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) + .match_action(MatchAction::None) + .tags(BTreeMap::from([ + ( + "sensitive_data_category".to_string(), + "credentials".to_string(), + ), + ( + "sensitive_data".to_string(), + "travis_ci_access_token".to_string(), + ), + ])); + + let scanner = ScannerBuilder::new(&[rule_0]).build().unwrap(); + let mut content = SimpleEvent::Map(BTreeMap::from([( + "key1".to_string(), + SimpleEvent::String("secret".to_string()), + )])); + + scanner.scan(&mut content).unwrap(); + }); + + let snapshot = snapshotter.snapshot().into_hashmap(); + + let metric_name = "scanning.match_count"; + let labels = vec![Label::new( + "sds_rule_name", + "credentials/travis_ci_access_token", + )]; + let metric_value = snapshot + .get(&CompositeKey::new( + Counter, + Key::from_parts(metric_name, labels), + )) + .expect("tagged match_count metric not found"); + + assert_eq!(metric_value, &(None, None, DebugValue::Counter(1))); +} + +#[test] +fn should_submit_match_count_metric_with_missing_category_tag() { + let recorder = DebuggingRecorder::new(); + let snapshotter = recorder.snapshotter(); + + metrics::with_local_recorder(&recorder, || { + let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) + .match_action(MatchAction::None) + .tags(BTreeMap::from([( + "sensitive_data".to_string(), + "travis_ci_access_token".to_string(), + )])); + + let scanner = ScannerBuilder::new(&[rule_0]).build().unwrap(); + let mut content = SimpleEvent::Map(BTreeMap::from([( + "key1".to_string(), + SimpleEvent::String("secret".to_string()), + )])); + + scanner.scan(&mut content).unwrap(); + }); + + let snapshot = snapshotter.snapshot().into_hashmap(); + + let metric_name = "scanning.match_count"; + let labels = vec![Label::new( + "sds_rule_name", + "missing_category/travis_ci_access_token", + )]; + let metric_value = snapshot + .get(&CompositeKey::new( + Counter, + Key::from_parts(metric_name, labels), + )) + .expect("tagged match_count metric not found"); + + assert_eq!(metric_value, &(None, None, DebugValue::Counter(1))); +} + +#[test] +fn should_submit_untagged_match_count_metric_when_sensitive_data_tag_is_missing() { + let recorder = DebuggingRecorder::new(); + let snapshotter = recorder.snapshotter(); + + metrics::with_local_recorder(&recorder, || { + let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) + .match_action(MatchAction::None) + .tags(BTreeMap::from([( + "sensitive_data_category".to_string(), + "credentials".to_string(), + )])); + + let scanner = ScannerBuilder::new(&[rule_0]).build().unwrap(); + let mut content = SimpleEvent::Map(BTreeMap::from([( + "key1".to_string(), + SimpleEvent::String("secret".to_string()), + )])); + + scanner.scan(&mut content).unwrap(); + }); + + let snapshot = snapshotter.snapshot().into_hashmap(); + + let metric_name = "scanning.match_count"; + let metric_value = snapshot + .get(&CompositeKey::new(Counter, Key::from_name(metric_name))) + .expect("untagged match_count metric not found"); + + assert_eq!(metric_value, &(None, None, DebugValue::Counter(1))); +} + +#[test] +fn should_submit_separately_tagged_match_count_metrics_for_multiple_rules() { + let recorder = DebuggingRecorder::new(); + let snapshotter = recorder.snapshotter(); + + metrics::with_local_recorder(&recorder, || { + let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) + .match_action(MatchAction::None) + .tags(BTreeMap::from([ + ( + "sensitive_data_category".to_string(), + "credentials".to_string(), + ), + ( + "sensitive_data".to_string(), + "travis_ci_access_token".to_string(), + ), + ])); + let rule_1 = RootRuleConfig::new(RegexRuleConfig::new("foo").build()) + .match_action(MatchAction::None) + .tags(BTreeMap::from([ + ("sensitive_data_category".to_string(), "pii".to_string()), + ("sensitive_data".to_string(), "email".to_string()), + ])); + + let scanner = ScannerBuilder::new(&[rule_0, rule_1]).build().unwrap(); + let mut content = SimpleEvent::Map(BTreeMap::from([( + "key1".to_string(), + SimpleEvent::String("secret foo".to_string()), + )])); + + scanner.scan(&mut content).unwrap(); + }); + + let snapshot = snapshotter.snapshot().into_hashmap(); + + let metric_name = "scanning.match_count"; + + let credentials_labels = vec![Label::new( + "sds_rule_name", + "credentials/travis_ci_access_token", + )]; + let credentials_metric_value = snapshot + .get(&CompositeKey::new( + Counter, + Key::from_parts(metric_name, credentials_labels), + )) + .expect("credentials match_count metric not found"); + assert_eq!( + credentials_metric_value, + &(None, None, DebugValue::Counter(1)) + ); + + let pii_labels = vec![Label::new("sds_rule_name", "pii/email")]; + let pii_metric_value = snapshot + .get(&CompositeKey::new( + Counter, + Key::from_parts(metric_name, pii_labels), + )) + .expect("pii match_count metric not found"); + assert_eq!(pii_metric_value, &(None, None, DebugValue::Counter(1))); +} + #[test] fn should_submit_excluded_match_metric() { let recorder = DebuggingRecorder::new(); From 9c648800a136cb730947fe74d68d7d13a5d77f3d Mon Sep 17 00:00:00 2001 From: Taiki Date: Fri, 28 Aug 2026 16:31:30 +0200 Subject: [PATCH 6/6] Update the code to receive a vector of strings as tags, instead of a parsed map --- sds/src/scanner/metrics.rs | 62 +++++++++++++++++++-------------- sds/src/scanner/mod.rs | 10 +++--- sds/src/scanner/test/metrics.rs | 46 ++++++++---------------- 3 files changed, 55 insertions(+), 63 deletions(-) diff --git a/sds/src/scanner/metrics.rs b/sds/src/scanner/metrics.rs index 706532e0..91599776 100644 --- a/sds/src/scanner/metrics.rs +++ b/sds/src/scanner/metrics.rs @@ -1,16 +1,23 @@ use crate::Labels; use metrics::{Counter, counter}; -use std::collections::BTreeMap; + +/// Looks up the value of a `"key:value"` tag entry by key, matching the format used by +/// standard rule definitions (e.g. `sensitive_data:travis_ci_access_token`). +fn get_tag_value<'a>(tags: &'a [String], key: &str) -> Option<&'a str> { + let prefix_len = key.len() + 1; + tags.iter().find_map(|tag| { + (tag.len() > prefix_len && tag.starts_with(key) && tag.as_bytes()[key.len()] == b':') + .then(|| &tag[prefix_len..]) + }) +} /// Computes the `"{sensitive_data_category}/{sensitive_data}"` tag value for a rule from its /// `tags`. Missing `sensitive_data_category` falls back to `"missing_category"`. Missing /// `sensitive_data` yields `None`, since there's nothing meaningful to tag with. -pub fn compute_sds_rule_name(tags: &BTreeMap) -> Option { - let sensitive_data = tags.get("sensitive_data")?; - let sensitive_data_category = tags - .get("sensitive_data_category") - .map(String::as_str) - .unwrap_or("missing_category"); +pub fn compute_sds_rule_name(tags: &[String]) -> Option { + let sensitive_data = get_tag_value(tags, "sensitive_data")?; + let sensitive_data_category = + get_tag_value(tags, "sensitive_data_category").unwrap_or("missing_category"); Some(format!("{sensitive_data_category}/{sensitive_data}")) } @@ -66,25 +73,18 @@ impl ScannerMetrics { #[cfg(test)] mod test { use super::compute_sds_rule_name; - use std::collections::BTreeMap; #[test] fn compute_sds_rule_name_returns_none_for_empty_tags() { - assert_eq!(compute_sds_rule_name(&BTreeMap::new()), None); + assert_eq!(compute_sds_rule_name(&[]), None); } #[test] fn compute_sds_rule_name_concatenates_category_and_data() { - let tags = BTreeMap::from([ - ( - "sensitive_data_category".to_string(), - "credentials".to_string(), - ), - ( - "sensitive_data".to_string(), - "travis_ci_access_token".to_string(), - ), - ]); + let tags = vec![ + "sensitive_data_category:credentials".to_string(), + "sensitive_data:travis_ci_access_token".to_string(), + ]; assert_eq!( compute_sds_rule_name(&tags), Some("credentials/travis_ci_access_token".to_string()) @@ -93,22 +93,30 @@ mod test { #[test] fn compute_sds_rule_name_returns_none_when_category_present_but_data_missing() { - let tags = BTreeMap::from([( - "sensitive_data_category".to_string(), - "credentials".to_string(), - )]); + let tags = vec!["sensitive_data_category:credentials".to_string()]; assert_eq!(compute_sds_rule_name(&tags), None); } #[test] fn compute_sds_rule_name_falls_back_to_missing_category_when_category_absent() { - let tags = BTreeMap::from([( - "sensitive_data".to_string(), - "travis_ci_access_token".to_string(), - )]); + let tags = vec!["sensitive_data:travis_ci_access_token".to_string()]; assert_eq!( compute_sds_rule_name(&tags), Some("missing_category/travis_ci_access_token".to_string()) ); } + + #[test] + fn compute_sds_rule_name_does_not_confuse_sensitive_data_category_with_sensitive_data() { + // "sensitive_data_category" starts with the "sensitive_data" key, so the lookup + // must not mistake one tag for the other. + let tags = vec!["sensitive_data_category:credentials".to_string()]; + assert_eq!(compute_sds_rule_name(&tags), None); + } + + #[test] + fn compute_sds_rule_name_ignores_malformed_tags_without_a_colon() { + let tags = vec!["sensitive_data".to_string()]; + assert_eq!(compute_sds_rule_name(&tags), None); + } } diff --git a/sds/src/scanner/mod.rs b/sds/src/scanner/mod.rs index 2537eaeb..9de9a161 100644 --- a/sds/src/scanner/mod.rs +++ b/sds/src/scanner/mod.rs @@ -32,7 +32,6 @@ use ahash::AHashMap; use futures::executor::block_on; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use std::collections::BTreeMap; use std::ops::Deref; use std::pin::Pin; use std::sync::Arc; @@ -109,8 +108,11 @@ pub struct RootRuleConfig { precedence: Precedence, #[serde(default)] pub is_supporting_rule: bool, + /// Raw `"key:value"` strings, matching the format used by standard rule definitions + /// (e.g. `sensitive_data:travis_ci_access_token`). Not parsed into a map since no + /// producer of these values does so either; consumers parse the entries they need. #[serde(default)] - pub tags: BTreeMap, + pub tags: Vec, #[serde(flatten)] pub inner: T, } @@ -139,7 +141,7 @@ impl RootRuleConfig { suppressions: None, precedence: Precedence::default(), is_supporting_rule: false, - tags: BTreeMap::new(), + tags: Vec::new(), inner, } } @@ -192,7 +194,7 @@ impl RootRuleConfig { self } - pub fn tags(mut self, tags: BTreeMap) -> Self { + pub fn tags(mut self, tags: Vec) -> Self { self.tags = tags; self } diff --git a/sds/src/scanner/test/metrics.rs b/sds/src/scanner/test/metrics.rs index 0cd8d25d..c6ac2e0c 100644 --- a/sds/src/scanner/test/metrics.rs +++ b/sds/src/scanner/test/metrics.rs @@ -69,16 +69,10 @@ fn should_submit_match_count_metric_tagged_with_sds_rule_name() { metrics::with_local_recorder(&recorder, || { let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) .match_action(MatchAction::None) - .tags(BTreeMap::from([ - ( - "sensitive_data_category".to_string(), - "credentials".to_string(), - ), - ( - "sensitive_data".to_string(), - "travis_ci_access_token".to_string(), - ), - ])); + .tags(vec![ + "sensitive_data_category:credentials".to_string(), + "sensitive_data:travis_ci_access_token".to_string(), + ]); let scanner = ScannerBuilder::new(&[rule_0]).build().unwrap(); let mut content = SimpleEvent::Map(BTreeMap::from([( @@ -114,10 +108,7 @@ fn should_submit_match_count_metric_with_missing_category_tag() { metrics::with_local_recorder(&recorder, || { let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) .match_action(MatchAction::None) - .tags(BTreeMap::from([( - "sensitive_data".to_string(), - "travis_ci_access_token".to_string(), - )])); + .tags(vec!["sensitive_data:travis_ci_access_token".to_string()]); let scanner = ScannerBuilder::new(&[rule_0]).build().unwrap(); let mut content = SimpleEvent::Map(BTreeMap::from([( @@ -153,10 +144,7 @@ fn should_submit_untagged_match_count_metric_when_sensitive_data_tag_is_missing( metrics::with_local_recorder(&recorder, || { let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) .match_action(MatchAction::None) - .tags(BTreeMap::from([( - "sensitive_data_category".to_string(), - "credentials".to_string(), - )])); + .tags(vec!["sensitive_data_category:credentials".to_string()]); let scanner = ScannerBuilder::new(&[rule_0]).build().unwrap(); let mut content = SimpleEvent::Map(BTreeMap::from([( @@ -185,22 +173,16 @@ fn should_submit_separately_tagged_match_count_metrics_for_multiple_rules() { metrics::with_local_recorder(&recorder, || { let rule_0 = RootRuleConfig::new(RegexRuleConfig::new("secret").build()) .match_action(MatchAction::None) - .tags(BTreeMap::from([ - ( - "sensitive_data_category".to_string(), - "credentials".to_string(), - ), - ( - "sensitive_data".to_string(), - "travis_ci_access_token".to_string(), - ), - ])); + .tags(vec![ + "sensitive_data_category:credentials".to_string(), + "sensitive_data:travis_ci_access_token".to_string(), + ]); let rule_1 = RootRuleConfig::new(RegexRuleConfig::new("foo").build()) .match_action(MatchAction::None) - .tags(BTreeMap::from([ - ("sensitive_data_category".to_string(), "pii".to_string()), - ("sensitive_data".to_string(), "email".to_string()), - ])); + .tags(vec![ + "sensitive_data_category:pii".to_string(), + "sensitive_data:email".to_string(), + ]); let scanner = ScannerBuilder::new(&[rule_0, rule_1]).build().unwrap(); let mut content = SimpleEvent::Map(BTreeMap::from([(