dd_sds/scanner/
suppression.rs

1use 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}