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(mut new_obj)) =
122        (old_val, new_val)
123    {
124        for (k, v) in old_obj {
125            if let Some(new_v) = new_obj.remove(&k) {
126                #[allow(clippy::collapsible_if)]
127                if v != new_v {
128                    let masked_v = mask_if_sensitive(&k, v);
129                    let masked_new_v = mask_if_sensitive(&k, new_v);
130                    diff_new.insert(k.clone(), masked_new_v);
131                    diff_old.insert(k, masked_v);
132                }
133            } else {
134                let masked_v = mask_if_sensitive(&k, v);
135                diff_new.insert(k.clone(), serde_json::Value::Null);
136                diff_old.insert(k, masked_v);
137            }
138        }
139        for (k, new_v) in new_obj {
140            let masked_new_v = mask_if_sensitive(&k, new_v);
141            diff_old.insert(k.clone(), serde_json::Value::Null);
142            diff_new.insert(k, masked_new_v);
143        }
144    }
145
146    if diff_old.is_empty() && diff_new.is_empty() {
147        return (None, None); // Nothing changed
148    }
149
150    let final_old = serde_json::to_string(&diff_old).ok();
151    let final_new = serde_json::to_string(&diff_new).ok();
152
153    (final_old, final_new)
154}
155
156#[cfg_attr(test, mutants::skip)]
157pub async fn log_audit_diff(
158    model_type: &str,
159    model_id: i32,
160    event: &str,
161    old_json: &str,
162    new_json: &str,
163) -> Result<(), crate::Error> {
164    const MAX_PAYLOAD_LEN: usize = 5 * 1024 * 1024; // 5 MB
165
166    if old_json.len() > MAX_PAYLOAD_LEN || new_json.len() > MAX_PAYLOAD_LEN {
167        return log_audit(
168            model_type,
169            model_id,
170            event,
171            Some(r#"{"error":"payload_too_large_for_diff"}"#.to_string()),
172            Some(r#"{"error":"payload_too_large_for_diff"}"#.to_string()),
173        )
174        .await;
175    }
176
177    let (final_old, final_new) = compute_diff(old_json, new_json);
178    if final_old.is_none() && final_new.is_none() {
179        return Ok(()); // Nothing changed
180    }
181    log_audit(model_type, model_id, event, final_old, final_new).await
182}
183
184#[cfg_attr(test, mutants::skip)]
185pub async fn create_audit_table() -> Result<(), crate::Error> {
186    let pool = Orm::pool();
187    let driver = Orm::driver();
188
189    let query = if driver == "postgres" {
190        r#"
191        CREATE TABLE IF NOT EXISTS rullst_audits (
192            id SERIAL PRIMARY KEY,
193            model_type VARCHAR(255) NOT NULL,
194            model_id INT NOT NULL,
195            event VARCHAR(50) NOT NULL,
196            old_values TEXT,
197            new_values TEXT,
198            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
199        )
200        "#
201    } else if driver == "mysql" {
202        r#"
203        CREATE TABLE IF NOT EXISTS rullst_audits (
204            id INT AUTO_INCREMENT PRIMARY KEY,
205            model_type VARCHAR(255) NOT NULL,
206            model_id INT NOT NULL,
207            event VARCHAR(50) NOT NULL,
208            old_values TEXT,
209            new_values TEXT,
210            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
211        )
212        "#
213    } else {
214        r#"
215        CREATE TABLE IF NOT EXISTS rullst_audits (
216            id INTEGER PRIMARY KEY AUTOINCREMENT,
217            model_type TEXT NOT NULL,
218            model_id INTEGER NOT NULL,
219            event TEXT NOT NULL,
220            old_values TEXT,
221            new_values TEXT,
222            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
223        )
224        "#
225    };
226
227    sqlx::query(query).execute(pool).await?;
228    Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233    use super::AuditLog;
234
235    #[test]
236    fn test_audit_log_serialization_round_trip() {
237        let log = AuditLog {
238            id: 1,
239            model_type: "User".to_string(),
240            model_id: 42,
241            event: "created".to_string(),
242            old_values: None,
243            new_values: Some(r#"{"name":"Alice"}"#.to_string()),
244            created_at: Some("2024-01-01T00:00:00Z".to_string()),
245        };
246
247        let json_str = serde_json::to_string(&log).expect("serialize");
248        assert!(json_str.contains("\"model_type\":\"User\""));
249        assert!(json_str.contains("\"event\":\"created\""));
250
251        let deserialized: AuditLog = serde_json::from_str(&json_str).expect("deserialize");
252        assert_eq!(deserialized.id, 1);
253        assert_eq!(deserialized.model_id, 42);
254        assert_eq!(deserialized.event, "created");
255        assert!(deserialized.old_values.is_none());
256    }
257
258    #[test]
259    fn test_audit_log_clone_debug() {
260        let log = AuditLog {
261            id: 5,
262            model_type: "Post".to_string(),
263            model_id: 99,
264            event: "updated".to_string(),
265            old_values: Some(r#"{"title":"Old"}"#.to_string()),
266            new_values: Some(r#"{"title":"New"}"#.to_string()),
267            created_at: None,
268        };
269        let cloned = log.clone();
270        assert_eq!(cloned.model_type, "Post");
271        // Debug must not panic
272        let _ = format!("{:?}", cloned);
273    }
274
275    #[test]
276    fn test_compute_diff_changes() {
277        let old_json = r#"{"name":"Alice","age":30}"#;
278        let new_json = r#"{"name":"Alice","age":31}"#;
279        let (old_diff, new_diff) = super::compute_diff(old_json, new_json);
280        assert_eq!(old_diff.unwrap(), r#"{"age":30}"#);
281        assert_eq!(new_diff.unwrap(), r#"{"age":31}"#);
282    }
283
284    #[test]
285    fn test_compute_diff_no_changes() {
286        let json = r#"{"name":"Alice","age":30}"#;
287        let (old_diff, new_diff) = super::compute_diff(json, json);
288        assert!(old_diff.is_none());
289        assert!(new_diff.is_none());
290    }
291
292    #[test]
293    fn test_compute_diff_invalid_json() {
294        let (old_diff, new_diff) = super::compute_diff("not json", "{invalid}");
295        assert!(old_diff.is_none());
296        assert!(new_diff.is_none());
297    }
298
299    #[tokio::test]
300    async fn test_log_audit_diff_bypass() {
301        // Should not panic or hit the database if the old and new JSONs are identical
302        let result = super::log_audit_diff(
303            "User",
304            1,
305            "update",
306            r#"{"name":"Alice"}"#,
307            r#"{"name":"Alice"}"#,
308        )
309        .await;
310        assert!(result.is_ok());
311    }
312
313    #[test]
314    fn test_compute_diff_explicit_null_vs_omitted() {
315        let old_json = r#"{"name":"Alice","age":30}"#;
316        let new_json = r#"{"name":"Alice"}"#;
317        let (old_diff, new_diff) = super::compute_diff(old_json, new_json);
318        assert_eq!(old_diff.unwrap(), r#"{"age":30}"#);
319        assert_eq!(new_diff.unwrap(), r#"{"age":null}"#);
320
321        let old_json2 = r#"{"name":"Alice"}"#;
322        let new_json2 = r#"{"name":"Alice","age":null}"#;
323        let (old_diff2, new_diff2) = super::compute_diff(old_json2, new_json2);
324        assert_eq!(old_diff2.unwrap(), r#"{"age":null}"#);
325        assert_eq!(new_diff2.unwrap(), r#"{"age":null}"#);
326
327        let old_json3 = r#"{"name":"Alice"}"#;
328        let new_json3 = r#"{"name":"Alice","age":30}"#;
329        let (old_diff3, new_diff3) = super::compute_diff(old_json3, new_json3);
330        assert_eq!(old_diff3.unwrap(), r#"{"age":null}"#);
331        assert_eq!(new_diff3.unwrap(), r#"{"age":30}"#);
332    }
333
334    #[test]
335    fn test_validate_and_prepare_payloads() {
336        // Normal case
337        let res = super::validate_and_prepare_payloads(
338            "User",
339            "create",
340            Some("old".to_string()),
341            Some("new".to_string()),
342        );
343        assert!(res.is_ok());
344
345        // Model type too long
346        let long_model = "A".repeat(256);
347        let res = super::validate_and_prepare_payloads(&long_model, "create", None, None);
348        assert!(res.is_err());
349
350        // Event too long
351        let long_event = "A".repeat(51);
352        let res = super::validate_and_prepare_payloads("User", &long_event, None, None);
353        assert!(res.is_err());
354
355        // Payload too large
356        let large_payload = Some("A".repeat(5 * 1024 * 1024 + 1));
357        let (old_val, new_val) = super::validate_and_prepare_payloads(
358            "User",
359            "create",
360            large_payload.clone(),
361            large_payload,
362        )
363        .unwrap();
364        assert_eq!(old_val.unwrap(), r#"{"error":"payload_too_large"}"#);
365        assert_eq!(new_val.unwrap(), r#"{"error":"payload_too_large"}"#);
366    }
367
368    #[test]
369    fn test_is_sensitive() {
370        assert!(super::is_sensitive("password"));
371        assert!(super::is_sensitive("PASSWORD"));
372        assert!(super::is_sensitive("user_token"));
373        assert!(super::is_sensitive("client_secret"));
374        assert!(super::is_sensitive("senha"));
375        assert!(super::is_sensitive("api_key"));
376        assert!(super::is_sensitive("card_cvv"));
377        assert!(super::is_sensitive("ssn_number"));
378        assert!(super::is_sensitive("credit_card"));
379        assert!(super::is_sensitive("auth_code"));
380        assert!(!super::is_sensitive("name"));
381        assert!(!super::is_sensitive("email"));
382        assert!(!super::is_sensitive("age"));
383    }
384
385    #[test]
386    fn test_mask_if_sensitive() {
387        let val = serde_json::Value::String("my-secret-val".to_string());
388        let masked = super::mask_if_sensitive("password", val.clone());
389        assert_eq!(masked, serde_json::Value::String("***".to_string()));
390
391        let unmasked = super::mask_if_sensitive("username", val);
392        assert_eq!(
393            unmasked,
394            serde_json::Value::String("my-secret-val".to_string())
395        );
396    }
397}
398
399#[cfg(kani)]
400mod kani_proofs {
401    use super::*;
402
403    #[cfg_attr(test, mutants::skip)]
404    #[kani::proof]
405    #[kani::unwind(10)]
406    fn proof_is_sensitive_never_panics() {
407        // Gera uma string simbólica de até 10 caracteres
408        let mut bytes: [u8; 10] = kani::any();
409        if let Ok(s) = std::str::from_utf8(&bytes) {
410            // Garante que is_sensitive não dá panic para nenhuma combinação válida de UTF-8
411            let _ = is_sensitive(s);
412        }
413    }
414}