relay_pii/
json.rs

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/// Describes the error cases that can happen during JSON scrubbing.
10#[derive(Debug, thiserror::Error)]
11pub enum JsonScrubError {
12    /// If the transcoding process fails. This will most likely happen if a JSON document
13    /// is invalid.
14    #[error("transcoding json failed")]
15    TranscodeFailed,
16}
17
18/// Visitor for JSON file scrubbing. It will be used to walk through the structure and scrub
19/// PII based on the config defined in the processor.
20pub struct JsonScrubVisitor<'a> {
21    processor: PiiProcessor<'a>,
22    /// The state encoding the current path, which is fed by `push_path` and `pop_path`.
23    state: ProcessingState<'a>,
24    /// The current path. This is redundant with `state`, which also contains the full path,
25    /// but easier to match on.
26    path: Vec<String>,
27}
28
29impl<'a> JsonScrubVisitor<'a> {
30    /// Creates a new [`JsonScrubVisitor`] using the  supplied config.
31    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), // Pretend everything is a string.
49        );
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()); // pop_path should never be called on an empty state.
58    }
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}