dd_sds/scanner/
suppression.rs1use ahash::AHashSet;
2use serde::{Deserialize, Serialize};
3use serde_with::serde_as;
4use thiserror::Error;
5
6const MAX_SUPPRESSIONS_COUNT: usize = 30;
7const MAX_SUPPRESSION_LENGTH: usize = 1000;
8
9#[serde_as]
10#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
11pub struct Suppressions {
12 #[serde(default)]
13 pub starts_with: Vec<String>,
14 #[serde(default)]
15 pub ends_with: Vec<String>,
16 #[serde(default)]
17 pub exact_match: Vec<String>,
18}
19
20#[derive(Debug, PartialEq, Eq, Error)]
21pub enum SuppressionValidationError {
22 #[error("No more than {} suppressions are allowed", MAX_SUPPRESSIONS_COUNT)]
23 TooManySuppressions,
24
25 #[error("Individual suppressions cannot be empty")]
26 EmptySuppression,
27
28 #[error(
29 "Suppressions cannot be longer than {} characters",
30 MAX_SUPPRESSION_LENGTH
31 )]
32 SuppressionTooLong,
33
34 #[error("Duplicate suppressions are not allowed")]
35 DuplicateSuppression,
36}
37
38pub struct CompiledSuppressions {
39 pub starts_with: Vec<String>,
40 pub ends_with: Vec<String>,
41 pub exact_match: AHashSet<String>,
42}
43
44impl CompiledSuppressions {
45 pub fn should_match_be_suppressed(&self, match_content: &str) -> bool {
46 if self.exact_match.contains(match_content) {
47 return true;
48 }
49 if self
50 .starts_with
51 .iter()
52 .any(|start| match_content.starts_with(start))
53 {
54 return true;
55 }
56 if self
57 .ends_with
58 .iter()
59 .any(|end| match_content.ends_with(end))
60 {
61 return true;
62 }
63 false
64 }
65}
66
67fn validate_suppressions_list(suppressions: &[String]) -> Result<(), SuppressionValidationError> {
68 if suppressions.len() > MAX_SUPPRESSIONS_COUNT {
69 return Err(SuppressionValidationError::TooManySuppressions);
70 }
71 if AHashSet::from_iter(suppressions).len() != suppressions.len() {
72 return Err(SuppressionValidationError::DuplicateSuppression);
73 }
74 for suppression in suppressions {
75 if suppression.len() > MAX_SUPPRESSION_LENGTH {
76 return Err(SuppressionValidationError::SuppressionTooLong);
77 }
78 if suppression.is_empty() {
79 return Err(SuppressionValidationError::EmptySuppression);
80 }
81 }
82 Ok(())
83}
84
85impl TryFrom<Suppressions> for CompiledSuppressions {
86 type Error = SuppressionValidationError;
87
88 fn try_from(config: Suppressions) -> Result<Self, SuppressionValidationError> {
89 validate_suppressions_list(&config.starts_with)?;
90 validate_suppressions_list(&config.ends_with)?;
91 validate_suppressions_list(&config.exact_match)?;
92 Ok(Self {
93 starts_with: config.starts_with,
94 ends_with: config.ends_with,
95 exact_match: config.exact_match.into_iter().collect(),
96 })
97 }
98}
99
100#[cfg(test)]
101mod test {
102
103 use super::*;
104
105 #[test]
106 fn test_suppression_correctly_suppresses_correctly() {
107 let config = Suppressions {
108 starts_with: vec!["mary".to_string()],
109 ends_with: vec!["@datadoghq.com".to_string()],
110 exact_match: vec!["nathan@yahoo.com".to_string()],
111 };
112 let compiled_config = CompiledSuppressions::try_from(config).unwrap();
113 assert!(compiled_config.should_match_be_suppressed("mary@datadoghq.com"));
114 assert!(compiled_config.should_match_be_suppressed("nathan@yahoo.com"));
115 assert!(compiled_config.should_match_be_suppressed("john@datadoghq.com"));
116 assert!(!compiled_config.should_match_be_suppressed("john@yahoo.com"));
117 assert!(!compiled_config.should_match_be_suppressed("john mary john"));
118 assert!(compiled_config.should_match_be_suppressed("mary john john"));
119 }
120}