Skip to main content

sz_orm_audit/
lib.rs

1//! # SZ-ORM Audit — SQL Audit Log
2//!
3//! Provides SQL execution audit records and performs case-insensitive masking
4//! on sensitive keywords such as password/token/credit_card, ensuring that
5//! audit logs never leak sensitive information.
6//!
7//! ## Main Types
8//!
9//! - [`SqlAuditContext`] — Audit context (SQL/user/timestamp)
10//! - [`SqlAuditor`] — Audit executor
11
12use 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
39/// Sensitive keywords that should be masked in audit logs. Matching is
40/// case-insensitive on the ASCII bytes of the SQL string.
41const 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    /// Log an audit entry. The SQL is masked for sensitive keywords before
70    /// being stored in the in-memory buffer.
71    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    /// Return a snapshot of all stored audit entries.
86    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    /// Flush all stored audit entries to a JSON file at `path`.
95    /// Returns the number of entries written.
96    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    /// Mask all sensitive keywords in `sql` with `******`. Matching is
108    /// case-insensitive.
109    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
120/// Mask all sensitive keywords in `sql` with `******`. Matching is
121/// case-insensitive over the ASCII bytes of the string.
122fn 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                // Only treat as a keyword match if it's not part of a longer identifier.
135                // Boundary check: previous and next char must be non-alphanumeric/underscore
136                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            // Push one char (handles UTF-8 properly since we step by char).
150            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
161/// Returns true if `b` is an ASCII identifier character (alphanumeric or '_').
162fn is_ident_char(b: u8) -> bool {
163    b.is_ascii_alphanumeric() || b == b'_'
164}
165
166// ============================================================================
167// 审计规则配置(允许/拒绝列表)
168// ============================================================================
169
170/// Audit rules: allow/deny lists used to decide whether a given SQL statement
171/// should be recorded in the audit log.
172///
173/// Rule evaluation order:
174/// 1. If the SQL matches any pattern in the deny list → not recorded
175/// 2. If the allow list is empty → record all SQL not denied
176/// 3. If the allow list is non-empty → record only SQL that matches the allow list
177#[derive(Debug, Clone, Default)]
178pub struct AuditRules {
179    /// Allow-list patterns (case-insensitive substring match); empty means allow all
180    allow_patterns: Vec<String>,
181    /// Deny-list patterns (case-insensitive substring match); a match means deny
182    deny_patterns: Vec<String>,
183}
184
185impl AuditRules {
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Add an allow pattern (case-insensitive substring match)
191    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    /// Add a deny pattern (case-insensitive substring match)
198    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    /// Determine whether the given SQL should be audit-recorded
204    pub fn should_audit(&self, sql: &str) -> bool {
205        let lower = sql.to_ascii_lowercase();
206        // 拒绝列表优先
207        for pat in &self.deny_patterns {
208            if lower.contains(pat) {
209                return false;
210            }
211        }
212        // 允许列表为空 → 允许所有
213        if self.allow_patterns.is_empty() {
214            return true;
215        }
216        // 允许列表非空 → 仅允许命中项
217        self.allow_patterns.iter().any(|pat| lower.contains(pat))
218    }
219
220    /// Return the number of allow patterns
221    pub fn allow_count(&self) -> usize {
222        self.allow_patterns.len()
223    }
224
225    /// Return the number of deny patterns
226    pub fn deny_count(&self) -> usize {
227        self.deny_patterns.len()
228    }
229}
230
231// ============================================================================
232// 审计日志轮转策略(按大小/时间)
233// ============================================================================
234
235/// Audit log rotation policy configuration.
236///
237/// - `max_entries`: maximum number of log entries retained in memory; once
238///   exceeded, rotation is triggered automatically (old logs are cleared or
239///   flushed to disk)
240/// - `max_age_ms`: maximum age of a log entry in milliseconds; once exceeded,
241///   rotation is triggered
242#[derive(Debug, Clone)]
243pub struct RotationPolicy {
244    /// Maximum entry count (0 means unlimited)
245    pub max_entries: usize,
246    /// Maximum age in milliseconds (0 means unlimited)
247    pub max_age_ms: i64,
248}
249
250impl RotationPolicy {
251    /// Create an unlimited rotation policy
252    pub fn none() -> Self {
253        Self {
254            max_entries: 0,
255            max_age_ms: 0,
256        }
257    }
258
259    /// Rotate by entry count
260    pub fn by_size(max_entries: usize) -> Self {
261        Self {
262            max_entries,
263            max_age_ms: 0,
264        }
265    }
266
267    /// Rotate by age (milliseconds)
268    pub fn by_age(max_age_ms: i64) -> Self {
269        Self {
270            max_entries: 0,
271            max_age_ms,
272        }
273    }
274
275    /// Rotate by both entry count and age
276    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    /// Determine whether rotation is needed
284    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
301// ============================================================================
302// 带轮转和规则的审计器
303// ============================================================================
304
305/// Enhanced auditor with rotation policy and audit rules.
306///
307/// Built on top of `SqlAuditor`, it adds:
308/// - Log rotation (automatic cleanup of old logs by size or age)
309/// - Audit rules (allow/deny list filtering)
310pub struct RotatingAuditor {
311    logs: Mutex<Vec<SqlAuditContext>>,
312    rules: AuditRules,
313    policy: RotationPolicy,
314    /// Number of rotations (cleanups) performed
315    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    /// Create an auditor that rotates only by size, with no rule filtering
329    pub fn with_max_entries(max_entries: usize) -> Self {
330        Self::new(RotationPolicy::by_size(max_entries), AuditRules::new())
331    }
332
333    /// Create an auditor that rotates only by age, with no rule filtering
334    pub fn with_max_age(max_age_ms: i64) -> Self {
335        Self::new(RotationPolicy::by_age(max_age_ms), AuditRules::new())
336    }
337
338    /// Record an audit log, automatically applying rule filtering and rotation policy
339    pub fn log(&self, ctx: &SqlAuditContext) -> bool {
340        // 规则过滤
341        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        // 在添加新条目前检查轮转(确保新条目不被立即清除)
356        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    /// Return a snapshot of the current logs
371    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    /// Return the number of rotations performed
379    pub fn rotation_count(&self) -> usize {
380        *self
381            .rotations
382            .lock()
383            .expect("RotatingAuditor rotations lock poisoned (rotation_count)")
384    }
385
386    /// Manually trigger rotation (clears the current logs)
387    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    /// Return the current number of log entries
402    pub fn len(&self) -> usize {
403        self.logs
404            .lock()
405            .expect("RotatingAuditor logs lock poisoned (len)")
406            .len()
407    }
408
409    /// Whether the auditor is empty
410    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
418// ============================================================================
419// 异步审计写入器
420// ============================================================================
421
422/// Asynchronous audit writer: writes audit logs via a background thread to
423/// avoid blocking the main thread.
424///
425/// Uses an `std::sync::mpsc` channel to send logs to a background thread that
426/// handles storage. Call `shutdown` to wait for the background thread to exit
427/// and return all written logs.
428pub 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    /// Create an async writer and start the background thread
440    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    /// Asynchronously record an audit log (non-blocking)
466    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    /// Shut down the background thread and return all written logs
473    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// ============================================================================
496// 审计日志查询过滤
497// ============================================================================
498
499/// Audit log query filter
500#[derive(Debug, Clone, Default)]
501pub struct AuditQuery {
502    /// Filter by username (exact match; None means no filter)
503    pub user: Option<String>,
504    /// Time range start (millisecond timestamp; None means no lower bound)
505    pub from_ts: Option<i64>,
506    /// Time range end (millisecond timestamp; None means no upper bound)
507    pub to_ts: Option<i64>,
508    /// SQL keyword filter (case-insensitive substring match; None means no filter)
509    pub sql_contains: Option<String>,
510    /// Limit on the number of returned entries (0 means unlimited)
511    pub limit: usize,
512}
513
514impl AuditQuery {
515    pub fn new() -> Self {
516        Self::default()
517    }
518
519    /// Filter by username
520    pub fn by_user(mut self, user: impl Into<String>) -> Self {
521        self.user = Some(user.into());
522        self
523    }
524
525    /// Filter by time range (millisecond timestamps)
526    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    /// Filter by SQL keyword (case-insensitive)
533    pub fn by_sql_contains(mut self, keyword: impl Into<String>) -> Self {
534        self.sql_contains = Some(keyword.into());
535        self
536    }
537
538    /// Limit the number of returned entries
539    pub fn with_limit(mut self, limit: usize) -> Self {
540        self.limit = limit;
541        self
542    }
543
544    /// Apply the query filter to a list of logs
545    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
581/// Query logs from `SqlAuditor` by the given conditions
582pub fn query_logs(auditor: &SqlAuditor, query: &AuditQuery) -> Vec<SqlAuditContext> {
583    let logs = auditor.get_logs();
584    query.filter(&logs)
585}
586
587// ============================================================================
588// 审计日志持久化存储
589// ============================================================================
590
591/// Audit log persistent storage backend trait
592///
593/// Abstracts the persistent storage capability for audit logs across different
594/// storage media (file, database, object storage, etc.). Implementors must
595/// ensure that `append` is thread-safe; they should call `mask_sensitive`
596/// themselves before storage to mask sensitive data.
597pub trait AuditLogStore: Send + Sync {
598    /// Append an audit log entry (already masked); returns whether it succeeded
599    fn append(&self, entry: &SqlAuditContext) -> Result<(), String>;
600    /// Read all persisted audit logs
601    fn read_all(&self) -> Result<Vec<SqlAuditContext>, String>;
602    /// Clear the persistent storage
603    fn clear(&self) -> Result<(), String>;
604}
605
606/// File-based audit log persistent storage (JSONL format: one JSON per line)
607///
608/// Applicable scenarios: single-machine deployment, lightweight audit archiving,
609/// development debugging. For high-concurrency production scenarios, it is
610/// recommended to combine `AsyncAuditWriter` + `FileAuditLogStore` so that a
611/// background thread performs serial writes and avoids lock contention.
612pub struct FileAuditLogStore {
613    path: String,
614    write_lock: Mutex<()>,
615}
616
617impl FileAuditLogStore {
618    /// Create a file audit log store; the target file is created on the first
619    /// `append` if it does not exist
620    pub fn new(path: impl Into<String>) -> Self {
621        Self {
622            path: path.into(),
623            write_lock: Mutex::new(()),
624        }
625    }
626
627    /// Return the storage file path
628    pub fn path(&self) -> &str {
629        &self.path
630    }
631}
632
633impl AuditLogStore for FileAuditLogStore {
634    /// Append an audit log entry (JSONL format; automatically masked before write)
635    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        // 脱敏后序列化,确保落盘内容不含敏感信息
641        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        // 以追加模式打开,每条日志占一行(JSONL)
649        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    /// Read all persisted audit logs (parse JSONL line by line)
659    ///
660    /// Returns an empty vec when the file does not exist (treated as no logs
661    /// persisted yet).
662    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    /// Clear the persistent file (deletes the file; the next append recreates it)
684    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            // 文件不存在视为已清空
691            if e.kind() == std::io::ErrorKind::NotFound {
692                Ok(())
693            } else {
694                Err(format!("clear failed: {}", e))
695            }
696        })
697    }
698}
699
700// ============================================================================
701// #11 修复:审计日志哈希链防篡改(Tamper-Evident Hash Chain)
702// ============================================================================
703
704/// Genesis hash (the previous hash of the chain head), fixed as an all-zero
705/// 64-character hexadecimal string.
706///
707/// The first record of every hash chain uses `GENESIS_HASH` as its `prev_hash`,
708/// making it easy to verify that the chain start has not been truncated.
709pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
710
711/// Audit log entry with a hash chain.
712///
713/// Each record contains:
714/// - `prev_hash`: the `current_hash` of the previous record (the first record
715///   uses `GENESIS_HASH`)
716/// - `current_hash`: the SHA-256 hash of this record (computed from
717///   `prev_hash + entry`)
718/// - `entry`: the original audit context (already masked)
719///
720/// Any tampering with a historical record causes `current_hash` to mismatch the
721/// `prev_hash` of the next record, which is detected by `verify_chain`.
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct HashChainEntry {
724    /// Hash of the previous record (the first record uses [`GENESIS_HASH`])
725    pub prev_hash: String,
726    /// Hash of this record (SHA-256 hexadecimal string, 64 characters)
727    pub current_hash: String,
728    /// Original audit context (already masked)
729    pub entry: SqlAuditContext,
730}
731
732impl HashChainEntry {
733    /// Compute the `current_hash` of a single record.
734    ///
735    /// The hash input is the UTF-8 byte concatenation of
736    /// `prev_hash || sql || user || timestamp`, using the SHA-256 algorithm.
737    /// This way, tampering with any field changes the hash, which in turn
738    /// affects the `prev_hash` of the next record, forming a chain of
739    /// verification.
740    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        // timestamp 使用固定宽度的字节表示,避免可变长度编码导致歧义
747        hasher.update(entry.timestamp.to_le_bytes());
748        let result = hasher.finalize();
749        // 转为小写十六进制字符串(64 字符)
750        hex_encode(&result)
751    }
752
753    /// Create the chain head record (prev_hash = GENESIS_HASH)
754    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    /// Append a record to the given previous hash
765    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
775/// Encode a byte array as a lowercase hexadecimal string.
776///
777/// Consistent with the behavior of the `hex` crate's `encode`, but avoids
778/// introducing an extra dependency.
779fn 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
789/// Auditor with a hash chain: all logs are linked by SHA-256 chained hashes,
790/// supporting tamper detection.
791///
792/// # Tamper-Evidence Mechanism
793///
794/// 1. Each record's `current_hash = SHA256(prev_hash || sql || user || timestamp)`
795/// 2. The `prev_hash` of the next record equals the `current_hash` of the
796///    previous record
797/// 3. Any modification to a historical record changes `current_hash`, which
798///    then mismatches the `prev_hash` of the next record
799/// 4. Deleting a middle record breaks the chain; inserting a record changes
800///    all subsequent hashes
801///
802/// # Example
803///
804/// ```
805/// use sz_orm_audit::{HashChainAuditor, SqlAuditContext};
806///
807/// let mut auditor = HashChainAuditor::new();
808/// auditor.log(&SqlAuditContext {
809///     sql: "SELECT * FROM users".to_string(),
810///     user: "admin".to_string(),
811///     timestamp: 1000,
812/// });
813/// // Verify chain integrity
814/// assert!(auditor.verify().is_ok());
815/// ```
816pub struct HashChainAuditor {
817    /// Hash chain log entries (stored in append order)
818    entries: Mutex<Vec<HashChainEntry>>,
819}
820
821impl Default for HashChainAuditor {
822    fn default() -> Self {
823        Self::new()
824    }
825}
826
827impl HashChainAuditor {
828    /// Create an empty hash chain auditor
829    pub fn new() -> Self {
830        Self {
831            entries: Mutex::new(Vec::new()),
832        }
833    }
834
835    /// Append an audit log to the end of the hash chain.
836    ///
837    /// - If the chain is empty, uses [`GENESIS_HASH`] as `prev_hash`
838    /// - Otherwise uses the `current_hash` of the last record as `prev_hash`
839    ///
840    /// The SQL is first masked by `mask_sensitive` before being written to the
841    /// chain, ensuring that the stored audit logs contain no sensitive
842    /// information.
843    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    /// Return a snapshot (clone) of all log entries
867    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    /// Return the number of log entries
875    pub fn len(&self) -> usize {
876        self.entries
877            .lock()
878            .expect("HashChainAuditor entries lock poisoned (len)")
879            .len()
880    }
881
882    /// Whether the auditor is empty
883    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    /// Verify the integrity of the hash chain.
891    ///
892    /// Checks performed:
893    /// 1. The `prev_hash` of the first record equals [`GENESIS_HASH`]
894    /// 2. The `current_hash` of each record equals
895    ///    `compute_hash(prev_hash, entry)`
896    /// 3. The `prev_hash` of each adjacent record equals the `current_hash` of
897    ///    the previous record
898    ///
899    /// # Return value
900    ///
901    /// - `Ok(())`: the chain is intact and has not been tampered with
902    /// - `Err(reason)`: the chain has been tampered with; `reason` describes
903    ///   the location and type of the first anomaly
904    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            // 检查 1:首条记录的 prev_hash 必须为 GENESIS_HASH
911            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                // 检查 3:非首条记录的 prev_hash 必须等于上一条的 current_hash
920                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            // 检查 2:current_hash 必须等于重新计算的哈希
929            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    /// Persist the hash chain to a JSONL file (one JSON per line).
941    ///
942    /// Returns the number of entries written.
943    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    /// Test data directory: prefers F:\test\data (user convention), falls back
960    /// to the environment variable or system temp (CI/Linux).
961    ///
962    /// Note: checking only that the directory exists is not enough to guarantee
963    /// usability — write permission must also be verified, to avoid test
964    /// failures in restricted sandbox environments (e.g. TRAE Sandbox) where
965    /// the directory exists but is not writable.
966    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    /// Check whether the directory exists and is writable: try to create and
981    /// delete a probe file inside it
982    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        // "passworded" should not be partially matched as "password"
1090        // because we require word boundaries.
1091        let a = SqlAuditor::new();
1092        let masked = a.mask_sensitive("SELECT * FROM users WHERE note='passworded'");
1093        // The 'passworded' word should remain intact because of boundary check
1094        assert!(masked.contains("passworded"));
1095        // Ensure we did NOT replace anything (no ****** from this substring)
1096        // Actually, "passworded" still has 'password' as a prefix but our
1097        // boundary check requires the char AFTER the keyword to be non-ident.
1098        // In "passworded", after "password" comes "e" which IS an ident char,
1099        // so it should NOT be matched.
1100        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        // Both keywords should be masked
1108        let lower = masked.to_lowercase();
1109        assert!(!lower.contains("password"));
1110        assert!(!lower.contains("token"));
1111        // Verify there are at least 2 mask replacements
1112        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        // Read back the file and verify it contains valid JSON
1142        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        // Verify masking was applied during log()
1149        assert!(!parsed[0].sql.contains("password"));
1150        // Cleanup
1151        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        // Backward compatibility: the original test asserts that masking
1175        // "SELECT * FROM users WHERE password='secret'" removes "password".
1176        let a = SqlAuditor::new();
1177        let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
1178        assert!(!masked.contains("password"));
1179    }
1180
1181    // ===== 审计规则配置测试 =====
1182
1183    #[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        // 包含 password 的 SELECT 应被拒绝
1211        assert!(!rules.should_audit("SELECT * FROM users WHERE password='x'"));
1212        // 不含 password 的 SELECT 应被允许
1213        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    // ===== 轮转策略测试 =====
1237
1238    #[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        // 旧日志 4000ms 前,未超时
1257        assert!(!policy.needs_rotation(10, 5000, 9000));
1258        // 旧日志 6000ms 前,已超时
1259        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        // 大小未达上限且时间未超 → 不轮转
1266        assert!(!policy.needs_rotation(50, 5000, 9000));
1267        // 大小达上限 → 轮转
1268        assert!(policy.needs_rotation(100, 5000, 5000));
1269        // 时间超 → 轮转
1270        assert!(policy.needs_rotation(10, 5000, 11000));
1271    }
1272
1273    // ===== RotatingAuditor 测试 =====
1274
1275    #[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        // 第 6 条触发轮转
1294        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        // 时间差超过 1000ms → 轮转
1307        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    // ===== 异步审计写入器测试 =====
1357
1358    #[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    // ===== 审计日志查询过滤测试 =====
1408
1409    #[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    // ===== 审计日志持久化存储测试 =====
1526
1527    #[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        // 清理可能残留的旧文件
1533        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        // 落盘内容应已脱敏
1570        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        // 清空后读取应返回空
1590        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        // 清空不存在的文件不应报错
1598        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        // 确保文件不存在
1602        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        // 未写入任何内容,read_all 应返回空 vec(文件不存在视为空)
1614        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        // 手动追加空行模拟人工编辑或异常写入
1629
1630        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        // 空行应被跳过,只解析到 2 条有效日志
1643        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        // 4 线程 × 10 条 = 40 条,全部应成功落盘
1677        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        // 验证 FileAuditLogStore 可作为 trait object 使用
1686        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    // ===== #11 修复:哈希链防篡改测试 =====
1699
1700    #[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        // 空链应通过验证
1706        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        // 首条记录的 prev_hash 必须为 GENESIS_HASH
1717        assert_eq!(entries[0].prev_hash, GENESIS_HASH);
1718        // current_hash 必须为 64 字符的十六进制串
1719        assert_eq!(entries[0].current_hash.len(), 64);
1720        // 验证链完整
1721        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        // 验证相邻记录的 prev_hash 链接
1734        assert_eq!(entries[1].prev_hash, entries[0].current_hash);
1735        assert_eq!(entries[2].prev_hash, entries[1].current_hash);
1736        // 验证链完整
1737        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        // 篡改第一条记录的 SQL(模拟攻击者修改历史日志)
1747        {
1748            let mut entries = auditor.entries.lock().unwrap();
1749            entries[0].entry.sql = "DROP TABLE users".to_string();
1750        }
1751
1752        // 验证应失败
1753        let result = auditor.verify();
1754        assert!(result.is_err());
1755        let err = result.unwrap_err();
1756        // 错误信息应包含篡改位置
1757        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        // 篡改第二条记录的 prev_hash(模拟删除中间记录)
1768        {
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        // 篡改首条记录的 prev_hash(模拟裁剪链首)
1785        {
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        // 链中存储的 SQL 应已被脱敏
1807        assert!(!entries[0].entry.sql.contains("password"));
1808        assert!(!entries[0].entry.sql.contains("secret"));
1809        assert!(entries[0].entry.sql.contains("******"));
1810        // 脱敏后的链仍应通过验证
1811        assert!(auditor.verify().is_ok());
1812    }
1813
1814    #[test]
1815    fn test_hash_chain_deterministic_hashes() {
1816        // 相同输入应产生相同哈希(便于跨节点对账)
1817        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        // 验证文件存在且非空
1843        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        // 4 线程 × 25 条 = 100 条
1870        assert_eq!(auditor.len(), 100);
1871        // 并发写入后链仍应完整
1872        assert!(auditor.verify().is_ok());
1873    }
1874
1875    #[test]
1876    fn test_genesis_hash_constant_is_64_zeros() {
1877        // 验证 GENESIS_HASH 为 64 字符全零(SHA-256 输出长度)
1878        assert_eq!(GENESIS_HASH.len(), 64);
1879        assert!(GENESIS_HASH.chars().all(|c| c == '0'));
1880    }
1881}