Skip to main content

relay_pii/
eap.rs

1//! Provides the `scrub` function for scrubbing EAP items.
2//!
3//! This module also contains tests for scrubbing attributes.
4
5use relay_event_schema::processor::{
6    ProcessValue, ProcessingResult, ProcessingState, ValueType, process_value,
7};
8use relay_protocol::Annotated;
9
10use crate::{AttributeMode, PiiConfig, PiiProcessor};
11
12/// Scrubs an EAP item (such as an `OurLog` or `SpanV2`) with the given PII configs.
13pub fn scrub<T: ProcessValue>(
14    value_type: ValueType,
15    item: &mut Annotated<T>,
16    advanced_rules: Option<&PiiConfig>,
17    legacy_rule: Option<&PiiConfig>,
18) -> ProcessingResult {
19    let state = ProcessingState::root().enter_borrowed("", None, [value_type]);
20
21    if let Some(config) = advanced_rules {
22        let mut processor = PiiProcessor::new(config.compiled())
23            // For advanced rules we want to treat attributes as objects.
24            .attribute_mode(AttributeMode::Object);
25        process_value(item, &mut processor, &state)?;
26    }
27
28    if let Some(config) = legacy_rule {
29        let mut processor = PiiProcessor::new(config.compiled())
30            // For "legacy" rules we want to identify attributes with their values.
31            .attribute_mode(AttributeMode::ValueOnly);
32        process_value(item, &mut processor, &state)?;
33    }
34
35    Ok(())
36}
37
38#[cfg(test)]
39mod tests {
40    use relay_event_schema::processor::ValueType;
41    use relay_event_schema::protocol::{Attributes, OurLog, SpanV2, TraceMetric};
42    use relay_protocol::{Annotated, SerializableAnnotated};
43
44    use crate::{DataScrubbingConfig, PiiConfig, eap};
45
46    #[test]
47    fn test_scrub_attributes_pii_default_rules() {
48        // `sentry.description`, `user.name`, `sentry.release`, `url.domain`, and `url.path` are marked as follows in `sentry-conventions`:
49        // * `sentry.description`: `true`
50        // * `user.name`: `true`
51        // * `sentry.release`: `false`
52        // * `url.domain`: `maybe`
53        // * `url.path`: `true`
54        // Therefore, default rules should scrub the `true` attributes and leave the `false` and `maybe` attributes intact.
55        let json = r#"{
56              "sentry.description": {
57                  "type": "string",
58                  "value": "secret123"
59              },
60              "user.name": {
61                  "type": "string",
62                  "value": "secret123"
63              },
64              "sentry.release": {
65                  "type": "string",
66                  "value": "secret123"
67              },
68              "url.domain": {
69                  "type": "string",
70                  "value": "secret123"
71              },
72              "url.path": {
73                  "type": "string",
74                  "value": "secret123"
75              },
76              "password": {
77                  "type": "string",
78                  "value": "secret123"
79              },
80              "secret": {
81                  "type": "string",
82                  "value": "topsecret"
83              },
84              "api_key": {
85                  "type": "string",
86                  "value": "sk-1234567890abcdef"
87              },
88              "auth_token": {
89                  "type": "string",
90                  "value": "bearer_token_123"
91              },
92              "credit_card": {
93                  "type": "string",
94                  "value": "4571234567890111"
95              },
96              "visa_card": {
97                  "type": "string",
98                  "value": "4571 2345 6789 0111"
99              },
100              "bank_account": {
101                  "type": "string",
102                  "value": "DE89370400440532013000"
103              },
104              "iban_code": {
105                  "type": "string",
106                  "value": "NO9386011117945"
107              },
108              "authorization": {
109                  "type": "string",
110                  "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
111              },
112              "private_key": {
113                  "type": "string",
114                  "value": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKB\n-----END PRIVATE KEY-----"
115              },
116              "database_url": {
117                  "type": "string",
118                  "value": "https://username:password@example.com/db"
119              },
120              "file_path": {
121                  "type": "string",
122                  "value": "C:\\Users\\john\\Documents\\secret.txt"
123              },
124              "unix_path": {
125                  "type": "string",
126                  "value": "/home/alice/private/data.json"
127              },
128              "social_security": {
129                  "type": "string",
130                  "value": "078-05-1120"
131              },
132              "user_email": {
133                  "type": "string",
134                  "value": "john.doe@example.com"
135              },
136              "client_ip": {
137                  "type": "string",
138                  "value": "192.168.1.100"
139              },
140              "ipv6_addr": {
141                  "type": "string",
142                  "value": "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
143              },
144              "mac_address": {
145                  "type": "string",
146                  "value": "4a:00:04:10:9b:50"
147              },
148              "session_id": {
149                  "type": "string",
150                  "value": "ceee0822-ed8f-4622-b2a3-789e73e75cd1"
151              },
152              "device_imei": {
153                  "type": "string",
154                  "value": "356938035643809"
155              },
156              "public_data": {
157                  "type": "string",
158                  "value": "public_data"
159              },
160              "very_sensitive_data": {
161                  "type": "string",
162                  "value": "very_sensitive_data"
163              }
164        }"#;
165
166        let mut data = Annotated::<Attributes>::from_json(json).unwrap();
167
168        let data_scrubbing_config = DataScrubbingConfig {
169            scrub_data: true,
170            scrub_defaults: true,
171            scrub_ip_addresses: true,
172            exclude_fields: vec!["public_data".to_owned()],
173            sensitive_fields: vec![
174                "value".to_owned(), // Make sure the inner 'value' of the attribute object isn't scrubbed.
175                "very_sensitive_data".to_owned(),
176            ],
177            ..Default::default()
178        };
179
180        let scrubbing_config = data_scrubbing_config.pii_config();
181
182        eap::scrub(ValueType::Span, &mut data, None, scrubbing_config.as_ref()).unwrap();
183
184        insta::assert_json_snapshot!(SerializableAnnotated(&data));
185    }
186
187    #[test]
188    fn test_scrub_attributes_pii_custom_object_rules() {
189        // `sentry.description`, `user.name`, `sentry.release`, `url.domain`, and `url.path` are marked as follows in `sentry-conventions`:
190        // * `sentry.description`: `true`
191        // * `user.name`: `true`
192        // * `sentry.release`: `false`
193        // * `url.domain`: `maybe`
194        // * `url.path`: `true`
195        // Therefore, custom rules should scrub the `true` and `maybe` attributes addressed explicitly, but not the `false` attribute.
196        let json = r#"
197        {
198          "sentry.description": {
199              "type": "string",
200              "value": "secret123"
201          },
202          "user.name": {
203              "type": "string",
204              "value": "secret123"
205          },
206          "sentry.release": {
207              "type": "string",
208              "value": "secret123"
209          },
210          "url.path": {
211              "type": "string",
212              "value": "secret123"
213          },
214          "url.domain": {
215              "type": "string",
216              "value": "secret123"
217          },
218          "password": {
219              "type": "string",
220              "value": "default_scrubbing_rules_are_off"
221          },
222          "test_field_mac": {
223              "type": "string",
224              "value": "4a:00:04:10:9b:50"
225          },
226          "test_field_imei": {
227              "type": "string",
228              "value": "356938035643809"
229          },
230          "test_field_uuid": {
231              "type": "string",
232              "value": "123e4567-e89b-12d3-a456-426614174000"
233          },
234          "test_field_regex_passes": {
235              "type": "string",
236              "value": "wxyz"
237          },
238          "test_field_regex_fails": {
239              "type": "string",
240              "value": "abc"
241          }
242        }"#;
243
244        let mut data = Annotated::<Attributes>::from_json(json).unwrap();
245
246        let scrubbing_config = DataScrubbingConfig {
247            scrub_data: true,
248            scrub_defaults: false,
249            scrub_ip_addresses: false,
250            ..Default::default()
251        };
252
253        let scrubbing_config = scrubbing_config.pii_config();
254
255        let config = serde_json::from_value::<PiiConfig>(serde_json::json!(
256        {
257            "rules": {
258                "project:0": {
259                    "type": "mac",
260                    "redaction": {
261                        "method": "replace",
262                        "text": "ITS_GONE"
263                    }
264                },
265                "project:1": {
266                    "type": "imei",
267                    "redaction": {
268                        "method": "replace",
269                        "text": "ITS_GONE"
270                    }
271                },
272                "project:2": {
273                    "type": "uuid",
274                    "redaction": {
275                        "method": "replace",
276                        "text": "BYE"
277                    }
278                },
279                "project:3": {
280                    "type": "pattern",
281                    "pattern": "[w-z]+",
282                    "redaction": {
283                        "method": "replace",
284                        "text": "REGEXED"
285                    }
286                },
287                "project:4": {
288                    "type": "anything",
289                    "redaction": {
290                        "method": "replace",
291                        "text": "[USER NAME]"
292                    }
293                },
294                "project:5": {
295                    "type": "anything",
296                    "redaction": {
297                        "method": "replace",
298                        "text": "[RELEASE]"
299                    }
300                },
301                "project:6": {
302                    "type": "anything",
303                    "redaction": {
304                        "method": "replace",
305                        "text": "[URL PATH]"
306                    }
307                },
308                "project:7": {
309                    "type": "anything",
310                    "redaction": {
311                        "method": "replace",
312                        "text": "[DESCRIPTION]"
313                    }
314                },
315                "project:8": {
316                    "type": "anything",
317                    "redaction": {
318                        "method": "replace",
319                        "text": "[URL DOMAIN]"
320                    }
321                }
322            },
323            "applications": {
324                "test_field_mac.value": [
325                    "project:0"
326                ],
327                "test_field_imei.value": [
328                    "project:1"
329                ],
330                "test_field_uuid.value": [
331                    "project:2"
332                ],
333                "test_field_regex_passes.value || test_field_regex_fails.value": [
334                    "project:3"
335                ],
336                "'user.name'.value": [
337                    "project:4"
338                ],
339                "'sentry.release'.value": [
340                    "project:5"
341                ],
342                "'url.path'.value": [
343                    "project:6"
344                ],
345                "'sentry.description'.value": [
346                    "project:7"
347                ],
348                "'url.domain'.value": [
349                    "project:8"
350                ]
351            }
352        }
353        ))
354        .unwrap();
355
356        eap::scrub(
357            ValueType::Span,
358            &mut data,
359            Some(&config),
360            scrubbing_config.as_ref(),
361        )
362        .unwrap();
363
364        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
365        {
366          "password": {
367            "type": "string",
368            "value": "default_scrubbing_rules_are_off"
369          },
370          "sentry.description": {
371            "type": "string",
372            "value": "[DESCRIPTION]"
373          },
374          "sentry.release": {
375            "type": "string",
376            "value": "secret123"
377          },
378          "test_field_imei": {
379            "type": "string",
380            "value": "ITS_GONE"
381          },
382          "test_field_mac": {
383            "type": "string",
384            "value": "ITS_GONE"
385          },
386          "test_field_regex_fails": {
387            "type": "string",
388            "value": "abc"
389          },
390          "test_field_regex_passes": {
391            "type": "string",
392            "value": "REGEXED"
393          },
394          "test_field_uuid": {
395            "type": "string",
396            "value": "BYE"
397          },
398          "url.domain": {
399            "type": "string",
400            "value": "[URL DOMAIN]"
401          },
402          "url.path": {
403            "type": "string",
404            "value": "[URL PATH]"
405          },
406          "user.name": {
407            "type": "string",
408            "value": "[USER NAME]"
409          },
410          "_meta": {
411            "sentry.description": {
412              "value": {
413                "": {
414                  "rem": [
415                    [
416                      "project:7",
417                      "s",
418                      0,
419                      13
420                    ]
421                  ],
422                  "len": 9
423                }
424              }
425            },
426            "test_field_imei": {
427              "value": {
428                "": {
429                  "rem": [
430                    [
431                      "project:1",
432                      "s",
433                      0,
434                      8
435                    ]
436                  ],
437                  "len": 15
438                }
439              }
440            },
441            "test_field_mac": {
442              "value": {
443                "": {
444                  "rem": [
445                    [
446                      "project:0",
447                      "s",
448                      0,
449                      8
450                    ]
451                  ],
452                  "len": 17
453                }
454              }
455            },
456            "test_field_regex_passes": {
457              "value": {
458                "": {
459                  "rem": [
460                    [
461                      "project:3",
462                      "s",
463                      0,
464                      7
465                    ]
466                  ],
467                  "len": 4
468                }
469              }
470            },
471            "test_field_uuid": {
472              "value": {
473                "": {
474                  "rem": [
475                    [
476                      "project:2",
477                      "s",
478                      0,
479                      3
480                    ]
481                  ],
482                  "len": 36
483                }
484              }
485            },
486            "url.domain": {
487              "value": {
488                "": {
489                  "rem": [
490                    [
491                      "project:8",
492                      "s",
493                      0,
494                      12
495                    ]
496                  ],
497                  "len": 9
498                }
499              }
500            },
501            "url.path": {
502              "value": {
503                "": {
504                  "rem": [
505                    [
506                      "project:6",
507                      "s",
508                      0,
509                      10
510                    ]
511                  ],
512                  "len": 9
513                }
514              }
515            },
516            "user.name": {
517              "value": {
518                "": {
519                  "rem": [
520                    [
521                      "project:4",
522                      "s",
523                      0,
524                      11
525                    ]
526                  ],
527                  "len": 9
528                }
529              }
530            }
531          }
532        }
533        "###);
534    }
535
536    #[test]
537    fn test_scrub_attributes_sensitive_fields() {
538        let json = r#"
539        {
540            "normal_field": {
541                "type": "string",
542                "value": "normal_data"
543            },
544            "sensitive_custom": {
545                "type": "string",
546                "value": "should_be_removed"
547            },
548            "another_sensitive": {
549                "type": "integer",
550                "value": 42
551            },
552            "my_value": {
553                "type": "string",
554                "value": "this_should_be_removed_as_sensitive"
555            }
556        }
557        "#;
558
559        let mut data = Annotated::<Attributes>::from_json(json).unwrap();
560
561        let scrubbing_config = DataScrubbingConfig {
562            scrub_data: true,
563            scrub_defaults: false,
564            scrub_ip_addresses: false,
565            sensitive_fields: vec![
566                "value".to_owned(), // Make sure the inner 'value' of the attribute object isn't scrubbed.
567                "sensitive_custom".to_owned(),
568                "another_sensitive".to_owned(),
569            ],
570            ..Default::default()
571        };
572
573        let scrubbing_config = scrubbing_config.pii_config();
574
575        eap::scrub(ValueType::Span, &mut data, None, scrubbing_config.as_ref()).unwrap();
576
577        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
578        {
579          "another_sensitive": {
580            "type": "integer",
581            "value": null
582          },
583          "my_value": {
584            "type": "string",
585            "value": "[Filtered]"
586          },
587          "normal_field": {
588            "type": "string",
589            "value": "normal_data"
590          },
591          "sensitive_custom": {
592            "type": "string",
593            "value": "[Filtered]"
594          },
595          "_meta": {
596            "another_sensitive": {
597              "value": {
598                "": {
599                  "rem": [
600                    [
601                      "strip-fields",
602                      "x"
603                    ]
604                  ]
605                }
606              }
607            },
608            "my_value": {
609              "value": {
610                "": {
611                  "rem": [
612                    [
613                      "strip-fields",
614                      "s",
615                      0,
616                      10
617                    ]
618                  ],
619                  "len": 35
620                }
621              }
622            },
623            "sensitive_custom": {
624              "value": {
625                "": {
626                  "rem": [
627                    [
628                      "strip-fields",
629                      "s",
630                      0,
631                      10
632                    ]
633                  ],
634                  "len": 17
635                }
636              }
637            }
638          }
639        }
640        "###);
641    }
642
643    #[test]
644    fn test_scrub_attributes_safe_fields() {
645        let json = r#"
646        {
647            "password": {
648                "type": "string",
649                "value": "secret123"
650            },
651            "credit_card": {
652                "type": "string",
653                "value": "4242424242424242"
654            },
655            "secret": {
656                "type": "string",
657                "value": "this_should_stay"
658            }
659        }
660        "#;
661
662        let mut data = Annotated::<Attributes>::from_json(json).unwrap();
663
664        let scrubbing_config = DataScrubbingConfig {
665            scrub_data: true,
666            scrub_defaults: true,
667            scrub_ip_addresses: false,
668            exclude_fields: vec!["secret".to_owned()], // Only 'secret' is safe
669            ..Default::default()
670        };
671
672        let scrubbing_config = scrubbing_config.pii_config();
673
674        eap::scrub(ValueType::Span, &mut data, None, scrubbing_config.as_ref()).unwrap();
675
676        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
677        {
678          "credit_card": {
679            "type": "string",
680            "value": "[Filtered]"
681          },
682          "password": {
683            "type": "string",
684            "value": "[Filtered]"
685          },
686          "secret": {
687            "type": "string",
688            "value": "this_should_stay"
689          },
690          "_meta": {
691            "credit_card": {
692              "value": {
693                "": {
694                  "rem": [
695                    [
696                      "@creditcard:filter",
697                      "s",
698                      0,
699                      10
700                    ]
701                  ],
702                  "len": 16
703                }
704              }
705            },
706            "password": {
707              "value": {
708                "": {
709                  "rem": [
710                    [
711                      "@password:filter",
712                      "s",
713                      0,
714                      10
715                    ]
716                  ],
717                  "len": 9
718                }
719              }
720            }
721          }
722        }
723        "###);
724    }
725
726    #[test]
727    fn test_scrub_attributes_implicit_attribute_value() {
728        let json = r#"
729        {
730            "remove_this_string_abc123": {
731                "type": "string",
732                "value": "abc123"
733            }
734        }
735        "#;
736
737        let mut data = Annotated::<Attributes>::from_json(json).unwrap();
738
739        let config = serde_json::from_value::<PiiConfig>(serde_json::json!({
740            "rules": {
741                "remove_abc123": {
742                    "type": "pattern",
743                    "pattern": "abc123",
744                    "redaction": {
745                        "method": "replace",
746                        "text": "abc---"
747                    }
748                },
749            },
750            "applications": {
751                "remove_this_string_abc123": ["remove_abc123"], // This selector should NOT match
752            }
753        }))
754        .unwrap();
755
756        let scrubbing_config = DataScrubbingConfig {
757            scrub_data: true,
758            scrub_defaults: true,
759            ..Default::default()
760        };
761
762        let scrubbing_config = scrubbing_config.pii_config();
763
764        eap::scrub(
765            ValueType::OurLog,
766            &mut data,
767            Some(&config),
768            scrubbing_config.as_ref(),
769        )
770        .unwrap();
771
772        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
773        {
774          "remove_this_string_abc123": {
775            "type": "string",
776            "value": "abc123"
777          }
778        }
779        "###);
780
781        let config = serde_json::from_value::<PiiConfig>(serde_json::json!({
782            "rules": {
783                "remove_abc123": {
784                    "type": "pattern",
785                    "pattern": "abc123",
786                    "redaction": {
787                        "method": "replace",
788                        "text": "abc---"
789                    }
790                },
791            },
792            "applications": {
793                "remove_this_string_abc123.value": ["remove_abc123"],
794            }
795        }))
796        .unwrap();
797
798        eap::scrub(
799            ValueType::OurLog,
800            &mut data,
801            Some(&config),
802            scrubbing_config.as_ref(),
803        )
804        .unwrap();
805
806        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
807        {
808          "remove_this_string_abc123": {
809            "type": "string",
810            "value": "abc---"
811          },
812          "_meta": {
813            "remove_this_string_abc123": {
814              "value": {
815                "": {
816                  "rem": [
817                    [
818                      "remove_abc123",
819                      "s",
820                      0,
821                      6
822                    ]
823                  ],
824                  "len": 6
825                }
826              }
827            }
828          }
829        }
830        "###);
831    }
832
833    macro_rules! attribute_rule_test {
834        ($test_name:ident, $rule_type:expr, $test_value:expr, @$snapshot:literal) => {
835            #[test]
836            fn $test_name() {
837                let config = serde_json::from_value::<PiiConfig>(serde_json::json!({
838                    "applications": {
839                        "$string": [$rule_type]
840                    }
841                }))
842                .unwrap();
843
844                let mut scrubbing_config = crate::DataScrubbingConfig::default();
845                scrubbing_config.scrub_data = true;
846                scrubbing_config.scrub_defaults = false;
847                scrubbing_config.scrub_ip_addresses = false;
848
849                let scrubbing_config = scrubbing_config.pii_config();
850
851                let span_json = format!(r#"{{
852                    "start_timestamp": 1544719859.0,
853                    "end_timestamp": 1544719860.0,
854                    "trace_id": "5b8efff798038103d269b633813fc60c",
855                    "span_id": "eee19b7ec3c1b174",
856                    "name": "test",
857                    "attributes": {{
858                        "{rule_type}|{value}": {{
859                            "type": "string",
860                            "value": "{value}"
861                        }}
862                    }}
863                }}"#,
864                rule_type = $rule_type,
865                value = $test_value
866                    .replace('\\', "\\\\")
867                    .replace('"', "\\\"")
868                    .replace('\n', "\\n")
869                );
870
871                let mut span = Annotated::<SpanV2>::from_json(&span_json).unwrap();
872
873                eap::scrub(ValueType::Span, &mut span, Some(&config), scrubbing_config.as_ref()).unwrap();
874
875                insta::allow_duplicates!(insta::assert_json_snapshot!(SerializableAnnotated(&span.value().unwrap().attributes), @$snapshot));
876
877                let log_json = format!(r#"{{
878                    "timestamp": 1544719860.0,
879                    "trace_id": "5b8efff798038103d269b633813fc60c",
880                    "span_id": "eee19b7ec3c1b174",
881                    "level": "info",
882                    "body": "Test log",
883                    "attributes": {{
884                        "{rule_type}|{value}": {{
885                            "type": "string",
886                            "value": "{value}"
887                        }}
888                    }}
889                }}"#,
890                rule_type = $rule_type,
891                value = $test_value
892                    .replace('\\', "\\\\")
893                    .replace('"', "\\\"")
894                    .replace('\n', "\\n")
895                );
896
897                let mut log = Annotated::<OurLog>::from_json(&log_json).unwrap();
898
899                eap::scrub(ValueType::OurLog, &mut log, Some(&config), scrubbing_config.as_ref()).unwrap();
900
901                insta::allow_duplicates!(insta::assert_json_snapshot!(SerializableAnnotated(&log.value().unwrap().attributes), @$snapshot));
902
903                let metric_json = format!(r#"{{
904                    "timestamp": 1544719860.0,
905                    "trace_id": "5b8efff798038103d269b633813fc60c",
906                    "name": "test.metric",
907                    "type": "counter",
908                    "value": 1.0,
909                    "attributes": {{
910                        "{rule_type}|{value}": {{
911                            "type": "string",
912                            "value": "{value}"
913                        }}
914                    }}
915                }}"#,
916                rule_type = $rule_type,
917                value = $test_value
918                    .replace('\\', "\\\\")
919                    .replace('"', "\\\"")
920                    .replace('\n', "\\n")
921                );
922
923                let mut metric = Annotated::<TraceMetric>::from_json(&metric_json).unwrap();
924
925                eap::scrub(ValueType::TraceMetric, &mut metric, Some(&config), scrubbing_config.as_ref()).unwrap();
926
927                insta::allow_duplicates!(insta::assert_json_snapshot!(SerializableAnnotated(&metric.value().unwrap().attributes), @$snapshot));
928            }
929        };
930    }
931
932    // IP rules
933    attribute_rule_test!(test_scrub_attributes_pii_string_rules_ip, "@ip", "127.0.0.1", @r###"
934    {
935      "@ip|127.0.0.1": {
936        "type": "string",
937        "value": "[ip]"
938      },
939      "_meta": {
940        "@ip|127.0.0.1": {
941          "value": {
942            "": {
943              "rem": [
944                [
945                  "@ip",
946                  "s",
947                  0,
948                  4
949                ]
950              ],
951              "len": 9
952            }
953          }
954        }
955      }
956    }
957    "###);
958
959    attribute_rule_test!(test_scrub_attributes_pii_string_rules_ip_replace, "@ip:replace", "127.0.0.1", @r###"
960    {
961      "@ip:replace|127.0.0.1": {
962        "type": "string",
963        "value": "[ip]"
964      },
965      "_meta": {
966        "@ip:replace|127.0.0.1": {
967          "value": {
968            "": {
969              "rem": [
970                [
971                  "@ip:replace",
972                  "s",
973                  0,
974                  4
975                ]
976              ],
977              "len": 9
978            }
979          }
980        }
981      }
982    }
983    "###);
984
985    attribute_rule_test!(test_scrub_attributes_pii_string_rules_ip_remove, "@ip:remove", "127.0.0.1", @r###"
986    {
987      "@ip:remove|127.0.0.1": {
988        "type": "string",
989        "value": ""
990      },
991      "_meta": {
992        "@ip:remove|127.0.0.1": {
993          "value": {
994            "": {
995              "rem": [
996                [
997                  "@ip:remove",
998                  "x",
999                  0,
1000                  0
1001                ]
1002              ],
1003              "len": 9
1004            }
1005          }
1006        }
1007      }
1008    }
1009    "###);
1010
1011    attribute_rule_test!(test_scrub_attributes_pii_string_rules_ip_mask, "@ip:mask", "127.0.0.1", @r###"
1012    {
1013      "@ip:mask|127.0.0.1": {
1014        "type": "string",
1015        "value": "*********"
1016      },
1017      "_meta": {
1018        "@ip:mask|127.0.0.1": {
1019          "value": {
1020            "": {
1021              "rem": [
1022                [
1023                  "@ip:mask",
1024                  "m",
1025                  0,
1026                  9
1027                ]
1028              ],
1029              "len": 9
1030            }
1031          }
1032        }
1033      }
1034    }
1035    "###);
1036
1037    // Email rules
1038    attribute_rule_test!(test_scrub_attributes_pii_string_rules_email, "@email", "test@example.com", @r###"
1039    {
1040      "@email|test@example.com": {
1041        "type": "string",
1042        "value": "[email]"
1043      },
1044      "_meta": {
1045        "@email|test@example.com": {
1046          "value": {
1047            "": {
1048              "rem": [
1049                [
1050                  "@email",
1051                  "s",
1052                  0,
1053                  7
1054                ]
1055              ],
1056              "len": 16
1057            }
1058          }
1059        }
1060      }
1061    }
1062    "###);
1063
1064    attribute_rule_test!(test_scrub_attributes_pii_string_rules_email_replace, "@email:replace", "test@example.com", @r###"
1065    {
1066      "@email:replace|test@example.com": {
1067        "type": "string",
1068        "value": "[email]"
1069      },
1070      "_meta": {
1071        "@email:replace|test@example.com": {
1072          "value": {
1073            "": {
1074              "rem": [
1075                [
1076                  "@email:replace",
1077                  "s",
1078                  0,
1079                  7
1080                ]
1081              ],
1082              "len": 16
1083            }
1084          }
1085        }
1086      }
1087    }
1088    "###);
1089
1090    attribute_rule_test!(test_scrub_attributes_pii_string_rules_email_remove, "@email:remove", "test@example.com", @r###"
1091    {
1092      "@email:remove|test@example.com": {
1093        "type": "string",
1094        "value": ""
1095      },
1096      "_meta": {
1097        "@email:remove|test@example.com": {
1098          "value": {
1099            "": {
1100              "rem": [
1101                [
1102                  "@email:remove",
1103                  "x",
1104                  0,
1105                  0
1106                ]
1107              ],
1108              "len": 16
1109            }
1110          }
1111        }
1112      }
1113    }
1114    "###);
1115
1116    attribute_rule_test!(test_scrub_attributes_pii_string_rules_email_mask, "@email:mask", "test@example.com", @r###"
1117    {
1118      "@email:mask|test@example.com": {
1119        "type": "string",
1120        "value": "****************"
1121      },
1122      "_meta": {
1123        "@email:mask|test@example.com": {
1124          "value": {
1125            "": {
1126              "rem": [
1127                [
1128                  "@email:mask",
1129                  "m",
1130                  0,
1131                  16
1132                ]
1133              ],
1134              "len": 16
1135            }
1136          }
1137        }
1138      }
1139    }
1140    "###);
1141
1142    // Credit card rules
1143    attribute_rule_test!(test_scrub_attributes_pii_string_rules_creditcard, "@creditcard", "4242424242424242", @r###"
1144    {
1145      "@creditcard|4242424242424242": {
1146        "type": "string",
1147        "value": "[creditcard]"
1148      },
1149      "_meta": {
1150        "@creditcard|4242424242424242": {
1151          "value": {
1152            "": {
1153              "rem": [
1154                [
1155                  "@creditcard",
1156                  "s",
1157                  0,
1158                  12
1159                ]
1160              ],
1161              "len": 16
1162            }
1163          }
1164        }
1165      }
1166    }
1167    "###);
1168
1169    attribute_rule_test!(test_scrub_attributes_pii_string_rules_creditcard_replace, "@creditcard:replace", "4242424242424242", @r###"
1170    {
1171      "@creditcard:replace|4242424242424242": {
1172        "type": "string",
1173        "value": "[creditcard]"
1174      },
1175      "_meta": {
1176        "@creditcard:replace|4242424242424242": {
1177          "value": {
1178            "": {
1179              "rem": [
1180                [
1181                  "@creditcard:replace",
1182                  "s",
1183                  0,
1184                  12
1185                ]
1186              ],
1187              "len": 16
1188            }
1189          }
1190        }
1191      }
1192    }
1193    "###);
1194
1195    attribute_rule_test!(test_scrub_attributes_pii_string_rules_creditcard_remove, "@creditcard:remove", "4242424242424242", @r###"
1196    {
1197      "@creditcard:remove|4242424242424242": {
1198        "type": "string",
1199        "value": ""
1200      },
1201      "_meta": {
1202        "@creditcard:remove|4242424242424242": {
1203          "value": {
1204            "": {
1205              "rem": [
1206                [
1207                  "@creditcard:remove",
1208                  "x",
1209                  0,
1210                  0
1211                ]
1212              ],
1213              "len": 16
1214            }
1215          }
1216        }
1217      }
1218    }
1219    "###);
1220
1221    attribute_rule_test!(test_scrub_attributes_pii_string_rules_creditcard_mask, "@creditcard:mask", "4242424242424242", @r###"
1222    {
1223      "@creditcard:mask|4242424242424242": {
1224        "type": "string",
1225        "value": "****************"
1226      },
1227      "_meta": {
1228        "@creditcard:mask|4242424242424242": {
1229          "value": {
1230            "": {
1231              "rem": [
1232                [
1233                  "@creditcard:mask",
1234                  "m",
1235                  0,
1236                  16
1237                ]
1238              ],
1239              "len": 16
1240            }
1241          }
1242        }
1243      }
1244    }
1245    "###);
1246
1247    // IBAN rules
1248    attribute_rule_test!(test_scrub_attributes_pii_string_rules_iban, "@iban", "DE89370400440532013000", @r###"
1249    {
1250      "@iban|DE89370400440532013000": {
1251        "type": "string",
1252        "value": "[iban]"
1253      },
1254      "_meta": {
1255        "@iban|DE89370400440532013000": {
1256          "value": {
1257            "": {
1258              "rem": [
1259                [
1260                  "@iban",
1261                  "s",
1262                  0,
1263                  6
1264                ]
1265              ],
1266              "len": 22
1267            }
1268          }
1269        }
1270      }
1271    }
1272    "###);
1273
1274    attribute_rule_test!(test_scrub_attributes_pii_string_rules_iban_replace, "@iban:replace", "DE89370400440532013000", @r###"
1275    {
1276      "@iban:replace|DE89370400440532013000": {
1277        "type": "string",
1278        "value": "[iban]"
1279      },
1280      "_meta": {
1281        "@iban:replace|DE89370400440532013000": {
1282          "value": {
1283            "": {
1284              "rem": [
1285                [
1286                  "@iban:replace",
1287                  "s",
1288                  0,
1289                  6
1290                ]
1291              ],
1292              "len": 22
1293            }
1294          }
1295        }
1296      }
1297    }
1298    "###);
1299
1300    attribute_rule_test!(test_scrub_attributes_pii_string_rules_iban_remove, "@iban:remove", "DE89370400440532013000", @r###"
1301    {
1302      "@iban:remove|DE89370400440532013000": {
1303        "type": "string",
1304        "value": ""
1305      },
1306      "_meta": {
1307        "@iban:remove|DE89370400440532013000": {
1308          "value": {
1309            "": {
1310              "rem": [
1311                [
1312                  "@iban:remove",
1313                  "x",
1314                  0,
1315                  0
1316                ]
1317              ],
1318              "len": 22
1319            }
1320          }
1321        }
1322      }
1323    }
1324    "###);
1325
1326    attribute_rule_test!(test_scrub_attributes_pii_string_rules_iban_mask, "@iban:mask", "DE89370400440532013000", @r###"
1327    {
1328      "@iban:mask|DE89370400440532013000": {
1329        "type": "string",
1330        "value": "**********************"
1331      },
1332      "_meta": {
1333        "@iban:mask|DE89370400440532013000": {
1334          "value": {
1335            "": {
1336              "rem": [
1337                [
1338                  "@iban:mask",
1339                  "m",
1340                  0,
1341                  22
1342                ]
1343              ],
1344              "len": 22
1345            }
1346          }
1347        }
1348      }
1349    }
1350    "###);
1351
1352    // MAC address rules
1353    attribute_rule_test!(test_scrub_attributes_pii_string_rules_mac, "@mac", "4a:00:04:10:9b:50", @r###"
1354    {
1355      "@mac|4a:00:04:10:9b:50": {
1356        "type": "string",
1357        "value": "*****************"
1358      },
1359      "_meta": {
1360        "@mac|4a:00:04:10:9b:50": {
1361          "value": {
1362            "": {
1363              "rem": [
1364                [
1365                  "@mac",
1366                  "m",
1367                  0,
1368                  17
1369                ]
1370              ],
1371              "len": 17
1372            }
1373          }
1374        }
1375      }
1376    }
1377    "###);
1378
1379    attribute_rule_test!(test_scrub_attributes_pii_string_rules_mac_replace, "@mac:replace", "4a:00:04:10:9b:50", @r###"
1380    {
1381      "@mac:replace|4a:00:04:10:9b:50": {
1382        "type": "string",
1383        "value": "[mac]"
1384      },
1385      "_meta": {
1386        "@mac:replace|4a:00:04:10:9b:50": {
1387          "value": {
1388            "": {
1389              "rem": [
1390                [
1391                  "@mac:replace",
1392                  "s",
1393                  0,
1394                  5
1395                ]
1396              ],
1397              "len": 17
1398            }
1399          }
1400        }
1401      }
1402    }
1403    "###);
1404
1405    attribute_rule_test!(test_scrub_attributes_pii_string_rules_mac_remove, "@mac:remove", "4a:00:04:10:9b:50", @r###"
1406    {
1407      "@mac:remove|4a:00:04:10:9b:50": {
1408        "type": "string",
1409        "value": ""
1410      },
1411      "_meta": {
1412        "@mac:remove|4a:00:04:10:9b:50": {
1413          "value": {
1414            "": {
1415              "rem": [
1416                [
1417                  "@mac:remove",
1418                  "x",
1419                  0,
1420                  0
1421                ]
1422              ],
1423              "len": 17
1424            }
1425          }
1426        }
1427      }
1428    }
1429    "###);
1430
1431    attribute_rule_test!(test_scrub_attributes_pii_string_rules_mac_mask, "@mac:mask", "4a:00:04:10:9b:50", @r###"
1432    {
1433      "@mac:mask|4a:00:04:10:9b:50": {
1434        "type": "string",
1435        "value": "*****************"
1436      },
1437      "_meta": {
1438        "@mac:mask|4a:00:04:10:9b:50": {
1439          "value": {
1440            "": {
1441              "rem": [
1442                [
1443                  "@mac:mask",
1444                  "m",
1445                  0,
1446                  17
1447                ]
1448              ],
1449              "len": 17
1450            }
1451          }
1452        }
1453      }
1454    }
1455    "###);
1456
1457    // UUID rules
1458    attribute_rule_test!(test_scrub_attributes_pii_string_rules_uuid, "@uuid", "ceee0822-ed8f-4622-b2a3-789e73e75cd1", @r###"
1459    {
1460      "@uuid|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1461        "type": "string",
1462        "value": "************************************"
1463      },
1464      "_meta": {
1465        "@uuid|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1466          "value": {
1467            "": {
1468              "rem": [
1469                [
1470                  "@uuid",
1471                  "m",
1472                  0,
1473                  36
1474                ]
1475              ],
1476              "len": 36
1477            }
1478          }
1479        }
1480      }
1481    }
1482    "###);
1483
1484    attribute_rule_test!(test_scrub_attributes_pii_string_rules_uuid_replace, "@uuid:replace", "ceee0822-ed8f-4622-b2a3-789e73e75cd1", @r###"
1485    {
1486      "@uuid:replace|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1487        "type": "string",
1488        "value": "[uuid]"
1489      },
1490      "_meta": {
1491        "@uuid:replace|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1492          "value": {
1493            "": {
1494              "rem": [
1495                [
1496                  "@uuid:replace",
1497                  "s",
1498                  0,
1499                  6
1500                ]
1501              ],
1502              "len": 36
1503            }
1504          }
1505        }
1506      }
1507    }
1508    "###);
1509
1510    attribute_rule_test!(test_scrub_attributes_pii_string_rules_uuid_remove, "@uuid:remove", "ceee0822-ed8f-4622-b2a3-789e73e75cd1", @r###"
1511    {
1512      "@uuid:remove|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1513        "type": "string",
1514        "value": ""
1515      },
1516      "_meta": {
1517        "@uuid:remove|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1518          "value": {
1519            "": {
1520              "rem": [
1521                [
1522                  "@uuid:remove",
1523                  "x",
1524                  0,
1525                  0
1526                ]
1527              ],
1528              "len": 36
1529            }
1530          }
1531        }
1532      }
1533    }
1534    "###);
1535
1536    attribute_rule_test!(test_scrub_attributes_pii_string_rules_uuid_mask, "@uuid:mask", "ceee0822-ed8f-4622-b2a3-789e73e75cd1", @r###"
1537    {
1538      "@uuid:mask|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1539        "type": "string",
1540        "value": "************************************"
1541      },
1542      "_meta": {
1543        "@uuid:mask|ceee0822-ed8f-4622-b2a3-789e73e75cd1": {
1544          "value": {
1545            "": {
1546              "rem": [
1547                [
1548                  "@uuid:mask",
1549                  "m",
1550                  0,
1551                  36
1552                ]
1553              ],
1554              "len": 36
1555            }
1556          }
1557        }
1558      }
1559    }
1560    "###);
1561
1562    // IMEI rules
1563    attribute_rule_test!(test_scrub_attributes_pii_string_rules_imei, "@imei", "356938035643809", @r###"
1564    {
1565      "@imei|356938035643809": {
1566        "type": "string",
1567        "value": "[imei]"
1568      },
1569      "_meta": {
1570        "@imei|356938035643809": {
1571          "value": {
1572            "": {
1573              "rem": [
1574                [
1575                  "@imei",
1576                  "s",
1577                  0,
1578                  6
1579                ]
1580              ],
1581              "len": 15
1582            }
1583          }
1584        }
1585      }
1586    }
1587    "###);
1588
1589    attribute_rule_test!(test_scrub_attributes_pii_string_rules_imei_replace, "@imei:replace", "356938035643809", @r###"
1590    {
1591      "@imei:replace|356938035643809": {
1592        "type": "string",
1593        "value": "[imei]"
1594      },
1595      "_meta": {
1596        "@imei:replace|356938035643809": {
1597          "value": {
1598            "": {
1599              "rem": [
1600                [
1601                  "@imei:replace",
1602                  "s",
1603                  0,
1604                  6
1605                ]
1606              ],
1607              "len": 15
1608            }
1609          }
1610        }
1611      }
1612    }
1613    "###);
1614
1615    attribute_rule_test!(test_scrub_attributes_pii_string_rules_imei_remove, "@imei:remove", "356938035643809", @r###"
1616    {
1617      "@imei:remove|356938035643809": {
1618        "type": "string",
1619        "value": ""
1620      },
1621      "_meta": {
1622        "@imei:remove|356938035643809": {
1623          "value": {
1624            "": {
1625              "rem": [
1626                [
1627                  "@imei:remove",
1628                  "x",
1629                  0,
1630                  0
1631                ]
1632              ],
1633              "len": 15
1634            }
1635          }
1636        }
1637      }
1638    }
1639    "###);
1640
1641    // PEM key rules
1642    attribute_rule_test!(
1643        test_scrub_attributes_pii_string_rules_pemkey,
1644        "@pemkey",
1645        "-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----",
1646    @r###"
1647    {
1648      "@pemkey|-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----": {
1649        "type": "string",
1650        "value": "-----BEGIN EC PRIVATE KEY-----\n[pemkey]\n-----END EC PRIVATE KEY-----"
1651      },
1652      "_meta": {
1653        "@pemkey|-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----": {
1654          "value": {
1655            "": {
1656              "rem": [
1657                [
1658                  "@pemkey",
1659                  "s",
1660                  31,
1661                  39
1662                ]
1663              ],
1664              "len": 124
1665            }
1666          }
1667        }
1668      }
1669    }
1670    "###);
1671
1672    attribute_rule_test!(
1673        test_scrub_attributes_pii_string_rules_pemkey_replace,
1674        "@pemkey:replace",
1675        "-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----",
1676    @r###"
1677    {
1678      "@pemkey:replace|-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----": {
1679        "type": "string",
1680        "value": "-----BEGIN EC PRIVATE KEY-----\n[pemkey]\n-----END EC PRIVATE KEY-----"
1681      },
1682      "_meta": {
1683        "@pemkey:replace|-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----": {
1684          "value": {
1685            "": {
1686              "rem": [
1687                [
1688                  "@pemkey:replace",
1689                  "s",
1690                  31,
1691                  39
1692                ]
1693              ],
1694              "len": 124
1695            }
1696          }
1697        }
1698      }
1699    }
1700    "###);
1701
1702    attribute_rule_test!(
1703        test_scrub_attributes_pii_string_rules_pemkey_remove,
1704        "@pemkey:remove",
1705        "-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----",
1706    @r###"
1707    {
1708      "@pemkey:remove|-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----": {
1709        "type": "string",
1710        "value": "-----BEGIN EC PRIVATE KEY-----\n\n-----END EC PRIVATE KEY-----"
1711      },
1712      "_meta": {
1713        "@pemkey:remove|-----BEGIN EC PRIVATE KEY-----\nMIHbAgEBBEFbLvIaAaez3q0u6BQYMHZ28B7iSdMPPaODUMGkdorl3ShgTbYmzqGL\n-----END EC PRIVATE KEY-----": {
1714          "value": {
1715            "": {
1716              "rem": [
1717                [
1718                  "@pemkey:remove",
1719                  "x",
1720                  31,
1721                  31
1722                ]
1723              ],
1724              "len": 124
1725            }
1726          }
1727        }
1728      }
1729    }
1730    "###);
1731
1732    // URL auth rules
1733    attribute_rule_test!(test_scrub_attributes_pii_string_rules_urlauth, "@urlauth", "https://username:password@example.com/", @r###"
1734    {
1735      "@urlauth|https://username:password@example.com/": {
1736        "type": "string",
1737        "value": "https://[auth]@example.com/"
1738      },
1739      "_meta": {
1740        "@urlauth|https://username:password@example.com/": {
1741          "value": {
1742            "": {
1743              "rem": [
1744                [
1745                  "@urlauth",
1746                  "s",
1747                  8,
1748                  14
1749                ]
1750              ],
1751              "len": 38
1752            }
1753          }
1754        }
1755      }
1756    }
1757    "###);
1758
1759    attribute_rule_test!(test_scrub_attributes_pii_string_rules_urlauth_replace, "@urlauth:replace", "https://username:password@example.com/", @r###"
1760    {
1761      "@urlauth:replace|https://username:password@example.com/": {
1762        "type": "string",
1763        "value": "https://[auth]@example.com/"
1764      },
1765      "_meta": {
1766        "@urlauth:replace|https://username:password@example.com/": {
1767          "value": {
1768            "": {
1769              "rem": [
1770                [
1771                  "@urlauth:replace",
1772                  "s",
1773                  8,
1774                  14
1775                ]
1776              ],
1777              "len": 38
1778            }
1779          }
1780        }
1781      }
1782    }
1783    "###);
1784
1785    attribute_rule_test!(test_scrub_attributes_pii_string_rules_urlauth_remove, "@urlauth:remove", "https://username:password@example.com/", @r###"
1786    {
1787      "@urlauth:remove|https://username:password@example.com/": {
1788        "type": "string",
1789        "value": "https://@example.com/"
1790      },
1791      "_meta": {
1792        "@urlauth:remove|https://username:password@example.com/": {
1793          "value": {
1794            "": {
1795              "rem": [
1796                [
1797                  "@urlauth:remove",
1798                  "x",
1799                  8,
1800                  8
1801                ]
1802              ],
1803              "len": 38
1804            }
1805          }
1806        }
1807      }
1808    }
1809    "###);
1810
1811    attribute_rule_test!(test_scrub_attributes_pii_string_rules_urlauth_mask, "@urlauth:mask", "https://username:password@example.com/", @r###"
1812    {
1813      "@urlauth:mask|https://username:password@example.com/": {
1814        "type": "string",
1815        "value": "https://*****************@example.com/"
1816      },
1817      "_meta": {
1818        "@urlauth:mask|https://username:password@example.com/": {
1819          "value": {
1820            "": {
1821              "rem": [
1822                [
1823                  "@urlauth:mask",
1824                  "m",
1825                  8,
1826                  25
1827                ]
1828              ],
1829              "len": 38
1830            }
1831          }
1832        }
1833      }
1834    }
1835    "###);
1836
1837    // US SSN rules
1838    attribute_rule_test!(test_scrub_attributes_pii_string_rules_usssn, "@usssn", "078-05-1120", @r###"
1839    {
1840      "@usssn|078-05-1120": {
1841        "type": "string",
1842        "value": "***********"
1843      },
1844      "_meta": {
1845        "@usssn|078-05-1120": {
1846          "value": {
1847            "": {
1848              "rem": [
1849                [
1850                  "@usssn",
1851                  "m",
1852                  0,
1853                  11
1854                ]
1855              ],
1856              "len": 11
1857            }
1858          }
1859        }
1860      }
1861    }
1862    "###);
1863
1864    attribute_rule_test!(test_scrub_attributes_pii_string_rules_usssn_replace, "@usssn:replace", "078-05-1120", @r###"
1865    {
1866      "@usssn:replace|078-05-1120": {
1867        "type": "string",
1868        "value": "[us-ssn]"
1869      },
1870      "_meta": {
1871        "@usssn:replace|078-05-1120": {
1872          "value": {
1873            "": {
1874              "rem": [
1875                [
1876                  "@usssn:replace",
1877                  "s",
1878                  0,
1879                  8
1880                ]
1881              ],
1882              "len": 11
1883            }
1884          }
1885        }
1886      }
1887    }
1888    "###);
1889
1890    attribute_rule_test!(test_scrub_attributes_pii_string_rules_usssn_remove, "@usssn:remove", "078-05-1120", @r###"
1891    {
1892      "@usssn:remove|078-05-1120": {
1893        "type": "string",
1894        "value": ""
1895      },
1896      "_meta": {
1897        "@usssn:remove|078-05-1120": {
1898          "value": {
1899            "": {
1900              "rem": [
1901                [
1902                  "@usssn:remove",
1903                  "x",
1904                  0,
1905                  0
1906                ]
1907              ],
1908              "len": 11
1909            }
1910          }
1911        }
1912      }
1913    }
1914    "###);
1915
1916    attribute_rule_test!(test_scrub_attributes_pii_string_rules_usssn_mask, "@usssn:mask", "078-05-1120", @r###"
1917    {
1918      "@usssn:mask|078-05-1120": {
1919        "type": "string",
1920        "value": "***********"
1921      },
1922      "_meta": {
1923        "@usssn:mask|078-05-1120": {
1924          "value": {
1925            "": {
1926              "rem": [
1927                [
1928                  "@usssn:mask",
1929                  "m",
1930                  0,
1931                  11
1932                ]
1933              ],
1934              "len": 11
1935            }
1936          }
1937        }
1938      }
1939    }
1940    "###);
1941
1942    // User path rules
1943    attribute_rule_test!(test_scrub_attributes_pii_string_rules_userpath, "@userpath", "/Users/john/Documents", @r###"
1944    {
1945      "@userpath|/Users/john/Documents": {
1946        "type": "string",
1947        "value": "/Users/[user]/Documents"
1948      },
1949      "_meta": {
1950        "@userpath|/Users/john/Documents": {
1951          "value": {
1952            "": {
1953              "rem": [
1954                [
1955                  "@userpath",
1956                  "s",
1957                  7,
1958                  13
1959                ]
1960              ],
1961              "len": 21
1962            }
1963          }
1964        }
1965      }
1966    }
1967    "###);
1968
1969    attribute_rule_test!(test_scrub_attributes_pii_string_rules_userpath_replace, "@userpath:replace", "/Users/john/Documents", @r###"
1970    {
1971      "@userpath:replace|/Users/john/Documents": {
1972        "type": "string",
1973        "value": "/Users/[user]/Documents"
1974      },
1975      "_meta": {
1976        "@userpath:replace|/Users/john/Documents": {
1977          "value": {
1978            "": {
1979              "rem": [
1980                [
1981                  "@userpath:replace",
1982                  "s",
1983                  7,
1984                  13
1985                ]
1986              ],
1987              "len": 21
1988            }
1989          }
1990        }
1991      }
1992    }
1993    "###);
1994
1995    attribute_rule_test!(test_scrub_attributes_pii_string_rules_userpath_remove, "@userpath:remove", "/Users/john/Documents", @r###"
1996    {
1997      "@userpath:remove|/Users/john/Documents": {
1998        "type": "string",
1999        "value": "/Users//Documents"
2000      },
2001      "_meta": {
2002        "@userpath:remove|/Users/john/Documents": {
2003          "value": {
2004            "": {
2005              "rem": [
2006                [
2007                  "@userpath:remove",
2008                  "x",
2009                  7,
2010                  7
2011                ]
2012              ],
2013              "len": 21
2014            }
2015          }
2016        }
2017      }
2018    }
2019    "###);
2020
2021    attribute_rule_test!(test_scrub_attributes_pii_string_rules_userpath_mask, "@userpath:mask", "/Users/john/Documents", @r###"
2022    {
2023      "@userpath:mask|/Users/john/Documents": {
2024        "type": "string",
2025        "value": "/Users/****/Documents"
2026      },
2027      "_meta": {
2028        "@userpath:mask|/Users/john/Documents": {
2029          "value": {
2030            "": {
2031              "rem": [
2032                [
2033                  "@userpath:mask",
2034                  "m",
2035                  7,
2036                  11
2037                ]
2038              ],
2039              "len": 21
2040            }
2041          }
2042        }
2043      }
2044    }
2045    "###);
2046
2047    // Password rules
2048    // @password defaults to remove
2049    attribute_rule_test!(test_scrub_attributes_pii_string_rules_password, "@password", "my_password_123", @r###"
2050    {
2051      "@password|my_password_123": {
2052        "type": "string",
2053        "value": ""
2054      },
2055      "_meta": {
2056        "@password|my_password_123": {
2057          "value": {
2058            "": {
2059              "rem": [
2060                [
2061                  "@password",
2062                  "x",
2063                  0,
2064                  0
2065                ]
2066              ],
2067              "len": 15
2068            }
2069          }
2070        }
2071      }
2072    }
2073    "###);
2074
2075    attribute_rule_test!(test_scrub_attributes_pii_string_rules_password_remove, "@password:remove", "my_password_123", @r###"
2076    {
2077      "@password:remove|my_password_123": {
2078        "type": "string",
2079        "value": ""
2080      },
2081      "_meta": {
2082        "@password:remove|my_password_123": {
2083          "value": {
2084            "": {
2085              "rem": [
2086                [
2087                  "@password:remove",
2088                  "x",
2089                  0,
2090                  0
2091                ]
2092              ],
2093              "len": 15
2094            }
2095          }
2096        }
2097      }
2098    }
2099    "###);
2100
2101    attribute_rule_test!(test_scrub_attributes_pii_string_rules_password_replace, "@password:replace", "my_password_123", @r###"
2102    {
2103      "@password:replace|my_password_123": {
2104        "type": "string",
2105        "value": "[password]"
2106      },
2107      "_meta": {
2108        "@password:replace|my_password_123": {
2109          "value": {
2110            "": {
2111              "rem": [
2112                [
2113                  "@password:replace",
2114                  "s",
2115                  0,
2116                  10
2117                ]
2118              ],
2119              "len": 15
2120            }
2121          }
2122        }
2123      }
2124    }
2125    "###);
2126
2127    attribute_rule_test!(test_scrub_attributes_pii_string_rules_password_mask, "@password:mask", "my_password_123", @r###"
2128    {
2129      "@password:mask|my_password_123": {
2130        "type": "string",
2131        "value": "***************"
2132      },
2133      "_meta": {
2134        "@password:mask|my_password_123": {
2135          "value": {
2136            "": {
2137              "rem": [
2138                [
2139                  "@password:mask",
2140                  "m",
2141                  0,
2142                  15
2143                ]
2144              ],
2145              "len": 15
2146            }
2147          }
2148        }
2149      }
2150    }
2151    "###);
2152
2153    // Bearer token rules
2154    attribute_rule_test!(test_scrub_attributes_pii_string_rules_bearer, "@bearer", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", @r###"
2155    {
2156      "@bearer|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2157        "type": "string",
2158        "value": "Bearer [token]"
2159      },
2160      "_meta": {
2161        "@bearer|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2162          "value": {
2163            "": {
2164              "rem": [
2165                [
2166                  "@bearer",
2167                  "s",
2168                  0,
2169                  14
2170                ]
2171              ],
2172              "len": 43
2173            }
2174          }
2175        }
2176      }
2177    }
2178    "###);
2179
2180    attribute_rule_test!(
2181        test_scrub_attributes_pii_string_rules_bearer_replace,
2182        "@bearer:replace",
2183        "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
2184    @r###"
2185    {
2186      "@bearer:replace|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2187        "type": "string",
2188        "value": "Bearer [token]"
2189      },
2190      "_meta": {
2191        "@bearer:replace|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2192          "value": {
2193            "": {
2194              "rem": [
2195                [
2196                  "@bearer:replace",
2197                  "s",
2198                  0,
2199                  14
2200                ]
2201              ],
2202              "len": 43
2203            }
2204          }
2205        }
2206      }
2207    }
2208    "###);
2209
2210    attribute_rule_test!(
2211        test_scrub_attributes_pii_string_rules_bearer_remove,
2212        "@bearer:remove",
2213        "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
2214    @r###"
2215    {
2216      "@bearer:remove|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2217        "type": "string",
2218        "value": ""
2219      },
2220      "_meta": {
2221        "@bearer:remove|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2222          "value": {
2223            "": {
2224              "rem": [
2225                [
2226                  "@bearer:remove",
2227                  "x",
2228                  0,
2229                  0
2230                ]
2231              ],
2232              "len": 43
2233            }
2234          }
2235        }
2236      }
2237    }
2238    "###);
2239
2240    attribute_rule_test!(
2241        test_scrub_attributes_pii_string_rules_bearer_mask,
2242        "@bearer:mask",
2243        "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
2244    @r###"
2245    {
2246      "@bearer:mask|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2247        "type": "string",
2248        "value": "*******************************************"
2249      },
2250      "_meta": {
2251        "@bearer:mask|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9": {
2252          "value": {
2253            "": {
2254              "rem": [
2255                [
2256                  "@bearer:mask",
2257                  "m",
2258                  0,
2259                  43
2260                ]
2261              ],
2262              "len": 43
2263            }
2264          }
2265        }
2266      }
2267    }
2268    "###);
2269}