snitch_transform/
transform.rs

1use snitch_gjson as gjson;
2
3#[derive(Debug)]
4pub enum TransformError {
5    Generic(String),
6}
7
8pub struct Request {
9    pub data: Vec<u8>,
10    pub path: String,
11    pub value: String,
12}
13
14pub fn overwrite(req: &Request) -> Result<String, TransformError> {
15    validate_request(req, true)?;
16
17    let data = gjson::set_overwrite(
18        convert_bytes_to_string(&req.data)?,
19        req.path.as_str(),
20        req.value.as_str(),
21    )
22    .map_err(|e| TransformError::Generic(format!("unable to overwrite data: {}", e)))?;
23
24    Ok(data)
25}
26
27pub fn obfuscate(req: &Request) -> Result<String, TransformError> {
28    validate_request(req, false)?;
29
30    let data_as_str = convert_bytes_to_string(&req.data)?;
31    let value = gjson::get(data_as_str, req.path.as_str());
32
33    match value.kind() {
34        gjson::Kind::String => _obfuscate(data_as_str, req.path.as_str()),
35        _ => Err(TransformError::Generic(format!(
36            "unable to mask data: path '{}' is not a string or number",
37            req.path
38        ))),
39    }
40}
41
42fn _obfuscate(data: &str, path: &str) -> Result<String, TransformError> {
43    let contents = gjson::get(data, path);
44    let hashed = sha256::digest(contents.str().as_bytes());
45
46    let obfuscated = format!("\"sha256:{}\"", hashed);
47
48    gjson::set_overwrite(data, path, &obfuscated)
49        .map_err(|e| TransformError::Generic(format!("unable to obfuscate data: {}", e)))
50}
51
52pub fn mask(req: &Request) -> Result<String, TransformError> {
53    validate_request(req, false)?;
54
55    let data_as_str = convert_bytes_to_string(&req.data)?;
56    let value = gjson::get(data_as_str, req.path.as_str());
57
58    match value.kind() {
59        gjson::Kind::String => _mask(data_as_str, req.path.as_str(), '*', true),
60        gjson::Kind::Number => _mask(data_as_str, req.path.as_str(), '0', false),
61        _ => Err(TransformError::Generic(format!(
62            "unable to mask data: path '{}' is not a string or number",
63            req.path
64        ))),
65    }
66}
67
68fn _mask(data: &str, path: &str, mask_char: char, quote: bool) -> Result<String, TransformError> {
69    let contents = gjson::get(data, path);
70    let num_chars_to_mask = (0.8 * contents.str().len() as f64).round() as usize;
71    let num_chars_to_skip = contents.str().len() - num_chars_to_mask;
72
73    let mut masked = contents.str()[0..num_chars_to_skip].to_string()
74        + mask_char.to_string().repeat(num_chars_to_mask).as_str();
75
76    if quote {
77        masked = format!("\"{}\"", masked);
78    }
79
80    gjson::set_overwrite(data, path, &masked)
81        .map_err(|e| TransformError::Generic(format!("unable to mask data: {}", e)))
82}
83
84fn validate_request(req: &Request, value_check: bool) -> Result<(), TransformError> {
85    if req.path.is_empty() {
86        return Err(TransformError::Generic("path cannot be empty".to_string()));
87    }
88
89    if req.data.is_empty() {
90        return Err(TransformError::Generic("data cannot be empty".to_string()));
91    }
92
93    if value_check && req.value.is_empty() {
94        return Err(TransformError::Generic("value cannot be empty".to_string()));
95    }
96
97    // Is this valid JSON?
98    if !gjson::valid(convert_bytes_to_string(&req.data)?) {
99        return Err(TransformError::Generic(
100            "data is not valid JSON".to_string(),
101        ));
102    }
103
104    // Valid path?
105    if !gjson::get(convert_bytes_to_string(&req.data)?, req.path.as_str()).exists() {
106        return Err(TransformError::Generic(format!(
107            "path '{}' not found in data",
108            req.path
109        )));
110    }
111
112    Ok(())
113}
114
115fn convert_bytes_to_string(bytes: &Vec<u8>) -> Result<&str, TransformError> {
116    Ok(std::str::from_utf8(bytes.as_slice())
117        .map_err(|e| TransformError::Generic(format!("unable to parse data as UTF-8: {}", e))))?
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    const TEST_DATA: &str = r#"{
125    "foo": "bar",
126    "baz": {
127        "qux": "quux"
128    },
129    "bool": true
130}"#;
131
132    #[test]
133    fn test_overwrite() {
134        let mut req = Request {
135            data: TEST_DATA.as_bytes().to_vec(),
136            path: "baz.qux".to_string(),
137            value: "\"test\"".to_string(),
138        };
139
140        let result = overwrite(&req).unwrap();
141
142        assert!(gjson::valid(&TEST_DATA));
143        assert!(gjson::valid(&result));
144        assert_eq!(result, TEST_DATA.replace("quux", "test"));
145
146        let v = gjson::get(TEST_DATA, "baz.qux");
147        assert_eq!(v.str(), "quux");
148
149        let v = gjson::get(result.as_str(), "baz.qux");
150        assert_eq!(v.str(), "test");
151
152        req.path = "does-not-exist".to_string();
153        assert!(
154            overwrite(&req).is_err(),
155            "should error when path does not exist"
156        );
157
158        // Can overwrite anything
159        req.path = "bool".to_string();
160        assert!(
161            overwrite(&req).is_ok(),
162            "should be able to replace any value, regardless of type"
163        );
164    }
165
166    #[test]
167    fn test_obfuscate() {
168        let mut req = Request {
169            data: TEST_DATA.as_bytes().to_vec(),
170            path: "baz.qux".to_string(),
171            value: "".to_string(), // needs a default
172        };
173
174        let result = obfuscate(&req).unwrap();
175        let hashed_value = sha256::digest("quux".as_bytes());
176
177        assert!(gjson::valid(&TEST_DATA));
178        assert!(gjson::valid(&result));
179
180        let v = gjson::get(TEST_DATA, "baz.qux");
181        assert_eq!(v.str(), "quux");
182
183        let v = gjson::get(result.as_str(), "baz.qux");
184        assert_eq!(v.str(), format!("sha256:{}", hashed_value));
185
186        // path does not exist
187        req.path = "does-not-exist".to_string();
188        assert!(mask(&req).is_err());
189
190        // path not a string
191        req.path = "bool".to_string();
192        assert!(mask(&req).is_err());
193    }
194
195    #[test]
196    fn test_mask() {
197        let mut req = Request {
198            data: TEST_DATA.as_bytes().to_vec(),
199            path: "baz.qux".to_string(),
200            value: "".to_string(), // needs a default
201        };
202
203        let result = mask(&req).unwrap();
204
205        assert!(gjson::valid(TEST_DATA));
206        assert!(gjson::valid(&result));
207
208        let v = gjson::get(TEST_DATA, "baz.qux");
209        assert_eq!(v.str(), "quux");
210
211        let v = gjson::get(result.as_str(), "baz.qux");
212        assert_eq!(v.str(), "q***");
213
214        // path does not exist
215        req.path = "does-not-exist".to_string();
216        assert!(mask(&req).is_err());
217
218        // path not a string
219        req.path = "bool".to_string();
220        assert!(mask(&req).is_err());
221    }
222}