Skip to main content

relay_pii/
config.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::sync::OnceLock;
4
5use regex::{Regex, RegexBuilder};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::{CompiledPiiConfig, Redaction, SelectorSpec};
9
10const COMPILED_PATTERN_MAX_SIZE: usize = 262_144;
11
12/// Helper method to check whether a flag is false.
13#[allow(clippy::trivially_copy_pass_by_ref)]
14pub(crate) fn is_flag_default(flag: &bool) -> bool {
15    !*flag
16}
17
18/// An error returned when parsing [`PiiConfig`].
19#[derive(Clone, Debug, thiserror::Error)]
20pub enum PiiConfigError {
21    /// A match pattern in a PII rule config could not be parsed.
22    #[error("could not parse pattern")]
23    RegexError(#[source] regex::Error),
24}
25
26/// Wrapper for the regex and the raw pattern string.
27///
28/// The regex will be compiled only when it used once, and the compiled version will be reused on
29/// consecutive calls.
30#[derive(Debug, Clone)]
31pub struct LazyPattern {
32    raw: Cow<'static, str>,
33    case_insensitive: bool,
34    pattern: OnceLock<Result<Regex, PiiConfigError>>,
35}
36
37impl PartialEq for LazyPattern {
38    fn eq(&self, other: &Self) -> bool {
39        self.raw.to_lowercase() == other.raw.to_lowercase()
40    }
41}
42
43impl LazyPattern {
44    /// Create a new [`LazyPattern`] from a raw string.
45    pub fn new<S>(raw: S) -> Self
46    where
47        Cow<'static, str>: From<S>,
48    {
49        Self {
50            raw: raw.into(),
51            case_insensitive: false,
52            pattern: OnceLock::new(),
53        }
54    }
55
56    /// Change the case sensativity settings for the underlying regex.
57    ///
58    /// It's possible to set the case sensativity on already compiled [`LazyPattern`], which will
59    /// be recompiled (re-built) once it's used again.
60    pub fn case_insensitive(mut self, value: bool) -> Self {
61        self.case_insensitive = value;
62        self.pattern.take();
63        self
64    }
65
66    /// Compiles the regex from the internal raw string.
67    pub fn compiled(&self) -> Result<&Regex, &PiiConfigError> {
68        self.pattern
69            .get_or_init(|| {
70                let regex_result = RegexBuilder::new(&self.raw)
71                    .size_limit(COMPILED_PATTERN_MAX_SIZE)
72                    .case_insensitive(self.case_insensitive)
73                    .build()
74                    .map_err(PiiConfigError::RegexError);
75
76                if let Err(ref error) = regex_result {
77                    relay_log::error!(
78                        error = error as &dyn std::error::Error,
79                        "unable to compile pattern into regex"
80                    );
81                }
82                regex_result
83            })
84            .as_ref()
85    }
86}
87
88impl From<&'static str> for LazyPattern {
89    fn from(pattern: &'static str) -> LazyPattern {
90        LazyPattern::new(pattern)
91    }
92}
93
94impl Serialize for LazyPattern {
95    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96        serializer.serialize_str(&self.raw)
97    }
98}
99
100impl<'de> Deserialize<'de> for LazyPattern {
101    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
102        let raw = String::deserialize(deserializer)?;
103        Ok(LazyPattern::new(raw))
104    }
105}
106
107#[allow(clippy::unnecessary_wraps)]
108fn replace_groups_default() -> Option<BTreeSet<u8>> {
109    let mut set = BTreeSet::new();
110    set.insert(0);
111    Some(set)
112}
113
114/// A rule that matches a regex pattern.
115#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
116#[serde(rename_all = "camelCase")]
117pub struct PatternRule {
118    /// The regular expression to apply.
119    pub pattern: LazyPattern,
120    /// The match group indices to replace.
121    #[serde(default = "replace_groups_default")]
122    pub replace_groups: Option<BTreeSet<u8>>,
123}
124
125/// A rule that dispatches to multiple other rules.
126#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct MultipleRule {
129    /// A reference to other rules to apply
130    pub rules: Vec<String>,
131    /// When set to true, the outer rule is reported.
132    #[serde(default, skip_serializing_if = "is_flag_default")]
133    pub hide_inner: bool,
134}
135
136/// An alias for another rule.
137#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
138#[serde(rename_all = "camelCase")]
139pub struct AliasRule {
140    /// A reference to another rule to apply.
141    pub rule: String,
142    /// When set to true, the outer rule is reported.
143    #[serde(default, skip_serializing_if = "is_flag_default")]
144    pub hide_inner: bool,
145}
146
147/// A pair redaction rule.
148#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub struct RedactPairRule {
151    /// A pattern to match for keys.
152    ///
153    /// Note: despite the name, this is matched against both the
154    /// key and value. The reason is that the value may itself
155    /// be (the string representation of) a complex object with
156    /// keys of its own, and we explicitly want to match those
157    /// keys as well. See the `test_breadcrumb_message` test
158    /// for an example of this.
159    pub key_pattern: LazyPattern,
160}
161
162/// Supported scrubbing rules.
163#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
164#[serde(tag = "type", rename_all = "snake_case")]
165pub enum RuleType {
166    /// Matches any value.
167    Anything,
168    /// Applies a regular expression.
169    Pattern(PatternRule),
170    /// Matchse an IMEI or IMEISV
171    Imei,
172    /// Matches a mac address
173    Mac,
174    /// Matches a UUID
175    Uuid,
176    /// Matches an email
177    Email,
178    /// Matches any IP address
179    Ip,
180    /// Matches a creditcard number
181    Creditcard,
182    /// Matches an IBAN
183    Iban,
184    /// Sanitizes a path from user data
185    Userpath,
186    /// A PEM encoded key
187    Pemkey,
188    /// Auth info from URLs
189    UrlAuth,
190    /// US SSN.
191    UsSsn,
192    /// A Bearer token
193    Bearer,
194    /// Keys that look like passwords
195    Password,
196    /// Known sensitive cookies
197    Cookies,
198    /// When a regex matches a key, a value is removed
199    #[serde(alias = "redactPair")]
200    RedactPair(RedactPairRule),
201    /// Applies multiple rules.
202    Multiple(MultipleRule),
203    /// Applies another rule.  Works like a single multiple.
204    Alias(AliasRule),
205    /// Unknown ruletype for forward compatibility
206    Unknown(String),
207}
208
209/// A single rule configuration.
210#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
211pub struct RuleSpec {
212    /// The matching rule to apply on fields.
213    #[serde(flatten)]
214    pub ty: RuleType,
215
216    /// The redaction to apply on matched fields.
217    #[serde(default)]
218    pub redaction: Redaction,
219}
220
221/// Configuration for rule parameters.
222#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
223#[serde(rename_all = "camelCase")]
224pub struct Vars {
225    /// The default secret key for hashing operations.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub hash_key: Option<String>,
228}
229
230impl Vars {
231    fn is_empty(&self) -> bool {
232        self.hash_key.is_none()
233    }
234}
235
236/// A set of named rule configurations.
237#[derive(Serialize, Deserialize, Debug, Default, Clone)]
238pub struct PiiConfig {
239    /// A map of custom PII rules.
240    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
241    pub rules: BTreeMap<String, RuleSpec>,
242
243    /// Parameters for PII rules.
244    #[serde(default, skip_serializing_if = "Vars::is_empty")]
245    pub vars: Vars,
246
247    /// Mapping of selectors to rules.
248    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
249    pub applications: BTreeMap<SelectorSpec, Vec<String>>,
250
251    /// PII config derived from datascrubbing settings.
252    ///
253    /// Cached because the conversion process is expensive.
254    #[serde(skip)]
255    pub(super) compiled: OnceLock<CompiledPiiConfig>,
256}
257
258impl PartialEq for PiiConfig {
259    fn eq(&self, other: &PiiConfig) -> bool {
260        // This is written in this way such that people will not forget to update this PartialEq
261        // impl when they add more fields.
262        let PiiConfig {
263            rules,
264            vars,
265            applications,
266            compiled: _compiled,
267        } = &self;
268
269        rules == &other.rules && vars == &other.vars && applications == &other.applications
270    }
271}
272
273impl PiiConfig {
274    /// Get a representation of this `PiiConfig` that is more (CPU-)efficient for processing.
275    ///
276    /// This can be computationally expensive when called for the first time. The result is cached
277    /// internally and reused on the second call.
278    pub fn compiled(&self) -> &CompiledPiiConfig {
279        self.compiled.get_or_init(|| self.compiled_uncached())
280    }
281
282    /// Like [`compiled`](Self::compiled) but without internal caching.
283    #[inline]
284    pub fn compiled_uncached(&self) -> CompiledPiiConfig {
285        CompiledPiiConfig::new(self)
286    }
287}