1use serde::{Deserialize, Serialize};
12use std::sync::Mutex;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SqlAuditContext {
16 pub sql: String,
17 pub user: String,
18 pub timestamp: i64,
19}
20
21const SENSITIVE_KEYWORDS: &[&str] = &[
24 "password",
25 "pwd",
26 "passwd",
27 "secret",
28 "token",
29 "api_key",
30 "apikey",
31 "access_key",
32 "accesskey",
33 "session",
34 "credit_card",
35 "creditcard",
36 "cvv",
37 "ssn",
38];
39
40pub struct SqlAuditor {
41 logs: Mutex<Vec<SqlAuditContext>>,
42}
43
44impl SqlAuditor {
45 pub fn new() -> Self {
46 Self {
47 logs: Mutex::new(vec![]),
48 }
49 }
50
51 pub fn log(&self, ctx: &SqlAuditContext) {
54 let masked_sql = mask_sensitive(&ctx.sql);
55 let entry = SqlAuditContext {
56 sql: masked_sql,
57 user: ctx.user.clone(),
58 timestamp: ctx.timestamp,
59 };
60 let mut logs = self
61 .logs
62 .lock()
63 .expect("SqlAuditor logs lock poisoned (log)");
64 logs.push(entry);
65 }
66
67 pub fn get_logs(&self) -> Vec<SqlAuditContext> {
69 let logs = self
70 .logs
71 .lock()
72 .expect("SqlAuditor logs lock poisoned (get_logs)");
73 logs.iter().cloned().collect()
74 }
75
76 pub fn flush(&self, path: &str) -> Result<usize, String> {
79 let logs = self
80 .logs
81 .lock()
82 .expect("SqlAuditor logs lock poisoned (flush)");
83 let snapshot: Vec<&SqlAuditContext> = logs.iter().collect();
84 let json = serde_json::to_string_pretty(&snapshot).map_err(|e| e.to_string())?;
85 std::fs::write(path, json).map_err(|e| e.to_string())?;
86 Ok(logs.len())
87 }
88
89 pub fn mask_sensitive(&self, sql: &str) -> String {
92 mask_sensitive(sql)
93 }
94}
95
96impl Default for SqlAuditor {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102fn mask_sensitive(sql: &str) -> String {
105 let lower = sql.to_ascii_lowercase();
106 let mut result = String::with_capacity(sql.len());
107 let mut i = 0;
108 let bytes = sql.as_bytes();
109 let lower_bytes = lower.as_bytes();
110 while i < bytes.len() {
111 let mut matched_len: Option<usize> = None;
112 for keyword in SENSITIVE_KEYWORDS {
113 let kw_bytes = keyword.as_bytes();
114 if i + kw_bytes.len() <= bytes.len() && &lower_bytes[i..i + kw_bytes.len()] == kw_bytes
115 {
116 let prev_ok = i == 0 || !is_ident_char(bytes[i - 1]);
119 let next_idx = i + kw_bytes.len();
120 let next_ok = next_idx >= bytes.len() || !is_ident_char(bytes[next_idx]);
121 if prev_ok && next_ok {
122 matched_len = Some(kw_bytes.len());
123 break;
124 }
125 }
126 }
127 if let Some(kw_len) = matched_len {
128 result.push_str("******");
129 i += kw_len;
130 } else {
131 let ch = sql[i..]
133 .chars()
134 .next()
135 .expect("i < bytes.len() guarantees non-empty slice");
136 result.push(ch);
137 i += ch.len_utf8();
138 }
139 }
140 result
141}
142
143fn is_ident_char(b: u8) -> bool {
145 b.is_ascii_alphanumeric() || b == b'_'
146}
147
148#[derive(Debug, Clone, Default)]
159pub struct AuditRules {
160 allow_patterns: Vec<String>,
162 deny_patterns: Vec<String>,
164}
165
166impl AuditRules {
167 pub fn new() -> Self {
168 Self::default()
169 }
170
171 pub fn allow(mut self, pattern: impl Into<String>) -> Self {
173 self.allow_patterns
174 .push(pattern.into().to_ascii_lowercase());
175 self
176 }
177
178 pub fn deny(mut self, pattern: impl Into<String>) -> Self {
180 self.deny_patterns.push(pattern.into().to_ascii_lowercase());
181 self
182 }
183
184 pub fn should_audit(&self, sql: &str) -> bool {
186 let lower = sql.to_ascii_lowercase();
187 for pat in &self.deny_patterns {
189 if lower.contains(pat) {
190 return false;
191 }
192 }
193 if self.allow_patterns.is_empty() {
195 return true;
196 }
197 self.allow_patterns.iter().any(|pat| lower.contains(pat))
199 }
200
201 pub fn allow_count(&self) -> usize {
203 self.allow_patterns.len()
204 }
205
206 pub fn deny_count(&self) -> usize {
208 self.deny_patterns.len()
209 }
210}
211
212#[derive(Debug, Clone)]
221pub struct RotationPolicy {
222 pub max_entries: usize,
224 pub max_age_ms: i64,
226}
227
228impl RotationPolicy {
229 pub fn none() -> Self {
231 Self {
232 max_entries: 0,
233 max_age_ms: 0,
234 }
235 }
236
237 pub fn by_size(max_entries: usize) -> Self {
239 Self {
240 max_entries,
241 max_age_ms: 0,
242 }
243 }
244
245 pub fn by_age(max_age_ms: i64) -> Self {
247 Self {
248 max_entries: 0,
249 max_age_ms,
250 }
251 }
252
253 pub fn by_size_and_age(max_entries: usize, max_age_ms: i64) -> Self {
255 Self {
256 max_entries,
257 max_age_ms,
258 }
259 }
260
261 fn needs_rotation(&self, entry_count: usize, oldest_ts: i64, now_ts: i64) -> bool {
263 if self.max_entries > 0 && entry_count >= self.max_entries {
264 return true;
265 }
266 if self.max_age_ms > 0 && oldest_ts > 0 && (now_ts - oldest_ts) > self.max_age_ms {
267 return true;
268 }
269 false
270 }
271}
272
273impl Default for RotationPolicy {
274 fn default() -> Self {
275 Self::none()
276 }
277}
278
279pub struct RotatingAuditor {
289 logs: Mutex<Vec<SqlAuditContext>>,
290 rules: AuditRules,
291 policy: RotationPolicy,
292 rotations: Mutex<usize>,
294}
295
296impl RotatingAuditor {
297 pub fn new(policy: RotationPolicy, rules: AuditRules) -> Self {
298 Self {
299 logs: Mutex::new(vec![]),
300 rules,
301 policy,
302 rotations: Mutex::new(0),
303 }
304 }
305
306 pub fn with_max_entries(max_entries: usize) -> Self {
308 Self::new(RotationPolicy::by_size(max_entries), AuditRules::new())
309 }
310
311 pub fn with_max_age(max_age_ms: i64) -> Self {
313 Self::new(RotationPolicy::by_age(max_age_ms), AuditRules::new())
314 }
315
316 pub fn log(&self, ctx: &SqlAuditContext) -> bool {
318 if !self.rules.should_audit(&ctx.sql) {
320 return false;
321 }
322 let masked_sql = mask_sensitive(&ctx.sql);
323 let entry = SqlAuditContext {
324 sql: masked_sql,
325 user: ctx.user.clone(),
326 timestamp: ctx.timestamp,
327 };
328 let mut logs = self
329 .logs
330 .lock()
331 .expect("RotatingAuditor logs lock poisoned (log)");
332
333 let now = ctx.timestamp;
335 let oldest = logs.first().map(|e| e.timestamp).unwrap_or(now);
336 if self.policy.needs_rotation(logs.len(), oldest, now) {
337 logs.clear();
338 *self
339 .rotations
340 .lock()
341 .expect("RotatingAuditor rotations lock poisoned (log)") += 1;
342 }
343
344 logs.push(entry);
345 true
346 }
347
348 pub fn get_logs(&self) -> Vec<SqlAuditContext> {
350 self.logs
351 .lock()
352 .expect("RotatingAuditor logs lock poisoned (get_logs)")
353 .clone()
354 }
355
356 pub fn rotation_count(&self) -> usize {
358 *self
359 .rotations
360 .lock()
361 .expect("RotatingAuditor rotations lock poisoned (rotation_count)")
362 }
363
364 pub fn rotate(&self) -> usize {
366 let mut logs = self
367 .logs
368 .lock()
369 .expect("RotatingAuditor logs lock poisoned (rotate)");
370 let count = logs.len();
371 logs.clear();
372 *self
373 .rotations
374 .lock()
375 .expect("RotatingAuditor rotations lock poisoned (rotate)") += 1;
376 count
377 }
378
379 pub fn len(&self) -> usize {
381 self.logs
382 .lock()
383 .expect("RotatingAuditor logs lock poisoned (len)")
384 .len()
385 }
386
387 pub fn is_empty(&self) -> bool {
389 self.logs
390 .lock()
391 .expect("RotatingAuditor logs lock poisoned (is_empty)")
392 .is_empty()
393 }
394}
395
396pub struct AsyncAuditWriter {
405 sender: std::sync::mpsc::Sender<AsyncCommand>,
406 handle: Mutex<Option<std::thread::JoinHandle<Vec<SqlAuditContext>>>>,
407}
408
409enum AsyncCommand {
410 Log(SqlAuditContext),
411 Shutdown,
412}
413
414impl AsyncAuditWriter {
415 pub fn new() -> Self {
417 let (sender, receiver) = std::sync::mpsc::channel::<AsyncCommand>();
418 let handle = std::thread::spawn(move || {
419 let mut logs: Vec<SqlAuditContext> = Vec::new();
420 for cmd in receiver {
421 match cmd {
422 AsyncCommand::Log(ctx) => {
423 let masked_sql = mask_sensitive(&ctx.sql);
424 logs.push(SqlAuditContext {
425 sql: masked_sql,
426 user: ctx.user,
427 timestamp: ctx.timestamp,
428 });
429 }
430 AsyncCommand::Shutdown => break,
431 }
432 }
433 logs
434 });
435 Self {
436 sender,
437 handle: Mutex::new(Some(handle)),
438 }
439 }
440
441 pub fn log(&self, ctx: &SqlAuditContext) -> Result<(), String> {
443 self.sender
444 .send(AsyncCommand::Log(ctx.clone()))
445 .map_err(|e| format!("AsyncAuditWriter channel closed: {}", e))
446 }
447
448 pub fn shutdown(&self) -> Result<Vec<SqlAuditContext>, String> {
450 let _ = self.sender.send(AsyncCommand::Shutdown);
451 let mut handle_guard = self
452 .handle
453 .lock()
454 .expect("AsyncAuditWriter handle lock poisoned (shutdown)");
455 if let Some(handle) = handle_guard.take() {
456 handle
457 .join()
458 .map_err(|e| format!("Thread panicked: {:?}", e))
459 } else {
460 Err("Already shut down".to_string())
461 }
462 }
463}
464
465impl Default for AsyncAuditWriter {
466 fn default() -> Self {
467 Self::new()
468 }
469}
470
471#[derive(Debug, Clone, Default)]
477pub struct AuditQuery {
478 pub user: Option<String>,
480 pub from_ts: Option<i64>,
482 pub to_ts: Option<i64>,
484 pub sql_contains: Option<String>,
486 pub limit: usize,
488}
489
490impl AuditQuery {
491 pub fn new() -> Self {
492 Self::default()
493 }
494
495 pub fn by_user(mut self, user: impl Into<String>) -> Self {
497 self.user = Some(user.into());
498 self
499 }
500
501 pub fn by_time_range(mut self, from: i64, to: i64) -> Self {
503 self.from_ts = Some(from);
504 self.to_ts = Some(to);
505 self
506 }
507
508 pub fn by_sql_contains(mut self, keyword: impl Into<String>) -> Self {
510 self.sql_contains = Some(keyword.into());
511 self
512 }
513
514 pub fn with_limit(mut self, limit: usize) -> Self {
516 self.limit = limit;
517 self
518 }
519
520 pub fn filter(&self, logs: &[SqlAuditContext]) -> Vec<SqlAuditContext> {
522 let keyword_lower = self.sql_contains.as_ref().map(|s| s.to_ascii_lowercase());
523 let mut result: Vec<SqlAuditContext> = logs
524 .iter()
525 .filter(|entry| {
526 if let Some(u) = &self.user {
527 if entry.user != *u {
528 return false;
529 }
530 }
531 if let Some(from) = self.from_ts {
532 if entry.timestamp < from {
533 return false;
534 }
535 }
536 if let Some(to) = self.to_ts {
537 if entry.timestamp > to {
538 return false;
539 }
540 }
541 if let Some(kw) = &keyword_lower {
542 if !entry.sql.to_ascii_lowercase().contains(kw) {
543 return false;
544 }
545 }
546 true
547 })
548 .cloned()
549 .collect();
550 if self.limit > 0 && result.len() > self.limit {
551 result.truncate(self.limit);
552 }
553 result
554 }
555}
556
557pub fn query_logs(auditor: &SqlAuditor, query: &AuditQuery) -> Vec<SqlAuditContext> {
559 let logs = auditor.get_logs();
560 query.filter(&logs)
561}
562
563pub trait AuditLogStore: Send + Sync {
572 fn append(&self, entry: &SqlAuditContext) -> Result<(), String>;
574 fn read_all(&self) -> Result<Vec<SqlAuditContext>, String>;
576 fn clear(&self) -> Result<(), String>;
578}
579
580pub struct FileAuditLogStore {
586 path: String,
587 write_lock: Mutex<()>,
588}
589
590impl FileAuditLogStore {
591 pub fn new(path: impl Into<String>) -> Self {
593 Self {
594 path: path.into(),
595 write_lock: Mutex::new(()),
596 }
597 }
598
599 pub fn path(&self) -> &str {
601 &self.path
602 }
603}
604
605impl AuditLogStore for FileAuditLogStore {
606 fn append(&self, entry: &SqlAuditContext) -> Result<(), String> {
608 let _guard = self
609 .write_lock
610 .lock()
611 .map_err(|e| format!("write_lock poisoned: {}", e))?;
612 let masked_sql = mask_sensitive(&entry.sql);
614 let stored = SqlAuditContext {
615 sql: masked_sql,
616 user: entry.user.clone(),
617 timestamp: entry.timestamp,
618 };
619 let line = serde_json::to_string(&stored).map_err(|e| e.to_string())?;
620 use std::io::Write;
622 let mut file = std::fs::OpenOptions::new()
623 .create(true)
624 .append(true)
625 .open(&self.path)
626 .map_err(|e| format!("open '{}' failed: {}", self.path, e))?;
627 writeln!(file, "{}", line).map_err(|e| e.to_string())
628 }
629
630 fn read_all(&self) -> Result<Vec<SqlAuditContext>, String> {
634 let content = match std::fs::read_to_string(&self.path) {
635 Ok(c) => c,
636 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
637 return Ok(Vec::new());
638 }
639 Err(e) => return Err(format!("read failed: {}", e)),
640 };
641 let mut result = Vec::new();
642 for (lineno, line) in content.lines().enumerate() {
643 let line = line.trim();
644 if line.is_empty() {
645 continue;
646 }
647 let entry: SqlAuditContext = serde_json::from_str(line)
648 .map_err(|e| format!("parse line {} failed: {}", lineno + 1, e))?;
649 result.push(entry);
650 }
651 Ok(result)
652 }
653
654 fn clear(&self) -> Result<(), String> {
656 let _guard = self
657 .write_lock
658 .lock()
659 .map_err(|e| format!("write_lock poisoned: {}", e))?;
660 std::fs::remove_file(&self.path).or_else(|e| {
661 if e.kind() == std::io::ErrorKind::NotFound {
663 Ok(())
664 } else {
665 Err(format!("clear failed: {}", e))
666 }
667 })
668 }
669}
670
671pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct HashChainEntry {
692 pub prev_hash: String,
694 pub current_hash: String,
696 pub entry: SqlAuditContext,
698}
699
700impl HashChainEntry {
701 fn compute_hash(prev_hash: &str, entry: &SqlAuditContext) -> String {
707 use sha2::{Digest, Sha256};
708 let mut hasher = Sha256::new();
709 hasher.update(prev_hash.as_bytes());
710 hasher.update(entry.sql.as_bytes());
711 hasher.update(entry.user.as_bytes());
712 hasher.update(entry.timestamp.to_le_bytes());
714 let result = hasher.finalize();
715 hex_encode(&result)
717 }
718
719 pub fn genesis(entry: SqlAuditContext) -> Self {
721 let prev_hash = GENESIS_HASH.to_string();
722 let current_hash = Self::compute_hash(&prev_hash, &entry);
723 Self {
724 prev_hash,
725 current_hash,
726 entry,
727 }
728 }
729
730 pub fn append(prev_hash: &str, entry: SqlAuditContext) -> Self {
732 let current_hash = Self::compute_hash(prev_hash, &entry);
733 Self {
734 prev_hash: prev_hash.to_string(),
735 current_hash,
736 entry,
737 }
738 }
739}
740
741fn hex_encode(bytes: &[u8]) -> String {
745 const HEX_CHARS: &[u8] = b"0123456789abcdef";
746 let mut s = String::with_capacity(bytes.len() * 2);
747 for &b in bytes {
748 s.push(HEX_CHARS[(b >> 4) as usize] as char);
749 s.push(HEX_CHARS[(b & 0x0f) as usize] as char);
750 }
751 s
752}
753
754pub struct HashChainAuditor {
779 entries: Mutex<Vec<HashChainEntry>>,
781}
782
783impl Default for HashChainAuditor {
784 fn default() -> Self {
785 Self::new()
786 }
787}
788
789impl HashChainAuditor {
790 pub fn new() -> Self {
792 Self {
793 entries: Mutex::new(Vec::new()),
794 }
795 }
796
797 pub fn log(&self, ctx: &SqlAuditContext) {
805 let masked_sql = mask_sensitive(&ctx.sql);
806 let entry = SqlAuditContext {
807 sql: masked_sql,
808 user: ctx.user.clone(),
809 timestamp: ctx.timestamp,
810 };
811 let mut entries = self
812 .entries
813 .lock()
814 .expect("HashChainAuditor entries lock poisoned (log)");
815 let prev_hash = entries
816 .last()
817 .map(|e| e.current_hash.as_str())
818 .unwrap_or(GENESIS_HASH);
819 let chain_entry = if entries.is_empty() {
820 HashChainEntry::genesis(entry)
821 } else {
822 HashChainEntry::append(prev_hash, entry)
823 };
824 entries.push(chain_entry);
825 }
826
827 pub fn get_entries(&self) -> Vec<HashChainEntry> {
829 self.entries
830 .lock()
831 .expect("HashChainAuditor entries lock poisoned (get_entries)")
832 .clone()
833 }
834
835 pub fn len(&self) -> usize {
837 self.entries
838 .lock()
839 .expect("HashChainAuditor entries lock poisoned (len)")
840 .len()
841 }
842
843 pub fn is_empty(&self) -> bool {
845 self.entries
846 .lock()
847 .expect("HashChainAuditor entries lock poisoned (is_empty)")
848 .is_empty()
849 }
850
851 pub fn verify(&self) -> Result<(), String> {
863 let entries = self
864 .entries
865 .lock()
866 .expect("HashChainAuditor entries lock poisoned (verify)");
867 for (i, entry) in entries.iter().enumerate() {
868 if i == 0 {
870 if entry.prev_hash != GENESIS_HASH {
871 return Err(format!(
872 "chain genesis prev_hash mismatch at index 0: expected '{}', got '{}'",
873 GENESIS_HASH, entry.prev_hash
874 ));
875 }
876 } else {
877 let prev = &entries[i - 1];
879 if entry.prev_hash != prev.current_hash {
880 return Err(format!(
881 "chain broken at index {}: prev_hash '{}' != previous current_hash '{}'",
882 i, entry.prev_hash, prev.current_hash
883 ));
884 }
885 }
886 let recomputed = HashChainEntry::compute_hash(&entry.prev_hash, &entry.entry);
888 if entry.current_hash != recomputed {
889 return Err(format!(
890 "hash mismatch at index {}: stored '{}' != recomputed '{}'",
891 i, entry.current_hash, recomputed
892 ));
893 }
894 }
895 Ok(())
896 }
897
898 pub fn flush(&self, path: &str) -> Result<usize, String> {
902 let entries = self
903 .entries
904 .lock()
905 .expect("HashChainAuditor entries lock poisoned (flush)");
906 let snapshot: Vec<&HashChainEntry> = entries.iter().collect();
907 let json = serde_json::to_string_pretty(&snapshot).map_err(|e| e.to_string())?;
908 std::fs::write(path, json).map_err(|e| e.to_string())?;
909 Ok(entries.len())
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916
917 fn test_data_dir() -> std::path::PathBuf {
922 let f_drive = std::path::Path::new("F:\\test\\data");
923 if is_dir_writable(f_drive) {
924 return f_drive.to_path_buf();
925 }
926 if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
927 let p = std::path::PathBuf::from(&dir);
928 if is_dir_writable(&p) {
929 return p;
930 }
931 }
932 std::env::temp_dir()
933 }
934
935 fn is_dir_writable(dir: &std::path::Path) -> bool {
937 if !dir.exists() {
938 return false;
939 }
940 let probe = dir.join(format!(".probe_{}", std::process::id()));
941 match std::fs::File::create(&probe) {
942 Ok(_) => {
943 let _ = std::fs::remove_file(&probe);
944 true
945 }
946 Err(_) => false,
947 }
948 }
949
950 fn ctx(sql: &str, user: &str, ts: i64) -> SqlAuditContext {
951 SqlAuditContext {
952 sql: sql.to_string(),
953 user: user.to_string(),
954 timestamp: ts,
955 }
956 }
957
958 #[test]
959 fn test_log_stores_in_memory() {
960 let a = SqlAuditor::new();
961 a.log(&ctx("SELECT * FROM users", "admin", 1000));
962 a.log(&ctx("INSERT INTO logs VALUES(1)", "admin", 1001));
963 let logs = a.get_logs();
964 assert_eq!(logs.len(), 2);
965 assert_eq!(logs[0].sql, "SELECT * FROM users");
966 assert_eq!(logs[0].user, "admin");
967 assert_eq!(logs[0].timestamp, 1000);
968 assert_eq!(logs[1].timestamp, 1001);
969 }
970
971 #[test]
972 fn test_log_masks_sensitive_in_storage() {
973 let a = SqlAuditor::new();
974 a.log(&ctx(
975 "SELECT * FROM users WHERE password='secret'",
976 "admin",
977 1000,
978 ));
979 let logs = a.get_logs();
980 assert_eq!(logs.len(), 1);
981 let stored_sql = &logs[0].sql;
982 assert!(!stored_sql.contains("password"));
983 assert!(!stored_sql.contains("secret"));
984 assert!(stored_sql.contains("******"));
985 }
986
987 #[test]
988 fn test_mask_sensitive_password() {
989 let a = SqlAuditor::new();
990 let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
991 assert!(!masked.contains("password"));
992 assert!(!masked.contains("secret"));
993 assert!(masked.contains("******"));
994 }
995
996 #[test]
997 fn test_mask_sensitive_case_insensitive() {
998 let a = SqlAuditor::new();
999 let masked = a.mask_sensitive("UPDATE users SET PASSWORD='abc', Token='x'");
1000 let lower = masked.to_lowercase();
1001 assert!(!lower.contains("password"));
1002 assert!(!lower.contains("token"));
1003 assert!(masked.contains("******"));
1004 }
1005
1006 #[test]
1007 fn test_mask_sensitive_extended_keywords() {
1008 let a = SqlAuditor::new();
1009 let inputs = [
1010 "pwd",
1011 "passwd",
1012 "secret",
1013 "api_key",
1014 "access_key",
1015 "session",
1016 "credit_card",
1017 "cvv",
1018 "ssn",
1019 ];
1020 for kw in inputs {
1021 let sql = format!("SELECT * FROM t WHERE k = '{}'", kw);
1022 let masked = a.mask_sensitive(&sql);
1023 let lower = masked.to_lowercase();
1024 assert!(
1025 !lower.contains(kw),
1026 "keyword '{}' should be masked in: {}",
1027 kw,
1028 masked
1029 );
1030 assert!(masked.contains("******"));
1031 }
1032 }
1033
1034 #[test]
1035 fn test_mask_sensitive_preserves_non_sensitive() {
1036 let a = SqlAuditor::new();
1037 let masked = a.mask_sensitive("SELECT id, name FROM users WHERE active = 1");
1038 assert_eq!(masked, "SELECT id, name FROM users WHERE active = 1");
1039 }
1040
1041 #[test]
1042 fn test_mask_sensitive_does_not_match_substrings() {
1043 let a = SqlAuditor::new();
1046 let masked = a.mask_sensitive("SELECT * FROM users WHERE note='passworded'");
1047 assert!(masked.contains("passworded"));
1049 assert_eq!(masked, "SELECT * FROM users WHERE note='passworded'");
1055 }
1056
1057 #[test]
1058 fn test_mask_sensitive_multiple_occurrences() {
1059 let a = SqlAuditor::new();
1060 let masked = a.mask_sensitive("INSERT INTO t (password, token) VALUES ('p1', 't1')");
1061 let lower = masked.to_lowercase();
1063 assert!(!lower.contains("password"));
1064 assert!(!lower.contains("token"));
1065 let count = masked.matches("******").count();
1067 assert!(count >= 2, "expected at least 2 masks, got: {}", masked);
1068 }
1069
1070 #[test]
1071 fn test_get_logs_empty_initially() {
1072 let a = SqlAuditor::new();
1073 assert!(a.get_logs().is_empty());
1074 }
1075
1076 #[test]
1077 fn test_get_logs_returns_snapshot_independent_of_changes() {
1078 let a = SqlAuditor::new();
1079 a.log(&ctx("SELECT 1", "u", 1));
1080 let snap = a.get_logs();
1081 a.log(&ctx("SELECT 2", "u", 2));
1082 assert_eq!(snap.len(), 1, "snapshot should not change after new log");
1083 assert_eq!(a.get_logs().len(), 2);
1084 }
1085
1086 #[test]
1087 fn test_flush_writes_json_file() {
1088 let a = SqlAuditor::new();
1089 a.log(&ctx("SELECT * FROM users WHERE password='p'", "admin", 123));
1090 a.log(&ctx("INSERT INTO logs VALUES(1)", "user2", 456));
1091 let path = test_data_dir().join("sz_orm_audit_flush_test.json");
1092 let path_str = path.to_str().unwrap();
1093 let count = a.flush(path_str).expect("flush should succeed");
1094 assert_eq!(count, 2);
1095 let content = std::fs::read_to_string(path_str).expect("file should be readable");
1097 let parsed: Vec<SqlAuditContext> =
1098 serde_json::from_str(&content).expect("should parse as JSON array");
1099 assert_eq!(parsed.len(), 2);
1100 assert_eq!(parsed[0].user, "admin");
1101 assert_eq!(parsed[1].timestamp, 456);
1102 assert!(!parsed[0].sql.contains("password"));
1104 let _ = std::fs::remove_file(path_str);
1106 }
1107
1108 #[test]
1109 fn test_flush_empty_writes_empty_array() {
1110 let a = SqlAuditor::new();
1111 let path = test_data_dir().join("sz_orm_audit_flush_empty_test.json");
1112 let path_str = path.to_str().unwrap();
1113 let count = a.flush(path_str).expect("flush should succeed");
1114 assert_eq!(count, 0);
1115 let content = std::fs::read_to_string(path_str).expect("file should be readable");
1116 assert_eq!(content.trim(), "[]");
1117 let _ = std::fs::remove_file(path_str);
1118 }
1119
1120 #[test]
1121 fn test_default_creates_new_auditor() {
1122 let a = SqlAuditor::default();
1123 assert!(a.get_logs().is_empty());
1124 }
1125
1126 #[test]
1127 fn test_original_test_compatibility() {
1128 let a = SqlAuditor::new();
1131 let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
1132 assert!(!masked.contains("password"));
1133 }
1134
1135 #[test]
1138 fn test_audit_rules_empty_allows_all() {
1139 let rules = AuditRules::new();
1140 assert!(rules.should_audit("SELECT * FROM users"));
1141 assert!(rules.should_audit("DELETE FROM orders"));
1142 assert_eq!(rules.allow_count(), 0);
1143 assert_eq!(rules.deny_count(), 0);
1144 }
1145
1146 #[test]
1147 fn test_audit_rules_deny_blocks() {
1148 let rules = AuditRules::new().deny("pg_catalog");
1149 assert!(!rules.should_audit("SELECT * FROM pg_catalog.tables"));
1150 assert!(rules.should_audit("SELECT * FROM users"));
1151 }
1152
1153 #[test]
1154 fn test_audit_rules_allow_filters() {
1155 let rules = AuditRules::new().allow("select").allow("insert");
1156 assert!(rules.should_audit("SELECT * FROM users"));
1157 assert!(rules.should_audit("INSERT INTO logs VALUES(1)"));
1158 assert!(!rules.should_audit("DELETE FROM users"));
1159 }
1160
1161 #[test]
1162 fn test_audit_rules_deny_overrides_allow() {
1163 let rules = AuditRules::new().allow("select").deny("password");
1164 assert!(!rules.should_audit("SELECT * FROM users WHERE password='x'"));
1166 assert!(rules.should_audit("SELECT * FROM users"));
1168 }
1169
1170 #[test]
1171 fn test_audit_rules_case_insensitive() {
1172 let rules = AuditRules::new().deny("DROP");
1173 assert!(!rules.should_audit("drop table users"));
1174 assert!(!rules.should_audit("DROP TABLE users"));
1175 assert!(rules.should_audit("SELECT * FROM users"));
1176 }
1177
1178 #[test]
1179 fn test_audit_rules_multiple_deny() {
1180 let rules = AuditRules::new()
1181 .deny("drop")
1182 .deny("truncate")
1183 .deny("shutdown");
1184 assert!(!rules.should_audit("DROP TABLE x"));
1185 assert!(!rules.should_audit("TRUNCATE TABLE y"));
1186 assert!(!rules.should_audit("SHUTDOWN"));
1187 assert!(rules.should_audit("SELECT 1"));
1188 }
1189
1190 #[test]
1193 fn test_rotation_policy_none_never_rotates() {
1194 let policy = RotationPolicy::none();
1195 assert!(!policy.needs_rotation(1_000_000, 0, 1_000_000));
1196 assert!(!policy.needs_rotation(0, 0, 0));
1197 }
1198
1199 #[test]
1200 fn test_rotation_policy_by_size() {
1201 let policy = RotationPolicy::by_size(100);
1202 assert!(!policy.needs_rotation(99, 0, 1000));
1203 assert!(policy.needs_rotation(100, 0, 1000));
1204 assert!(policy.needs_rotation(200, 0, 1000));
1205 }
1206
1207 #[test]
1208 fn test_rotation_policy_by_age() {
1209 let policy = RotationPolicy::by_age(5000);
1210 assert!(!policy.needs_rotation(10, 5000, 9000));
1212 assert!(policy.needs_rotation(10, 5000, 11000));
1214 }
1215
1216 #[test]
1217 fn test_rotation_policy_by_size_and_age() {
1218 let policy = RotationPolicy::by_size_and_age(100, 5000);
1219 assert!(!policy.needs_rotation(50, 5000, 9000));
1221 assert!(policy.needs_rotation(100, 5000, 5000));
1223 assert!(policy.needs_rotation(10, 5000, 11000));
1225 }
1226
1227 #[test]
1230 fn test_rotating_auditor_no_rotation_stores_all() {
1231 let auditor = RotatingAuditor::new(RotationPolicy::none(), AuditRules::new());
1232 for i in 0..100 {
1233 auditor.log(&ctx(&format!("SELECT {}", i), "user", i));
1234 }
1235 assert_eq!(auditor.len(), 100);
1236 assert_eq!(auditor.rotation_count(), 0);
1237 }
1238
1239 #[test]
1240 fn test_rotating_auditor_rotates_by_size() {
1241 let auditor = RotatingAuditor::with_max_entries(5);
1242 for i in 0..5 {
1243 auditor.log(&ctx(&format!("SELECT {}", i), "user", i));
1244 }
1245 assert_eq!(auditor.len(), 5);
1246 assert_eq!(auditor.rotation_count(), 0);
1247 auditor.log(&ctx("SELECT 6", "user", 100));
1249 assert_eq!(auditor.len(), 1);
1250 assert_eq!(auditor.rotation_count(), 1);
1251 }
1252
1253 #[test]
1254 fn test_rotating_auditor_rotates_by_age() {
1255 let auditor = RotatingAuditor::with_max_age(1000);
1256 auditor.log(&ctx("SELECT 1", "user", 100));
1257 auditor.log(&ctx("SELECT 2", "user", 200));
1258 assert_eq!(auditor.len(), 2);
1259 assert_eq!(auditor.rotation_count(), 0);
1260 auditor.log(&ctx("SELECT 3", "user", 1500));
1262 assert_eq!(auditor.len(), 1);
1263 assert_eq!(auditor.rotation_count(), 1);
1264 }
1265
1266 #[test]
1267 fn test_rotating_auditor_rules_filter() {
1268 let rules = AuditRules::new().deny("drop").allow("select");
1269 let auditor = RotatingAuditor::new(RotationPolicy::none(), rules);
1270 let logged1 = auditor.log(&ctx("SELECT * FROM users", "u", 1));
1271 let logged2 = auditor.log(&ctx("DROP TABLE users", "u", 2));
1272 let logged3 = auditor.log(&ctx("DELETE FROM users", "u", 3));
1273 assert!(logged1);
1274 assert!(!logged2);
1275 assert!(!logged3);
1276 assert_eq!(auditor.len(), 1);
1277 }
1278
1279 #[test]
1280 fn test_rotating_auditor_manual_rotate() {
1281 let auditor = RotatingAuditor::with_max_entries(100);
1282 auditor.log(&ctx("SELECT 1", "u", 1));
1283 auditor.log(&ctx("SELECT 2", "u", 2));
1284 let cleared = auditor.rotate();
1285 assert_eq!(cleared, 2);
1286 assert!(auditor.is_empty());
1287 assert_eq!(auditor.rotation_count(), 1);
1288 }
1289
1290 #[test]
1291 fn test_rotating_auditor_masks_sensitive() {
1292 let auditor = RotatingAuditor::with_max_entries(100);
1293 auditor.log(&ctx("SELECT * FROM users WHERE password='x'", "u", 1));
1294 let logs = auditor.get_logs();
1295 assert_eq!(logs.len(), 1);
1296 assert!(!logs[0].sql.contains("password"));
1297 assert!(logs[0].sql.contains("******"));
1298 }
1299
1300 #[test]
1301 fn test_rotating_auditor_get_logs_snapshot() {
1302 let auditor = RotatingAuditor::with_max_entries(100);
1303 auditor.log(&ctx("SELECT 1", "u", 1));
1304 let snap = auditor.get_logs();
1305 auditor.log(&ctx("SELECT 2", "u", 2));
1306 assert_eq!(snap.len(), 1, "snapshot should be independent");
1307 assert_eq!(auditor.len(), 2);
1308 }
1309
1310 #[test]
1313 fn test_async_writer_log_and_shutdown() {
1314 let writer = AsyncAuditWriter::new();
1315 writer
1316 .log(&ctx("SELECT * FROM users", "admin", 1000))
1317 .unwrap();
1318 writer
1319 .log(&ctx("INSERT INTO logs VALUES(1)", "user2", 2000))
1320 .unwrap();
1321 let logs = writer.shutdown().expect("shutdown should succeed");
1322 assert_eq!(logs.len(), 2);
1323 assert_eq!(logs[0].user, "admin");
1324 assert_eq!(logs[1].timestamp, 2000);
1325 }
1326
1327 #[test]
1328 fn test_async_writer_masks_sensitive() {
1329 let writer = AsyncAuditWriter::new();
1330 writer
1331 .log(&ctx("SELECT * FROM users WHERE password='secret'", "u", 1))
1332 .unwrap();
1333 let logs = writer.shutdown().unwrap();
1334 assert_eq!(logs.len(), 1);
1335 assert!(!logs[0].sql.contains("password"));
1336 }
1337
1338 #[test]
1339 fn test_async_writer_empty_shutdown() {
1340 let writer = AsyncAuditWriter::new();
1341 let logs = writer.shutdown().expect("shutdown should succeed");
1342 assert!(logs.is_empty());
1343 }
1344
1345 #[test]
1346 fn test_async_writer_double_shutdown_errors() {
1347 let writer = AsyncAuditWriter::new();
1348 let _ = writer.shutdown().unwrap();
1349 let result = writer.shutdown();
1350 assert!(result.is_err(), "double shutdown should error");
1351 }
1352
1353 #[test]
1354 fn test_async_writer_default() {
1355 let writer = AsyncAuditWriter::default();
1356 writer.log(&ctx("SELECT 1", "u", 1)).unwrap();
1357 let logs = writer.shutdown().unwrap();
1358 assert_eq!(logs.len(), 1);
1359 }
1360
1361 #[test]
1364 fn test_audit_query_by_user() {
1365 let auditor = SqlAuditor::new();
1366 auditor.log(&ctx("SELECT 1", "alice", 100));
1367 auditor.log(&ctx("SELECT 2", "bob", 200));
1368 auditor.log(&ctx("SELECT 3", "alice", 300));
1369 let query = AuditQuery::new().by_user("alice");
1370 let results = query_logs(&auditor, &query);
1371 assert_eq!(results.len(), 2);
1372 assert!(results.iter().all(|r| r.user == "alice"));
1373 }
1374
1375 #[test]
1376 fn test_audit_query_by_time_range() {
1377 let auditor = SqlAuditor::new();
1378 auditor.log(&ctx("SELECT 1", "u", 100));
1379 auditor.log(&ctx("SELECT 2", "u", 200));
1380 auditor.log(&ctx("SELECT 3", "u", 300));
1381 auditor.log(&ctx("SELECT 4", "u", 400));
1382 let query = AuditQuery::new().by_time_range(150, 350);
1383 let results = query_logs(&auditor, &query);
1384 assert_eq!(results.len(), 2);
1385 assert!(results
1386 .iter()
1387 .all(|r| r.timestamp >= 150 && r.timestamp <= 350));
1388 }
1389
1390 #[test]
1391 fn test_audit_query_by_sql_contains() {
1392 let auditor = SqlAuditor::new();
1393 auditor.log(&ctx("SELECT * FROM users", "u", 1));
1394 auditor.log(&ctx("INSERT INTO orders", "u", 2));
1395 auditor.log(&ctx("SELECT * FROM orders", "u", 3));
1396 let query = AuditQuery::new().by_sql_contains("orders");
1397 let results = query_logs(&auditor, &query);
1398 assert_eq!(results.len(), 2);
1399 assert!(results
1400 .iter()
1401 .all(|r| r.sql.to_lowercase().contains("orders")));
1402 }
1403
1404 #[test]
1405 fn test_audit_query_sql_contains_case_insensitive() {
1406 let auditor = SqlAuditor::new();
1407 auditor.log(&ctx("select * from Users", "u", 1));
1408 let query = AuditQuery::new().by_sql_contains("USERS");
1409 let results = query_logs(&auditor, &query);
1410 assert_eq!(results.len(), 1);
1411 }
1412
1413 #[test]
1414 fn test_audit_query_with_limit() {
1415 let auditor = SqlAuditor::new();
1416 for i in 0..10 {
1417 auditor.log(&ctx(&format!("SELECT {}", i), "u", i));
1418 }
1419 let query = AuditQuery::new().with_limit(3);
1420 let results = query_logs(&auditor, &query);
1421 assert_eq!(results.len(), 3);
1422 }
1423
1424 #[test]
1425 fn test_audit_query_combined_filters() {
1426 let auditor = SqlAuditor::new();
1427 auditor.log(&ctx("SELECT * FROM users", "alice", 100));
1428 auditor.log(&ctx("INSERT INTO users", "alice", 200));
1429 auditor.log(&ctx("SELECT * FROM orders", "alice", 300));
1430 auditor.log(&ctx("SELECT * FROM users", "bob", 400));
1431 let query = AuditQuery::new()
1432 .by_user("alice")
1433 .by_sql_contains("select")
1434 .with_limit(10);
1435 let results = query_logs(&auditor, &query);
1436 assert_eq!(results.len(), 2);
1437 assert!(results.iter().all(|r| r.user == "alice"));
1438 }
1439
1440 #[test]
1441 fn test_audit_query_empty_returns_all() {
1442 let auditor = SqlAuditor::new();
1443 auditor.log(&ctx("SELECT 1", "u", 1));
1444 auditor.log(&ctx("SELECT 2", "u", 2));
1445 let query = AuditQuery::new();
1446 let results = query_logs(&auditor, &query);
1447 assert_eq!(results.len(), 2);
1448 }
1449
1450 #[test]
1451 fn test_audit_query_no_match_returns_empty() {
1452 let auditor = SqlAuditor::new();
1453 auditor.log(&ctx("SELECT 1", "u", 1));
1454 let query = AuditQuery::new().by_user("nonexistent");
1455 let results = query_logs(&auditor, &query);
1456 assert!(results.is_empty());
1457 }
1458
1459 #[test]
1460 fn test_audit_query_filter_directly() {
1461 let logs = vec![
1462 ctx("SELECT 1", "a", 10),
1463 ctx("SELECT 2", "b", 20),
1464 ctx("SELECT 3", "a", 30),
1465 ];
1466 let query = AuditQuery::new().by_user("a");
1467 let results = query.filter(&logs);
1468 assert_eq!(results.len(), 2);
1469 }
1470
1471 #[test]
1472 fn test_audit_query_limit_zero_means_no_limit() {
1473 let logs = vec![ctx("SELECT 1", "a", 10), ctx("SELECT 2", "a", 20)];
1474 let query = AuditQuery::new().with_limit(0);
1475 let results = query.filter(&logs);
1476 assert_eq!(results.len(), 2);
1477 }
1478
1479 #[test]
1482 fn test_file_audit_log_store_append_and_read_all() {
1483 let path = test_data_dir().join("sz_orm_audit_store_append.jsonl");
1484 let path_str = path.to_str().unwrap();
1485 let store = FileAuditLogStore::new(path_str);
1486 let _ = store.clear();
1488
1489 store
1490 .append(&ctx("SELECT * FROM users", "alice", 1000))
1491 .unwrap();
1492 store
1493 .append(&ctx("INSERT INTO logs VALUES(1)", "bob", 2000))
1494 .unwrap();
1495
1496 let logs = store.read_all().unwrap();
1497 assert_eq!(logs.len(), 2);
1498 assert_eq!(logs[0].user, "alice");
1499 assert_eq!(logs[0].sql, "SELECT * FROM users");
1500 assert_eq!(logs[1].user, "bob");
1501 assert_eq!(logs[1].timestamp, 2000);
1502
1503 let _ = store.clear();
1504 }
1505
1506 #[test]
1507 fn test_file_audit_log_store_masks_sensitive() {
1508 let path = test_data_dir().join("sz_orm_audit_store_mask.jsonl");
1509 let path_str = path.to_str().unwrap();
1510 let store = FileAuditLogStore::new(path_str);
1511 let _ = store.clear();
1512
1513 store
1514 .append(&ctx(
1515 "SELECT * FROM users WHERE password='secret'",
1516 "admin",
1517 1000,
1518 ))
1519 .unwrap();
1520
1521 let logs = store.read_all().unwrap();
1522 assert_eq!(logs.len(), 1);
1523 assert!(!logs[0].sql.contains("password"));
1525 assert!(!logs[0].sql.contains("secret"));
1526 assert!(logs[0].sql.contains("******"));
1527
1528 let _ = store.clear();
1529 }
1530
1531 #[test]
1532 fn test_file_audit_log_store_clear_removes_entries() {
1533 let path = test_data_dir().join("sz_orm_audit_store_clear.jsonl");
1534 let path_str = path.to_str().unwrap();
1535 let store = FileAuditLogStore::new(path_str);
1536 let _ = store.clear();
1537
1538 store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1539 store.append(&ctx("SELECT 2", "u", 2)).unwrap();
1540 assert_eq!(store.read_all().unwrap().len(), 2);
1541
1542 store.clear().unwrap();
1543 assert_eq!(store.read_all().unwrap().len(), 0);
1545
1546 let _ = store.clear();
1547 }
1548
1549 #[test]
1550 fn test_file_audit_log_store_clear_nonexistent_is_ok() {
1551 let path = test_data_dir().join("sz_orm_audit_store_nonexistent.jsonl");
1553 let path_str = path.to_str().unwrap();
1554 let store = FileAuditLogStore::new(path_str);
1555 let _ = std::fs::remove_file(path_str);
1557 assert!(store.clear().is_ok());
1558 }
1559
1560 #[test]
1561 fn test_file_audit_log_store_read_all_empty_file() {
1562 let path = test_data_dir().join("sz_orm_audit_store_empty_read.jsonl");
1563 let path_str = path.to_str().unwrap();
1564 let store = FileAuditLogStore::new(path_str);
1565 let _ = store.clear();
1566
1567 let logs = store.read_all().unwrap();
1569 assert!(logs.is_empty());
1570
1571 let _ = store.clear();
1572 }
1573
1574 #[test]
1575 fn test_file_audit_log_store_skips_blank_lines() {
1576 let path = test_data_dir().join("sz_orm_audit_store_blank_lines.jsonl");
1577 let path_str = path.to_str().unwrap();
1578 let store = FileAuditLogStore::new(path_str);
1579 let _ = store.clear();
1580
1581 store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1582 let mut file = std::fs::OpenOptions::new()
1585 .append(true)
1586 .open(path_str)
1587 .unwrap();
1588 use std::io::Write;
1589 writeln!(file).unwrap();
1590 writeln!(file, " ").unwrap();
1591 drop(file);
1592
1593 store.append(&ctx("SELECT 2", "u", 2)).unwrap();
1594
1595 let logs = store.read_all().unwrap();
1596 assert_eq!(logs.len(), 2);
1598
1599 let _ = store.clear();
1600 }
1601
1602 #[test]
1603 fn test_file_audit_log_store_path_accessor() {
1604 let store = FileAuditLogStore::new("/tmp/sz_orm_audit_path_test.jsonl");
1605 assert_eq!(store.path(), "/tmp/sz_orm_audit_path_test.jsonl");
1606 }
1607
1608 #[test]
1609 fn test_file_audit_log_store_concurrent_append() {
1610 use std::sync::Arc;
1611 let path = test_data_dir().join("sz_orm_audit_store_concurrent.jsonl");
1612 let path_str = path.to_str().unwrap();
1613 let store = Arc::new(FileAuditLogStore::new(path_str));
1614 let _ = store.clear();
1615
1616 let mut handles = vec![];
1617 for i in 0..4 {
1618 let s = Arc::clone(&store);
1619 handles.push(std::thread::spawn(move || {
1620 for j in 0..10 {
1621 s.append(&ctx(&format!("SELECT {}_{}", i, j), "u", j as i64))
1622 .unwrap();
1623 }
1624 }));
1625 }
1626 for h in handles {
1627 h.join().unwrap();
1628 }
1629
1630 let logs = store.read_all().unwrap();
1632 assert_eq!(logs.len(), 40);
1633
1634 let _ = store.clear();
1635 }
1636
1637 #[test]
1638 fn test_audit_log_store_trait_object() {
1639 let path = test_data_dir().join("sz_orm_audit_store_trait.jsonl");
1641 let path_str = path.to_str().unwrap();
1642 let store: Box<dyn AuditLogStore> = Box::new(FileAuditLogStore::new(path_str));
1643 let _ = store.clear();
1644
1645 store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1646 let logs = store.read_all().unwrap();
1647 assert_eq!(logs.len(), 1);
1648
1649 let _ = store.clear();
1650 }
1651
1652 #[test]
1655 fn test_hash_chain_empty_auditor_verify_ok() {
1656 let auditor = HashChainAuditor::new();
1657 assert!(auditor.is_empty());
1658 assert_eq!(auditor.len(), 0);
1659 assert!(auditor.verify().is_ok());
1661 }
1662
1663 #[test]
1664 fn test_hash_chain_single_entry_genesis() {
1665 let auditor = HashChainAuditor::new();
1666 auditor.log(&ctx("SELECT 1", "admin", 1000));
1667 assert_eq!(auditor.len(), 1);
1668
1669 let entries = auditor.get_entries();
1670 assert_eq!(entries[0].prev_hash, GENESIS_HASH);
1672 assert_eq!(entries[0].current_hash.len(), 64);
1674 assert!(auditor.verify().is_ok());
1676 }
1677
1678 #[test]
1679 fn test_hash_chain_multiple_entries_linked() {
1680 let auditor = HashChainAuditor::new();
1681 auditor.log(&ctx("SELECT 1", "admin", 1000));
1682 auditor.log(&ctx("SELECT 2", "admin", 1001));
1683 auditor.log(&ctx("SELECT 3", "admin", 1002));
1684 assert_eq!(auditor.len(), 3);
1685
1686 let entries = auditor.get_entries();
1687 assert_eq!(entries[1].prev_hash, entries[0].current_hash);
1689 assert_eq!(entries[2].prev_hash, entries[1].current_hash);
1690 assert!(auditor.verify().is_ok());
1692 }
1693
1694 #[test]
1695 fn test_hash_chain_detects_tampered_sql() {
1696 let auditor = HashChainAuditor::new();
1697 auditor.log(&ctx("SELECT 1", "admin", 1000));
1698 auditor.log(&ctx("SELECT 2", "admin", 1001));
1699
1700 {
1702 let mut entries = auditor.entries.lock().unwrap();
1703 entries[0].entry.sql = "DROP TABLE users".to_string();
1704 }
1705
1706 let result = auditor.verify();
1708 assert!(result.is_err());
1709 let err = result.unwrap_err();
1710 assert!(err.contains("index 0"), "error: {}", err);
1712 assert!(err.contains("hash mismatch"), "error: {}", err);
1713 }
1714
1715 #[test]
1716 fn test_hash_chain_detects_broken_link() {
1717 let auditor = HashChainAuditor::new();
1718 auditor.log(&ctx("SELECT 1", "admin", 1000));
1719 auditor.log(&ctx("SELECT 2", "admin", 1001));
1720
1721 {
1723 let mut entries = auditor.entries.lock().unwrap();
1724 entries[1].prev_hash = "deadbeef".to_string();
1725 }
1726
1727 let result = auditor.verify();
1728 assert!(result.is_err());
1729 let err = result.unwrap_err();
1730 assert!(err.contains("chain broken at index 1"), "error: {}", err);
1731 }
1732
1733 #[test]
1734 fn test_hash_chain_detects_genesis_tamper() {
1735 let auditor = HashChainAuditor::new();
1736 auditor.log(&ctx("SELECT 1", "admin", 1000));
1737
1738 {
1740 let mut entries = auditor.entries.lock().unwrap();
1741 entries[0].prev_hash = "deadbeef".to_string();
1742 }
1743
1744 let result = auditor.verify();
1745 assert!(result.is_err());
1746 let err = result.unwrap_err();
1747 assert!(err.contains("genesis prev_hash mismatch"), "error: {}", err);
1748 }
1749
1750 #[test]
1751 fn test_hash_chain_masks_sensitive_data() {
1752 let auditor = HashChainAuditor::new();
1753 auditor.log(&ctx(
1754 "SELECT * FROM users WHERE password='secret'",
1755 "admin",
1756 1000,
1757 ));
1758
1759 let entries = auditor.get_entries();
1760 assert!(!entries[0].entry.sql.contains("password"));
1762 assert!(!entries[0].entry.sql.contains("secret"));
1763 assert!(entries[0].entry.sql.contains("******"));
1764 assert!(auditor.verify().is_ok());
1766 }
1767
1768 #[test]
1769 fn test_hash_chain_deterministic_hashes() {
1770 let entry = ctx("SELECT 1", "admin", 1000);
1772 let e1 = HashChainEntry::genesis(entry.clone());
1773 let e2 = HashChainEntry::genesis(entry);
1774 assert_eq!(e1.current_hash, e2.current_hash);
1775 assert_eq!(e1.prev_hash, e2.prev_hash);
1776 }
1777
1778 #[test]
1779 fn test_hash_chain_different_inputs_different_hashes() {
1780 let e1 = HashChainEntry::genesis(ctx("SELECT 1", "admin", 1000));
1781 let e2 = HashChainEntry::genesis(ctx("SELECT 2", "admin", 1000));
1782 assert_ne!(e1.current_hash, e2.current_hash);
1783 }
1784
1785 #[test]
1786 fn test_hash_chain_flush_and_persist() {
1787 let auditor = HashChainAuditor::new();
1788 auditor.log(&ctx("SELECT 1", "admin", 1000));
1789 auditor.log(&ctx("SELECT 2", "admin", 1001));
1790
1791 let path = test_data_dir().join("sz_orm_audit_hash_chain.json");
1792 let path_str = path.to_str().unwrap();
1793 let count = auditor.flush(path_str).unwrap();
1794 assert_eq!(count, 2);
1795
1796 let content = std::fs::read_to_string(path_str).unwrap();
1798 assert!(!content.is_empty());
1799 assert!(content.contains("current_hash"));
1800
1801 let _ = std::fs::remove_file(path_str);
1802 }
1803
1804 #[test]
1805 fn test_hash_chain_concurrent_log_thread_safe() {
1806 use std::sync::Arc;
1807 use std::thread;
1808
1809 let auditor = Arc::new(HashChainAuditor::new());
1810 let mut handles = vec![];
1811 for i in 0..4 {
1812 let a = Arc::clone(&auditor);
1813 handles.push(thread::spawn(move || {
1814 for j in 0..25 {
1815 a.log(&ctx(&format!("SELECT {}_{}", i, j), "u", j as i64));
1816 }
1817 }));
1818 }
1819 for h in handles {
1820 h.join().unwrap();
1821 }
1822
1823 assert_eq!(auditor.len(), 100);
1825 assert!(auditor.verify().is_ok());
1827 }
1828
1829 #[test]
1830 fn test_genesis_hash_constant_is_64_zeros() {
1831 assert_eq!(GENESIS_HASH.len(), 64);
1833 assert!(GENESIS_HASH.chars().all(|c| c == '0'));
1834 }
1835}