1use crate::Orm;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Serialize, Deserialize)]
5pub struct AuditLog {
6 pub id: i32,
7 pub model_type: String,
8 pub model_id: i32,
9 pub event: String,
10 pub old_values: Option<String>,
11 pub new_values: Option<String>,
12 pub created_at: Option<String>,
13}
14
15#[cfg_attr(test, mutants::skip)]
16fn validate_and_prepare_payloads(
17 model_type: &str,
18 event: &str,
19 mut old_values: Option<String>,
20 mut new_values: Option<String>,
21) -> Result<(Option<String>, Option<String>), crate::Error> {
22 const MAX_PAYLOAD_LEN: usize = 5 * 1024 * 1024; if model_type.len() > 255 || event.len() > 50 {
25 return Err(crate::Error::Validation(
26 "Audit model_type or event string too long".to_string(),
27 ));
28 }
29
30 if let Some(val) = &old_values
31 && val.len() > MAX_PAYLOAD_LEN
32 {
33 old_values = Some(r#"{"error":"payload_too_large"}"#.to_string());
34 }
35
36 if let Some(val) = &new_values
37 && val.len() > MAX_PAYLOAD_LEN
38 {
39 new_values = Some(r#"{"error":"payload_too_large"}"#.to_string());
40 }
41
42 Ok((old_values, new_values))
43}
44
45#[cfg_attr(test, mutants::skip)]
46pub async fn log_audit(
47 model_type: &str,
48 model_id: i32,
49 event: &str,
50 old_values: Option<String>,
51 new_values: Option<String>,
52) -> Result<(), crate::Error> {
53 let (old_values, new_values) =
54 validate_and_prepare_payloads(model_type, event, old_values, new_values)?;
55
56 let pool = Orm::pool();
57 let driver = Orm::driver();
58
59 if driver == "postgres" {
60 sqlx::query(
61 "INSERT INTO rullst_audits (model_type, model_id, event, old_values, new_values) VALUES ($1, $2, $3, $4, $5)"
62 )
63 .bind(model_type)
64 .bind(model_id)
65 .bind(event)
66 .bind(old_values)
67 .bind(new_values)
68 .execute(pool)
69 .await?;
70 } else {
71 sqlx::query(
72 "INSERT INTO rullst_audits (model_type, model_id, event, old_values, new_values) VALUES (?, ?, ?, ?, ?)"
73 )
74 .bind(model_type)
75 .bind(model_id)
76 .bind(event)
77 .bind(old_values)
78 .bind(new_values)
79 .execute(pool)
80 .await?;
81 }
82
83 Ok(())
84}
85
86fn is_sensitive(key: &str) -> bool {
87 let k = key.to_lowercase();
88 k.contains("password")
89 || k.contains("token")
90 || k.contains("secret")
91 || k.contains("senha")
92 || k.contains("api_key")
93 || k.contains("cvv")
94 || k.contains("ssn")
95 || k.contains("credit_card")
96 || k.contains("auth_code")
97}
98
99fn mask_if_sensitive(key: &str, value: serde_json::Value) -> serde_json::Value {
100 if is_sensitive(key) {
101 serde_json::Value::String("***".to_string())
102 } else {
103 value
104 }
105}
106
107#[cfg_attr(test, mutants::skip)]
108pub fn compute_diff(old_json: &str, new_json: &str) -> (Option<String>, Option<String>) {
109 if old_json == new_json {
110 return (None, None);
111 }
112
113 let old_val: serde_json::Value =
114 serde_json::from_str(old_json).unwrap_or(serde_json::Value::Null);
115 let new_val: serde_json::Value =
116 serde_json::from_str(new_json).unwrap_or(serde_json::Value::Null);
117
118 let mut diff_old = serde_json::Map::new();
119 let mut diff_new = serde_json::Map::new();
120
121 if let (serde_json::Value::Object(old_obj), serde_json::Value::Object(new_obj)) =
122 (old_val, new_val)
123 {
124 for (k, v) in &old_obj {
125 if let Some(new_v) = new_obj.get(k) {
126 #[allow(clippy::collapsible_if)]
127 if v != new_v {
128 let masked_v = mask_if_sensitive(k, v.clone());
129 let masked_new_v = mask_if_sensitive(k, new_v.clone());
130 diff_new.insert(k.clone(), masked_new_v);
131 diff_old.insert(k.clone(), masked_v);
132 }
133 } else {
134 let masked_v = mask_if_sensitive(k, v.clone());
135 diff_new.insert(k.clone(), serde_json::Value::Null);
136 diff_old.insert(k.clone(), masked_v);
137 }
138 }
139 for (k, new_v) in &new_obj {
140 if !old_obj.contains_key(k) {
141 let masked_new_v = mask_if_sensitive(k, new_v.clone());
142 diff_old.insert(k.clone(), serde_json::Value::Null);
143 diff_new.insert(k.clone(), masked_new_v);
144 }
145 }
146 }
147
148 if diff_old.is_empty() && diff_new.is_empty() {
149 return (None, None); }
151
152 let final_old = serde_json::to_string(&diff_old).ok();
153 let final_new = serde_json::to_string(&diff_new).ok();
154
155 (final_old, final_new)
156}
157
158#[cfg_attr(test, mutants::skip)]
159pub async fn log_audit_diff(
160 model_type: &str,
161 model_id: i32,
162 event: &str,
163 old_json: &str,
164 new_json: &str,
165) -> Result<(), crate::Error> {
166 const MAX_PAYLOAD_LEN: usize = 5 * 1024 * 1024; if old_json.len() > MAX_PAYLOAD_LEN || new_json.len() > MAX_PAYLOAD_LEN {
169 return log_audit(
170 model_type,
171 model_id,
172 event,
173 Some(r#"{"error":"payload_too_large_for_diff"}"#.to_string()),
174 Some(r#"{"error":"payload_too_large_for_diff"}"#.to_string()),
175 )
176 .await;
177 }
178
179 let (final_old, final_new) = compute_diff(old_json, new_json);
180 if final_old.is_none() && final_new.is_none() {
181 return Ok(()); }
183 log_audit(model_type, model_id, event, final_old, final_new).await
184}
185
186#[cfg_attr(test, mutants::skip)]
187pub async fn create_audit_table() -> Result<(), crate::Error> {
188 let pool = Orm::pool();
189 let driver = Orm::driver();
190
191 let query = if driver == "postgres" {
192 r#"
193 CREATE TABLE IF NOT EXISTS rullst_audits (
194 id SERIAL PRIMARY KEY,
195 model_type VARCHAR(255) NOT NULL,
196 model_id INT NOT NULL,
197 event VARCHAR(50) NOT NULL,
198 old_values TEXT,
199 new_values TEXT,
200 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
201 )
202 "#
203 } else if driver == "mysql" {
204 r#"
205 CREATE TABLE IF NOT EXISTS rullst_audits (
206 id INT AUTO_INCREMENT PRIMARY KEY,
207 model_type VARCHAR(255) NOT NULL,
208 model_id INT NOT NULL,
209 event VARCHAR(50) NOT NULL,
210 old_values TEXT,
211 new_values TEXT,
212 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
213 )
214 "#
215 } else {
216 r#"
217 CREATE TABLE IF NOT EXISTS rullst_audits (
218 id INTEGER PRIMARY KEY AUTOINCREMENT,
219 model_type TEXT NOT NULL,
220 model_id INTEGER NOT NULL,
221 event TEXT NOT NULL,
222 old_values TEXT,
223 new_values TEXT,
224 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
225 )
226 "#
227 };
228
229 sqlx::query(query).execute(pool).await?;
230 Ok(())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::AuditLog;
236
237 #[test]
238 fn test_audit_log_serialization_round_trip() {
239 let log = AuditLog {
240 id: 1,
241 model_type: "User".to_string(),
242 model_id: 42,
243 event: "created".to_string(),
244 old_values: None,
245 new_values: Some(r#"{"name":"Alice"}"#.to_string()),
246 created_at: Some("2024-01-01T00:00:00Z".to_string()),
247 };
248
249 let json_str = serde_json::to_string(&log).expect("serialize");
250 assert!(json_str.contains("\"model_type\":\"User\""));
251 assert!(json_str.contains("\"event\":\"created\""));
252
253 let deserialized: AuditLog = serde_json::from_str(&json_str).expect("deserialize");
254 assert_eq!(deserialized.id, 1);
255 assert_eq!(deserialized.model_id, 42);
256 assert_eq!(deserialized.event, "created");
257 assert!(deserialized.old_values.is_none());
258 }
259
260 #[test]
261 fn test_audit_log_clone_debug() {
262 let log = AuditLog {
263 id: 5,
264 model_type: "Post".to_string(),
265 model_id: 99,
266 event: "updated".to_string(),
267 old_values: Some(r#"{"title":"Old"}"#.to_string()),
268 new_values: Some(r#"{"title":"New"}"#.to_string()),
269 created_at: None,
270 };
271 let cloned = log.clone();
272 assert_eq!(cloned.model_type, "Post");
273 let _ = format!("{:?}", cloned);
275 }
276
277 #[test]
278 fn test_compute_diff_changes() {
279 let old_json = r#"{"name":"Alice","age":30}"#;
280 let new_json = r#"{"name":"Alice","age":31}"#;
281 let (old_diff, new_diff) = super::compute_diff(old_json, new_json);
282 assert_eq!(old_diff.unwrap(), r#"{"age":30}"#);
283 assert_eq!(new_diff.unwrap(), r#"{"age":31}"#);
284 }
285
286 #[test]
287 fn test_compute_diff_no_changes() {
288 let json = r#"{"name":"Alice","age":30}"#;
289 let (old_diff, new_diff) = super::compute_diff(json, json);
290 assert!(old_diff.is_none());
291 assert!(new_diff.is_none());
292 }
293
294 #[test]
295 fn test_compute_diff_invalid_json() {
296 let (old_diff, new_diff) = super::compute_diff("not json", "{invalid}");
297 assert!(old_diff.is_none());
298 assert!(new_diff.is_none());
299
300 let (old_diff2, new_diff2) = super::compute_diff(r#"{"a":1}"#, "invalid");
302 assert!(old_diff2.is_none());
303 assert!(new_diff2.is_none());
304
305 let (old_diff3, new_diff3) = super::compute_diff("[1, 2]", "[1, 2, 3]");
307 assert!(old_diff3.is_none());
308 assert!(new_diff3.is_none());
309
310 let (old_diff4, new_diff4) = super::compute_diff("{}", "{}");
312 assert!(old_diff4.is_none());
313 assert!(new_diff4.is_none());
314 }
315
316 #[tokio::test]
317 async fn test_log_audit_diff_bypass() {
318 let result = super::log_audit_diff(
320 "User",
321 1,
322 "update",
323 r#"{"name":"Alice"}"#,
324 r#"{"name":"Alice"}"#,
325 )
326 .await;
327 assert!(result.is_ok());
328 }
329
330 #[test]
331 fn test_compute_diff_explicit_null_vs_omitted() {
332 let old_json = r#"{"name":"Alice","age":30}"#;
333 let new_json = r#"{"name":"Alice"}"#;
334 let (old_diff, new_diff) = super::compute_diff(old_json, new_json);
335 assert_eq!(old_diff.unwrap(), r#"{"age":30}"#);
336 assert_eq!(new_diff.unwrap(), r#"{"age":null}"#);
337
338 let old_json2 = r#"{"name":"Alice"}"#;
339 let new_json2 = r#"{"name":"Alice","age":null}"#;
340 let (old_diff2, new_diff2) = super::compute_diff(old_json2, new_json2);
341 assert_eq!(old_diff2.unwrap(), r#"{"age":null}"#);
342 assert_eq!(new_diff2.unwrap(), r#"{"age":null}"#);
343
344 let old_json3 = r#"{"name":"Alice"}"#;
345 let new_json3 = r#"{"name":"Alice","age":30}"#;
346 let (old_diff3, new_diff3) = super::compute_diff(old_json3, new_json3);
347 assert_eq!(old_diff3.unwrap(), r#"{"age":null}"#);
348 assert_eq!(new_diff3.unwrap(), r#"{"age":30}"#);
349 }
350
351 #[test]
352 fn test_validate_and_prepare_payloads() {
353 let res = super::validate_and_prepare_payloads(
355 "User",
356 "create",
357 Some("old".to_string()),
358 Some("new".to_string()),
359 );
360 assert!(res.is_ok());
361
362 let long_model = "A".repeat(256);
364 let res = super::validate_and_prepare_payloads(&long_model, "create", None, None);
365 assert!(res.is_err());
366
367 let long_event = "A".repeat(51);
369 let res = super::validate_and_prepare_payloads("User", &long_event, None, None);
370 assert!(res.is_err());
371
372 let large_payload = Some("A".repeat(5 * 1024 * 1024 + 1));
374 let (old_val, new_val) = super::validate_and_prepare_payloads(
375 "User",
376 "create",
377 large_payload.clone(),
378 large_payload,
379 )
380 .unwrap();
381 assert_eq!(old_val.unwrap(), r#"{"error":"payload_too_large"}"#);
382 assert_eq!(new_val.unwrap(), r#"{"error":"payload_too_large"}"#);
383 }
384
385 #[test]
386 fn test_is_sensitive() {
387 assert!(super::is_sensitive("password"));
388 assert!(super::is_sensitive("PASSWORD"));
389 assert!(super::is_sensitive("user_token"));
390 assert!(super::is_sensitive("client_secret"));
391 assert!(super::is_sensitive("senha"));
392 assert!(super::is_sensitive("api_key"));
393 assert!(super::is_sensitive("card_cvv"));
394 assert!(super::is_sensitive("ssn_number"));
395 assert!(super::is_sensitive("credit_card"));
396 assert!(super::is_sensitive("auth_code"));
397 assert!(!super::is_sensitive("name"));
398 assert!(!super::is_sensitive("email"));
399 assert!(!super::is_sensitive("age"));
400 }
401
402 #[test]
403 fn test_mask_if_sensitive() {
404 let val = serde_json::Value::String("my-secret-val".to_string());
405 let masked = super::mask_if_sensitive("password", val.clone());
406 assert_eq!(masked, serde_json::Value::String("***".to_string()));
407
408 let unmasked = super::mask_if_sensitive("username", val);
409 assert_eq!(
410 unmasked,
411 serde_json::Value::String("my-secret-val".to_string())
412 );
413 }
414}
415
416#[cfg(kani)]
417mod kani_proofs {
418 use super::*;
419
420 #[cfg_attr(test, mutants::skip)]
421 #[kani::proof]
422 #[kani::unwind(10)]
423 fn proof_is_sensitive_never_panics() {
424 let mut bytes: [u8; 10] = kani::any();
426 if let Ok(s) = std::str::from_utf8(&bytes) {
427 let _ = is_sensitive(s);
429 }
430 }
431}