1use crate::transform::Transform;
2use crate::{CompiledPiiConfig, PiiProcessor};
3use relay_event_schema::processor::{FieldAttrs, Pii, ProcessingState, Processor, ValueType};
4use relay_protocol::Meta;
5use std::borrow::Cow;
6
7const FIELD_ATTRS_PII_TRUE: FieldAttrs = FieldAttrs::new().pii(Pii::True);
8
9#[derive(Debug, thiserror::Error)]
11pub enum JsonScrubError {
12 #[error("transcoding json failed")]
15 TranscodeFailed,
16}
17
18pub struct JsonScrubVisitor<'a> {
21 processor: PiiProcessor<'a>,
22 state: ProcessingState<'a>,
24 path: Vec<String>,
27}
28
29impl<'a> JsonScrubVisitor<'a> {
30 pub fn new(config: &'a CompiledPiiConfig) -> Self {
32 let processor = PiiProcessor::new(config);
33 Self {
34 processor,
35 state: ProcessingState::new_root(None, None),
36 path: Vec::new(),
37 }
38 }
39}
40
41impl<'de> Transform<'de> for JsonScrubVisitor<'de> {
42 fn push_path(&mut self, key: &'de str) {
43 self.path.push(key.to_owned());
44
45 self.state = std::mem::take(&mut self.state).enter_owned(
46 key.to_owned(),
47 Some(Cow::Borrowed(&FIELD_ATTRS_PII_TRUE)),
48 Some(ValueType::String), );
50 }
51
52 fn pop_path(&mut self) {
53 if let Ok(Some(parent)) = std::mem::take(&mut self.state).try_into_parent() {
54 self.state = parent;
55 }
56 let popped = self.path.pop();
57 debug_assert!(popped.is_some()); }
59
60 fn transform_str<'a>(&mut self, v: &'a str) -> Cow<'a, str> {
61 self.transform_string(v.to_owned())
62 }
63
64 fn transform_string(&mut self, mut v: String) -> Cow<'static, str> {
65 let mut meta = Meta::default();
66 if self
67 .processor
68 .process_string(&mut v, &mut meta, &self.state)
69 .is_err()
70 {
71 return Cow::Borrowed("");
72 }
73 Cow::Owned(v)
74 }
75}
76
77#[cfg(test)]
78mod test {
79 use crate::{PiiAttachmentsProcessor, PiiConfig};
80 use serde_json::Value;
81
82 #[test]
83 pub fn test_view_hierarchy() {
84 let payload = r#"
85 {
86 "rendering_system": "UIKIT",
87 "identifier": "192.45.128.54",
88 "windows": [
89 {
90 "type": "UIWindow",
91 "identifier": "123.123.123.123",
92 "width": 414,
93 "height": 896,
94 "x": 0,
95 "y": 0,
96 "alpha": 1,
97 "visible": true,
98 "children": []
99 }
100 ]
101 }
102 "#
103 .as_bytes();
104 let config = serde_json::from_str::<PiiConfig>(
105 r#"
106 {
107 "applications": {
108 "$string": ["@ip"]
109 }
110 }
111 "#,
112 )
113 .unwrap();
114 let processor = PiiAttachmentsProcessor::new(config.compiled());
115 let result = processor.scrub_json(payload).unwrap();
116 let parsed: Value = serde_json::from_slice(&result).unwrap();
117 assert_eq!("[ip]", parsed["identifier"].as_str().unwrap());
118 }
119
120 #[test]
121 pub fn test_view_hierarchy_nested_path_rule() {
122 let payload = r#"
123 {
124 "nested": {
125 "stuff": {
126 "ident": "10.0.0.1"
127 }
128 }
129 }
130 "#
131 .as_bytes();
132 let config = serde_json::from_str::<PiiConfig>(
133 r#"
134 {
135 "applications": {
136 "nested.stuff.ident": ["@ip"]
137 }
138 }
139 "#,
140 )
141 .unwrap();
142
143 let processor = PiiAttachmentsProcessor::new(config.compiled());
144 let result = processor.scrub_json(payload).unwrap();
145 let parsed: Value = serde_json::from_slice(&result).unwrap();
146 assert_eq!("[ip]", parsed["nested"]["stuff"]["ident"].as_str().unwrap());
147 }
148
149 #[test]
150 pub fn test_view_hierarchy_not_existing_path() {
151 let payload = r#"
152 {
153 "nested": {
154 "stuff": {
155 "ident": "10.0.0.1"
156 }
157 }
158 }
159 "#
160 .as_bytes();
161 let config = serde_json::from_str::<PiiConfig>(
162 r#"
163 {
164 "applications": {
165 "non.existent.path": ["@ip"]
166 }
167 }
168 "#,
169 )
170 .unwrap();
171
172 let processor = PiiAttachmentsProcessor::new(config.compiled());
173 let result = processor.scrub_json(payload).unwrap();
174 let parsed: Value = serde_json::from_slice(&result).unwrap();
175 assert_eq!(
176 "10.0.0.1",
177 parsed["nested"]["stuff"]["ident"].as_str().unwrap()
178 );
179 }
180}