1use serde::{Deserialize, Serialize};
13use std::sync::Mutex;
14
15#[cfg(feature = "data-lineage")]
16pub mod lineage;
17
18#[cfg(feature = "data-quality")]
19pub mod data_quality;
20
21#[cfg(feature = "hash-chain-enhanced")]
22pub mod hash_chain_enhanced;
23
24#[cfg(feature = "lineage-viz")]
25pub use lineage::{downstream_impact, upstream_trace, ImpactEdge};
26#[cfg(feature = "data-lineage")]
27pub use lineage::{
28 EdgeType, LineageDialect, LineageEdge, LineageError, LineageExportFormat, LineageGraph,
29 LineageNode, LineageNodeId, LineageTracker, LineageUpdate, NodeType,
30};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SqlAuditContext {
34 pub sql: String,
35 pub user: String,
36 pub timestamp: i64,
37}
38
39const SENSITIVE_KEYWORDS: &[&str] = &[
42 "password",
43 "pwd",
44 "passwd",
45 "secret",
46 "token",
47 "api_key",
48 "apikey",
49 "access_key",
50 "accesskey",
51 "session",
52 "credit_card",
53 "creditcard",
54 "cvv",
55 "ssn",
56];
57
58pub struct SqlAuditor {
59 logs: Mutex<Vec<SqlAuditContext>>,
60}
61
62impl SqlAuditor {
63 pub fn new() -> Self {
64 Self {
65 logs: Mutex::new(vec![]),
66 }
67 }
68
69 pub fn log(&self, ctx: &SqlAuditContext) {
72 let masked_sql = mask_sensitive(&ctx.sql);
73 let entry = SqlAuditContext {
74 sql: masked_sql,
75 user: ctx.user.clone(),
76 timestamp: ctx.timestamp,
77 };
78 let mut logs = self
79 .logs
80 .lock()
81 .expect("SqlAuditor logs lock poisoned (log)");
82 logs.push(entry);
83 }
84
85 pub fn get_logs(&self) -> Vec<SqlAuditContext> {
87 let logs = self
88 .logs
89 .lock()
90 .expect("SqlAuditor logs lock poisoned (get_logs)");
91 logs.iter().cloned().collect()
92 }
93
94 pub fn flush(&self, path: &str) -> Result<usize, String> {
97 let logs = self
98 .logs
99 .lock()
100 .expect("SqlAuditor logs lock poisoned (flush)");
101 let snapshot: Vec<&SqlAuditContext> = logs.iter().collect();
102 let json = serde_json::to_string_pretty(&snapshot).map_err(|e| e.to_string())?;
103 std::fs::write(path, json).map_err(|e| e.to_string())?;
104 Ok(logs.len())
105 }
106
107 pub fn mask_sensitive(&self, sql: &str) -> String {
110 mask_sensitive(sql)
111 }
112}
113
114impl Default for SqlAuditor {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120fn mask_sensitive(sql: &str) -> String {
123 let lower = sql.to_ascii_lowercase();
124 let mut result = String::with_capacity(sql.len());
125 let mut i = 0;
126 let bytes = sql.as_bytes();
127 let lower_bytes = lower.as_bytes();
128 while i < bytes.len() {
129 let mut matched_len: Option<usize> = None;
130 for keyword in SENSITIVE_KEYWORDS {
131 let kw_bytes = keyword.as_bytes();
132 if i + kw_bytes.len() <= bytes.len() && &lower_bytes[i..i + kw_bytes.len()] == kw_bytes
133 {
134 let prev_ok = i == 0 || !is_ident_char(bytes[i - 1]);
137 let next_idx = i + kw_bytes.len();
138 let next_ok = next_idx >= bytes.len() || !is_ident_char(bytes[next_idx]);
139 if prev_ok && next_ok {
140 matched_len = Some(kw_bytes.len());
141 break;
142 }
143 }
144 }
145 if let Some(kw_len) = matched_len {
146 result.push_str("******");
147 i += kw_len;
148 } else {
149 let ch = sql[i..]
151 .chars()
152 .next()
153 .expect("i < bytes.len() guarantees non-empty slice");
154 result.push(ch);
155 i += ch.len_utf8();
156 }
157 }
158 result
159}
160
161fn is_ident_char(b: u8) -> bool {
163 b.is_ascii_alphanumeric() || b == b'_'
164}
165
166#[derive(Debug, Clone, Default)]
178pub struct AuditRules {
179 allow_patterns: Vec<String>,
181 deny_patterns: Vec<String>,
183}
184
185impl AuditRules {
186 pub fn new() -> Self {
187 Self::default()
188 }
189
190 pub fn allow(mut self, pattern: impl Into<String>) -> Self {
192 self.allow_patterns
193 .push(pattern.into().to_ascii_lowercase());
194 self
195 }
196
197 pub fn deny(mut self, pattern: impl Into<String>) -> Self {
199 self.deny_patterns.push(pattern.into().to_ascii_lowercase());
200 self
201 }
202
203 pub fn should_audit(&self, sql: &str) -> bool {
205 let lower = sql.to_ascii_lowercase();
206 for pat in &self.deny_patterns {
208 if lower.contains(pat) {
209 return false;
210 }
211 }
212 if self.allow_patterns.is_empty() {
214 return true;
215 }
216 self.allow_patterns.iter().any(|pat| lower.contains(pat))
218 }
219
220 pub fn allow_count(&self) -> usize {
222 self.allow_patterns.len()
223 }
224
225 pub fn deny_count(&self) -> usize {
227 self.deny_patterns.len()
228 }
229}
230
231#[derive(Debug, Clone)]
243pub struct RotationPolicy {
244 pub max_entries: usize,
246 pub max_age_ms: i64,
248}
249
250impl RotationPolicy {
251 pub fn none() -> Self {
253 Self {
254 max_entries: 0,
255 max_age_ms: 0,
256 }
257 }
258
259 pub fn by_size(max_entries: usize) -> Self {
261 Self {
262 max_entries,
263 max_age_ms: 0,
264 }
265 }
266
267 pub fn by_age(max_age_ms: i64) -> Self {
269 Self {
270 max_entries: 0,
271 max_age_ms,
272 }
273 }
274
275 pub fn by_size_and_age(max_entries: usize, max_age_ms: i64) -> Self {
277 Self {
278 max_entries,
279 max_age_ms,
280 }
281 }
282
283 fn needs_rotation(&self, entry_count: usize, oldest_ts: i64, now_ts: i64) -> bool {
285 if self.max_entries > 0 && entry_count >= self.max_entries {
286 return true;
287 }
288 if self.max_age_ms > 0 && oldest_ts > 0 && (now_ts - oldest_ts) > self.max_age_ms {
289 return true;
290 }
291 false
292 }
293}
294
295impl Default for RotationPolicy {
296 fn default() -> Self {
297 Self::none()
298 }
299}
300
301pub struct RotatingAuditor {
311 logs: Mutex<Vec<SqlAuditContext>>,
312 rules: AuditRules,
313 policy: RotationPolicy,
314 rotations: Mutex<usize>,
316}
317
318impl RotatingAuditor {
319 pub fn new(policy: RotationPolicy, rules: AuditRules) -> Self {
320 Self {
321 logs: Mutex::new(vec![]),
322 rules,
323 policy,
324 rotations: Mutex::new(0),
325 }
326 }
327
328 pub fn with_max_entries(max_entries: usize) -> Self {
330 Self::new(RotationPolicy::by_size(max_entries), AuditRules::new())
331 }
332
333 pub fn with_max_age(max_age_ms: i64) -> Self {
335 Self::new(RotationPolicy::by_age(max_age_ms), AuditRules::new())
336 }
337
338 pub fn log(&self, ctx: &SqlAuditContext) -> bool {
340 if !self.rules.should_audit(&ctx.sql) {
342 return false;
343 }
344 let masked_sql = mask_sensitive(&ctx.sql);
345 let entry = SqlAuditContext {
346 sql: masked_sql,
347 user: ctx.user.clone(),
348 timestamp: ctx.timestamp,
349 };
350 let mut logs = self
351 .logs
352 .lock()
353 .expect("RotatingAuditor logs lock poisoned (log)");
354
355 let now = ctx.timestamp;
357 let oldest = logs.first().map(|e| e.timestamp).unwrap_or(now);
358 if self.policy.needs_rotation(logs.len(), oldest, now) {
359 logs.clear();
360 *self
361 .rotations
362 .lock()
363 .expect("RotatingAuditor rotations lock poisoned (log)") += 1;
364 }
365
366 logs.push(entry);
367 true
368 }
369
370 pub fn get_logs(&self) -> Vec<SqlAuditContext> {
372 self.logs
373 .lock()
374 .expect("RotatingAuditor logs lock poisoned (get_logs)")
375 .clone()
376 }
377
378 pub fn rotation_count(&self) -> usize {
380 *self
381 .rotations
382 .lock()
383 .expect("RotatingAuditor rotations lock poisoned (rotation_count)")
384 }
385
386 pub fn rotate(&self) -> usize {
388 let mut logs = self
389 .logs
390 .lock()
391 .expect("RotatingAuditor logs lock poisoned (rotate)");
392 let count = logs.len();
393 logs.clear();
394 *self
395 .rotations
396 .lock()
397 .expect("RotatingAuditor rotations lock poisoned (rotate)") += 1;
398 count
399 }
400
401 pub fn len(&self) -> usize {
403 self.logs
404 .lock()
405 .expect("RotatingAuditor logs lock poisoned (len)")
406 .len()
407 }
408
409 pub fn is_empty(&self) -> bool {
411 self.logs
412 .lock()
413 .expect("RotatingAuditor logs lock poisoned (is_empty)")
414 .is_empty()
415 }
416}
417
418pub struct AsyncAuditWriter {
429 sender: std::sync::mpsc::Sender<AsyncCommand>,
430 handle: Mutex<Option<std::thread::JoinHandle<Vec<SqlAuditContext>>>>,
431}
432
433enum AsyncCommand {
434 Log(SqlAuditContext),
435 Shutdown,
436}
437
438impl AsyncAuditWriter {
439 pub fn new() -> Self {
441 let (sender, receiver) = std::sync::mpsc::channel::<AsyncCommand>();
442 let handle = std::thread::spawn(move || {
443 let mut logs: Vec<SqlAuditContext> = Vec::new();
444 for cmd in receiver {
445 match cmd {
446 AsyncCommand::Log(ctx) => {
447 let masked_sql = mask_sensitive(&ctx.sql);
448 logs.push(SqlAuditContext {
449 sql: masked_sql,
450 user: ctx.user,
451 timestamp: ctx.timestamp,
452 });
453 }
454 AsyncCommand::Shutdown => break,
455 }
456 }
457 logs
458 });
459 Self {
460 sender,
461 handle: Mutex::new(Some(handle)),
462 }
463 }
464
465 pub fn log(&self, ctx: &SqlAuditContext) -> Result<(), String> {
467 self.sender
468 .send(AsyncCommand::Log(ctx.clone()))
469 .map_err(|e| format!("AsyncAuditWriter channel closed: {}", e))
470 }
471
472 pub fn shutdown(&self) -> Result<Vec<SqlAuditContext>, String> {
474 let _ = self.sender.send(AsyncCommand::Shutdown);
475 let mut handle_guard = self
476 .handle
477 .lock()
478 .expect("AsyncAuditWriter handle lock poisoned (shutdown)");
479 if let Some(handle) = handle_guard.take() {
480 handle
481 .join()
482 .map_err(|e| format!("Thread panicked: {:?}", e))
483 } else {
484 Err("Already shut down".to_string())
485 }
486 }
487}
488
489impl Default for AsyncAuditWriter {
490 fn default() -> Self {
491 Self::new()
492 }
493}
494
495#[derive(Debug, Clone, Default)]
501pub struct AuditQuery {
502 pub user: Option<String>,
504 pub from_ts: Option<i64>,
506 pub to_ts: Option<i64>,
508 pub sql_contains: Option<String>,
510 pub limit: usize,
512}
513
514impl AuditQuery {
515 pub fn new() -> Self {
516 Self::default()
517 }
518
519 pub fn by_user(mut self, user: impl Into<String>) -> Self {
521 self.user = Some(user.into());
522 self
523 }
524
525 pub fn by_time_range(mut self, from: i64, to: i64) -> Self {
527 self.from_ts = Some(from);
528 self.to_ts = Some(to);
529 self
530 }
531
532 pub fn by_sql_contains(mut self, keyword: impl Into<String>) -> Self {
534 self.sql_contains = Some(keyword.into());
535 self
536 }
537
538 pub fn with_limit(mut self, limit: usize) -> Self {
540 self.limit = limit;
541 self
542 }
543
544 pub fn filter(&self, logs: &[SqlAuditContext]) -> Vec<SqlAuditContext> {
546 let keyword_lower = self.sql_contains.as_ref().map(|s| s.to_ascii_lowercase());
547 let mut result: Vec<SqlAuditContext> = logs
548 .iter()
549 .filter(|entry| {
550 if let Some(u) = &self.user {
551 if entry.user != *u {
552 return false;
553 }
554 }
555 if let Some(from) = self.from_ts {
556 if entry.timestamp < from {
557 return false;
558 }
559 }
560 if let Some(to) = self.to_ts {
561 if entry.timestamp > to {
562 return false;
563 }
564 }
565 if let Some(kw) = &keyword_lower {
566 if !entry.sql.to_ascii_lowercase().contains(kw) {
567 return false;
568 }
569 }
570 true
571 })
572 .cloned()
573 .collect();
574 if self.limit > 0 && result.len() > self.limit {
575 result.truncate(self.limit);
576 }
577 result
578 }
579}
580
581pub fn query_logs(auditor: &SqlAuditor, query: &AuditQuery) -> Vec<SqlAuditContext> {
583 let logs = auditor.get_logs();
584 query.filter(&logs)
585}
586
587pub trait AuditLogStore: Send + Sync {
598 fn append(&self, entry: &SqlAuditContext) -> Result<(), String>;
600 fn read_all(&self) -> Result<Vec<SqlAuditContext>, String>;
602 fn clear(&self) -> Result<(), String>;
604}
605
606pub struct FileAuditLogStore {
613 path: String,
614 write_lock: Mutex<()>,
615}
616
617impl FileAuditLogStore {
618 pub fn new(path: impl Into<String>) -> Self {
621 Self {
622 path: path.into(),
623 write_lock: Mutex::new(()),
624 }
625 }
626
627 pub fn path(&self) -> &str {
629 &self.path
630 }
631}
632
633impl AuditLogStore for FileAuditLogStore {
634 fn append(&self, entry: &SqlAuditContext) -> Result<(), String> {
636 let _guard = self
637 .write_lock
638 .lock()
639 .map_err(|e| format!("write_lock poisoned: {}", e))?;
640 let masked_sql = mask_sensitive(&entry.sql);
642 let stored = SqlAuditContext {
643 sql: masked_sql,
644 user: entry.user.clone(),
645 timestamp: entry.timestamp,
646 };
647 let line = serde_json::to_string(&stored).map_err(|e| e.to_string())?;
648 use std::io::Write;
650 let mut file = std::fs::OpenOptions::new()
651 .create(true)
652 .append(true)
653 .open(&self.path)
654 .map_err(|e| format!("open '{}' failed: {}", self.path, e))?;
655 writeln!(file, "{}", line).map_err(|e| e.to_string())
656 }
657
658 fn read_all(&self) -> Result<Vec<SqlAuditContext>, String> {
663 let content = match std::fs::read_to_string(&self.path) {
664 Ok(c) => c,
665 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
666 return Ok(Vec::new());
667 }
668 Err(e) => return Err(format!("read failed: {}", e)),
669 };
670 let mut result = Vec::new();
671 for (lineno, line) in content.lines().enumerate() {
672 let line = line.trim();
673 if line.is_empty() {
674 continue;
675 }
676 let entry: SqlAuditContext = serde_json::from_str(line)
677 .map_err(|e| format!("parse line {} failed: {}", lineno + 1, e))?;
678 result.push(entry);
679 }
680 Ok(result)
681 }
682
683 fn clear(&self) -> Result<(), String> {
685 let _guard = self
686 .write_lock
687 .lock()
688 .map_err(|e| format!("write_lock poisoned: {}", e))?;
689 std::fs::remove_file(&self.path).or_else(|e| {
690 if e.kind() == std::io::ErrorKind::NotFound {
692 Ok(())
693 } else {
694 Err(format!("clear failed: {}", e))
695 }
696 })
697 }
698}
699
700pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
710
711#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct HashChainEntry {
724 pub prev_hash: String,
726 pub current_hash: String,
728 pub entry: SqlAuditContext,
730}
731
732impl HashChainEntry {
733 fn compute_hash(prev_hash: &str, entry: &SqlAuditContext) -> String {
741 use sha2::{Digest, Sha256};
742 let mut hasher = Sha256::new();
743 hasher.update(prev_hash.as_bytes());
744 hasher.update(entry.sql.as_bytes());
745 hasher.update(entry.user.as_bytes());
746 hasher.update(entry.timestamp.to_le_bytes());
748 let result = hasher.finalize();
749 hex_encode(&result)
751 }
752
753 pub fn genesis(entry: SqlAuditContext) -> Self {
755 let prev_hash = GENESIS_HASH.to_string();
756 let current_hash = Self::compute_hash(&prev_hash, &entry);
757 Self {
758 prev_hash,
759 current_hash,
760 entry,
761 }
762 }
763
764 pub fn append(prev_hash: &str, entry: SqlAuditContext) -> Self {
766 let current_hash = Self::compute_hash(prev_hash, &entry);
767 Self {
768 prev_hash: prev_hash.to_string(),
769 current_hash,
770 entry,
771 }
772 }
773}
774
775fn hex_encode(bytes: &[u8]) -> String {
780 const HEX_CHARS: &[u8] = b"0123456789abcdef";
781 let mut s = String::with_capacity(bytes.len() * 2);
782 for &b in bytes {
783 s.push(HEX_CHARS[(b >> 4) as usize] as char);
784 s.push(HEX_CHARS[(b & 0x0f) as usize] as char);
785 }
786 s
787}
788
789pub struct HashChainAuditor {
817 entries: Mutex<Vec<HashChainEntry>>,
819}
820
821impl Default for HashChainAuditor {
822 fn default() -> Self {
823 Self::new()
824 }
825}
826
827impl HashChainAuditor {
828 pub fn new() -> Self {
830 Self {
831 entries: Mutex::new(Vec::new()),
832 }
833 }
834
835 pub fn log(&self, ctx: &SqlAuditContext) {
844 let masked_sql = mask_sensitive(&ctx.sql);
845 let entry = SqlAuditContext {
846 sql: masked_sql,
847 user: ctx.user.clone(),
848 timestamp: ctx.timestamp,
849 };
850 let mut entries = self
851 .entries
852 .lock()
853 .expect("HashChainAuditor entries lock poisoned (log)");
854 let prev_hash = entries
855 .last()
856 .map(|e| e.current_hash.as_str())
857 .unwrap_or(GENESIS_HASH);
858 let chain_entry = if entries.is_empty() {
859 HashChainEntry::genesis(entry)
860 } else {
861 HashChainEntry::append(prev_hash, entry)
862 };
863 entries.push(chain_entry);
864 }
865
866 pub fn get_entries(&self) -> Vec<HashChainEntry> {
868 self.entries
869 .lock()
870 .expect("HashChainAuditor entries lock poisoned (get_entries)")
871 .clone()
872 }
873
874 pub fn len(&self) -> usize {
876 self.entries
877 .lock()
878 .expect("HashChainAuditor entries lock poisoned (len)")
879 .len()
880 }
881
882 pub fn is_empty(&self) -> bool {
884 self.entries
885 .lock()
886 .expect("HashChainAuditor entries lock poisoned (is_empty)")
887 .is_empty()
888 }
889
890 pub fn verify(&self) -> Result<(), String> {
905 let entries = self
906 .entries
907 .lock()
908 .expect("HashChainAuditor entries lock poisoned (verify)");
909 for (i, entry) in entries.iter().enumerate() {
910 if i == 0 {
912 if entry.prev_hash != GENESIS_HASH {
913 return Err(format!(
914 "chain genesis prev_hash mismatch at index 0: expected '{}', got '{}'",
915 GENESIS_HASH, entry.prev_hash
916 ));
917 }
918 } else {
919 let prev = &entries[i - 1];
921 if entry.prev_hash != prev.current_hash {
922 return Err(format!(
923 "chain broken at index {}: prev_hash '{}' != previous current_hash '{}'",
924 i, entry.prev_hash, prev.current_hash
925 ));
926 }
927 }
928 let recomputed = HashChainEntry::compute_hash(&entry.prev_hash, &entry.entry);
930 if entry.current_hash != recomputed {
931 return Err(format!(
932 "hash mismatch at index {}: stored '{}' != recomputed '{}'",
933 i, entry.current_hash, recomputed
934 ));
935 }
936 }
937 Ok(())
938 }
939
940 pub fn flush(&self, path: &str) -> Result<usize, String> {
944 let entries = self
945 .entries
946 .lock()
947 .expect("HashChainAuditor entries lock poisoned (flush)");
948 let snapshot: Vec<&HashChainEntry> = entries.iter().collect();
949 let json = serde_json::to_string_pretty(&snapshot).map_err(|e| e.to_string())?;
950 std::fs::write(path, json).map_err(|e| e.to_string())?;
951 Ok(entries.len())
952 }
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 fn test_data_dir() -> std::path::PathBuf {
967 let f_drive = std::path::Path::new("F:\\test\\data");
968 if is_dir_writable(f_drive) {
969 return f_drive.to_path_buf();
970 }
971 if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
972 let p = std::path::PathBuf::from(&dir);
973 if is_dir_writable(&p) {
974 return p;
975 }
976 }
977 std::env::temp_dir()
978 }
979
980 fn is_dir_writable(dir: &std::path::Path) -> bool {
983 if !dir.exists() {
984 return false;
985 }
986 let probe = dir.join(format!(".probe_{}", std::process::id()));
987 match std::fs::File::create(&probe) {
988 Ok(_) => {
989 let _ = std::fs::remove_file(&probe);
990 true
991 }
992 Err(_) => false,
993 }
994 }
995
996 fn ctx(sql: &str, user: &str, ts: i64) -> SqlAuditContext {
997 SqlAuditContext {
998 sql: sql.to_string(),
999 user: user.to_string(),
1000 timestamp: ts,
1001 }
1002 }
1003
1004 #[test]
1005 fn test_log_stores_in_memory() {
1006 let a = SqlAuditor::new();
1007 a.log(&ctx("SELECT * FROM users", "admin", 1000));
1008 a.log(&ctx("INSERT INTO logs VALUES(1)", "admin", 1001));
1009 let logs = a.get_logs();
1010 assert_eq!(logs.len(), 2);
1011 assert_eq!(logs[0].sql, "SELECT * FROM users");
1012 assert_eq!(logs[0].user, "admin");
1013 assert_eq!(logs[0].timestamp, 1000);
1014 assert_eq!(logs[1].timestamp, 1001);
1015 }
1016
1017 #[test]
1018 fn test_log_masks_sensitive_in_storage() {
1019 let a = SqlAuditor::new();
1020 a.log(&ctx(
1021 "SELECT * FROM users WHERE password='secret'",
1022 "admin",
1023 1000,
1024 ));
1025 let logs = a.get_logs();
1026 assert_eq!(logs.len(), 1);
1027 let stored_sql = &logs[0].sql;
1028 assert!(!stored_sql.contains("password"));
1029 assert!(!stored_sql.contains("secret"));
1030 assert!(stored_sql.contains("******"));
1031 }
1032
1033 #[test]
1034 fn test_mask_sensitive_password() {
1035 let a = SqlAuditor::new();
1036 let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
1037 assert!(!masked.contains("password"));
1038 assert!(!masked.contains("secret"));
1039 assert!(masked.contains("******"));
1040 }
1041
1042 #[test]
1043 fn test_mask_sensitive_case_insensitive() {
1044 let a = SqlAuditor::new();
1045 let masked = a.mask_sensitive("UPDATE users SET PASSWORD='abc', Token='x'");
1046 let lower = masked.to_lowercase();
1047 assert!(!lower.contains("password"));
1048 assert!(!lower.contains("token"));
1049 assert!(masked.contains("******"));
1050 }
1051
1052 #[test]
1053 fn test_mask_sensitive_extended_keywords() {
1054 let a = SqlAuditor::new();
1055 let inputs = [
1056 "pwd",
1057 "passwd",
1058 "secret",
1059 "api_key",
1060 "access_key",
1061 "session",
1062 "credit_card",
1063 "cvv",
1064 "ssn",
1065 ];
1066 for kw in inputs {
1067 let sql = format!("SELECT * FROM t WHERE k = '{}'", kw);
1068 let masked = a.mask_sensitive(&sql);
1069 let lower = masked.to_lowercase();
1070 assert!(
1071 !lower.contains(kw),
1072 "keyword '{}' should be masked in: {}",
1073 kw,
1074 masked
1075 );
1076 assert!(masked.contains("******"));
1077 }
1078 }
1079
1080 #[test]
1081 fn test_mask_sensitive_preserves_non_sensitive() {
1082 let a = SqlAuditor::new();
1083 let masked = a.mask_sensitive("SELECT id, name FROM users WHERE active = 1");
1084 assert_eq!(masked, "SELECT id, name FROM users WHERE active = 1");
1085 }
1086
1087 #[test]
1088 fn test_mask_sensitive_does_not_match_substrings() {
1089 let a = SqlAuditor::new();
1092 let masked = a.mask_sensitive("SELECT * FROM users WHERE note='passworded'");
1093 assert!(masked.contains("passworded"));
1095 assert_eq!(masked, "SELECT * FROM users WHERE note='passworded'");
1101 }
1102
1103 #[test]
1104 fn test_mask_sensitive_multiple_occurrences() {
1105 let a = SqlAuditor::new();
1106 let masked = a.mask_sensitive("INSERT INTO t (password, token) VALUES ('p1', 't1')");
1107 let lower = masked.to_lowercase();
1109 assert!(!lower.contains("password"));
1110 assert!(!lower.contains("token"));
1111 let count = masked.matches("******").count();
1113 assert!(count >= 2, "expected at least 2 masks, got: {}", masked);
1114 }
1115
1116 #[test]
1117 fn test_get_logs_empty_initially() {
1118 let a = SqlAuditor::new();
1119 assert!(a.get_logs().is_empty());
1120 }
1121
1122 #[test]
1123 fn test_get_logs_returns_snapshot_independent_of_changes() {
1124 let a = SqlAuditor::new();
1125 a.log(&ctx("SELECT 1", "u", 1));
1126 let snap = a.get_logs();
1127 a.log(&ctx("SELECT 2", "u", 2));
1128 assert_eq!(snap.len(), 1, "snapshot should not change after new log");
1129 assert_eq!(a.get_logs().len(), 2);
1130 }
1131
1132 #[test]
1133 fn test_flush_writes_json_file() {
1134 let a = SqlAuditor::new();
1135 a.log(&ctx("SELECT * FROM users WHERE password='p'", "admin", 123));
1136 a.log(&ctx("INSERT INTO logs VALUES(1)", "user2", 456));
1137 let path = test_data_dir().join("sz_orm_audit_flush_test.json");
1138 let path_str = path.to_str().unwrap();
1139 let count = a.flush(path_str).expect("flush should succeed");
1140 assert_eq!(count, 2);
1141 let content = std::fs::read_to_string(path_str).expect("file should be readable");
1143 let parsed: Vec<SqlAuditContext> =
1144 serde_json::from_str(&content).expect("should parse as JSON array");
1145 assert_eq!(parsed.len(), 2);
1146 assert_eq!(parsed[0].user, "admin");
1147 assert_eq!(parsed[1].timestamp, 456);
1148 assert!(!parsed[0].sql.contains("password"));
1150 let _ = std::fs::remove_file(path_str);
1152 }
1153
1154 #[test]
1155 fn test_flush_empty_writes_empty_array() {
1156 let a = SqlAuditor::new();
1157 let path = test_data_dir().join("sz_orm_audit_flush_empty_test.json");
1158 let path_str = path.to_str().unwrap();
1159 let count = a.flush(path_str).expect("flush should succeed");
1160 assert_eq!(count, 0);
1161 let content = std::fs::read_to_string(path_str).expect("file should be readable");
1162 assert_eq!(content.trim(), "[]");
1163 let _ = std::fs::remove_file(path_str);
1164 }
1165
1166 #[test]
1167 fn test_default_creates_new_auditor() {
1168 let a = SqlAuditor::default();
1169 assert!(a.get_logs().is_empty());
1170 }
1171
1172 #[test]
1173 fn test_original_test_compatibility() {
1174 let a = SqlAuditor::new();
1177 let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
1178 assert!(!masked.contains("password"));
1179 }
1180
1181 #[test]
1184 fn test_audit_rules_empty_allows_all() {
1185 let rules = AuditRules::new();
1186 assert!(rules.should_audit("SELECT * FROM users"));
1187 assert!(rules.should_audit("DELETE FROM orders"));
1188 assert_eq!(rules.allow_count(), 0);
1189 assert_eq!(rules.deny_count(), 0);
1190 }
1191
1192 #[test]
1193 fn test_audit_rules_deny_blocks() {
1194 let rules = AuditRules::new().deny("pg_catalog");
1195 assert!(!rules.should_audit("SELECT * FROM pg_catalog.tables"));
1196 assert!(rules.should_audit("SELECT * FROM users"));
1197 }
1198
1199 #[test]
1200 fn test_audit_rules_allow_filters() {
1201 let rules = AuditRules::new().allow("select").allow("insert");
1202 assert!(rules.should_audit("SELECT * FROM users"));
1203 assert!(rules.should_audit("INSERT INTO logs VALUES(1)"));
1204 assert!(!rules.should_audit("DELETE FROM users"));
1205 }
1206
1207 #[test]
1208 fn test_audit_rules_deny_overrides_allow() {
1209 let rules = AuditRules::new().allow("select").deny("password");
1210 assert!(!rules.should_audit("SELECT * FROM users WHERE password='x'"));
1212 assert!(rules.should_audit("SELECT * FROM users"));
1214 }
1215
1216 #[test]
1217 fn test_audit_rules_case_insensitive() {
1218 let rules = AuditRules::new().deny("DROP");
1219 assert!(!rules.should_audit("drop table users"));
1220 assert!(!rules.should_audit("DROP TABLE users"));
1221 assert!(rules.should_audit("SELECT * FROM users"));
1222 }
1223
1224 #[test]
1225 fn test_audit_rules_multiple_deny() {
1226 let rules = AuditRules::new()
1227 .deny("drop")
1228 .deny("truncate")
1229 .deny("shutdown");
1230 assert!(!rules.should_audit("DROP TABLE x"));
1231 assert!(!rules.should_audit("TRUNCATE TABLE y"));
1232 assert!(!rules.should_audit("SHUTDOWN"));
1233 assert!(rules.should_audit("SELECT 1"));
1234 }
1235
1236 #[test]
1239 fn test_rotation_policy_none_never_rotates() {
1240 let policy = RotationPolicy::none();
1241 assert!(!policy.needs_rotation(1_000_000, 0, 1_000_000));
1242 assert!(!policy.needs_rotation(0, 0, 0));
1243 }
1244
1245 #[test]
1246 fn test_rotation_policy_by_size() {
1247 let policy = RotationPolicy::by_size(100);
1248 assert!(!policy.needs_rotation(99, 0, 1000));
1249 assert!(policy.needs_rotation(100, 0, 1000));
1250 assert!(policy.needs_rotation(200, 0, 1000));
1251 }
1252
1253 #[test]
1254 fn test_rotation_policy_by_age() {
1255 let policy = RotationPolicy::by_age(5000);
1256 assert!(!policy.needs_rotation(10, 5000, 9000));
1258 assert!(policy.needs_rotation(10, 5000, 11000));
1260 }
1261
1262 #[test]
1263 fn test_rotation_policy_by_size_and_age() {
1264 let policy = RotationPolicy::by_size_and_age(100, 5000);
1265 assert!(!policy.needs_rotation(50, 5000, 9000));
1267 assert!(policy.needs_rotation(100, 5000, 5000));
1269 assert!(policy.needs_rotation(10, 5000, 11000));
1271 }
1272
1273 #[test]
1276 fn test_rotating_auditor_no_rotation_stores_all() {
1277 let auditor = RotatingAuditor::new(RotationPolicy::none(), AuditRules::new());
1278 for i in 0..100 {
1279 auditor.log(&ctx(&format!("SELECT {}", i), "user", i));
1280 }
1281 assert_eq!(auditor.len(), 100);
1282 assert_eq!(auditor.rotation_count(), 0);
1283 }
1284
1285 #[test]
1286 fn test_rotating_auditor_rotates_by_size() {
1287 let auditor = RotatingAuditor::with_max_entries(5);
1288 for i in 0..5 {
1289 auditor.log(&ctx(&format!("SELECT {}", i), "user", i));
1290 }
1291 assert_eq!(auditor.len(), 5);
1292 assert_eq!(auditor.rotation_count(), 0);
1293 auditor.log(&ctx("SELECT 6", "user", 100));
1295 assert_eq!(auditor.len(), 1);
1296 assert_eq!(auditor.rotation_count(), 1);
1297 }
1298
1299 #[test]
1300 fn test_rotating_auditor_rotates_by_age() {
1301 let auditor = RotatingAuditor::with_max_age(1000);
1302 auditor.log(&ctx("SELECT 1", "user", 100));
1303 auditor.log(&ctx("SELECT 2", "user", 200));
1304 assert_eq!(auditor.len(), 2);
1305 assert_eq!(auditor.rotation_count(), 0);
1306 auditor.log(&ctx("SELECT 3", "user", 1500));
1308 assert_eq!(auditor.len(), 1);
1309 assert_eq!(auditor.rotation_count(), 1);
1310 }
1311
1312 #[test]
1313 fn test_rotating_auditor_rules_filter() {
1314 let rules = AuditRules::new().deny("drop").allow("select");
1315 let auditor = RotatingAuditor::new(RotationPolicy::none(), rules);
1316 let logged1 = auditor.log(&ctx("SELECT * FROM users", "u", 1));
1317 let logged2 = auditor.log(&ctx("DROP TABLE users", "u", 2));
1318 let logged3 = auditor.log(&ctx("DELETE FROM users", "u", 3));
1319 assert!(logged1);
1320 assert!(!logged2);
1321 assert!(!logged3);
1322 assert_eq!(auditor.len(), 1);
1323 }
1324
1325 #[test]
1326 fn test_rotating_auditor_manual_rotate() {
1327 let auditor = RotatingAuditor::with_max_entries(100);
1328 auditor.log(&ctx("SELECT 1", "u", 1));
1329 auditor.log(&ctx("SELECT 2", "u", 2));
1330 let cleared = auditor.rotate();
1331 assert_eq!(cleared, 2);
1332 assert!(auditor.is_empty());
1333 assert_eq!(auditor.rotation_count(), 1);
1334 }
1335
1336 #[test]
1337 fn test_rotating_auditor_masks_sensitive() {
1338 let auditor = RotatingAuditor::with_max_entries(100);
1339 auditor.log(&ctx("SELECT * FROM users WHERE password='x'", "u", 1));
1340 let logs = auditor.get_logs();
1341 assert_eq!(logs.len(), 1);
1342 assert!(!logs[0].sql.contains("password"));
1343 assert!(logs[0].sql.contains("******"));
1344 }
1345
1346 #[test]
1347 fn test_rotating_auditor_get_logs_snapshot() {
1348 let auditor = RotatingAuditor::with_max_entries(100);
1349 auditor.log(&ctx("SELECT 1", "u", 1));
1350 let snap = auditor.get_logs();
1351 auditor.log(&ctx("SELECT 2", "u", 2));
1352 assert_eq!(snap.len(), 1, "snapshot should be independent");
1353 assert_eq!(auditor.len(), 2);
1354 }
1355
1356 #[test]
1359 fn test_async_writer_log_and_shutdown() {
1360 let writer = AsyncAuditWriter::new();
1361 writer
1362 .log(&ctx("SELECT * FROM users", "admin", 1000))
1363 .unwrap();
1364 writer
1365 .log(&ctx("INSERT INTO logs VALUES(1)", "user2", 2000))
1366 .unwrap();
1367 let logs = writer.shutdown().expect("shutdown should succeed");
1368 assert_eq!(logs.len(), 2);
1369 assert_eq!(logs[0].user, "admin");
1370 assert_eq!(logs[1].timestamp, 2000);
1371 }
1372
1373 #[test]
1374 fn test_async_writer_masks_sensitive() {
1375 let writer = AsyncAuditWriter::new();
1376 writer
1377 .log(&ctx("SELECT * FROM users WHERE password='secret'", "u", 1))
1378 .unwrap();
1379 let logs = writer.shutdown().unwrap();
1380 assert_eq!(logs.len(), 1);
1381 assert!(!logs[0].sql.contains("password"));
1382 }
1383
1384 #[test]
1385 fn test_async_writer_empty_shutdown() {
1386 let writer = AsyncAuditWriter::new();
1387 let logs = writer.shutdown().expect("shutdown should succeed");
1388 assert!(logs.is_empty());
1389 }
1390
1391 #[test]
1392 fn test_async_writer_double_shutdown_errors() {
1393 let writer = AsyncAuditWriter::new();
1394 let _ = writer.shutdown().unwrap();
1395 let result = writer.shutdown();
1396 assert!(result.is_err(), "double shutdown should error");
1397 }
1398
1399 #[test]
1400 fn test_async_writer_default() {
1401 let writer = AsyncAuditWriter::default();
1402 writer.log(&ctx("SELECT 1", "u", 1)).unwrap();
1403 let logs = writer.shutdown().unwrap();
1404 assert_eq!(logs.len(), 1);
1405 }
1406
1407 #[test]
1410 fn test_audit_query_by_user() {
1411 let auditor = SqlAuditor::new();
1412 auditor.log(&ctx("SELECT 1", "alice", 100));
1413 auditor.log(&ctx("SELECT 2", "bob", 200));
1414 auditor.log(&ctx("SELECT 3", "alice", 300));
1415 let query = AuditQuery::new().by_user("alice");
1416 let results = query_logs(&auditor, &query);
1417 assert_eq!(results.len(), 2);
1418 assert!(results.iter().all(|r| r.user == "alice"));
1419 }
1420
1421 #[test]
1422 fn test_audit_query_by_time_range() {
1423 let auditor = SqlAuditor::new();
1424 auditor.log(&ctx("SELECT 1", "u", 100));
1425 auditor.log(&ctx("SELECT 2", "u", 200));
1426 auditor.log(&ctx("SELECT 3", "u", 300));
1427 auditor.log(&ctx("SELECT 4", "u", 400));
1428 let query = AuditQuery::new().by_time_range(150, 350);
1429 let results = query_logs(&auditor, &query);
1430 assert_eq!(results.len(), 2);
1431 assert!(results
1432 .iter()
1433 .all(|r| r.timestamp >= 150 && r.timestamp <= 350));
1434 }
1435
1436 #[test]
1437 fn test_audit_query_by_sql_contains() {
1438 let auditor = SqlAuditor::new();
1439 auditor.log(&ctx("SELECT * FROM users", "u", 1));
1440 auditor.log(&ctx("INSERT INTO orders", "u", 2));
1441 auditor.log(&ctx("SELECT * FROM orders", "u", 3));
1442 let query = AuditQuery::new().by_sql_contains("orders");
1443 let results = query_logs(&auditor, &query);
1444 assert_eq!(results.len(), 2);
1445 assert!(results
1446 .iter()
1447 .all(|r| r.sql.to_lowercase().contains("orders")));
1448 }
1449
1450 #[test]
1451 fn test_audit_query_sql_contains_case_insensitive() {
1452 let auditor = SqlAuditor::new();
1453 auditor.log(&ctx("select * from Users", "u", 1));
1454 let query = AuditQuery::new().by_sql_contains("USERS");
1455 let results = query_logs(&auditor, &query);
1456 assert_eq!(results.len(), 1);
1457 }
1458
1459 #[test]
1460 fn test_audit_query_with_limit() {
1461 let auditor = SqlAuditor::new();
1462 for i in 0..10 {
1463 auditor.log(&ctx(&format!("SELECT {}", i), "u", i));
1464 }
1465 let query = AuditQuery::new().with_limit(3);
1466 let results = query_logs(&auditor, &query);
1467 assert_eq!(results.len(), 3);
1468 }
1469
1470 #[test]
1471 fn test_audit_query_combined_filters() {
1472 let auditor = SqlAuditor::new();
1473 auditor.log(&ctx("SELECT * FROM users", "alice", 100));
1474 auditor.log(&ctx("INSERT INTO users", "alice", 200));
1475 auditor.log(&ctx("SELECT * FROM orders", "alice", 300));
1476 auditor.log(&ctx("SELECT * FROM users", "bob", 400));
1477 let query = AuditQuery::new()
1478 .by_user("alice")
1479 .by_sql_contains("select")
1480 .with_limit(10);
1481 let results = query_logs(&auditor, &query);
1482 assert_eq!(results.len(), 2);
1483 assert!(results.iter().all(|r| r.user == "alice"));
1484 }
1485
1486 #[test]
1487 fn test_audit_query_empty_returns_all() {
1488 let auditor = SqlAuditor::new();
1489 auditor.log(&ctx("SELECT 1", "u", 1));
1490 auditor.log(&ctx("SELECT 2", "u", 2));
1491 let query = AuditQuery::new();
1492 let results = query_logs(&auditor, &query);
1493 assert_eq!(results.len(), 2);
1494 }
1495
1496 #[test]
1497 fn test_audit_query_no_match_returns_empty() {
1498 let auditor = SqlAuditor::new();
1499 auditor.log(&ctx("SELECT 1", "u", 1));
1500 let query = AuditQuery::new().by_user("nonexistent");
1501 let results = query_logs(&auditor, &query);
1502 assert!(results.is_empty());
1503 }
1504
1505 #[test]
1506 fn test_audit_query_filter_directly() {
1507 let logs = vec![
1508 ctx("SELECT 1", "a", 10),
1509 ctx("SELECT 2", "b", 20),
1510 ctx("SELECT 3", "a", 30),
1511 ];
1512 let query = AuditQuery::new().by_user("a");
1513 let results = query.filter(&logs);
1514 assert_eq!(results.len(), 2);
1515 }
1516
1517 #[test]
1518 fn test_audit_query_limit_zero_means_no_limit() {
1519 let logs = vec![ctx("SELECT 1", "a", 10), ctx("SELECT 2", "a", 20)];
1520 let query = AuditQuery::new().with_limit(0);
1521 let results = query.filter(&logs);
1522 assert_eq!(results.len(), 2);
1523 }
1524
1525 #[test]
1528 fn test_file_audit_log_store_append_and_read_all() {
1529 let path = test_data_dir().join("sz_orm_audit_store_append.jsonl");
1530 let path_str = path.to_str().unwrap();
1531 let store = FileAuditLogStore::new(path_str);
1532 let _ = store.clear();
1534
1535 store
1536 .append(&ctx("SELECT * FROM users", "alice", 1000))
1537 .unwrap();
1538 store
1539 .append(&ctx("INSERT INTO logs VALUES(1)", "bob", 2000))
1540 .unwrap();
1541
1542 let logs = store.read_all().unwrap();
1543 assert_eq!(logs.len(), 2);
1544 assert_eq!(logs[0].user, "alice");
1545 assert_eq!(logs[0].sql, "SELECT * FROM users");
1546 assert_eq!(logs[1].user, "bob");
1547 assert_eq!(logs[1].timestamp, 2000);
1548
1549 let _ = store.clear();
1550 }
1551
1552 #[test]
1553 fn test_file_audit_log_store_masks_sensitive() {
1554 let path = test_data_dir().join("sz_orm_audit_store_mask.jsonl");
1555 let path_str = path.to_str().unwrap();
1556 let store = FileAuditLogStore::new(path_str);
1557 let _ = store.clear();
1558
1559 store
1560 .append(&ctx(
1561 "SELECT * FROM users WHERE password='secret'",
1562 "admin",
1563 1000,
1564 ))
1565 .unwrap();
1566
1567 let logs = store.read_all().unwrap();
1568 assert_eq!(logs.len(), 1);
1569 assert!(!logs[0].sql.contains("password"));
1571 assert!(!logs[0].sql.contains("secret"));
1572 assert!(logs[0].sql.contains("******"));
1573
1574 let _ = store.clear();
1575 }
1576
1577 #[test]
1578 fn test_file_audit_log_store_clear_removes_entries() {
1579 let path = test_data_dir().join("sz_orm_audit_store_clear.jsonl");
1580 let path_str = path.to_str().unwrap();
1581 let store = FileAuditLogStore::new(path_str);
1582 let _ = store.clear();
1583
1584 store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1585 store.append(&ctx("SELECT 2", "u", 2)).unwrap();
1586 assert_eq!(store.read_all().unwrap().len(), 2);
1587
1588 store.clear().unwrap();
1589 assert_eq!(store.read_all().unwrap().len(), 0);
1591
1592 let _ = store.clear();
1593 }
1594
1595 #[test]
1596 fn test_file_audit_log_store_clear_nonexistent_is_ok() {
1597 let path = test_data_dir().join("sz_orm_audit_store_nonexistent.jsonl");
1599 let path_str = path.to_str().unwrap();
1600 let store = FileAuditLogStore::new(path_str);
1601 let _ = std::fs::remove_file(path_str);
1603 assert!(store.clear().is_ok());
1604 }
1605
1606 #[test]
1607 fn test_file_audit_log_store_read_all_empty_file() {
1608 let path = test_data_dir().join("sz_orm_audit_store_empty_read.jsonl");
1609 let path_str = path.to_str().unwrap();
1610 let store = FileAuditLogStore::new(path_str);
1611 let _ = store.clear();
1612
1613 let logs = store.read_all().unwrap();
1615 assert!(logs.is_empty());
1616
1617 let _ = store.clear();
1618 }
1619
1620 #[test]
1621 fn test_file_audit_log_store_skips_blank_lines() {
1622 let path = test_data_dir().join("sz_orm_audit_store_blank_lines.jsonl");
1623 let path_str = path.to_str().unwrap();
1624 let store = FileAuditLogStore::new(path_str);
1625 let _ = store.clear();
1626
1627 store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1628 let mut file = std::fs::OpenOptions::new()
1631 .append(true)
1632 .open(path_str)
1633 .unwrap();
1634 use std::io::Write;
1635 writeln!(file).unwrap();
1636 writeln!(file, " ").unwrap();
1637 drop(file);
1638
1639 store.append(&ctx("SELECT 2", "u", 2)).unwrap();
1640
1641 let logs = store.read_all().unwrap();
1642 assert_eq!(logs.len(), 2);
1644
1645 let _ = store.clear();
1646 }
1647
1648 #[test]
1649 fn test_file_audit_log_store_path_accessor() {
1650 let store = FileAuditLogStore::new("/tmp/sz_orm_audit_path_test.jsonl");
1651 assert_eq!(store.path(), "/tmp/sz_orm_audit_path_test.jsonl");
1652 }
1653
1654 #[test]
1655 fn test_file_audit_log_store_concurrent_append() {
1656 use std::sync::Arc;
1657 let path = test_data_dir().join("sz_orm_audit_store_concurrent.jsonl");
1658 let path_str = path.to_str().unwrap();
1659 let store = Arc::new(FileAuditLogStore::new(path_str));
1660 let _ = store.clear();
1661
1662 let mut handles = vec![];
1663 for i in 0..4 {
1664 let s = Arc::clone(&store);
1665 handles.push(std::thread::spawn(move || {
1666 for j in 0..10 {
1667 s.append(&ctx(&format!("SELECT {}_{}", i, j), "u", j as i64))
1668 .unwrap();
1669 }
1670 }));
1671 }
1672 for h in handles {
1673 h.join().unwrap();
1674 }
1675
1676 let logs = store.read_all().unwrap();
1678 assert_eq!(logs.len(), 40);
1679
1680 let _ = store.clear();
1681 }
1682
1683 #[test]
1684 fn test_audit_log_store_trait_object() {
1685 let path = test_data_dir().join("sz_orm_audit_store_trait.jsonl");
1687 let path_str = path.to_str().unwrap();
1688 let store: Box<dyn AuditLogStore> = Box::new(FileAuditLogStore::new(path_str));
1689 let _ = store.clear();
1690
1691 store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1692 let logs = store.read_all().unwrap();
1693 assert_eq!(logs.len(), 1);
1694
1695 let _ = store.clear();
1696 }
1697
1698 #[test]
1701 fn test_hash_chain_empty_auditor_verify_ok() {
1702 let auditor = HashChainAuditor::new();
1703 assert!(auditor.is_empty());
1704 assert_eq!(auditor.len(), 0);
1705 assert!(auditor.verify().is_ok());
1707 }
1708
1709 #[test]
1710 fn test_hash_chain_single_entry_genesis() {
1711 let auditor = HashChainAuditor::new();
1712 auditor.log(&ctx("SELECT 1", "admin", 1000));
1713 assert_eq!(auditor.len(), 1);
1714
1715 let entries = auditor.get_entries();
1716 assert_eq!(entries[0].prev_hash, GENESIS_HASH);
1718 assert_eq!(entries[0].current_hash.len(), 64);
1720 assert!(auditor.verify().is_ok());
1722 }
1723
1724 #[test]
1725 fn test_hash_chain_multiple_entries_linked() {
1726 let auditor = HashChainAuditor::new();
1727 auditor.log(&ctx("SELECT 1", "admin", 1000));
1728 auditor.log(&ctx("SELECT 2", "admin", 1001));
1729 auditor.log(&ctx("SELECT 3", "admin", 1002));
1730 assert_eq!(auditor.len(), 3);
1731
1732 let entries = auditor.get_entries();
1733 assert_eq!(entries[1].prev_hash, entries[0].current_hash);
1735 assert_eq!(entries[2].prev_hash, entries[1].current_hash);
1736 assert!(auditor.verify().is_ok());
1738 }
1739
1740 #[test]
1741 fn test_hash_chain_detects_tampered_sql() {
1742 let auditor = HashChainAuditor::new();
1743 auditor.log(&ctx("SELECT 1", "admin", 1000));
1744 auditor.log(&ctx("SELECT 2", "admin", 1001));
1745
1746 {
1748 let mut entries = auditor.entries.lock().unwrap();
1749 entries[0].entry.sql = "DROP TABLE users".to_string();
1750 }
1751
1752 let result = auditor.verify();
1754 assert!(result.is_err());
1755 let err = result.unwrap_err();
1756 assert!(err.contains("index 0"), "error: {}", err);
1758 assert!(err.contains("hash mismatch"), "error: {}", err);
1759 }
1760
1761 #[test]
1762 fn test_hash_chain_detects_broken_link() {
1763 let auditor = HashChainAuditor::new();
1764 auditor.log(&ctx("SELECT 1", "admin", 1000));
1765 auditor.log(&ctx("SELECT 2", "admin", 1001));
1766
1767 {
1769 let mut entries = auditor.entries.lock().unwrap();
1770 entries[1].prev_hash = "deadbeef".to_string();
1771 }
1772
1773 let result = auditor.verify();
1774 assert!(result.is_err());
1775 let err = result.unwrap_err();
1776 assert!(err.contains("chain broken at index 1"), "error: {}", err);
1777 }
1778
1779 #[test]
1780 fn test_hash_chain_detects_genesis_tamper() {
1781 let auditor = HashChainAuditor::new();
1782 auditor.log(&ctx("SELECT 1", "admin", 1000));
1783
1784 {
1786 let mut entries = auditor.entries.lock().unwrap();
1787 entries[0].prev_hash = "deadbeef".to_string();
1788 }
1789
1790 let result = auditor.verify();
1791 assert!(result.is_err());
1792 let err = result.unwrap_err();
1793 assert!(err.contains("genesis prev_hash mismatch"), "error: {}", err);
1794 }
1795
1796 #[test]
1797 fn test_hash_chain_masks_sensitive_data() {
1798 let auditor = HashChainAuditor::new();
1799 auditor.log(&ctx(
1800 "SELECT * FROM users WHERE password='secret'",
1801 "admin",
1802 1000,
1803 ));
1804
1805 let entries = auditor.get_entries();
1806 assert!(!entries[0].entry.sql.contains("password"));
1808 assert!(!entries[0].entry.sql.contains("secret"));
1809 assert!(entries[0].entry.sql.contains("******"));
1810 assert!(auditor.verify().is_ok());
1812 }
1813
1814 #[test]
1815 fn test_hash_chain_deterministic_hashes() {
1816 let entry = ctx("SELECT 1", "admin", 1000);
1818 let e1 = HashChainEntry::genesis(entry.clone());
1819 let e2 = HashChainEntry::genesis(entry);
1820 assert_eq!(e1.current_hash, e2.current_hash);
1821 assert_eq!(e1.prev_hash, e2.prev_hash);
1822 }
1823
1824 #[test]
1825 fn test_hash_chain_different_inputs_different_hashes() {
1826 let e1 = HashChainEntry::genesis(ctx("SELECT 1", "admin", 1000));
1827 let e2 = HashChainEntry::genesis(ctx("SELECT 2", "admin", 1000));
1828 assert_ne!(e1.current_hash, e2.current_hash);
1829 }
1830
1831 #[test]
1832 fn test_hash_chain_flush_and_persist() {
1833 let auditor = HashChainAuditor::new();
1834 auditor.log(&ctx("SELECT 1", "admin", 1000));
1835 auditor.log(&ctx("SELECT 2", "admin", 1001));
1836
1837 let path = test_data_dir().join("sz_orm_audit_hash_chain.json");
1838 let path_str = path.to_str().unwrap();
1839 let count = auditor.flush(path_str).unwrap();
1840 assert_eq!(count, 2);
1841
1842 let content = std::fs::read_to_string(path_str).unwrap();
1844 assert!(!content.is_empty());
1845 assert!(content.contains("current_hash"));
1846
1847 let _ = std::fs::remove_file(path_str);
1848 }
1849
1850 #[test]
1851 fn test_hash_chain_concurrent_log_thread_safe() {
1852 use std::sync::Arc;
1853 use std::thread;
1854
1855 let auditor = Arc::new(HashChainAuditor::new());
1856 let mut handles = vec![];
1857 for i in 0..4 {
1858 let a = Arc::clone(&auditor);
1859 handles.push(thread::spawn(move || {
1860 for j in 0..25 {
1861 a.log(&ctx(&format!("SELECT {}_{}", i, j), "u", j as i64));
1862 }
1863 }));
1864 }
1865 for h in handles {
1866 h.join().unwrap();
1867 }
1868
1869 assert_eq!(auditor.len(), 100);
1871 assert!(auditor.verify().is_ok());
1873 }
1874
1875 #[test]
1876 fn test_genesis_hash_constant_is_64_zeros() {
1877 assert_eq!(GENESIS_HASH.len(), 64);
1879 assert!(GENESIS_HASH.chars().all(|c| c == '0'));
1880 }
1881}