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