snitch_transform/
transform.rs1use 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 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 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 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(), };
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 req.path = "does-not-exist".to_string();
188 assert!(mask(&req).is_err());
189
190 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(), };
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 req.path = "does-not-exist".to_string();
216 assert!(mask(&req).is_err());
217
218 req.path = "bool".to_string();
220 assert!(mask(&req).is_err());
221 }
222}