Skip to main content

rullst_orm/
audit.rs

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; // 5 MB
23
24    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); // Nothing changed
150    }
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; // 5 MB
167
168    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(()); // Nothing changed
182    }
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        // Debug must not panic
274        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        // One valid, one invalid
301        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        // Arrays or primitive values instead of JSON Objects
306        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        // Empty objects
311        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        // Should not panic or hit the database if the old and new JSONs are identical
319        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        // Normal case
354        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        // Model type too long
363        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        // Event too long
368        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        // Payload too large
373        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        // Gera uma string simbólica de até 10 caracteres
425        let mut bytes: [u8; 10] = kani::any();
426        if let Ok(s) = std::str::from_utf8(&bytes) {
427            // Garante que is_sensitive não dá panic para nenhuma combinação válida de UTF-8
428            let _ = is_sensitive(s);
429        }
430    }
431}