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#[derive(Debug, Clone, Copy)]
24pub enum AttributeMode {
25 Object,
28 ValueOnly,
31}
32
33pub struct PiiProcessor<'a> {
35 attribute_mode: AttributeMode,
37 compiled_config: &'a CompiledPiiConfig,
38}
39
40impl<'a> PiiProcessor<'a> {
41 pub fn new(compiled_config: &'a CompiledPiiConfig) -> PiiProcessor<'a> {
43 PiiProcessor {
46 compiled_config,
47 attribute_mode: AttributeMode::Object,
48 }
49 }
50
51 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 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 meta.set_original_value(Option::<String>::None);
107 }
108 }
109 }
110
111 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 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 let key_state =
147 state.enter_borrowed(key_name, state.inner_attrs(), value_type);
148 process_value(&mut value, self, &key_state)?;
151 }
152
153 pair[1] = value;
155 }
156
157 *annotated = T::from_value(mapped);
159 }
160
161 Ok(())
162 } else {
163 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 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 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 AttributeMode::Object => value.process_child_values(self, state),
237 AttributeMode::ValueOnly => {
239 for (key, attribute) in value.0.iter_mut() {
240 let Some(attribute) = attribute.value_mut() else {
241 continue;
242 };
243
244 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 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 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 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 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
375pub fn scrub_graphql(event: &mut Event) {
377 let mut keys: BTreeSet<&str> = BTreeSet::new();
378
379 let mut is_graphql = false;
380
381 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 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 data.remove("data");
417 }
418 }
419}
420
421fn 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 let should_redact_chunks = !matches!(rule.redaction, Redaction::Default | Redaction::Remove);
446
447 if rule.ty == RuleType::Anything && (value.is_none() || !should_redact_chunks) {
450 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 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 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 return chunks;
510 }
511
512 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 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 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", "ip_address": "is this an ip address? 73.133.27.120", },
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 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 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 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 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 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 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 let configs = vec![
2167 r##"
2170 {
2171 "applications": {
2172 "exception.values.0.stacktrace.frames.0.vars.headers.authorization": ["@anything:replace"]
2173 }
2174 }
2175 "##,
2176 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}