Skip to main content

relay_pii/
processor.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::mem;
4use std::sync::OnceLock;
5
6use regex::Regex;
7use relay_event_schema::processor::{
8    self, Chunk, FieldAttrs, Pii, ProcessValue, ProcessingAction, ProcessingResult,
9    ProcessingState, Processor, ValueType, enum_set, process_value,
10};
11use relay_event_schema::protocol::{
12    AsPair, Event, IpAddr, NativeImagePath, PairList, Replay, ResponseContext, User,
13};
14use relay_protocol::{Annotated, Array, Meta, Remark, RemarkType, Value};
15
16use crate::compiledconfig::{CompiledPiiConfig, RuleRef};
17use crate::config::RuleType;
18use crate::redactions::Redaction;
19use crate::regexes::{self, ANYTHING_REGEX, PatternType, ReplaceBehavior};
20use crate::utils;
21
22/// Controls how scrubbing rules are applied to attributes.
23#[derive(Debug, Clone, Copy)]
24pub enum AttributeMode {
25    /// Treat the attribute as an object and allow referring
26    /// to individual fields.
27    Object,
28    /// Identify the attribute with its value and apply all
29    /// rules there directly.
30    ValueOnly,
31}
32
33/// A processor that performs PII stripping.
34pub struct PiiProcessor<'a> {
35    /// Controls how rules are applied to attributes.
36    attribute_mode: AttributeMode,
37    compiled_config: &'a CompiledPiiConfig,
38}
39
40impl<'a> PiiProcessor<'a> {
41    /// Creates a new processor based on a config.
42    pub fn new(compiled_config: &'a CompiledPiiConfig) -> PiiProcessor<'a> {
43        // this constructor needs to be cheap... a new PiiProcessor is created for each event. Move
44        // any init logic into CompiledPiiConfig::new.
45        PiiProcessor {
46            compiled_config,
47            attribute_mode: AttributeMode::Object,
48        }
49    }
50
51    /// Sets an `AttributeMode` on this processor.
52    pub fn attribute_mode(mut self, attribute_mode: AttributeMode) -> Self {
53        self.attribute_mode = attribute_mode;
54        self
55    }
56
57    fn apply_all_rules(
58        &self,
59        meta: &mut Meta,
60        state: &ProcessingState<'_>,
61        mut value: Option<&mut String>,
62    ) -> ProcessingResult {
63        let pii = state.pii();
64        if pii == Pii::False {
65            return Ok(());
66        }
67
68        for (selector, rules) in self.compiled_config.applications.iter() {
69            if selector.matches_path(&state.path()) {
70                #[allow(clippy::needless_option_as_deref)]
71                for rule in rules {
72                    let reborrowed_value = value.as_deref_mut();
73                    apply_rule_to_value(meta, rule, state.path().key(), reborrowed_value)?;
74                }
75            }
76        }
77
78        Ok(())
79    }
80}
81
82impl Processor for PiiProcessor<'_> {
83    fn before_process<T: ProcessValue>(
84        &mut self,
85        value: Option<&T>,
86        meta: &mut Meta,
87        state: &ProcessingState<'_>,
88    ) -> ProcessingResult {
89        if let Some(Value::String(original_value)) = meta.original_value_as_mut() {
90            // Also apply pii scrubbing to the original value (set by normalization or other processors),
91            // such that we do not leak sensitive data through meta. Deletes `original_value` if an Error
92            // value is returned.
93            if let Some(parent) = state.iter().next() {
94                let path = state.path();
95                let new_state = parent.enter_borrowed(
96                    path.key().unwrap_or(""),
97                    Some(Cow::Borrowed(state.attrs())),
98                    enum_set!(ValueType::String),
99                );
100
101                if self
102                    .apply_all_rules(&mut Meta::default(), &new_state, Some(original_value))
103                    .is_err()
104                {
105                    // `apply_all_rules` returned `DeleteValueHard` or `DeleteValueSoft`, so delete the original as well.
106                    meta.set_original_value(Option::<String>::None);
107                }
108            }
109        }
110
111        // booleans cannot be PII, and strings are handled in process_string
112        if state.value_type().contains(ValueType::Boolean)
113            || state.value_type().contains(ValueType::String)
114        {
115            return Ok(());
116        }
117
118        if value.is_none() {
119            return Ok(());
120        }
121
122        // apply rules based on key/path
123        self.apply_all_rules(meta, state, None)
124    }
125
126    fn process_array<T>(
127        &mut self,
128        array: &mut Array<T>,
129        _meta: &mut Meta,
130        state: &ProcessingState<'_>,
131    ) -> ProcessingResult
132    where
133        T: ProcessValue,
134    {
135        if is_pairlist(array) {
136            for annotated in array {
137                let mut mapped = mem::take(annotated).map_value(T::into_value);
138
139                if let Some(Value::Array(pair)) = mapped.value_mut() {
140                    let mut value = mem::take(&mut pair[1]);
141                    let value_type = ValueType::for_field(&value);
142
143                    if let Some(key_name) = &pair[0].as_str() {
144                        // We enter the key of the first element of the array, since we treat it
145                        // as a pair.
146                        let key_state =
147                            state.enter_borrowed(key_name, state.inner_attrs(), value_type);
148                        // We process the value with a state that "simulates" the first value of the
149                        // array as if it was the key of a dictionary.
150                        process_value(&mut value, self, &key_state)?;
151                    }
152
153                    // Put value back into pair.
154                    pair[1] = value;
155                }
156
157                // Put pair back into array.
158                *annotated = T::from_value(mapped);
159            }
160
161            Ok(())
162        } else {
163            // If we didn't find a pairlist, we can process child values as normal.
164            array.process_child_values(self, state)
165        }
166    }
167
168    fn process_string(
169        &mut self,
170        value: &mut String,
171        meta: &mut Meta,
172        state: &ProcessingState<'_>,
173    ) -> ProcessingResult {
174        if let "" | "true" | "false" | "null" | "undefined" = value.as_str() {
175            return Ok(());
176        }
177
178        // same as before_process. duplicated here because we can only check for "true",
179        // "false" etc in process_string.
180        self.apply_all_rules(meta, state, Some(value))
181    }
182
183    fn process_native_image_path(
184        &mut self,
185        NativeImagePath(value): &mut NativeImagePath,
186        meta: &mut Meta,
187        state: &ProcessingState<'_>,
188    ) -> ProcessingResult {
189        // In NativeImagePath we must not strip the file's basename because that would break
190        // processing.
191        //
192        // We pop the basename from the end of the string, call process_string and push the
193        // basename again.
194        //
195        // The ranges in Meta should still be right as long as we only pop/push from the end of the
196        // string. If we decide that we need to preserve anything other than suffixes all PII
197        // tooltips/annotations are potentially wrong.
198
199        if let Some(index) = value.rfind(['/', '\\']) {
200            let basename = value.split_off(index);
201            match self.process_string(value, meta, state) {
202                Ok(()) => value.push_str(&basename),
203                Err(
204                    ProcessingAction::DeleteValueHard
205                    | ProcessingAction::DeleteValueWithRemark(_)
206                    | ProcessingAction::DeleteValueSoft,
207                ) => {
208                    basename[1..].clone_into(value);
209                }
210                Err(ProcessingAction::InvalidTransaction(x)) => {
211                    return Err(ProcessingAction::InvalidTransaction(x));
212                }
213            }
214        }
215
216        Ok(())
217    }
218
219    fn process_pairlist<T: ProcessValue + AsPair>(
220        &mut self,
221        value: &mut PairList<T>,
222        _meta: &mut Meta,
223        state: &ProcessingState,
224    ) -> ProcessingResult {
225        utils::process_pairlist(self, value, state)
226    }
227
228    fn process_attributes(
229        &mut self,
230        value: &mut relay_event_schema::protocol::Attributes,
231        _meta: &mut Meta,
232        state: &ProcessingState,
233    ) -> ProcessingResult {
234        match self.attribute_mode {
235            // Treat each attribute as an object and just process them field by field.
236            AttributeMode::Object => value.process_child_values(self, state),
237            // Identify each attribute with its `value` and only process that.
238            AttributeMode::ValueOnly => {
239                for (key, attribute) in value.0.iter_mut() {
240                    let Some(attribute) = attribute.value_mut() else {
241                        continue;
242                    };
243
244                    // We need some manual state management here because we're bypassing all the
245                    // intermediate structures and pointing at the value directly. This essentially
246                    // mimics the attributes and value type that the metastructure derivation would
247                    // produce for the attribute vaue.
248                    let attrs = FieldAttrs::new()
249                        .pii_dynamic(relay_event_schema::protocol::attribute_pii_from_conventions);
250                    let inner_value = &mut attribute.value.value;
251                    let inner_value_type = ValueType::for_field(inner_value);
252                    let entered =
253                        state.enter_borrowed(key, Some(Cow::Borrowed(&attrs)), inner_value_type);
254
255                    processor::process_value(inner_value, self, &entered)?;
256                    self.process_other(&mut attribute.other, state)?;
257                }
258                Ok(())
259            }
260        }
261    }
262
263    fn process_user(
264        &mut self,
265        user: &mut User,
266        _meta: &mut Meta,
267        state: &ProcessingState<'_>,
268    ) -> ProcessingResult {
269        let ip_was_valid = user.ip_address.value().is_none_or(IpAddr::is_valid);
270
271        // Recurse into the user and does PII processing on fields.
272        user.process_child_values(self, state)?;
273
274        let has_other_fields = user.id.value().is_some()
275            || user.username.value().is_some()
276            || user.email.value().is_some();
277
278        let ip_is_still_valid = user.ip_address.value().is_none_or(IpAddr::is_valid);
279
280        // If the IP address has become invalid as part of PII processing, we move it into the user
281        // ID. That ensures people can do IP hashing and still have a correct users-affected count.
282        //
283        // Right now both Snuba and EventUser discard unparseable IPs for indexing, and we assume
284        // we want to keep it that way.
285        //
286        // If there are any other fields set that take priority over the IP for uniquely
287        // identifying a user (has_other_fields), we do not want to do anything. The value will be
288        // wiped out in renormalization anyway.
289        if ip_was_valid && !has_other_fields && !ip_is_still_valid {
290            user.id = mem::take(&mut user.ip_address).map_value(|ip| ip.into_inner().into());
291            user.ip_address.meta_mut().add_remark(Remark::new(
292                RemarkType::Removed,
293                "pii:ip_address".to_owned(),
294            ));
295        }
296
297        Ok(())
298    }
299
300    // Replay PII processor entry point.
301    fn process_replay(
302        &mut self,
303        replay: &mut Replay,
304        _meta: &mut Meta,
305        state: &ProcessingState<'_>,
306    ) -> ProcessingResult {
307        replay.process_child_values(self, state)?;
308        Ok(())
309    }
310}
311
312#[derive(Default)]
313struct PairListProcessor {
314    is_pair: bool,
315    has_string_key: bool,
316}
317
318impl PairListProcessor {
319    /// Returns true if the processor identified the supplied data as an array composed of
320    /// a key (string) and a value.
321    fn is_pair_array(&self) -> bool {
322        self.is_pair && self.has_string_key
323    }
324}
325
326impl Processor for PairListProcessor {
327    fn process_array<T>(
328        &mut self,
329        value: &mut Array<T>,
330        _meta: &mut Meta,
331        state: &ProcessingState<'_>,
332    ) -> ProcessingResult
333    where
334        T: ProcessValue,
335    {
336        self.is_pair = state.depth() == 0 && value.len() == 2;
337        if self.is_pair {
338            let key_type = ValueType::for_field(&value[0]);
339            process_value(
340                &mut value[0],
341                self,
342                &state.enter_index(0, state.inner_attrs(), key_type),
343            )?;
344        }
345
346        Ok(())
347    }
348
349    fn process_string(
350        &mut self,
351        _value: &mut String,
352        _meta: &mut Meta,
353        state: &ProcessingState<'_>,
354    ) -> ProcessingResult where {
355        if state.depth() == 1 && state.path().index() == Some(0) {
356            self.has_string_key = true;
357        }
358
359        Ok(())
360    }
361}
362
363fn is_pairlist<T: ProcessValue>(array: &mut Array<T>) -> bool {
364    for element in array.iter_mut() {
365        let mut visitor = PairListProcessor::default();
366        process_value(element, &mut visitor, ProcessingState::root()).ok();
367        if !visitor.is_pair_array() {
368            return false;
369        }
370    }
371
372    !array.is_empty()
373}
374
375/// Scrubs GraphQL variables from the event.
376pub fn scrub_graphql(event: &mut Event) {
377    let mut keys: BTreeSet<&str> = BTreeSet::new();
378
379    let mut is_graphql = false;
380
381    // Collect the variables keys and scrub them out.
382    if let Some(request) = event.request.value_mut()
383        && let Some(Value::Object(data)) = request.data.value_mut()
384    {
385        if let Some(api_target) = request.api_target.value()
386            && api_target.eq_ignore_ascii_case("graphql")
387        {
388            is_graphql = true;
389        }
390
391        if is_graphql
392            && let Some(Annotated(Some(Value::Object(variables)), _)) = data.get_mut("variables")
393        {
394            for (key, value) in variables.iter_mut() {
395                keys.insert(key);
396                value.set_value(Some(Value::String("[Filtered]".to_owned())));
397            }
398        }
399    }
400
401    if !is_graphql {
402        return;
403    }
404
405    // Scrub PII from the data object if they match the variables keys.
406    if let Some(contexts) = event.contexts.value_mut()
407        && let Some(response) = contexts.get_mut::<ResponseContext>()
408        && let Some(Value::Object(data)) = response.data.value_mut()
409        && let Some(Annotated(Some(Value::Object(graphql_data)), _)) = data.get_mut("data")
410    {
411        if !keys.is_empty() {
412            scrub_graphql_data(&keys, graphql_data);
413        } else {
414            // If we don't have the variable keys, we scrub the whole data object
415            // because the query or mutation weren't parameterized.
416            data.remove("data");
417        }
418    }
419}
420
421/// Scrubs values from the data object to `[Filtered]`.
422fn scrub_graphql_data(keys: &BTreeSet<&str>, data: &mut BTreeMap<String, Annotated<Value>>) {
423    for (key, value) in data.iter_mut() {
424        match value.value_mut() {
425            Some(Value::Object(item_data)) => {
426                scrub_graphql_data(keys, item_data);
427            }
428            _ => {
429                if keys.contains(key.as_str()) {
430                    value.set_value(Some(Value::String("[Filtered]".to_owned())));
431                }
432            }
433        }
434    }
435}
436
437fn apply_rule_to_value(
438    meta: &mut Meta,
439    rule: &RuleRef,
440    key: Option<&str>,
441    mut value: Option<&mut String>,
442) -> ProcessingResult {
443    // The rule might specify to remove or to redact. If redaction is chosen, we need to
444    // chunk up the value, otherwise we need to simply mark the value for deletion.
445    let should_redact_chunks = !matches!(rule.redaction, Redaction::Default | Redaction::Remove);
446
447    // In case the value is not a string (but a container, bool or number) and the rule matches on
448    // anything, we can only remove the value (not replace, hash, etc).
449    if rule.ty == RuleType::Anything && (value.is_none() || !should_redact_chunks) {
450        // The value is a container, @anything on a container can do nothing but delete.
451        meta.add_remark(Remark::new(RemarkType::Removed, rule.origin.clone()));
452        return Err(ProcessingAction::DeleteValueHard);
453    }
454
455    macro_rules! apply_regex {
456        ($regex:expr, $replace_behavior:expr) => {
457            if let Some(ref mut value) = value {
458                processor::process_chunked_value(value, meta, |chunks| {
459                    apply_regex_to_chunks(chunks, rule, $regex, $replace_behavior)
460                });
461            }
462        };
463    }
464
465    for (pattern_type, regex, replace_behavior) in regexes::get_regex_for_rule_type(&rule.ty) {
466        if matches!(pattern_type, PatternType::Key | PatternType::KeyValue)
467            && key.is_some_and(|key| regex.is_match(key))
468        {
469            if value.is_some() && should_redact_chunks {
470                // If we're given a string value here, redact the value like we would with
471                // @anything.
472                apply_regex!(&ANYTHING_REGEX, replace_behavior);
473            } else {
474                meta.add_remark(Remark::new(RemarkType::Removed, rule.origin.clone()));
475                return Err(ProcessingAction::DeleteValueHard);
476            }
477        } else if matches!(pattern_type, PatternType::Value | PatternType::KeyValue) {
478            apply_regex!(regex, replace_behavior);
479        }
480    }
481
482    Ok(())
483}
484
485fn apply_regex_to_chunks<'a>(
486    chunks: Vec<Chunk<'a>>,
487    rule: &RuleRef,
488    regex: &Regex,
489    replace_behavior: ReplaceBehavior,
490) -> Vec<Chunk<'a>> {
491    // NB: This function allocates the entire string and all chunks a second time. This means it
492    // cannot reuse chunks and reallocates them. Ideally, we would be able to run the regex directly
493    // on the chunks, but the `regex` crate does not support that.
494
495    let mut search_string = String::new();
496    let mut has_text = false;
497    for chunk in &chunks {
498        match chunk {
499            Chunk::Text { text } => {
500                has_text = true;
501                search_string.push_str(&text.replace('\x00', ""));
502            }
503            Chunk::Redaction { .. } => search_string.push('\x00'),
504        }
505    }
506
507    if !has_text {
508        // Nothing to replace.
509        return chunks;
510    }
511
512    // Early exit if this regex does not match and return the original chunks.
513    let mut captures_iter = regex.captures_iter(&search_string).peekable();
514    if captures_iter.peek().is_none() {
515        return chunks;
516    }
517
518    let mut replacement_chunks = vec![];
519    for chunk in chunks {
520        if let Chunk::Redaction { .. } = chunk {
521            replacement_chunks.push(chunk);
522        }
523    }
524    replacement_chunks.reverse();
525
526    fn process_text<'a>(
527        text: &str,
528        rv: &mut Vec<Chunk<'a>>,
529        replacement_chunks: &mut Vec<Chunk<'a>>,
530    ) {
531        if text.is_empty() {
532            return;
533        }
534
535        // ALERT: This logic assumes that `regex` doesn't match a capture
536        // group starting on a null byte. If you get an error in debug mode
537        // about `replacement_chunks` not being empty, check the regex.
538        static NULL_SPLIT_RE: OnceLock<Regex> = OnceLock::new();
539        let regex = NULL_SPLIT_RE.get_or_init(|| {
540            #[allow(clippy::trivial_regex)]
541            Regex::new("\x00").unwrap()
542        });
543
544        let mut pos = 0;
545        for piece in regex.find_iter(text) {
546            rv.push(Chunk::Text {
547                text: Cow::Owned(text[pos..piece.start()].to_string()),
548            });
549            rv.push(replacement_chunks.pop().unwrap());
550            pos = piece.end();
551        }
552
553        rv.push(Chunk::Text {
554            text: Cow::Owned(text[pos..].to_string()),
555        });
556    }
557
558    let mut pos = 0;
559    let mut rv = Vec::with_capacity(replacement_chunks.len());
560
561    match replace_behavior {
562        ReplaceBehavior::Groups(ref groups) => {
563            for m in captures_iter {
564                for (idx, g) in m.iter().enumerate() {
565                    if let Some(g) = g
566                        && groups.contains(&(idx as u8))
567                    {
568                        process_text(
569                            &search_string[pos..g.start()],
570                            &mut rv,
571                            &mut replacement_chunks,
572                        );
573                        insert_replacement_chunks(rule, g.as_str(), &mut rv);
574                        pos = g.end();
575                    }
576                }
577            }
578            process_text(&search_string[pos..], &mut rv, &mut replacement_chunks);
579            debug_assert!(replacement_chunks.is_empty());
580        }
581        ReplaceBehavior::Value => {
582            // We only want to replace a string value, and the replacement chunk for that is
583            // inserted by insert_replacement_chunks. Adding chunks from replacement_chunks
584            // results in the incorrect behavior of a total of more chunks than the input.
585            insert_replacement_chunks(rule, &search_string, &mut rv);
586        }
587    }
588    rv
589}
590
591fn insert_replacement_chunks(rule: &RuleRef, text: &str, output: &mut Vec<Chunk<'_>>) {
592    match &rule.redaction {
593        Redaction::Default | Redaction::Remove => {
594            output.push(Chunk::Redaction {
595                text: Cow::Borrowed(""),
596                rule_id: Cow::Owned(rule.origin.to_string()),
597                ty: RemarkType::Removed,
598            });
599        }
600        Redaction::Mask => {
601            let buf = vec!['*'; text.chars().count()];
602
603            output.push(Chunk::Redaction {
604                ty: RemarkType::Masked,
605                rule_id: Cow::Owned(rule.origin.to_string()),
606                text: buf.into_iter().collect(),
607            })
608        }
609        Redaction::Hash => {
610            output.push(Chunk::Redaction {
611                ty: RemarkType::Pseudonymized,
612                rule_id: Cow::Owned(rule.origin.to_string()),
613                text: Cow::Owned(utils::hash_value(text.as_bytes())),
614            });
615        }
616        Redaction::Replace(replace) => {
617            output.push(Chunk::Redaction {
618                ty: RemarkType::Substituted,
619                rule_id: Cow::Owned(rule.origin.to_string()),
620                text: Cow::Owned(replace.text.clone()),
621            });
622        }
623        Redaction::Other => relay_log::debug!("Incoming redaction is not supported"),
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use insta::{allow_duplicates, assert_debug_snapshot};
630    use relay_event_schema::processor::process_value;
631    use relay_event_schema::protocol::{
632        Addr, Breadcrumb, DebugImage, DebugMeta, ExtraValue, Headers, LogEntry, Message,
633        NativeDebugImage, Request, Span, TagEntry, Tags, TraceContext,
634    };
635    use relay_protocol::{FromValue, Object, assert_annotated_snapshot, get_value};
636    use serde_json::json;
637
638    use super::*;
639    use crate::{DataScrubbingConfig, PiiConfig, ReplaceRedaction};
640
641    fn to_pii_config(datascrubbing_config: &DataScrubbingConfig) -> Option<PiiConfig> {
642        use crate::convert::to_pii_config as to_pii_config_impl;
643        let rv = to_pii_config_impl(datascrubbing_config);
644        if let Some(ref config) = rv {
645            let roundtrip: PiiConfig =
646                serde_json::from_value(serde_json::to_value(config).unwrap()).unwrap();
647            assert_eq!(&roundtrip, config);
648        }
649        rv
650    }
651
652    #[test]
653    fn test_scrub_original_value() {
654        let mut data = Event::from_value(
655            json!({
656                "user": {
657                    "username": "hey  man 73.133.27.120", // should be stripped despite not being "known ip field"
658                    "ip_address": "is this an ip address? 73.133.27.120", //  <--------
659                },
660                "extra":"invalid data my ip address is  74.133.27.120 and my credit card number is  4571234567890111 ",
661            })
662            .into(),
663        );
664
665        let scrubbing_config = DataScrubbingConfig {
666            scrub_data: true,
667            scrub_ip_addresses: true,
668            scrub_defaults: true,
669            ..Default::default()
670        };
671
672        let pii_config = to_pii_config(&scrubbing_config).unwrap();
673        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
674
675        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
676
677        assert_debug_snapshot!(&data);
678    }
679
680    #[test]
681    fn test_remark_overlap() {
682        let mut data = Annotated::<Event>::from_json_bytes(
683            br#"{
684                "extra": {"foo": "bar"},
685                "_meta":{
686                    "extra": {
687                        "foo":{
688                            "":{
689                                "rem":[["some_rule","s",0,3],["some_rule","s",0,3]]
690                            }
691                        }
692                    }
693                }
694            }"#,
695        )
696        .unwrap();
697
698        let scrubbing_config = DataScrubbingConfig {
699            scrub_data: true,
700            scrub_ip_addresses: true,
701            scrub_defaults: true,
702            ..Default::default()
703        };
704
705        let pii_config = to_pii_config(&scrubbing_config).unwrap();
706        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
707
708        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
709
710        // Verify that overlapping remarks do not make the string longer:
711        assert_eq!(get_value!(data.extra["foo"]!).0.as_str(), Some("bar"));
712    }
713
714    #[test]
715    fn test_resume_after_gap() {
716        let mut data = Annotated::<Event>::from_json_bytes(
717            br#"{
718                "extra": {"foo": "abcdefghijklmnopqrstuvwxyz"},
719                "_meta":{
720                    "extra": {
721                        "foo":{
722                            "":{
723                                "rem":[["some_rule","s",0,3],["some_rule","s",6,5]]
724                            }
725                        }
726                    }
727                }
728            }"#,
729        )
730        .unwrap();
731
732        let scrubbing_config = DataScrubbingConfig {
733            scrub_data: true,
734            scrub_ip_addresses: true,
735            scrub_defaults: true,
736            ..Default::default()
737        };
738
739        let pii_config = to_pii_config(&scrubbing_config).unwrap();
740        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
741
742        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
743
744        // Verify that an invalid remark after a gap does not repeat the gap:
745        assert_eq!(
746            get_value!(data.extra["foo"]!).0.as_str(),
747            Some("abcdefghijklmnopqrstuvwxyz")
748        );
749    }
750
751    #[test]
752    fn test_sentry_user() {
753        let mut data = Event::from_value(
754            json!({
755                "user": {
756                    "ip_address": "73.133.27.120",
757                    "sentry_user": "ip:73.133.27.120",
758                },
759            })
760            .into(),
761        );
762
763        let scrubbing_config = DataScrubbingConfig {
764            scrub_data: true,
765            scrub_ip_addresses: true,
766            scrub_defaults: true,
767            ..Default::default()
768        };
769
770        let pii_config = to_pii_config(&scrubbing_config).unwrap();
771        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
772
773        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
774
775        assert_debug_snapshot!(&data);
776    }
777
778    #[test]
779    fn test_basic_stripping() {
780        let config = serde_json::from_str::<PiiConfig>(
781            r#"
782            {
783                "rules": {
784                    "remove_bad_headers": {
785                        "type": "redact_pair",
786                        "keyPattern": "(?i)cookie|secret[-_]?key"
787                    }
788                },
789                "applications": {
790                    "$string": ["@ip"],
791                    "$object.**": ["remove_bad_headers"]
792                }
793            }
794            "#,
795        )
796        .unwrap();
797
798        let mut event = Annotated::new(Event {
799            logentry: Annotated::new(LogEntry {
800                formatted: Annotated::new("Hello world!".to_owned().into()),
801                ..Default::default()
802            }),
803            request: Annotated::new(Request {
804                env: {
805                    let mut rv = Object::new();
806                    rv.insert(
807                        "SECRET_KEY".to_owned(),
808                        Annotated::new(Value::String("134141231231231231231312".into())),
809                    );
810                    Annotated::new(rv)
811                },
812                headers: {
813                    let rv = vec![
814                        Annotated::new((
815                            Annotated::new("Cookie".to_owned().into()),
816                            Annotated::new("super secret".to_owned().into()),
817                        )),
818                        Annotated::new((
819                            Annotated::new("X-Forwarded-For".to_owned().into()),
820                            Annotated::new("127.0.0.1".to_owned().into()),
821                        )),
822                    ];
823                    Annotated::new(Headers(PairList(rv)))
824                },
825                ..Default::default()
826            }),
827            tags: Annotated::new(Tags(
828                vec![Annotated::new(TagEntry(
829                    Annotated::new("forwarded_for".to_owned()),
830                    Annotated::new("127.0.0.1".to_owned()),
831                ))]
832                .into(),
833            )),
834            ..Default::default()
835        });
836
837        let mut processor = PiiProcessor::new(config.compiled());
838        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
839        assert_annotated_snapshot!(event);
840    }
841
842    #[test]
843    fn test_redact_containers() {
844        let config = serde_json::from_str::<PiiConfig>(
845            r#"
846            {
847                "applications": {
848                    "$object": ["@anything"]
849                }
850            }
851            "#,
852        )
853        .unwrap();
854
855        let mut event = Annotated::new(Event {
856            extra: {
857                let mut map = Object::new();
858                map.insert(
859                    "foo".to_owned(),
860                    Annotated::new(ExtraValue(Value::String("bar".to_owned()))),
861                );
862                Annotated::new(map)
863            },
864            ..Default::default()
865        });
866
867        let mut processor = PiiProcessor::new(config.compiled());
868        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
869        assert_annotated_snapshot!(event);
870    }
871
872    #[test]
873    fn test_redact_custom_pattern() {
874        let config = serde_json::from_str::<PiiConfig>(
875            r#"
876            {
877                "applications": {
878                    "$string": ["myrule"]
879                },
880                "rules": {
881                    "myrule": {
882                        "type": "pattern",
883                        "pattern": "foo",
884                        "redaction": {
885                            "method": "replace",
886                            "text": "asd"
887                        }
888                    }
889                }
890            }
891            "#,
892        )
893        .unwrap();
894
895        let mut event = Annotated::new(Event {
896            extra: {
897                let mut map = Object::new();
898                map.insert(
899                    "myvalue".to_owned(),
900                    Annotated::new(ExtraValue(Value::String("foobar".to_owned()))),
901                );
902                Annotated::new(map)
903            },
904            ..Default::default()
905        });
906
907        let mut processor = PiiProcessor::new(config.compiled());
908        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
909        assert_annotated_snapshot!(event);
910    }
911
912    #[test]
913    fn test_redact_custom_negative_pattern() {
914        let config = serde_json::from_str::<PiiConfig>(
915            r#"
916            {
917                "applications": {
918                    "$string": ["myrule"]
919                },
920                "rules": {
921                    "myrule": {
922                        "type": "pattern",
923                        "pattern": "the good string|.*OK.*|(.*)",
924                        "replaceGroups": [1],
925                        "redaction": {
926                            "method": "mask"
927                        }
928                    }
929                }
930            }
931            "#,
932        )
933        .unwrap();
934
935        let mut event = Annotated::<Event>::from_json(
936            r#"{
937            "extra": {
938                "1": "the good string",
939                "2": "a bad string",
940                "3": "another OK string",
941                "4": "another bad one"
942            }
943        }"#,
944        )
945        .unwrap();
946
947        let mut processor = PiiProcessor::new(config.compiled());
948        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
949        assert_annotated_snapshot!(event.value().unwrap().extra, @r#"
950        {
951          "1": "the good string",
952          "2": "************",
953          "3": "another OK string",
954          "4": "***************",
955          "_meta": {
956            "2": {
957              "": {
958                "rem": [
959                  [
960                    "myrule",
961                    "m",
962                    0,
963                    12
964                  ]
965                ],
966                "len": 12
967              }
968            },
969            "4": {
970              "": {
971                "rem": [
972                  [
973                    "myrule",
974                    "m",
975                    0,
976                    15
977                  ]
978                ],
979                "len": 15
980              }
981            }
982          }
983        }
984        "#);
985    }
986
987    #[test]
988    fn test_no_field_upsert() {
989        let config = serde_json::from_str::<PiiConfig>(
990            r#"
991            {
992                "applications": {
993                    "**": ["@anything:remove"]
994                }
995            }
996            "#,
997        )
998        .unwrap();
999
1000        let mut event = Annotated::new(Event {
1001            extra: {
1002                let mut map = Object::new();
1003                map.insert(
1004                    "myvalue".to_owned(),
1005                    Annotated::new(ExtraValue(Value::String("foobar".to_owned()))),
1006                );
1007                Annotated::new(map)
1008            },
1009            ..Default::default()
1010        });
1011
1012        let mut processor = PiiProcessor::new(config.compiled());
1013        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1014        assert_annotated_snapshot!(event);
1015    }
1016
1017    #[test]
1018    fn test_anything_hash_on_string() {
1019        let config = serde_json::from_str::<PiiConfig>(
1020            r#"
1021            {
1022                "applications": {
1023                    "$string": ["@anything:hash"]
1024                }
1025            }
1026            "#,
1027        )
1028        .unwrap();
1029
1030        let mut event = Annotated::new(Event {
1031            extra: {
1032                let mut map = Object::new();
1033                map.insert(
1034                    "myvalue".to_owned(),
1035                    Annotated::new(ExtraValue(Value::String("foobar".to_owned()))),
1036                );
1037                Annotated::new(map)
1038            },
1039            ..Default::default()
1040        });
1041
1042        let mut processor = PiiProcessor::new(config.compiled());
1043        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1044        assert_annotated_snapshot!(event);
1045    }
1046
1047    #[test]
1048    fn test_anything_hash_on_container() {
1049        let config = serde_json::from_str::<PiiConfig>(
1050            r#"
1051            {
1052                "applications": {
1053                    "$object": ["@anything:hash"]
1054                }
1055            }
1056            "#,
1057        )
1058        .unwrap();
1059
1060        let mut event = Annotated::new(Event {
1061            extra: {
1062                let mut map = Object::new();
1063                map.insert(
1064                    "myvalue".to_owned(),
1065                    Annotated::new(ExtraValue(Value::String("foobar".to_owned()))),
1066                );
1067                Annotated::new(map)
1068            },
1069            ..Default::default()
1070        });
1071
1072        let mut processor = PiiProcessor::new(config.compiled());
1073        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1074        assert_annotated_snapshot!(event);
1075    }
1076
1077    #[test]
1078    fn test_only_match_token_on_keys() {
1079        let mut data = Event::from_value(
1080            json!({
1081                "request": {
1082                    "headers": [
1083                        ["X-Token", "oof this is very sensitive"],
1084                        ["Token", "also bad"],
1085                    ]
1086                },
1087                "extra": {
1088                    "url": "foo.bar/endpoint?token=sensitive",
1089                    "url2": "foo.bar/endpoint?token_foobar=sensitive",
1090                    "aaa": "token:12345",
1091                    "foo-token-bar": "sensitive",
1092                    "llm": "token count",
1093                },
1094            })
1095            .into(),
1096        );
1097
1098        let scrubbing_config = DataScrubbingConfig {
1099            scrub_data: true,
1100            scrub_ip_addresses: true,
1101            scrub_defaults: true,
1102            ..Default::default()
1103        };
1104
1105        let pii_config = to_pii_config(&scrubbing_config).unwrap();
1106        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1107
1108        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
1109
1110        assert_annotated_snapshot!(&data);
1111    }
1112
1113    #[test]
1114    fn test_ignore_user_agent_ip_scrubbing() {
1115        let mut data = Event::from_value(
1116            json!({
1117                "request": {
1118                    "headers": [
1119                        ["User-Agent", "127.0.0.1"],
1120                        ["X-Client-Ip", "10.0.0.1"]
1121                    ]
1122                },
1123            })
1124            .into(),
1125        );
1126
1127        let scrubbing_config = DataScrubbingConfig {
1128            scrub_data: true,
1129            scrub_ip_addresses: true,
1130            scrub_defaults: true,
1131            ..Default::default()
1132        };
1133
1134        let pii_config = to_pii_config(&scrubbing_config).unwrap();
1135        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1136
1137        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
1138
1139        assert_annotated_snapshot!(&data);
1140    }
1141
1142    #[test]
1143    fn test_remove_debugmeta_path() {
1144        let config = serde_json::from_str::<PiiConfig>(
1145            r#"
1146            {
1147                "applications": {
1148                    "debug_meta.images.*.code_file": ["@anything:remove"],
1149                    "debug_meta.images.*.debug_file": ["@anything:remove"]
1150                }
1151            }
1152            "#,
1153        )
1154        .unwrap();
1155
1156        let mut event = Annotated::new(Event {
1157            debug_meta: Annotated::new(DebugMeta {
1158                images: Annotated::new(vec![Annotated::new(DebugImage::Symbolic(Box::new(
1159                    NativeDebugImage {
1160                        code_id: Annotated::new("59b0d8f3183000".parse().unwrap()),
1161                        code_file: Annotated::new("C:\\Windows\\System32\\ntdll.dll".into()),
1162                        debug_id: Annotated::new(
1163                            "971f98e5-ce60-41ff-b2d7-235bbeb34578-1".parse().unwrap(),
1164                        ),
1165                        debug_file: Annotated::new("wntdll.pdb".into()),
1166                        debug_checksum: Annotated::empty(),
1167                        arch: Annotated::new("arm64".to_owned()),
1168                        image_addr: Annotated::new(Addr(0)),
1169                        image_size: Annotated::new(4096),
1170                        image_vmaddr: Annotated::new(Addr(32768)),
1171                        other: {
1172                            let mut map = Object::new();
1173                            map.insert(
1174                                "other".to_owned(),
1175                                Annotated::new(Value::String("value".to_owned())),
1176                            );
1177                            map
1178                        },
1179                    },
1180                )))]),
1181                ..Default::default()
1182            }),
1183            ..Default::default()
1184        });
1185
1186        let mut processor = PiiProcessor::new(config.compiled());
1187        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1188        assert_annotated_snapshot!(event);
1189    }
1190
1191    #[test]
1192    fn test_replace_debugmeta_path() {
1193        let config = serde_json::from_str::<PiiConfig>(
1194            r#"
1195            {
1196                "applications": {
1197                    "debug_meta.images.*.code_file": ["@anything:replace"],
1198                    "debug_meta.images.*.debug_file": ["@anything:replace"]
1199                }
1200            }
1201            "#,
1202        )
1203        .unwrap();
1204
1205        let mut event = Annotated::new(Event {
1206            debug_meta: Annotated::new(DebugMeta {
1207                images: Annotated::new(vec![Annotated::new(DebugImage::Symbolic(Box::new(
1208                    NativeDebugImage {
1209                        code_id: Annotated::new("59b0d8f3183000".parse().unwrap()),
1210                        code_file: Annotated::new("C:\\Windows\\System32\\ntdll.dll".into()),
1211                        debug_id: Annotated::new(
1212                            "971f98e5-ce60-41ff-b2d7-235bbeb34578-1".parse().unwrap(),
1213                        ),
1214                        debug_file: Annotated::new("wntdll.pdb".into()),
1215                        debug_checksum: Annotated::empty(),
1216                        arch: Annotated::new("arm64".to_owned()),
1217                        image_addr: Annotated::new(Addr(0)),
1218                        image_size: Annotated::new(4096),
1219                        image_vmaddr: Annotated::new(Addr(32768)),
1220                        other: {
1221                            let mut map = Object::new();
1222                            map.insert(
1223                                "other".to_owned(),
1224                                Annotated::new(Value::String("value".to_owned())),
1225                            );
1226                            map
1227                        },
1228                    },
1229                )))]),
1230                ..Default::default()
1231            }),
1232            ..Default::default()
1233        });
1234
1235        let mut processor = PiiProcessor::new(config.compiled());
1236        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1237        assert_annotated_snapshot!(event);
1238    }
1239
1240    #[test]
1241    fn test_hash_debugmeta_path() {
1242        let config = serde_json::from_str::<PiiConfig>(
1243            r#"
1244            {
1245                "applications": {
1246                    "debug_meta.images.*.code_file": ["@anything:hash"],
1247                    "debug_meta.images.*.debug_file": ["@anything:hash"]
1248                }
1249            }
1250            "#,
1251        )
1252        .unwrap();
1253
1254        let mut event = Annotated::new(Event {
1255            debug_meta: Annotated::new(DebugMeta {
1256                images: Annotated::new(vec![Annotated::new(DebugImage::Symbolic(Box::new(
1257                    NativeDebugImage {
1258                        code_id: Annotated::new("59b0d8f3183000".parse().unwrap()),
1259                        code_file: Annotated::new("C:\\Windows\\System32\\ntdll.dll".into()),
1260                        debug_id: Annotated::new(
1261                            "971f98e5-ce60-41ff-b2d7-235bbeb34578-1".parse().unwrap(),
1262                        ),
1263                        debug_file: Annotated::new("wntdll.pdb".into()),
1264                        debug_checksum: Annotated::empty(),
1265                        arch: Annotated::new("arm64".to_owned()),
1266                        image_addr: Annotated::new(Addr(0)),
1267                        image_size: Annotated::new(4096),
1268                        image_vmaddr: Annotated::new(Addr(32768)),
1269                        other: {
1270                            let mut map = Object::new();
1271                            map.insert(
1272                                "other".to_owned(),
1273                                Annotated::new(Value::String("value".to_owned())),
1274                            );
1275                            map
1276                        },
1277                    },
1278                )))]),
1279                ..Default::default()
1280            }),
1281            ..Default::default()
1282        });
1283
1284        let mut processor = PiiProcessor::new(config.compiled());
1285        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1286        assert_annotated_snapshot!(event);
1287    }
1288
1289    #[test]
1290    fn test_debugmeta_path_not_addressible_with_wildcard_selector() {
1291        let config = serde_json::from_str::<PiiConfig>(
1292            r#"
1293            {
1294                "applications": {
1295                    "$string": ["@anything:remove"],
1296                    "**": ["@anything:remove"],
1297                    "debug_meta.**": ["@anything:remove"],
1298                    "(debug_meta.images.**.code_file & $string)": ["@anything:remove"]
1299                }
1300            }
1301            "#,
1302        )
1303        .unwrap();
1304
1305        let mut event = Annotated::new(Event {
1306            debug_meta: Annotated::new(DebugMeta {
1307                images: Annotated::new(vec![Annotated::new(DebugImage::Symbolic(Box::new(
1308                    NativeDebugImage {
1309                        code_id: Annotated::new("59b0d8f3183000".parse().unwrap()),
1310                        code_file: Annotated::new("C:\\Windows\\System32\\ntdll.dll".into()),
1311                        debug_id: Annotated::new(
1312                            "971f98e5-ce60-41ff-b2d7-235bbeb34578-1".parse().unwrap(),
1313                        ),
1314                        debug_file: Annotated::new("wntdll.pdb".into()),
1315                        debug_checksum: Annotated::empty(),
1316                        arch: Annotated::new("arm64".to_owned()),
1317                        image_addr: Annotated::new(Addr(0)),
1318                        image_size: Annotated::new(4096),
1319                        image_vmaddr: Annotated::new(Addr(32768)),
1320                        other: {
1321                            let mut map = Object::new();
1322                            map.insert(
1323                                "other".to_owned(),
1324                                Annotated::new(Value::String("value".to_owned())),
1325                            );
1326                            map
1327                        },
1328                    },
1329                )))]),
1330                ..Default::default()
1331            }),
1332            ..Default::default()
1333        });
1334
1335        let mut processor = PiiProcessor::new(config.compiled());
1336        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1337        assert_annotated_snapshot!(event);
1338    }
1339
1340    #[test]
1341    fn test_quoted_keys() {
1342        let config = serde_json::from_str::<PiiConfig>(
1343            r#"
1344            {
1345                "applications": {
1346                    "extra.'special ,./<>?!@#$%^&*())''gärbage'''": ["@anything:remove"]
1347                }
1348            }
1349            "#,
1350        )
1351        .unwrap();
1352
1353        let mut event = Annotated::new(Event {
1354            extra: {
1355                let mut map = Object::new();
1356                map.insert(
1357                    "do not ,./<>?!@#$%^&*())'ßtrip'".to_owned(),
1358                    Annotated::new(ExtraValue(Value::String("foo".to_owned()))),
1359                );
1360                map.insert(
1361                    "special ,./<>?!@#$%^&*())'gärbage'".to_owned(),
1362                    Annotated::new(ExtraValue(Value::String("bar".to_owned()))),
1363                );
1364                Annotated::new(map)
1365            },
1366            ..Default::default()
1367        });
1368
1369        let mut processor = PiiProcessor::new(config.compiled());
1370        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1371        assert_annotated_snapshot!(event);
1372    }
1373
1374    #[test]
1375    fn test_logentry_value_types() {
1376        // Assert that logentry.formatted is addressable as $string, $message and $logentry.formatted.
1377        for formatted_selector in &[
1378            "$logentry.formatted",
1379            "$message",
1380            "$logentry.formatted && $message",
1381            "$string",
1382        ] {
1383            let config = serde_json::from_str::<PiiConfig>(&format!(
1384                r##"
1385                {{
1386                    "applications": {{
1387                        "{formatted_selector}": ["@anything:remove"]
1388                    }}
1389                }}
1390                "##
1391            ))
1392            .unwrap();
1393
1394            let mut event = Annotated::new(Event {
1395                logentry: Annotated::new(LogEntry {
1396                    formatted: Annotated::new("Hello world!".to_owned().into()),
1397                    ..Default::default()
1398                }),
1399                ..Default::default()
1400            });
1401
1402            let mut processor = PiiProcessor::new(config.compiled());
1403            process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1404            assert!(
1405                event
1406                    .value()
1407                    .unwrap()
1408                    .logentry
1409                    .value()
1410                    .unwrap()
1411                    .formatted
1412                    .value()
1413                    .is_none()
1414            );
1415        }
1416    }
1417
1418    #[test]
1419    fn test_logentry_formatted_never_fully_filtered() {
1420        // Test that logentry.formatted gets smart PII scrubbing via to_pii_config
1421        // and is never completely filtered even with aggressive PII rules
1422        let config = crate::convert::to_pii_config(&crate::DataScrubbingConfig {
1423            scrub_data: true,
1424            scrub_defaults: true,
1425            scrub_ip_addresses: true,
1426            ..Default::default()
1427        })
1428        .unwrap();
1429
1430        let mut event = Annotated::new(Event {
1431            logentry: Annotated::new(LogEntry {
1432                formatted: Annotated::new(
1433                    "User john.doe@company.com failed login with card 4111-1111-1111-1111"
1434                        .to_owned()
1435                        .into(),
1436                ),
1437                ..Default::default()
1438            }),
1439            ..Default::default()
1440        });
1441
1442        let mut processor = PiiProcessor::new(config.compiled());
1443        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1444        assert_annotated_snapshot!(event, @r#"
1445        {
1446          "logentry": {
1447            "formatted": "User [email] failed login with card [creditcard]"
1448          },
1449          "_meta": {
1450            "logentry": {
1451              "formatted": {
1452                "": {
1453                  "rem": [
1454                    [
1455                      "@email:replace",
1456                      "s",
1457                      5,
1458                      12
1459                    ],
1460                    [
1461                      "@creditcard:replace",
1462                      "s",
1463                      36,
1464                      48
1465                    ]
1466                  ],
1467                  "len": 68
1468                }
1469              }
1470            }
1471          }
1472        }
1473        "#);
1474    }
1475
1476    #[test]
1477    fn test_logentry_formatted_bearer_token_scrubbing() {
1478        // Test that bearer tokens are properly scrubbed in logentry.formatted
1479        let config = crate::convert::to_pii_config(&crate::DataScrubbingConfig {
1480            scrub_data: true,
1481            scrub_defaults: true,
1482            ..Default::default()
1483        })
1484        .unwrap();
1485
1486        let mut event = Annotated::new(Event {
1487            logentry: Annotated::new(LogEntry {
1488                formatted: Annotated::new(
1489                    "API request failed with Bearer ABC123XYZ789TOKEN and other data"
1490                        .to_owned()
1491                        .into(),
1492                ),
1493                ..Default::default()
1494            }),
1495            ..Default::default()
1496        });
1497
1498        let mut processor = PiiProcessor::new(config.compiled());
1499        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1500        assert_annotated_snapshot!(event, @r#"
1501        {
1502          "logentry": {
1503            "formatted": "API request failed with Bearer [token] and other data"
1504          },
1505          "_meta": {
1506            "logentry": {
1507              "formatted": {
1508                "": {
1509                  "rem": [
1510                    [
1511                      "@bearer:replace",
1512                      "s",
1513                      24,
1514                      38
1515                    ]
1516                  ],
1517                  "len": 63
1518                }
1519              }
1520            }
1521          }
1522        }
1523        "#);
1524    }
1525
1526    #[test]
1527    fn test_logentry_formatted_password_word_not_scrubbed() {
1528        let config = PiiConfig::default();
1529        let mut event = Annotated::new(Event {
1530            logentry: Annotated::new(LogEntry {
1531                formatted: Annotated::new(
1532                    "User password is secret123 for authentication"
1533                        .to_owned()
1534                        .into(),
1535                ),
1536                ..Default::default()
1537            }),
1538            ..Default::default()
1539        });
1540
1541        let mut processor = PiiProcessor::new(config.compiled());
1542        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1543        assert_annotated_snapshot!(event, @r#"
1544        {
1545          "logentry": {
1546            "formatted": "User password is secret123 for authentication"
1547          }
1548        }
1549        "#);
1550    }
1551
1552    #[test]
1553    fn test_ip_address_hashing() {
1554        let config = serde_json::from_str::<PiiConfig>(
1555            r#"
1556            {
1557                "applications": {
1558                    "$user.ip_address": ["@ip:hash"]
1559                }
1560            }
1561            "#,
1562        )
1563        .unwrap();
1564
1565        let mut event = Annotated::new(Event {
1566            user: Annotated::new(User {
1567                ip_address: Annotated::new(IpAddr("127.0.0.1".to_owned())),
1568                ..Default::default()
1569            }),
1570            ..Default::default()
1571        });
1572
1573        let mut processor = PiiProcessor::new(config.compiled());
1574        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1575
1576        let user = event.value().unwrap().user.value().unwrap();
1577
1578        assert!(user.ip_address.value().is_none());
1579
1580        assert_eq!(
1581            user.id.value().unwrap().as_str(),
1582            "AE12FE3B5F129B5CC4CDD2B136B7B7947C4D2741"
1583        );
1584    }
1585
1586    #[test]
1587    fn test_ip_address_hashing_does_not_overwrite_id() {
1588        let config = serde_json::from_str::<PiiConfig>(
1589            r#"
1590            {
1591                "applications": {
1592                    "$user.ip_address": ["@ip:hash"]
1593                }
1594            }
1595            "#,
1596        )
1597        .unwrap();
1598
1599        let mut event = Annotated::new(Event {
1600            user: Annotated::new(User {
1601                id: Annotated::new("123".to_owned().into()),
1602                ip_address: Annotated::new(IpAddr("127.0.0.1".to_owned())),
1603                ..Default::default()
1604            }),
1605            ..Default::default()
1606        });
1607
1608        let mut processor = PiiProcessor::new(config.compiled());
1609        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1610
1611        let user = event.value().unwrap().user.value().unwrap();
1612
1613        // This will get wiped out in renormalization though
1614        assert_eq!(
1615            user.ip_address.value().unwrap().as_str(),
1616            "AE12FE3B5F129B5CC4CDD2B136B7B7947C4D2741"
1617        );
1618
1619        assert_eq!(user.id.value().unwrap().as_str(), "123");
1620    }
1621
1622    #[test]
1623    fn test_replace_replaced_text() {
1624        let chunks = vec![Chunk::Redaction {
1625            text: "[ip]".into(),
1626            rule_id: "@ip".into(),
1627            ty: RemarkType::Substituted,
1628        }];
1629        let rule = RuleRef {
1630            id: "@ip:replace".into(),
1631            origin: "@ip".into(),
1632            ty: RuleType::Ip,
1633            redaction: Redaction::Replace(ReplaceRedaction {
1634                text: "[ip]".into(),
1635            }),
1636        };
1637        let res = apply_regex_to_chunks(
1638            chunks.clone(),
1639            &rule,
1640            &Regex::new(r#".*"#).unwrap(),
1641            ReplaceBehavior::Value,
1642        );
1643        assert_eq!(chunks, res);
1644    }
1645
1646    #[test]
1647    fn test_replace_replaced_text_anything() {
1648        let chunks = vec![Chunk::Redaction {
1649            text: "[Filtered]".into(),
1650            rule_id: "@password:filter".into(),
1651            ty: RemarkType::Substituted,
1652        }];
1653        let rule = RuleRef {
1654            id: "@anything:filter".into(),
1655            origin: "@anything:filter".into(),
1656            ty: RuleType::Anything,
1657            redaction: Redaction::Replace(ReplaceRedaction {
1658                text: "[Filtered]".into(),
1659            }),
1660        };
1661        let res = apply_regex_to_chunks(
1662            chunks.clone(),
1663            &rule,
1664            &Regex::new(r#".*"#).unwrap(),
1665            ReplaceBehavior::Groups(smallvec::smallvec![0]),
1666        );
1667        assert_eq!(chunks, res);
1668    }
1669
1670    #[test]
1671    fn test_trace_route_params_scrubbed() {
1672        let mut trace_context: Annotated<TraceContext> = Annotated::from_json(
1673            r#"
1674            {
1675                "type": "trace",
1676                "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1677                "span_id": "fa90fdead5f74052",
1678                "data": {
1679                    "previousRoute": {
1680                        "params": {
1681                            "password": "test"
1682                        }
1683                    }
1684                }
1685            }
1686            "#,
1687        )
1688        .unwrap();
1689
1690        let ds_config = DataScrubbingConfig {
1691            scrub_data: true,
1692            scrub_defaults: true,
1693            ..Default::default()
1694        };
1695        let pii_config = ds_config.pii_config().as_ref().unwrap();
1696        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1697
1698        process_value(
1699            &mut trace_context,
1700            &mut pii_processor,
1701            ProcessingState::root(),
1702        )
1703        .unwrap();
1704        assert_annotated_snapshot!(trace_context);
1705    }
1706
1707    #[test]
1708    fn test_scrub_span_data_http_not_scrubbed() {
1709        let mut span: Annotated<Span> = Annotated::from_json(
1710            r#"{
1711                "data": {
1712                    "http": {
1713                        "query": "dance=true"
1714                    }
1715                }
1716            }"#,
1717        )
1718        .unwrap();
1719
1720        let ds_config = DataScrubbingConfig {
1721            scrub_data: true,
1722            scrub_defaults: true,
1723            ..Default::default()
1724        };
1725        let pii_config = ds_config.pii_config().as_ref().unwrap();
1726        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1727
1728        process_value(&mut span, &mut pii_processor, ProcessingState::root()).unwrap();
1729        assert_annotated_snapshot!(span);
1730    }
1731
1732    #[test]
1733    fn test_scrub_span_data_http_strings_are_scrubbed() {
1734        let mut span: Annotated<Span> = Annotated::from_json(
1735            r#"{
1736                "data": {
1737                    "http": {
1738                        "query": "ccnumber=5105105105105100&process_id=123",
1739                        "fragment": "ccnumber=5105105105105100,process_id=123"
1740                    }
1741                }
1742            }"#,
1743        )
1744        .unwrap();
1745
1746        let ds_config = DataScrubbingConfig {
1747            scrub_data: true,
1748            scrub_defaults: true,
1749            ..Default::default()
1750        };
1751        let pii_config = ds_config.pii_config().as_ref().unwrap();
1752        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1753
1754        process_value(&mut span, &mut pii_processor, ProcessingState::root()).unwrap();
1755        assert_annotated_snapshot!(span);
1756    }
1757
1758    #[test]
1759    fn test_scrub_span_data_http_objects_are_scrubbed() {
1760        let mut span: Annotated<Span> = Annotated::from_json(
1761            r#"{
1762                "data": {
1763                    "http": {
1764                        "query": {
1765                            "ccnumber": "5105105105105100",
1766                            "process_id": "123"
1767                        },
1768                        "fragment": {
1769                            "ccnumber": "5105105105105100",
1770                            "process_id": "123"
1771                        }
1772                    }
1773                }
1774            }"#,
1775        )
1776        .unwrap();
1777
1778        let ds_config = DataScrubbingConfig {
1779            scrub_data: true,
1780            scrub_defaults: true,
1781            ..Default::default()
1782        };
1783        let pii_config = ds_config.pii_config().as_ref().unwrap();
1784        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1785
1786        process_value(&mut span, &mut pii_processor, ProcessingState::root()).unwrap();
1787        assert_annotated_snapshot!(span);
1788    }
1789
1790    #[test]
1791    fn test_scrub_span_data_untyped_props_are_scrubbed() {
1792        let mut span: Annotated<Span> = Annotated::from_json(
1793            r#"{
1794                "data": {
1795                    "untyped": "ccnumber=5105105105105100",
1796                    "more_untyped": {
1797                        "typed": "no",
1798                        "scrubbed": "yes",
1799                        "ccnumber": "5105105105105100"
1800                    }
1801                }
1802            }"#,
1803        )
1804        .unwrap();
1805
1806        let ds_config = DataScrubbingConfig {
1807            scrub_data: true,
1808            scrub_defaults: true,
1809            ..Default::default()
1810        };
1811        let pii_config = ds_config.pii_config().as_ref().unwrap();
1812        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1813
1814        process_value(&mut span, &mut pii_processor, ProcessingState::root()).unwrap();
1815        assert_annotated_snapshot!(span);
1816    }
1817
1818    #[test]
1819    fn test_span_data_pii() {
1820        let mut span = Span::from_value(
1821            json!({
1822                "data": {
1823                    "code.filepath": "src/sentry/api/authentication.py",
1824                }
1825            })
1826            .into(),
1827        );
1828
1829        let ds_config = DataScrubbingConfig {
1830            scrub_data: true,
1831            scrub_defaults: true,
1832            ..Default::default()
1833        };
1834        let pii_config = ds_config.pii_config().as_ref().unwrap();
1835
1836        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1837        processor::process_value(&mut span, &mut pii_processor, ProcessingState::root()).unwrap();
1838        assert_eq!(
1839            span.0.unwrap().data.0.unwrap().other["code.filepath"].as_str(),
1840            Some("src/sentry/api/authentication.py")
1841        );
1842    }
1843
1844    #[test]
1845    fn test_csp_source_file_pii() {
1846        let mut event = Event::from_value(
1847            json!({
1848                "csp": {
1849                    "source_file": "authentication.js",
1850                }
1851            })
1852            .into(),
1853        );
1854
1855        let config = serde_json::from_str::<PiiConfig>(
1856            r#"
1857            {
1858                "applications": {
1859                    "csp.source_file": ["@anything:filter"]
1860                }
1861            }
1862            "#,
1863        )
1864        .unwrap();
1865
1866        let mut pii_processor = PiiProcessor::new(config.compiled());
1867        processor::process_value(&mut event, &mut pii_processor, ProcessingState::root()).unwrap();
1868        assert_eq!(get_value!(event.csp.source_file!).as_str(), "[Filtered]");
1869    }
1870
1871    #[test]
1872    fn test_scrub_breadcrumb_data_http_not_scrubbed() {
1873        let mut breadcrumb: Annotated<Breadcrumb> = Annotated::from_json(
1874            r#"{
1875                "data": {
1876                    "http": {
1877                        "query": "dance=true"
1878                    }
1879                }
1880            }"#,
1881        )
1882        .unwrap();
1883
1884        let ds_config = DataScrubbingConfig {
1885            scrub_data: true,
1886            scrub_defaults: true,
1887            ..Default::default()
1888        };
1889        let pii_config = ds_config.pii_config().as_ref().unwrap();
1890        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1891        process_value(&mut breadcrumb, &mut pii_processor, ProcessingState::root()).unwrap();
1892        assert_annotated_snapshot!(breadcrumb);
1893    }
1894
1895    #[test]
1896    fn test_scrub_breadcrumb_data_http_strings_are_scrubbed() {
1897        let mut breadcrumb: Annotated<Breadcrumb> = Annotated::from_json(
1898            r#"{
1899                "data": {
1900                    "http": {
1901                        "query": "ccnumber=5105105105105100&process_id=123",
1902                        "fragment": "ccnumber=5105105105105100,process_id=123"
1903                    }
1904                }
1905            }"#,
1906        )
1907        .unwrap();
1908
1909        let ds_config = DataScrubbingConfig {
1910            scrub_data: true,
1911            scrub_defaults: true,
1912            ..Default::default()
1913        };
1914        let pii_config = ds_config.pii_config().as_ref().unwrap();
1915        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1916        process_value(&mut breadcrumb, &mut pii_processor, ProcessingState::root()).unwrap();
1917        assert_annotated_snapshot!(breadcrumb);
1918    }
1919
1920    #[test]
1921    fn test_scrub_breadcrumb_data_http_objects_are_scrubbed() {
1922        let mut breadcrumb: Annotated<Breadcrumb> = Annotated::from_json(
1923            r#"{
1924                "data": {
1925                    "http": {
1926                        "query": {
1927                            "ccnumber": "5105105105105100",
1928                            "process_id": "123"
1929                        },
1930                        "fragment": {
1931                            "ccnumber": "5105105105105100",
1932                            "process_id": "123"
1933                        }
1934                    }
1935                }
1936            }"#,
1937        )
1938        .unwrap();
1939
1940        let ds_config = DataScrubbingConfig {
1941            scrub_data: true,
1942            scrub_defaults: true,
1943            ..Default::default()
1944        };
1945        let pii_config = ds_config.pii_config().as_ref().unwrap();
1946        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1947
1948        process_value(&mut breadcrumb, &mut pii_processor, ProcessingState::root()).unwrap();
1949        assert_annotated_snapshot!(breadcrumb);
1950    }
1951
1952    #[test]
1953    fn test_scrub_breadcrumb_data_untyped_props_are_scrubbed() {
1954        let mut breadcrumb: Annotated<Breadcrumb> = Annotated::from_json(
1955            r#"{
1956                "data": {
1957                    "untyped": "ccnumber=5105105105105100",
1958                    "more_untyped": {
1959                        "typed": "no",
1960                        "scrubbed": "yes",
1961                        "ccnumber": "5105105105105100"
1962                    }
1963                }
1964            }"#,
1965        )
1966        .unwrap();
1967
1968        let ds_config = DataScrubbingConfig {
1969            scrub_data: true,
1970            scrub_defaults: true,
1971            ..Default::default()
1972        };
1973        let pii_config = ds_config.pii_config().as_ref().unwrap();
1974        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
1975        process_value(&mut breadcrumb, &mut pii_processor, ProcessingState::root()).unwrap();
1976        assert_annotated_snapshot!(breadcrumb);
1977    }
1978
1979    #[test]
1980    fn test_scrub_graphql_response_data_with_variables() {
1981        let mut data = Event::from_value(
1982            json!({
1983              "request": {
1984                "data": {
1985                  "query": "{\n  viewer {\n    login\n  }\n}",
1986                  "variables": {
1987                    "login": "foo"
1988                  }
1989                },
1990                "api_target": "graphql"
1991              },
1992              "contexts": {
1993                "response": {
1994                  "type": "response",
1995                  "data": {
1996                    "data": {
1997                      "viewer": {
1998                        "login": "foo"
1999                      }
2000                    }
2001                  }
2002                }
2003              }
2004            })
2005            .into(),
2006        );
2007
2008        scrub_graphql(data.value_mut().as_mut().unwrap());
2009
2010        assert_debug_snapshot!(&data);
2011    }
2012
2013    #[test]
2014    fn test_scrub_graphql_response_data_without_variables() {
2015        let mut data = Event::from_value(
2016            json!({
2017              "request": {
2018                "data": {
2019                  "query": "{\n  viewer {\n    login\n  }\n}"
2020                },
2021                "api_target": "graphql"
2022              },
2023              "contexts": {
2024                "response": {
2025                  "type": "response",
2026                  "data": {
2027                    "data": {
2028                      "viewer": {
2029                        "login": "foo"
2030                      }
2031                    }
2032                  }
2033                }
2034              }
2035            })
2036            .into(),
2037        );
2038
2039        scrub_graphql(data.value_mut().as_mut().unwrap());
2040        assert_debug_snapshot!(&data);
2041    }
2042
2043    #[test]
2044    fn test_does_not_scrub_if_no_graphql() {
2045        let mut data = Event::from_value(
2046            json!({
2047              "request": {
2048                "data": {
2049                  "query": "{\n  viewer {\n    login\n  }\n}",
2050                  "variables": {
2051                    "login": "foo"
2052                  }
2053                },
2054              },
2055              "contexts": {
2056                "response": {
2057                  "type": "response",
2058                  "data": {
2059                    "data": {
2060                      "viewer": {
2061                        "login": "foo"
2062                      }
2063                    }
2064                  }
2065                }
2066              }
2067            })
2068            .into(),
2069        );
2070
2071        let scrubbing_config = DataScrubbingConfig {
2072            scrub_data: true,
2073            scrub_ip_addresses: true,
2074            scrub_defaults: true,
2075            ..Default::default()
2076        };
2077
2078        let pii_config = to_pii_config(&scrubbing_config).unwrap();
2079        let mut pii_processor = PiiProcessor::new(pii_config.compiled());
2080
2081        process_value(&mut data, &mut pii_processor, ProcessingState::root()).unwrap();
2082
2083        assert_debug_snapshot!(&data);
2084    }
2085
2086    #[test]
2087    fn test_logentry_params_scrubbed() {
2088        let config = serde_json::from_str::<PiiConfig>(
2089            r##"
2090                {
2091                    "applications": {
2092                        "$string": ["@anything:remove"]
2093                    }
2094                }
2095                "##,
2096        )
2097        .unwrap();
2098
2099        let mut event = Annotated::new(Event {
2100            logentry: Annotated::new(LogEntry {
2101                message: Annotated::new(Message::from("failed to parse report id=%s".to_owned())),
2102                formatted: Annotated::new("failed to parse report id=1".to_owned().into()),
2103                params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
2104                    "12345".to_owned(),
2105                ))])),
2106                ..Default::default()
2107            }),
2108            ..Default::default()
2109        });
2110
2111        let mut processor = PiiProcessor::new(config.compiled());
2112        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
2113
2114        let params = get_value!(event.logentry.params!);
2115        assert_debug_snapshot!(params, @r###"
2116        Array(
2117            [
2118                Meta {
2119                    remarks: [
2120                        Remark {
2121                            ty: Removed,
2122                            rule_id: "@anything:remove",
2123                            range: None,
2124                        },
2125                    ],
2126                    errors: [],
2127                    original_length: None,
2128                    original_value: None,
2129                },
2130            ],
2131        )
2132        "###);
2133    }
2134
2135    #[test]
2136    fn test_is_pairlist() {
2137        for (case, expected) in [
2138            (r#"[]"#, false),
2139            (r#"["foo"]"#, false),
2140            (r#"["foo", 123]"#, false),
2141            (r#"[[1, "foo"]]"#, false),
2142            (r#"[[["too_nested", 123]]]"#, false),
2143            (r#"[["foo", "bar"], [1, "foo"]]"#, false),
2144            (r#"[["foo", "bar"], ["foo", "bar", "baz"]]"#, false),
2145            (r#"[["foo", "bar", "baz"], ["foo", "bar"]]"#, false),
2146            (r#"["foo", ["bar", "baz"], ["foo", "bar"]]"#, false),
2147            (r#"[["foo", "bar"], [["too_nested", 123]]]"#, false),
2148            (r#"[["foo", 123]]"#, true),
2149            (r#"[["foo", "bar"]]"#, true),
2150            (
2151                r#"[["foo", "bar"], ["foo", {"nested": {"something": 1}}]]"#,
2152                true,
2153            ),
2154        ] {
2155            let v = Annotated::<Value>::from_json(case).unwrap();
2156            let Annotated(Some(Value::Array(mut a)), _) = v else {
2157                panic!()
2158            };
2159            assert_eq!(is_pairlist(&mut a), expected, "{case}");
2160        }
2161    }
2162
2163    #[test]
2164    fn test_tuple_array_scrubbed_with_path_selector() {
2165        // We expect that both of these configs express the same semantics.
2166        let configs = vec![
2167            // This configuration matches on the authorization element (the 1st element of the array
2168            // represents the key).
2169            r##"
2170                {
2171                    "applications": {
2172                        "exception.values.0.stacktrace.frames.0.vars.headers.authorization": ["@anything:replace"]
2173                    }
2174                }
2175                "##,
2176            // This configuration matches on the 2nd element of the array.
2177            r##"
2178                {
2179                    "applications": {
2180                        "exception.values.0.stacktrace.frames.0.vars.headers.0.1": ["@anything:replace"]
2181                    }
2182                }
2183                "##,
2184        ];
2185
2186        let mut event = Event::from_value(
2187            serde_json::json!(
2188            {
2189              "message": "hi",
2190              "exception": {
2191                "values": [
2192                  {
2193                    "type": "BrokenException",
2194                    "value": "Something failed",
2195                    "stacktrace": {
2196                      "frames": [
2197                        {
2198                            "vars": {
2199                                "headers": [
2200                                    ["authorization", "Bearer abc123"]
2201                                ]
2202                            }
2203                        }
2204                      ]
2205                    }
2206                  }
2207                ]
2208              }
2209            })
2210            .into(),
2211        );
2212
2213        for config in configs {
2214            let config = serde_json::from_str::<PiiConfig>(config).unwrap();
2215            let mut processor = PiiProcessor::new(config.compiled());
2216            process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
2217
2218            let vars = get_value!(event.exceptions.values[0].stacktrace.frames[0].vars).unwrap();
2219
2220            allow_duplicates!(assert_debug_snapshot!(vars, @r###"
2221                              FrameVars(
2222                                  {
2223                                      "headers": Array(
2224                                          [
2225                                              Array(
2226                                                  [
2227                                                      String(
2228                                                          "authorization",
2229                                                      ),
2230                                                      Annotated(
2231                                                          String(
2232                                                              "[Filtered]",
2233                                                          ),
2234                                                          Meta {
2235                                                              remarks: [
2236                                                                  Remark {
2237                                                                      ty: Substituted,
2238                                                                      rule_id: "@anything:replace",
2239                                                                      range: Some(
2240                                                                          (
2241                                                                              0,
2242                                                                              10,
2243                                                                          ),
2244                                                                      ),
2245                                                                  },
2246                                                              ],
2247                                                              errors: [],
2248                                                              original_length: Some(
2249                                                                  13,
2250                                                              ),
2251                                                              original_value: None,
2252                                                          },
2253                                                      ),
2254                                                  ],
2255                                              ),
2256                                          ],
2257                                      ),
2258                                  },
2259                              )
2260                              "###));
2261        }
2262    }
2263
2264    #[test]
2265    fn test_tuple_array_scrubbed_with_string_selector_and_password_matcher() {
2266        let config = serde_json::from_str::<PiiConfig>(
2267            r##"
2268                {
2269                    "applications": {
2270                        "$string": ["@password:remove"]
2271                    }
2272                }
2273                "##,
2274        )
2275        .unwrap();
2276
2277        let mut event = Event::from_value(
2278            serde_json::json!(
2279            {
2280              "message": "hi",
2281              "exception": {
2282                "values": [
2283                  {
2284                    "type": "BrokenException",
2285                    "value": "Something failed",
2286                    "stacktrace": {
2287                      "frames": [
2288                        {
2289                            "vars": {
2290                                "headers": [
2291                                    ["authorization", "abc123"]
2292                                ]
2293                            }
2294                        }
2295                      ]
2296                    }
2297                  }
2298                ]
2299              }
2300            })
2301            .into(),
2302        );
2303
2304        let mut processor = PiiProcessor::new(config.compiled());
2305        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
2306
2307        let vars = get_value!(event.exceptions.values[0].stacktrace.frames[0].vars).unwrap();
2308
2309        assert_debug_snapshot!(vars, @r###"
2310        FrameVars(
2311            {
2312                "headers": Array(
2313                    [
2314                        Array(
2315                            [
2316                                String(
2317                                    "authorization",
2318                                ),
2319                                Meta {
2320                                    remarks: [
2321                                        Remark {
2322                                            ty: Removed,
2323                                            rule_id: "@password:remove",
2324                                            range: None,
2325                                        },
2326                                    ],
2327                                    errors: [],
2328                                    original_length: None,
2329                                    original_value: None,
2330                                },
2331                            ],
2332                        ),
2333                    ],
2334                ),
2335            },
2336        )
2337        "###);
2338    }
2339}