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 = "lineage-viz")]
22pub use lineage::{downstream_impact, upstream_trace, ImpactEdge};
23#[cfg(feature = "data-lineage")]
24pub use lineage::{
25    EdgeType, LineageDialect, LineageEdge, LineageError, LineageExportFormat, LineageGraph,
26    LineageNode, LineageNodeId, LineageTracker, LineageUpdate, NodeType,
27};
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SqlAuditContext {
31    pub sql: String,
32    pub user: String,
33    pub timestamp: i64,
34}
35
36/// Sensitive keywords that should be masked in audit logs. Matching is
37/// case-insensitive on the ASCII bytes of the SQL string.
38const SENSITIVE_KEYWORDS: &[&str] = &[
39    "password",
40    "pwd",
41    "passwd",
42    "secret",
43    "token",
44    "api_key",
45    "apikey",
46    "access_key",
47    "accesskey",
48    "session",
49    "credit_card",
50    "creditcard",
51    "cvv",
52    "ssn",
53];
54
55pub struct SqlAuditor {
56    logs: Mutex<Vec<SqlAuditContext>>,
57}
58
59impl SqlAuditor {
60    pub fn new() -> Self {
61        Self {
62            logs: Mutex::new(vec![]),
63        }
64    }
65
66    /// Log an audit entry. The SQL is masked for sensitive keywords before
67    /// being stored in the in-memory buffer.
68    pub fn log(&self, ctx: &SqlAuditContext) {
69        let masked_sql = mask_sensitive(&ctx.sql);
70        let entry = SqlAuditContext {
71            sql: masked_sql,
72            user: ctx.user.clone(),
73            timestamp: ctx.timestamp,
74        };
75        let mut logs = self
76            .logs
77            .lock()
78            .expect("SqlAuditor logs lock poisoned (log)");
79        logs.push(entry);
80    }
81
82    /// Return a snapshot of all stored audit entries.
83    pub fn get_logs(&self) -> Vec<SqlAuditContext> {
84        let logs = self
85            .logs
86            .lock()
87            .expect("SqlAuditor logs lock poisoned (get_logs)");
88        logs.iter().cloned().collect()
89    }
90
91    /// Flush all stored audit entries to a JSON file at `path`.
92    /// Returns the number of entries written.
93    pub fn flush(&self, path: &str) -> Result<usize, String> {
94        let logs = self
95            .logs
96            .lock()
97            .expect("SqlAuditor logs lock poisoned (flush)");
98        let snapshot: Vec<&SqlAuditContext> = logs.iter().collect();
99        let json = serde_json::to_string_pretty(&snapshot).map_err(|e| e.to_string())?;
100        std::fs::write(path, json).map_err(|e| e.to_string())?;
101        Ok(logs.len())
102    }
103
104    /// Mask all sensitive keywords in `sql` with `******`. Matching is
105    /// case-insensitive.
106    pub fn mask_sensitive(&self, sql: &str) -> String {
107        mask_sensitive(sql)
108    }
109}
110
111impl Default for SqlAuditor {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117/// Mask all sensitive keywords in `sql` with `******`. Matching is
118/// case-insensitive over the ASCII bytes of the string.
119fn mask_sensitive(sql: &str) -> String {
120    let lower = sql.to_ascii_lowercase();
121    let mut result = String::with_capacity(sql.len());
122    let mut i = 0;
123    let bytes = sql.as_bytes();
124    let lower_bytes = lower.as_bytes();
125    while i < bytes.len() {
126        let mut matched_len: Option<usize> = None;
127        for keyword in SENSITIVE_KEYWORDS {
128            let kw_bytes = keyword.as_bytes();
129            if i + kw_bytes.len() <= bytes.len() && &lower_bytes[i..i + kw_bytes.len()] == kw_bytes
130            {
131                // Only treat as a keyword match if it's not part of a longer identifier.
132                // Boundary check: previous and next char must be non-alphanumeric/underscore
133                let prev_ok = i == 0 || !is_ident_char(bytes[i - 1]);
134                let next_idx = i + kw_bytes.len();
135                let next_ok = next_idx >= bytes.len() || !is_ident_char(bytes[next_idx]);
136                if prev_ok && next_ok {
137                    matched_len = Some(kw_bytes.len());
138                    break;
139                }
140            }
141        }
142        if let Some(kw_len) = matched_len {
143            result.push_str("******");
144            i += kw_len;
145        } else {
146            // Push one char (handles UTF-8 properly since we step by char).
147            let ch = sql[i..]
148                .chars()
149                .next()
150                .expect("i < bytes.len() guarantees non-empty slice");
151            result.push(ch);
152            i += ch.len_utf8();
153        }
154    }
155    result
156}
157
158/// Returns true if `b` is an ASCII identifier character (alphanumeric or '_').
159fn is_ident_char(b: u8) -> bool {
160    b.is_ascii_alphanumeric() || b == b'_'
161}
162
163// ============================================================================
164// 审计规则配置(允许/拒绝列表)
165// ============================================================================
166
167/// Audit rules: allow/deny lists used to decide whether a given SQL statement
168/// should be recorded in the audit log.
169///
170/// Rule evaluation order:
171/// 1. If the SQL matches any pattern in the deny list → not recorded
172/// 2. If the allow list is empty → record all SQL not denied
173/// 3. If the allow list is non-empty → record only SQL that matches the allow list
174#[derive(Debug, Clone, Default)]
175pub struct AuditRules {
176    /// Allow-list patterns (case-insensitive substring match); empty means allow all
177    allow_patterns: Vec<String>,
178    /// Deny-list patterns (case-insensitive substring match); a match means deny
179    deny_patterns: Vec<String>,
180}
181
182impl AuditRules {
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Add an allow pattern (case-insensitive substring match)
188    pub fn allow(mut self, pattern: impl Into<String>) -> Self {
189        self.allow_patterns
190            .push(pattern.into().to_ascii_lowercase());
191        self
192    }
193
194    /// Add a deny pattern (case-insensitive substring match)
195    pub fn deny(mut self, pattern: impl Into<String>) -> Self {
196        self.deny_patterns.push(pattern.into().to_ascii_lowercase());
197        self
198    }
199
200    /// Determine whether the given SQL should be audit-recorded
201    pub fn should_audit(&self, sql: &str) -> bool {
202        let lower = sql.to_ascii_lowercase();
203        // 拒绝列表优先
204        for pat in &self.deny_patterns {
205            if lower.contains(pat) {
206                return false;
207            }
208        }
209        // 允许列表为空 → 允许所有
210        if self.allow_patterns.is_empty() {
211            return true;
212        }
213        // 允许列表非空 → 仅允许命中项
214        self.allow_patterns.iter().any(|pat| lower.contains(pat))
215    }
216
217    /// Return the number of allow patterns
218    pub fn allow_count(&self) -> usize {
219        self.allow_patterns.len()
220    }
221
222    /// Return the number of deny patterns
223    pub fn deny_count(&self) -> usize {
224        self.deny_patterns.len()
225    }
226}
227
228// ============================================================================
229// 审计日志轮转策略(按大小/时间)
230// ============================================================================
231
232/// Audit log rotation policy configuration.
233///
234/// - `max_entries`: maximum number of log entries retained in memory; once
235///   exceeded, rotation is triggered automatically (old logs are cleared or
236///   flushed to disk)
237/// - `max_age_ms`: maximum age of a log entry in milliseconds; once exceeded,
238///   rotation is triggered
239#[derive(Debug, Clone)]
240pub struct RotationPolicy {
241    /// Maximum entry count (0 means unlimited)
242    pub max_entries: usize,
243    /// Maximum age in milliseconds (0 means unlimited)
244    pub max_age_ms: i64,
245}
246
247impl RotationPolicy {
248    /// Create an unlimited rotation policy
249    pub fn none() -> Self {
250        Self {
251            max_entries: 0,
252            max_age_ms: 0,
253        }
254    }
255
256    /// Rotate by entry count
257    pub fn by_size(max_entries: usize) -> Self {
258        Self {
259            max_entries,
260            max_age_ms: 0,
261        }
262    }
263
264    /// Rotate by age (milliseconds)
265    pub fn by_age(max_age_ms: i64) -> Self {
266        Self {
267            max_entries: 0,
268            max_age_ms,
269        }
270    }
271
272    /// Rotate by both entry count and age
273    pub fn by_size_and_age(max_entries: usize, max_age_ms: i64) -> Self {
274        Self {
275            max_entries,
276            max_age_ms,
277        }
278    }
279
280    /// Determine whether rotation is needed
281    fn needs_rotation(&self, entry_count: usize, oldest_ts: i64, now_ts: i64) -> bool {
282        if self.max_entries > 0 && entry_count >= self.max_entries {
283            return true;
284        }
285        if self.max_age_ms > 0 && oldest_ts > 0 && (now_ts - oldest_ts) > self.max_age_ms {
286            return true;
287        }
288        false
289    }
290}
291
292impl Default for RotationPolicy {
293    fn default() -> Self {
294        Self::none()
295    }
296}
297
298// ============================================================================
299// 带轮转和规则的审计器
300// ============================================================================
301
302/// Enhanced auditor with rotation policy and audit rules.
303///
304/// Built on top of `SqlAuditor`, it adds:
305/// - Log rotation (automatic cleanup of old logs by size or age)
306/// - Audit rules (allow/deny list filtering)
307pub struct RotatingAuditor {
308    logs: Mutex<Vec<SqlAuditContext>>,
309    rules: AuditRules,
310    policy: RotationPolicy,
311    /// Number of rotations (cleanups) performed
312    rotations: Mutex<usize>,
313}
314
315impl RotatingAuditor {
316    pub fn new(policy: RotationPolicy, rules: AuditRules) -> Self {
317        Self {
318            logs: Mutex::new(vec![]),
319            rules,
320            policy,
321            rotations: Mutex::new(0),
322        }
323    }
324
325    /// Create an auditor that rotates only by size, with no rule filtering
326    pub fn with_max_entries(max_entries: usize) -> Self {
327        Self::new(RotationPolicy::by_size(max_entries), AuditRules::new())
328    }
329
330    /// Create an auditor that rotates only by age, with no rule filtering
331    pub fn with_max_age(max_age_ms: i64) -> Self {
332        Self::new(RotationPolicy::by_age(max_age_ms), AuditRules::new())
333    }
334
335    /// Record an audit log, automatically applying rule filtering and rotation policy
336    pub fn log(&self, ctx: &SqlAuditContext) -> bool {
337        // 规则过滤
338        if !self.rules.should_audit(&ctx.sql) {
339            return false;
340        }
341        let masked_sql = mask_sensitive(&ctx.sql);
342        let entry = SqlAuditContext {
343            sql: masked_sql,
344            user: ctx.user.clone(),
345            timestamp: ctx.timestamp,
346        };
347        let mut logs = self
348            .logs
349            .lock()
350            .expect("RotatingAuditor logs lock poisoned (log)");
351
352        // 在添加新条目前检查轮转(确保新条目不被立即清除)
353        let now = ctx.timestamp;
354        let oldest = logs.first().map(|e| e.timestamp).unwrap_or(now);
355        if self.policy.needs_rotation(logs.len(), oldest, now) {
356            logs.clear();
357            *self
358                .rotations
359                .lock()
360                .expect("RotatingAuditor rotations lock poisoned (log)") += 1;
361        }
362
363        logs.push(entry);
364        true
365    }
366
367    /// Return a snapshot of the current logs
368    pub fn get_logs(&self) -> Vec<SqlAuditContext> {
369        self.logs
370            .lock()
371            .expect("RotatingAuditor logs lock poisoned (get_logs)")
372            .clone()
373    }
374
375    /// Return the number of rotations performed
376    pub fn rotation_count(&self) -> usize {
377        *self
378            .rotations
379            .lock()
380            .expect("RotatingAuditor rotations lock poisoned (rotation_count)")
381    }
382
383    /// Manually trigger rotation (clears the current logs)
384    pub fn rotate(&self) -> usize {
385        let mut logs = self
386            .logs
387            .lock()
388            .expect("RotatingAuditor logs lock poisoned (rotate)");
389        let count = logs.len();
390        logs.clear();
391        *self
392            .rotations
393            .lock()
394            .expect("RotatingAuditor rotations lock poisoned (rotate)") += 1;
395        count
396    }
397
398    /// Return the current number of log entries
399    pub fn len(&self) -> usize {
400        self.logs
401            .lock()
402            .expect("RotatingAuditor logs lock poisoned (len)")
403            .len()
404    }
405
406    /// Whether the auditor is empty
407    pub fn is_empty(&self) -> bool {
408        self.logs
409            .lock()
410            .expect("RotatingAuditor logs lock poisoned (is_empty)")
411            .is_empty()
412    }
413}
414
415// ============================================================================
416// 异步审计写入器
417// ============================================================================
418
419/// Asynchronous audit writer: writes audit logs via a background thread to
420/// avoid blocking the main thread.
421///
422/// Uses an `std::sync::mpsc` channel to send logs to a background thread that
423/// handles storage. Call `shutdown` to wait for the background thread to exit
424/// and return all written logs.
425pub struct AsyncAuditWriter {
426    sender: std::sync::mpsc::Sender<AsyncCommand>,
427    handle: Mutex<Option<std::thread::JoinHandle<Vec<SqlAuditContext>>>>,
428}
429
430enum AsyncCommand {
431    Log(SqlAuditContext),
432    Shutdown,
433}
434
435impl AsyncAuditWriter {
436    /// Create an async writer and start the background thread
437    pub fn new() -> Self {
438        let (sender, receiver) = std::sync::mpsc::channel::<AsyncCommand>();
439        let handle = std::thread::spawn(move || {
440            let mut logs: Vec<SqlAuditContext> = Vec::new();
441            for cmd in receiver {
442                match cmd {
443                    AsyncCommand::Log(ctx) => {
444                        let masked_sql = mask_sensitive(&ctx.sql);
445                        logs.push(SqlAuditContext {
446                            sql: masked_sql,
447                            user: ctx.user,
448                            timestamp: ctx.timestamp,
449                        });
450                    }
451                    AsyncCommand::Shutdown => break,
452                }
453            }
454            logs
455        });
456        Self {
457            sender,
458            handle: Mutex::new(Some(handle)),
459        }
460    }
461
462    /// Asynchronously record an audit log (non-blocking)
463    pub fn log(&self, ctx: &SqlAuditContext) -> Result<(), String> {
464        self.sender
465            .send(AsyncCommand::Log(ctx.clone()))
466            .map_err(|e| format!("AsyncAuditWriter channel closed: {}", e))
467    }
468
469    /// Shut down the background thread and return all written logs
470    pub fn shutdown(&self) -> Result<Vec<SqlAuditContext>, String> {
471        let _ = self.sender.send(AsyncCommand::Shutdown);
472        let mut handle_guard = self
473            .handle
474            .lock()
475            .expect("AsyncAuditWriter handle lock poisoned (shutdown)");
476        if let Some(handle) = handle_guard.take() {
477            handle
478                .join()
479                .map_err(|e| format!("Thread panicked: {:?}", e))
480        } else {
481            Err("Already shut down".to_string())
482        }
483    }
484}
485
486impl Default for AsyncAuditWriter {
487    fn default() -> Self {
488        Self::new()
489    }
490}
491
492// ============================================================================
493// 审计日志查询过滤
494// ============================================================================
495
496/// Audit log query filter
497#[derive(Debug, Clone, Default)]
498pub struct AuditQuery {
499    /// Filter by username (exact match; None means no filter)
500    pub user: Option<String>,
501    /// Time range start (millisecond timestamp; None means no lower bound)
502    pub from_ts: Option<i64>,
503    /// Time range end (millisecond timestamp; None means no upper bound)
504    pub to_ts: Option<i64>,
505    /// SQL keyword filter (case-insensitive substring match; None means no filter)
506    pub sql_contains: Option<String>,
507    /// Limit on the number of returned entries (0 means unlimited)
508    pub limit: usize,
509}
510
511impl AuditQuery {
512    pub fn new() -> Self {
513        Self::default()
514    }
515
516    /// Filter by username
517    pub fn by_user(mut self, user: impl Into<String>) -> Self {
518        self.user = Some(user.into());
519        self
520    }
521
522    /// Filter by time range (millisecond timestamps)
523    pub fn by_time_range(mut self, from: i64, to: i64) -> Self {
524        self.from_ts = Some(from);
525        self.to_ts = Some(to);
526        self
527    }
528
529    /// Filter by SQL keyword (case-insensitive)
530    pub fn by_sql_contains(mut self, keyword: impl Into<String>) -> Self {
531        self.sql_contains = Some(keyword.into());
532        self
533    }
534
535    /// Limit the number of returned entries
536    pub fn with_limit(mut self, limit: usize) -> Self {
537        self.limit = limit;
538        self
539    }
540
541    /// Apply the query filter to a list of logs
542    pub fn filter(&self, logs: &[SqlAuditContext]) -> Vec<SqlAuditContext> {
543        let keyword_lower = self.sql_contains.as_ref().map(|s| s.to_ascii_lowercase());
544        let mut result: Vec<SqlAuditContext> = logs
545            .iter()
546            .filter(|entry| {
547                if let Some(u) = &self.user {
548                    if entry.user != *u {
549                        return false;
550                    }
551                }
552                if let Some(from) = self.from_ts {
553                    if entry.timestamp < from {
554                        return false;
555                    }
556                }
557                if let Some(to) = self.to_ts {
558                    if entry.timestamp > to {
559                        return false;
560                    }
561                }
562                if let Some(kw) = &keyword_lower {
563                    if !entry.sql.to_ascii_lowercase().contains(kw) {
564                        return false;
565                    }
566                }
567                true
568            })
569            .cloned()
570            .collect();
571        if self.limit > 0 && result.len() > self.limit {
572            result.truncate(self.limit);
573        }
574        result
575    }
576}
577
578/// Query logs from `SqlAuditor` by the given conditions
579pub fn query_logs(auditor: &SqlAuditor, query: &AuditQuery) -> Vec<SqlAuditContext> {
580    let logs = auditor.get_logs();
581    query.filter(&logs)
582}
583
584// ============================================================================
585// 审计日志持久化存储
586// ============================================================================
587
588/// Audit log persistent storage backend trait
589///
590/// Abstracts the persistent storage capability for audit logs across different
591/// storage media (file, database, object storage, etc.). Implementors must
592/// ensure that `append` is thread-safe; they should call `mask_sensitive`
593/// themselves before storage to mask sensitive data.
594pub trait AuditLogStore: Send + Sync {
595    /// Append an audit log entry (already masked); returns whether it succeeded
596    fn append(&self, entry: &SqlAuditContext) -> Result<(), String>;
597    /// Read all persisted audit logs
598    fn read_all(&self) -> Result<Vec<SqlAuditContext>, String>;
599    /// Clear the persistent storage
600    fn clear(&self) -> Result<(), String>;
601}
602
603/// File-based audit log persistent storage (JSONL format: one JSON per line)
604///
605/// Applicable scenarios: single-machine deployment, lightweight audit archiving,
606/// development debugging. For high-concurrency production scenarios, it is
607/// recommended to combine `AsyncAuditWriter` + `FileAuditLogStore` so that a
608/// background thread performs serial writes and avoids lock contention.
609pub struct FileAuditLogStore {
610    path: String,
611    write_lock: Mutex<()>,
612}
613
614impl FileAuditLogStore {
615    /// Create a file audit log store; the target file is created on the first
616    /// `append` if it does not exist
617    pub fn new(path: impl Into<String>) -> Self {
618        Self {
619            path: path.into(),
620            write_lock: Mutex::new(()),
621        }
622    }
623
624    /// Return the storage file path
625    pub fn path(&self) -> &str {
626        &self.path
627    }
628}
629
630impl AuditLogStore for FileAuditLogStore {
631    /// Append an audit log entry (JSONL format; automatically masked before write)
632    fn append(&self, entry: &SqlAuditContext) -> Result<(), String> {
633        let _guard = self
634            .write_lock
635            .lock()
636            .map_err(|e| format!("write_lock poisoned: {}", e))?;
637        // 脱敏后序列化,确保落盘内容不含敏感信息
638        let masked_sql = mask_sensitive(&entry.sql);
639        let stored = SqlAuditContext {
640            sql: masked_sql,
641            user: entry.user.clone(),
642            timestamp: entry.timestamp,
643        };
644        let line = serde_json::to_string(&stored).map_err(|e| e.to_string())?;
645        // 以追加模式打开,每条日志占一行(JSONL)
646        use std::io::Write;
647        let mut file = std::fs::OpenOptions::new()
648            .create(true)
649            .append(true)
650            .open(&self.path)
651            .map_err(|e| format!("open '{}' failed: {}", self.path, e))?;
652        writeln!(file, "{}", line).map_err(|e| e.to_string())
653    }
654
655    /// Read all persisted audit logs (parse JSONL line by line)
656    ///
657    /// Returns an empty vec when the file does not exist (treated as no logs
658    /// persisted yet).
659    fn read_all(&self) -> Result<Vec<SqlAuditContext>, String> {
660        let content = match std::fs::read_to_string(&self.path) {
661            Ok(c) => c,
662            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
663                return Ok(Vec::new());
664            }
665            Err(e) => return Err(format!("read failed: {}", e)),
666        };
667        let mut result = Vec::new();
668        for (lineno, line) in content.lines().enumerate() {
669            let line = line.trim();
670            if line.is_empty() {
671                continue;
672            }
673            let entry: SqlAuditContext = serde_json::from_str(line)
674                .map_err(|e| format!("parse line {} failed: {}", lineno + 1, e))?;
675            result.push(entry);
676        }
677        Ok(result)
678    }
679
680    /// Clear the persistent file (deletes the file; the next append recreates it)
681    fn clear(&self) -> Result<(), String> {
682        let _guard = self
683            .write_lock
684            .lock()
685            .map_err(|e| format!("write_lock poisoned: {}", e))?;
686        std::fs::remove_file(&self.path).or_else(|e| {
687            // 文件不存在视为已清空
688            if e.kind() == std::io::ErrorKind::NotFound {
689                Ok(())
690            } else {
691                Err(format!("clear failed: {}", e))
692            }
693        })
694    }
695}
696
697// ============================================================================
698// #11 修复:审计日志哈希链防篡改(Tamper-Evident Hash Chain)
699// ============================================================================
700
701/// Genesis hash (the previous hash of the chain head), fixed as an all-zero
702/// 64-character hexadecimal string.
703///
704/// The first record of every hash chain uses `GENESIS_HASH` as its `prev_hash`,
705/// making it easy to verify that the chain start has not been truncated.
706pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
707
708/// Audit log entry with a hash chain.
709///
710/// Each record contains:
711/// - `prev_hash`: the `current_hash` of the previous record (the first record
712///   uses `GENESIS_HASH`)
713/// - `current_hash`: the SHA-256 hash of this record (computed from
714///   `prev_hash + entry`)
715/// - `entry`: the original audit context (already masked)
716///
717/// Any tampering with a historical record causes `current_hash` to mismatch the
718/// `prev_hash` of the next record, which is detected by `verify_chain`.
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct HashChainEntry {
721    /// Hash of the previous record (the first record uses [`GENESIS_HASH`])
722    pub prev_hash: String,
723    /// Hash of this record (SHA-256 hexadecimal string, 64 characters)
724    pub current_hash: String,
725    /// Original audit context (already masked)
726    pub entry: SqlAuditContext,
727}
728
729impl HashChainEntry {
730    /// Compute the `current_hash` of a single record.
731    ///
732    /// The hash input is the UTF-8 byte concatenation of
733    /// `prev_hash || sql || user || timestamp`, using the SHA-256 algorithm.
734    /// This way, tampering with any field changes the hash, which in turn
735    /// affects the `prev_hash` of the next record, forming a chain of
736    /// verification.
737    fn compute_hash(prev_hash: &str, entry: &SqlAuditContext) -> String {
738        use sha2::{Digest, Sha256};
739        let mut hasher = Sha256::new();
740        hasher.update(prev_hash.as_bytes());
741        hasher.update(entry.sql.as_bytes());
742        hasher.update(entry.user.as_bytes());
743        // timestamp 使用固定宽度的字节表示,避免可变长度编码导致歧义
744        hasher.update(entry.timestamp.to_le_bytes());
745        let result = hasher.finalize();
746        // 转为小写十六进制字符串(64 字符)
747        hex_encode(&result)
748    }
749
750    /// Create the chain head record (prev_hash = GENESIS_HASH)
751    pub fn genesis(entry: SqlAuditContext) -> Self {
752        let prev_hash = GENESIS_HASH.to_string();
753        let current_hash = Self::compute_hash(&prev_hash, &entry);
754        Self {
755            prev_hash,
756            current_hash,
757            entry,
758        }
759    }
760
761    /// Append a record to the given previous hash
762    pub fn append(prev_hash: &str, entry: SqlAuditContext) -> Self {
763        let current_hash = Self::compute_hash(prev_hash, &entry);
764        Self {
765            prev_hash: prev_hash.to_string(),
766            current_hash,
767            entry,
768        }
769    }
770}
771
772/// Encode a byte array as a lowercase hexadecimal string.
773///
774/// Consistent with the behavior of the `hex` crate's `encode`, but avoids
775/// introducing an extra dependency.
776fn hex_encode(bytes: &[u8]) -> String {
777    const HEX_CHARS: &[u8] = b"0123456789abcdef";
778    let mut s = String::with_capacity(bytes.len() * 2);
779    for &b in bytes {
780        s.push(HEX_CHARS[(b >> 4) as usize] as char);
781        s.push(HEX_CHARS[(b & 0x0f) as usize] as char);
782    }
783    s
784}
785
786/// Auditor with a hash chain: all logs are linked by SHA-256 chained hashes,
787/// supporting tamper detection.
788///
789/// # Tamper-Evidence Mechanism
790///
791/// 1. Each record's `current_hash = SHA256(prev_hash || sql || user || timestamp)`
792/// 2. The `prev_hash` of the next record equals the `current_hash` of the
793///    previous record
794/// 3. Any modification to a historical record changes `current_hash`, which
795///    then mismatches the `prev_hash` of the next record
796/// 4. Deleting a middle record breaks the chain; inserting a record changes
797///    all subsequent hashes
798///
799/// # Example
800///
801/// ```
802/// use sz_orm_audit::{HashChainAuditor, SqlAuditContext};
803///
804/// let mut auditor = HashChainAuditor::new();
805/// auditor.log(&SqlAuditContext {
806///     sql: "SELECT * FROM users".to_string(),
807///     user: "admin".to_string(),
808///     timestamp: 1000,
809/// });
810/// // Verify chain integrity
811/// assert!(auditor.verify().is_ok());
812/// ```
813pub struct HashChainAuditor {
814    /// Hash chain log entries (stored in append order)
815    entries: Mutex<Vec<HashChainEntry>>,
816}
817
818impl Default for HashChainAuditor {
819    fn default() -> Self {
820        Self::new()
821    }
822}
823
824impl HashChainAuditor {
825    /// Create an empty hash chain auditor
826    pub fn new() -> Self {
827        Self {
828            entries: Mutex::new(Vec::new()),
829        }
830    }
831
832    /// Append an audit log to the end of the hash chain.
833    ///
834    /// - If the chain is empty, uses [`GENESIS_HASH`] as `prev_hash`
835    /// - Otherwise uses the `current_hash` of the last record as `prev_hash`
836    ///
837    /// The SQL is first masked by `mask_sensitive` before being written to the
838    /// chain, ensuring that the stored audit logs contain no sensitive
839    /// information.
840    pub fn log(&self, ctx: &SqlAuditContext) {
841        let masked_sql = mask_sensitive(&ctx.sql);
842        let entry = SqlAuditContext {
843            sql: masked_sql,
844            user: ctx.user.clone(),
845            timestamp: ctx.timestamp,
846        };
847        let mut entries = self
848            .entries
849            .lock()
850            .expect("HashChainAuditor entries lock poisoned (log)");
851        let prev_hash = entries
852            .last()
853            .map(|e| e.current_hash.as_str())
854            .unwrap_or(GENESIS_HASH);
855        let chain_entry = if entries.is_empty() {
856            HashChainEntry::genesis(entry)
857        } else {
858            HashChainEntry::append(prev_hash, entry)
859        };
860        entries.push(chain_entry);
861    }
862
863    /// Return a snapshot (clone) of all log entries
864    pub fn get_entries(&self) -> Vec<HashChainEntry> {
865        self.entries
866            .lock()
867            .expect("HashChainAuditor entries lock poisoned (get_entries)")
868            .clone()
869    }
870
871    /// Return the number of log entries
872    pub fn len(&self) -> usize {
873        self.entries
874            .lock()
875            .expect("HashChainAuditor entries lock poisoned (len)")
876            .len()
877    }
878
879    /// Whether the auditor is empty
880    pub fn is_empty(&self) -> bool {
881        self.entries
882            .lock()
883            .expect("HashChainAuditor entries lock poisoned (is_empty)")
884            .is_empty()
885    }
886
887    /// Verify the integrity of the hash chain.
888    ///
889    /// Checks performed:
890    /// 1. The `prev_hash` of the first record equals [`GENESIS_HASH`]
891    /// 2. The `current_hash` of each record equals
892    ///    `compute_hash(prev_hash, entry)`
893    /// 3. The `prev_hash` of each adjacent record equals the `current_hash` of
894    ///    the previous record
895    ///
896    /// # Return value
897    ///
898    /// - `Ok(())`: the chain is intact and has not been tampered with
899    /// - `Err(reason)`: the chain has been tampered with; `reason` describes
900    ///   the location and type of the first anomaly
901    pub fn verify(&self) -> Result<(), String> {
902        let entries = self
903            .entries
904            .lock()
905            .expect("HashChainAuditor entries lock poisoned (verify)");
906        for (i, entry) in entries.iter().enumerate() {
907            // 检查 1:首条记录的 prev_hash 必须为 GENESIS_HASH
908            if i == 0 {
909                if entry.prev_hash != GENESIS_HASH {
910                    return Err(format!(
911                        "chain genesis prev_hash mismatch at index 0: expected '{}', got '{}'",
912                        GENESIS_HASH, entry.prev_hash
913                    ));
914                }
915            } else {
916                // 检查 3:非首条记录的 prev_hash 必须等于上一条的 current_hash
917                let prev = &entries[i - 1];
918                if entry.prev_hash != prev.current_hash {
919                    return Err(format!(
920                        "chain broken at index {}: prev_hash '{}' != previous current_hash '{}'",
921                        i, entry.prev_hash, prev.current_hash
922                    ));
923                }
924            }
925            // 检查 2:current_hash 必须等于重新计算的哈希
926            let recomputed = HashChainEntry::compute_hash(&entry.prev_hash, &entry.entry);
927            if entry.current_hash != recomputed {
928                return Err(format!(
929                    "hash mismatch at index {}: stored '{}' != recomputed '{}'",
930                    i, entry.current_hash, recomputed
931                ));
932            }
933        }
934        Ok(())
935    }
936
937    /// Persist the hash chain to a JSONL file (one JSON per line).
938    ///
939    /// Returns the number of entries written.
940    pub fn flush(&self, path: &str) -> Result<usize, String> {
941        let entries = self
942            .entries
943            .lock()
944            .expect("HashChainAuditor entries lock poisoned (flush)");
945        let snapshot: Vec<&HashChainEntry> = entries.iter().collect();
946        let json = serde_json::to_string_pretty(&snapshot).map_err(|e| e.to_string())?;
947        std::fs::write(path, json).map_err(|e| e.to_string())?;
948        Ok(entries.len())
949    }
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955
956    /// Test data directory: prefers F:\test\data (user convention), falls back
957    /// to the environment variable or system temp (CI/Linux).
958    ///
959    /// Note: checking only that the directory exists is not enough to guarantee
960    /// usability — write permission must also be verified, to avoid test
961    /// failures in restricted sandbox environments (e.g. TRAE Sandbox) where
962    /// the directory exists but is not writable.
963    fn test_data_dir() -> std::path::PathBuf {
964        let f_drive = std::path::Path::new("F:\\test\\data");
965        if is_dir_writable(f_drive) {
966            return f_drive.to_path_buf();
967        }
968        if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
969            let p = std::path::PathBuf::from(&dir);
970            if is_dir_writable(&p) {
971                return p;
972            }
973        }
974        std::env::temp_dir()
975    }
976
977    /// Check whether the directory exists and is writable: try to create and
978    /// delete a probe file inside it
979    fn is_dir_writable(dir: &std::path::Path) -> bool {
980        if !dir.exists() {
981            return false;
982        }
983        let probe = dir.join(format!(".probe_{}", std::process::id()));
984        match std::fs::File::create(&probe) {
985            Ok(_) => {
986                let _ = std::fs::remove_file(&probe);
987                true
988            }
989            Err(_) => false,
990        }
991    }
992
993    fn ctx(sql: &str, user: &str, ts: i64) -> SqlAuditContext {
994        SqlAuditContext {
995            sql: sql.to_string(),
996            user: user.to_string(),
997            timestamp: ts,
998        }
999    }
1000
1001    #[test]
1002    fn test_log_stores_in_memory() {
1003        let a = SqlAuditor::new();
1004        a.log(&ctx("SELECT * FROM users", "admin", 1000));
1005        a.log(&ctx("INSERT INTO logs VALUES(1)", "admin", 1001));
1006        let logs = a.get_logs();
1007        assert_eq!(logs.len(), 2);
1008        assert_eq!(logs[0].sql, "SELECT * FROM users");
1009        assert_eq!(logs[0].user, "admin");
1010        assert_eq!(logs[0].timestamp, 1000);
1011        assert_eq!(logs[1].timestamp, 1001);
1012    }
1013
1014    #[test]
1015    fn test_log_masks_sensitive_in_storage() {
1016        let a = SqlAuditor::new();
1017        a.log(&ctx(
1018            "SELECT * FROM users WHERE password='secret'",
1019            "admin",
1020            1000,
1021        ));
1022        let logs = a.get_logs();
1023        assert_eq!(logs.len(), 1);
1024        let stored_sql = &logs[0].sql;
1025        assert!(!stored_sql.contains("password"));
1026        assert!(!stored_sql.contains("secret"));
1027        assert!(stored_sql.contains("******"));
1028    }
1029
1030    #[test]
1031    fn test_mask_sensitive_password() {
1032        let a = SqlAuditor::new();
1033        let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
1034        assert!(!masked.contains("password"));
1035        assert!(!masked.contains("secret"));
1036        assert!(masked.contains("******"));
1037    }
1038
1039    #[test]
1040    fn test_mask_sensitive_case_insensitive() {
1041        let a = SqlAuditor::new();
1042        let masked = a.mask_sensitive("UPDATE users SET PASSWORD='abc', Token='x'");
1043        let lower = masked.to_lowercase();
1044        assert!(!lower.contains("password"));
1045        assert!(!lower.contains("token"));
1046        assert!(masked.contains("******"));
1047    }
1048
1049    #[test]
1050    fn test_mask_sensitive_extended_keywords() {
1051        let a = SqlAuditor::new();
1052        let inputs = [
1053            "pwd",
1054            "passwd",
1055            "secret",
1056            "api_key",
1057            "access_key",
1058            "session",
1059            "credit_card",
1060            "cvv",
1061            "ssn",
1062        ];
1063        for kw in inputs {
1064            let sql = format!("SELECT * FROM t WHERE k = '{}'", kw);
1065            let masked = a.mask_sensitive(&sql);
1066            let lower = masked.to_lowercase();
1067            assert!(
1068                !lower.contains(kw),
1069                "keyword '{}' should be masked in: {}",
1070                kw,
1071                masked
1072            );
1073            assert!(masked.contains("******"));
1074        }
1075    }
1076
1077    #[test]
1078    fn test_mask_sensitive_preserves_non_sensitive() {
1079        let a = SqlAuditor::new();
1080        let masked = a.mask_sensitive("SELECT id, name FROM users WHERE active = 1");
1081        assert_eq!(masked, "SELECT id, name FROM users WHERE active = 1");
1082    }
1083
1084    #[test]
1085    fn test_mask_sensitive_does_not_match_substrings() {
1086        // "passworded" should not be partially matched as "password"
1087        // because we require word boundaries.
1088        let a = SqlAuditor::new();
1089        let masked = a.mask_sensitive("SELECT * FROM users WHERE note='passworded'");
1090        // The 'passworded' word should remain intact because of boundary check
1091        assert!(masked.contains("passworded"));
1092        // Ensure we did NOT replace anything (no ****** from this substring)
1093        // Actually, "passworded" still has 'password' as a prefix but our
1094        // boundary check requires the char AFTER the keyword to be non-ident.
1095        // In "passworded", after "password" comes "e" which IS an ident char,
1096        // so it should NOT be matched.
1097        assert_eq!(masked, "SELECT * FROM users WHERE note='passworded'");
1098    }
1099
1100    #[test]
1101    fn test_mask_sensitive_multiple_occurrences() {
1102        let a = SqlAuditor::new();
1103        let masked = a.mask_sensitive("INSERT INTO t (password, token) VALUES ('p1', 't1')");
1104        // Both keywords should be masked
1105        let lower = masked.to_lowercase();
1106        assert!(!lower.contains("password"));
1107        assert!(!lower.contains("token"));
1108        // Verify there are at least 2 mask replacements
1109        let count = masked.matches("******").count();
1110        assert!(count >= 2, "expected at least 2 masks, got: {}", masked);
1111    }
1112
1113    #[test]
1114    fn test_get_logs_empty_initially() {
1115        let a = SqlAuditor::new();
1116        assert!(a.get_logs().is_empty());
1117    }
1118
1119    #[test]
1120    fn test_get_logs_returns_snapshot_independent_of_changes() {
1121        let a = SqlAuditor::new();
1122        a.log(&ctx("SELECT 1", "u", 1));
1123        let snap = a.get_logs();
1124        a.log(&ctx("SELECT 2", "u", 2));
1125        assert_eq!(snap.len(), 1, "snapshot should not change after new log");
1126        assert_eq!(a.get_logs().len(), 2);
1127    }
1128
1129    #[test]
1130    fn test_flush_writes_json_file() {
1131        let a = SqlAuditor::new();
1132        a.log(&ctx("SELECT * FROM users WHERE password='p'", "admin", 123));
1133        a.log(&ctx("INSERT INTO logs VALUES(1)", "user2", 456));
1134        let path = test_data_dir().join("sz_orm_audit_flush_test.json");
1135        let path_str = path.to_str().unwrap();
1136        let count = a.flush(path_str).expect("flush should succeed");
1137        assert_eq!(count, 2);
1138        // Read back the file and verify it contains valid JSON
1139        let content = std::fs::read_to_string(path_str).expect("file should be readable");
1140        let parsed: Vec<SqlAuditContext> =
1141            serde_json::from_str(&content).expect("should parse as JSON array");
1142        assert_eq!(parsed.len(), 2);
1143        assert_eq!(parsed[0].user, "admin");
1144        assert_eq!(parsed[1].timestamp, 456);
1145        // Verify masking was applied during log()
1146        assert!(!parsed[0].sql.contains("password"));
1147        // Cleanup
1148        let _ = std::fs::remove_file(path_str);
1149    }
1150
1151    #[test]
1152    fn test_flush_empty_writes_empty_array() {
1153        let a = SqlAuditor::new();
1154        let path = test_data_dir().join("sz_orm_audit_flush_empty_test.json");
1155        let path_str = path.to_str().unwrap();
1156        let count = a.flush(path_str).expect("flush should succeed");
1157        assert_eq!(count, 0);
1158        let content = std::fs::read_to_string(path_str).expect("file should be readable");
1159        assert_eq!(content.trim(), "[]");
1160        let _ = std::fs::remove_file(path_str);
1161    }
1162
1163    #[test]
1164    fn test_default_creates_new_auditor() {
1165        let a = SqlAuditor::default();
1166        assert!(a.get_logs().is_empty());
1167    }
1168
1169    #[test]
1170    fn test_original_test_compatibility() {
1171        // Backward compatibility: the original test asserts that masking
1172        // "SELECT * FROM users WHERE password='secret'" removes "password".
1173        let a = SqlAuditor::new();
1174        let masked = a.mask_sensitive("SELECT * FROM users WHERE password='secret'");
1175        assert!(!masked.contains("password"));
1176    }
1177
1178    // ===== 审计规则配置测试 =====
1179
1180    #[test]
1181    fn test_audit_rules_empty_allows_all() {
1182        let rules = AuditRules::new();
1183        assert!(rules.should_audit("SELECT * FROM users"));
1184        assert!(rules.should_audit("DELETE FROM orders"));
1185        assert_eq!(rules.allow_count(), 0);
1186        assert_eq!(rules.deny_count(), 0);
1187    }
1188
1189    #[test]
1190    fn test_audit_rules_deny_blocks() {
1191        let rules = AuditRules::new().deny("pg_catalog");
1192        assert!(!rules.should_audit("SELECT * FROM pg_catalog.tables"));
1193        assert!(rules.should_audit("SELECT * FROM users"));
1194    }
1195
1196    #[test]
1197    fn test_audit_rules_allow_filters() {
1198        let rules = AuditRules::new().allow("select").allow("insert");
1199        assert!(rules.should_audit("SELECT * FROM users"));
1200        assert!(rules.should_audit("INSERT INTO logs VALUES(1)"));
1201        assert!(!rules.should_audit("DELETE FROM users"));
1202    }
1203
1204    #[test]
1205    fn test_audit_rules_deny_overrides_allow() {
1206        let rules = AuditRules::new().allow("select").deny("password");
1207        // 包含 password 的 SELECT 应被拒绝
1208        assert!(!rules.should_audit("SELECT * FROM users WHERE password='x'"));
1209        // 不含 password 的 SELECT 应被允许
1210        assert!(rules.should_audit("SELECT * FROM users"));
1211    }
1212
1213    #[test]
1214    fn test_audit_rules_case_insensitive() {
1215        let rules = AuditRules::new().deny("DROP");
1216        assert!(!rules.should_audit("drop table users"));
1217        assert!(!rules.should_audit("DROP TABLE users"));
1218        assert!(rules.should_audit("SELECT * FROM users"));
1219    }
1220
1221    #[test]
1222    fn test_audit_rules_multiple_deny() {
1223        let rules = AuditRules::new()
1224            .deny("drop")
1225            .deny("truncate")
1226            .deny("shutdown");
1227        assert!(!rules.should_audit("DROP TABLE x"));
1228        assert!(!rules.should_audit("TRUNCATE TABLE y"));
1229        assert!(!rules.should_audit("SHUTDOWN"));
1230        assert!(rules.should_audit("SELECT 1"));
1231    }
1232
1233    // ===== 轮转策略测试 =====
1234
1235    #[test]
1236    fn test_rotation_policy_none_never_rotates() {
1237        let policy = RotationPolicy::none();
1238        assert!(!policy.needs_rotation(1_000_000, 0, 1_000_000));
1239        assert!(!policy.needs_rotation(0, 0, 0));
1240    }
1241
1242    #[test]
1243    fn test_rotation_policy_by_size() {
1244        let policy = RotationPolicy::by_size(100);
1245        assert!(!policy.needs_rotation(99, 0, 1000));
1246        assert!(policy.needs_rotation(100, 0, 1000));
1247        assert!(policy.needs_rotation(200, 0, 1000));
1248    }
1249
1250    #[test]
1251    fn test_rotation_policy_by_age() {
1252        let policy = RotationPolicy::by_age(5000);
1253        // 旧日志 4000ms 前,未超时
1254        assert!(!policy.needs_rotation(10, 5000, 9000));
1255        // 旧日志 6000ms 前,已超时
1256        assert!(policy.needs_rotation(10, 5000, 11000));
1257    }
1258
1259    #[test]
1260    fn test_rotation_policy_by_size_and_age() {
1261        let policy = RotationPolicy::by_size_and_age(100, 5000);
1262        // 大小未达上限且时间未超 → 不轮转
1263        assert!(!policy.needs_rotation(50, 5000, 9000));
1264        // 大小达上限 → 轮转
1265        assert!(policy.needs_rotation(100, 5000, 5000));
1266        // 时间超 → 轮转
1267        assert!(policy.needs_rotation(10, 5000, 11000));
1268    }
1269
1270    // ===== RotatingAuditor 测试 =====
1271
1272    #[test]
1273    fn test_rotating_auditor_no_rotation_stores_all() {
1274        let auditor = RotatingAuditor::new(RotationPolicy::none(), AuditRules::new());
1275        for i in 0..100 {
1276            auditor.log(&ctx(&format!("SELECT {}", i), "user", i));
1277        }
1278        assert_eq!(auditor.len(), 100);
1279        assert_eq!(auditor.rotation_count(), 0);
1280    }
1281
1282    #[test]
1283    fn test_rotating_auditor_rotates_by_size() {
1284        let auditor = RotatingAuditor::with_max_entries(5);
1285        for i in 0..5 {
1286            auditor.log(&ctx(&format!("SELECT {}", i), "user", i));
1287        }
1288        assert_eq!(auditor.len(), 5);
1289        assert_eq!(auditor.rotation_count(), 0);
1290        // 第 6 条触发轮转
1291        auditor.log(&ctx("SELECT 6", "user", 100));
1292        assert_eq!(auditor.len(), 1);
1293        assert_eq!(auditor.rotation_count(), 1);
1294    }
1295
1296    #[test]
1297    fn test_rotating_auditor_rotates_by_age() {
1298        let auditor = RotatingAuditor::with_max_age(1000);
1299        auditor.log(&ctx("SELECT 1", "user", 100));
1300        auditor.log(&ctx("SELECT 2", "user", 200));
1301        assert_eq!(auditor.len(), 2);
1302        assert_eq!(auditor.rotation_count(), 0);
1303        // 时间差超过 1000ms → 轮转
1304        auditor.log(&ctx("SELECT 3", "user", 1500));
1305        assert_eq!(auditor.len(), 1);
1306        assert_eq!(auditor.rotation_count(), 1);
1307    }
1308
1309    #[test]
1310    fn test_rotating_auditor_rules_filter() {
1311        let rules = AuditRules::new().deny("drop").allow("select");
1312        let auditor = RotatingAuditor::new(RotationPolicy::none(), rules);
1313        let logged1 = auditor.log(&ctx("SELECT * FROM users", "u", 1));
1314        let logged2 = auditor.log(&ctx("DROP TABLE users", "u", 2));
1315        let logged3 = auditor.log(&ctx("DELETE FROM users", "u", 3));
1316        assert!(logged1);
1317        assert!(!logged2);
1318        assert!(!logged3);
1319        assert_eq!(auditor.len(), 1);
1320    }
1321
1322    #[test]
1323    fn test_rotating_auditor_manual_rotate() {
1324        let auditor = RotatingAuditor::with_max_entries(100);
1325        auditor.log(&ctx("SELECT 1", "u", 1));
1326        auditor.log(&ctx("SELECT 2", "u", 2));
1327        let cleared = auditor.rotate();
1328        assert_eq!(cleared, 2);
1329        assert!(auditor.is_empty());
1330        assert_eq!(auditor.rotation_count(), 1);
1331    }
1332
1333    #[test]
1334    fn test_rotating_auditor_masks_sensitive() {
1335        let auditor = RotatingAuditor::with_max_entries(100);
1336        auditor.log(&ctx("SELECT * FROM users WHERE password='x'", "u", 1));
1337        let logs = auditor.get_logs();
1338        assert_eq!(logs.len(), 1);
1339        assert!(!logs[0].sql.contains("password"));
1340        assert!(logs[0].sql.contains("******"));
1341    }
1342
1343    #[test]
1344    fn test_rotating_auditor_get_logs_snapshot() {
1345        let auditor = RotatingAuditor::with_max_entries(100);
1346        auditor.log(&ctx("SELECT 1", "u", 1));
1347        let snap = auditor.get_logs();
1348        auditor.log(&ctx("SELECT 2", "u", 2));
1349        assert_eq!(snap.len(), 1, "snapshot should be independent");
1350        assert_eq!(auditor.len(), 2);
1351    }
1352
1353    // ===== 异步审计写入器测试 =====
1354
1355    #[test]
1356    fn test_async_writer_log_and_shutdown() {
1357        let writer = AsyncAuditWriter::new();
1358        writer
1359            .log(&ctx("SELECT * FROM users", "admin", 1000))
1360            .unwrap();
1361        writer
1362            .log(&ctx("INSERT INTO logs VALUES(1)", "user2", 2000))
1363            .unwrap();
1364        let logs = writer.shutdown().expect("shutdown should succeed");
1365        assert_eq!(logs.len(), 2);
1366        assert_eq!(logs[0].user, "admin");
1367        assert_eq!(logs[1].timestamp, 2000);
1368    }
1369
1370    #[test]
1371    fn test_async_writer_masks_sensitive() {
1372        let writer = AsyncAuditWriter::new();
1373        writer
1374            .log(&ctx("SELECT * FROM users WHERE password='secret'", "u", 1))
1375            .unwrap();
1376        let logs = writer.shutdown().unwrap();
1377        assert_eq!(logs.len(), 1);
1378        assert!(!logs[0].sql.contains("password"));
1379    }
1380
1381    #[test]
1382    fn test_async_writer_empty_shutdown() {
1383        let writer = AsyncAuditWriter::new();
1384        let logs = writer.shutdown().expect("shutdown should succeed");
1385        assert!(logs.is_empty());
1386    }
1387
1388    #[test]
1389    fn test_async_writer_double_shutdown_errors() {
1390        let writer = AsyncAuditWriter::new();
1391        let _ = writer.shutdown().unwrap();
1392        let result = writer.shutdown();
1393        assert!(result.is_err(), "double shutdown should error");
1394    }
1395
1396    #[test]
1397    fn test_async_writer_default() {
1398        let writer = AsyncAuditWriter::default();
1399        writer.log(&ctx("SELECT 1", "u", 1)).unwrap();
1400        let logs = writer.shutdown().unwrap();
1401        assert_eq!(logs.len(), 1);
1402    }
1403
1404    // ===== 审计日志查询过滤测试 =====
1405
1406    #[test]
1407    fn test_audit_query_by_user() {
1408        let auditor = SqlAuditor::new();
1409        auditor.log(&ctx("SELECT 1", "alice", 100));
1410        auditor.log(&ctx("SELECT 2", "bob", 200));
1411        auditor.log(&ctx("SELECT 3", "alice", 300));
1412        let query = AuditQuery::new().by_user("alice");
1413        let results = query_logs(&auditor, &query);
1414        assert_eq!(results.len(), 2);
1415        assert!(results.iter().all(|r| r.user == "alice"));
1416    }
1417
1418    #[test]
1419    fn test_audit_query_by_time_range() {
1420        let auditor = SqlAuditor::new();
1421        auditor.log(&ctx("SELECT 1", "u", 100));
1422        auditor.log(&ctx("SELECT 2", "u", 200));
1423        auditor.log(&ctx("SELECT 3", "u", 300));
1424        auditor.log(&ctx("SELECT 4", "u", 400));
1425        let query = AuditQuery::new().by_time_range(150, 350);
1426        let results = query_logs(&auditor, &query);
1427        assert_eq!(results.len(), 2);
1428        assert!(results
1429            .iter()
1430            .all(|r| r.timestamp >= 150 && r.timestamp <= 350));
1431    }
1432
1433    #[test]
1434    fn test_audit_query_by_sql_contains() {
1435        let auditor = SqlAuditor::new();
1436        auditor.log(&ctx("SELECT * FROM users", "u", 1));
1437        auditor.log(&ctx("INSERT INTO orders", "u", 2));
1438        auditor.log(&ctx("SELECT * FROM orders", "u", 3));
1439        let query = AuditQuery::new().by_sql_contains("orders");
1440        let results = query_logs(&auditor, &query);
1441        assert_eq!(results.len(), 2);
1442        assert!(results
1443            .iter()
1444            .all(|r| r.sql.to_lowercase().contains("orders")));
1445    }
1446
1447    #[test]
1448    fn test_audit_query_sql_contains_case_insensitive() {
1449        let auditor = SqlAuditor::new();
1450        auditor.log(&ctx("select * from Users", "u", 1));
1451        let query = AuditQuery::new().by_sql_contains("USERS");
1452        let results = query_logs(&auditor, &query);
1453        assert_eq!(results.len(), 1);
1454    }
1455
1456    #[test]
1457    fn test_audit_query_with_limit() {
1458        let auditor = SqlAuditor::new();
1459        for i in 0..10 {
1460            auditor.log(&ctx(&format!("SELECT {}", i), "u", i));
1461        }
1462        let query = AuditQuery::new().with_limit(3);
1463        let results = query_logs(&auditor, &query);
1464        assert_eq!(results.len(), 3);
1465    }
1466
1467    #[test]
1468    fn test_audit_query_combined_filters() {
1469        let auditor = SqlAuditor::new();
1470        auditor.log(&ctx("SELECT * FROM users", "alice", 100));
1471        auditor.log(&ctx("INSERT INTO users", "alice", 200));
1472        auditor.log(&ctx("SELECT * FROM orders", "alice", 300));
1473        auditor.log(&ctx("SELECT * FROM users", "bob", 400));
1474        let query = AuditQuery::new()
1475            .by_user("alice")
1476            .by_sql_contains("select")
1477            .with_limit(10);
1478        let results = query_logs(&auditor, &query);
1479        assert_eq!(results.len(), 2);
1480        assert!(results.iter().all(|r| r.user == "alice"));
1481    }
1482
1483    #[test]
1484    fn test_audit_query_empty_returns_all() {
1485        let auditor = SqlAuditor::new();
1486        auditor.log(&ctx("SELECT 1", "u", 1));
1487        auditor.log(&ctx("SELECT 2", "u", 2));
1488        let query = AuditQuery::new();
1489        let results = query_logs(&auditor, &query);
1490        assert_eq!(results.len(), 2);
1491    }
1492
1493    #[test]
1494    fn test_audit_query_no_match_returns_empty() {
1495        let auditor = SqlAuditor::new();
1496        auditor.log(&ctx("SELECT 1", "u", 1));
1497        let query = AuditQuery::new().by_user("nonexistent");
1498        let results = query_logs(&auditor, &query);
1499        assert!(results.is_empty());
1500    }
1501
1502    #[test]
1503    fn test_audit_query_filter_directly() {
1504        let logs = vec![
1505            ctx("SELECT 1", "a", 10),
1506            ctx("SELECT 2", "b", 20),
1507            ctx("SELECT 3", "a", 30),
1508        ];
1509        let query = AuditQuery::new().by_user("a");
1510        let results = query.filter(&logs);
1511        assert_eq!(results.len(), 2);
1512    }
1513
1514    #[test]
1515    fn test_audit_query_limit_zero_means_no_limit() {
1516        let logs = vec![ctx("SELECT 1", "a", 10), ctx("SELECT 2", "a", 20)];
1517        let query = AuditQuery::new().with_limit(0);
1518        let results = query.filter(&logs);
1519        assert_eq!(results.len(), 2);
1520    }
1521
1522    // ===== 审计日志持久化存储测试 =====
1523
1524    #[test]
1525    fn test_file_audit_log_store_append_and_read_all() {
1526        let path = test_data_dir().join("sz_orm_audit_store_append.jsonl");
1527        let path_str = path.to_str().unwrap();
1528        let store = FileAuditLogStore::new(path_str);
1529        // 清理可能残留的旧文件
1530        let _ = store.clear();
1531
1532        store
1533            .append(&ctx("SELECT * FROM users", "alice", 1000))
1534            .unwrap();
1535        store
1536            .append(&ctx("INSERT INTO logs VALUES(1)", "bob", 2000))
1537            .unwrap();
1538
1539        let logs = store.read_all().unwrap();
1540        assert_eq!(logs.len(), 2);
1541        assert_eq!(logs[0].user, "alice");
1542        assert_eq!(logs[0].sql, "SELECT * FROM users");
1543        assert_eq!(logs[1].user, "bob");
1544        assert_eq!(logs[1].timestamp, 2000);
1545
1546        let _ = store.clear();
1547    }
1548
1549    #[test]
1550    fn test_file_audit_log_store_masks_sensitive() {
1551        let path = test_data_dir().join("sz_orm_audit_store_mask.jsonl");
1552        let path_str = path.to_str().unwrap();
1553        let store = FileAuditLogStore::new(path_str);
1554        let _ = store.clear();
1555
1556        store
1557            .append(&ctx(
1558                "SELECT * FROM users WHERE password='secret'",
1559                "admin",
1560                1000,
1561            ))
1562            .unwrap();
1563
1564        let logs = store.read_all().unwrap();
1565        assert_eq!(logs.len(), 1);
1566        // 落盘内容应已脱敏
1567        assert!(!logs[0].sql.contains("password"));
1568        assert!(!logs[0].sql.contains("secret"));
1569        assert!(logs[0].sql.contains("******"));
1570
1571        let _ = store.clear();
1572    }
1573
1574    #[test]
1575    fn test_file_audit_log_store_clear_removes_entries() {
1576        let path = test_data_dir().join("sz_orm_audit_store_clear.jsonl");
1577        let path_str = path.to_str().unwrap();
1578        let store = FileAuditLogStore::new(path_str);
1579        let _ = store.clear();
1580
1581        store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1582        store.append(&ctx("SELECT 2", "u", 2)).unwrap();
1583        assert_eq!(store.read_all().unwrap().len(), 2);
1584
1585        store.clear().unwrap();
1586        // 清空后读取应返回空
1587        assert_eq!(store.read_all().unwrap().len(), 0);
1588
1589        let _ = store.clear();
1590    }
1591
1592    #[test]
1593    fn test_file_audit_log_store_clear_nonexistent_is_ok() {
1594        // 清空不存在的文件不应报错
1595        let path = test_data_dir().join("sz_orm_audit_store_nonexistent.jsonl");
1596        let path_str = path.to_str().unwrap();
1597        let store = FileAuditLogStore::new(path_str);
1598        // 确保文件不存在
1599        let _ = std::fs::remove_file(path_str);
1600        assert!(store.clear().is_ok());
1601    }
1602
1603    #[test]
1604    fn test_file_audit_log_store_read_all_empty_file() {
1605        let path = test_data_dir().join("sz_orm_audit_store_empty_read.jsonl");
1606        let path_str = path.to_str().unwrap();
1607        let store = FileAuditLogStore::new(path_str);
1608        let _ = store.clear();
1609
1610        // 未写入任何内容,read_all 应返回空 vec(文件不存在视为空)
1611        let logs = store.read_all().unwrap();
1612        assert!(logs.is_empty());
1613
1614        let _ = store.clear();
1615    }
1616
1617    #[test]
1618    fn test_file_audit_log_store_skips_blank_lines() {
1619        let path = test_data_dir().join("sz_orm_audit_store_blank_lines.jsonl");
1620        let path_str = path.to_str().unwrap();
1621        let store = FileAuditLogStore::new(path_str);
1622        let _ = store.clear();
1623
1624        store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1625        // 手动追加空行模拟人工编辑或异常写入
1626
1627        let mut file = std::fs::OpenOptions::new()
1628            .append(true)
1629            .open(path_str)
1630            .unwrap();
1631        use std::io::Write;
1632        writeln!(file).unwrap();
1633        writeln!(file, "   ").unwrap();
1634        drop(file);
1635
1636        store.append(&ctx("SELECT 2", "u", 2)).unwrap();
1637
1638        let logs = store.read_all().unwrap();
1639        // 空行应被跳过,只解析到 2 条有效日志
1640        assert_eq!(logs.len(), 2);
1641
1642        let _ = store.clear();
1643    }
1644
1645    #[test]
1646    fn test_file_audit_log_store_path_accessor() {
1647        let store = FileAuditLogStore::new("/tmp/sz_orm_audit_path_test.jsonl");
1648        assert_eq!(store.path(), "/tmp/sz_orm_audit_path_test.jsonl");
1649    }
1650
1651    #[test]
1652    fn test_file_audit_log_store_concurrent_append() {
1653        use std::sync::Arc;
1654        let path = test_data_dir().join("sz_orm_audit_store_concurrent.jsonl");
1655        let path_str = path.to_str().unwrap();
1656        let store = Arc::new(FileAuditLogStore::new(path_str));
1657        let _ = store.clear();
1658
1659        let mut handles = vec![];
1660        for i in 0..4 {
1661            let s = Arc::clone(&store);
1662            handles.push(std::thread::spawn(move || {
1663                for j in 0..10 {
1664                    s.append(&ctx(&format!("SELECT {}_{}", i, j), "u", j as i64))
1665                        .unwrap();
1666                }
1667            }));
1668        }
1669        for h in handles {
1670            h.join().unwrap();
1671        }
1672
1673        // 4 线程 × 10 条 = 40 条,全部应成功落盘
1674        let logs = store.read_all().unwrap();
1675        assert_eq!(logs.len(), 40);
1676
1677        let _ = store.clear();
1678    }
1679
1680    #[test]
1681    fn test_audit_log_store_trait_object() {
1682        // 验证 FileAuditLogStore 可作为 trait object 使用
1683        let path = test_data_dir().join("sz_orm_audit_store_trait.jsonl");
1684        let path_str = path.to_str().unwrap();
1685        let store: Box<dyn AuditLogStore> = Box::new(FileAuditLogStore::new(path_str));
1686        let _ = store.clear();
1687
1688        store.append(&ctx("SELECT 1", "u", 1)).unwrap();
1689        let logs = store.read_all().unwrap();
1690        assert_eq!(logs.len(), 1);
1691
1692        let _ = store.clear();
1693    }
1694
1695    // ===== #11 修复:哈希链防篡改测试 =====
1696
1697    #[test]
1698    fn test_hash_chain_empty_auditor_verify_ok() {
1699        let auditor = HashChainAuditor::new();
1700        assert!(auditor.is_empty());
1701        assert_eq!(auditor.len(), 0);
1702        // 空链应通过验证
1703        assert!(auditor.verify().is_ok());
1704    }
1705
1706    #[test]
1707    fn test_hash_chain_single_entry_genesis() {
1708        let auditor = HashChainAuditor::new();
1709        auditor.log(&ctx("SELECT 1", "admin", 1000));
1710        assert_eq!(auditor.len(), 1);
1711
1712        let entries = auditor.get_entries();
1713        // 首条记录的 prev_hash 必须为 GENESIS_HASH
1714        assert_eq!(entries[0].prev_hash, GENESIS_HASH);
1715        // current_hash 必须为 64 字符的十六进制串
1716        assert_eq!(entries[0].current_hash.len(), 64);
1717        // 验证链完整
1718        assert!(auditor.verify().is_ok());
1719    }
1720
1721    #[test]
1722    fn test_hash_chain_multiple_entries_linked() {
1723        let auditor = HashChainAuditor::new();
1724        auditor.log(&ctx("SELECT 1", "admin", 1000));
1725        auditor.log(&ctx("SELECT 2", "admin", 1001));
1726        auditor.log(&ctx("SELECT 3", "admin", 1002));
1727        assert_eq!(auditor.len(), 3);
1728
1729        let entries = auditor.get_entries();
1730        // 验证相邻记录的 prev_hash 链接
1731        assert_eq!(entries[1].prev_hash, entries[0].current_hash);
1732        assert_eq!(entries[2].prev_hash, entries[1].current_hash);
1733        // 验证链完整
1734        assert!(auditor.verify().is_ok());
1735    }
1736
1737    #[test]
1738    fn test_hash_chain_detects_tampered_sql() {
1739        let auditor = HashChainAuditor::new();
1740        auditor.log(&ctx("SELECT 1", "admin", 1000));
1741        auditor.log(&ctx("SELECT 2", "admin", 1001));
1742
1743        // 篡改第一条记录的 SQL(模拟攻击者修改历史日志)
1744        {
1745            let mut entries = auditor.entries.lock().unwrap();
1746            entries[0].entry.sql = "DROP TABLE users".to_string();
1747        }
1748
1749        // 验证应失败
1750        let result = auditor.verify();
1751        assert!(result.is_err());
1752        let err = result.unwrap_err();
1753        // 错误信息应包含篡改位置
1754        assert!(err.contains("index 0"), "error: {}", err);
1755        assert!(err.contains("hash mismatch"), "error: {}", err);
1756    }
1757
1758    #[test]
1759    fn test_hash_chain_detects_broken_link() {
1760        let auditor = HashChainAuditor::new();
1761        auditor.log(&ctx("SELECT 1", "admin", 1000));
1762        auditor.log(&ctx("SELECT 2", "admin", 1001));
1763
1764        // 篡改第二条记录的 prev_hash(模拟删除中间记录)
1765        {
1766            let mut entries = auditor.entries.lock().unwrap();
1767            entries[1].prev_hash = "deadbeef".to_string();
1768        }
1769
1770        let result = auditor.verify();
1771        assert!(result.is_err());
1772        let err = result.unwrap_err();
1773        assert!(err.contains("chain broken at index 1"), "error: {}", err);
1774    }
1775
1776    #[test]
1777    fn test_hash_chain_detects_genesis_tamper() {
1778        let auditor = HashChainAuditor::new();
1779        auditor.log(&ctx("SELECT 1", "admin", 1000));
1780
1781        // 篡改首条记录的 prev_hash(模拟裁剪链首)
1782        {
1783            let mut entries = auditor.entries.lock().unwrap();
1784            entries[0].prev_hash = "deadbeef".to_string();
1785        }
1786
1787        let result = auditor.verify();
1788        assert!(result.is_err());
1789        let err = result.unwrap_err();
1790        assert!(err.contains("genesis prev_hash mismatch"), "error: {}", err);
1791    }
1792
1793    #[test]
1794    fn test_hash_chain_masks_sensitive_data() {
1795        let auditor = HashChainAuditor::new();
1796        auditor.log(&ctx(
1797            "SELECT * FROM users WHERE password='secret'",
1798            "admin",
1799            1000,
1800        ));
1801
1802        let entries = auditor.get_entries();
1803        // 链中存储的 SQL 应已被脱敏
1804        assert!(!entries[0].entry.sql.contains("password"));
1805        assert!(!entries[0].entry.sql.contains("secret"));
1806        assert!(entries[0].entry.sql.contains("******"));
1807        // 脱敏后的链仍应通过验证
1808        assert!(auditor.verify().is_ok());
1809    }
1810
1811    #[test]
1812    fn test_hash_chain_deterministic_hashes() {
1813        // 相同输入应产生相同哈希(便于跨节点对账)
1814        let entry = ctx("SELECT 1", "admin", 1000);
1815        let e1 = HashChainEntry::genesis(entry.clone());
1816        let e2 = HashChainEntry::genesis(entry);
1817        assert_eq!(e1.current_hash, e2.current_hash);
1818        assert_eq!(e1.prev_hash, e2.prev_hash);
1819    }
1820
1821    #[test]
1822    fn test_hash_chain_different_inputs_different_hashes() {
1823        let e1 = HashChainEntry::genesis(ctx("SELECT 1", "admin", 1000));
1824        let e2 = HashChainEntry::genesis(ctx("SELECT 2", "admin", 1000));
1825        assert_ne!(e1.current_hash, e2.current_hash);
1826    }
1827
1828    #[test]
1829    fn test_hash_chain_flush_and_persist() {
1830        let auditor = HashChainAuditor::new();
1831        auditor.log(&ctx("SELECT 1", "admin", 1000));
1832        auditor.log(&ctx("SELECT 2", "admin", 1001));
1833
1834        let path = test_data_dir().join("sz_orm_audit_hash_chain.json");
1835        let path_str = path.to_str().unwrap();
1836        let count = auditor.flush(path_str).unwrap();
1837        assert_eq!(count, 2);
1838
1839        // 验证文件存在且非空
1840        let content = std::fs::read_to_string(path_str).unwrap();
1841        assert!(!content.is_empty());
1842        assert!(content.contains("current_hash"));
1843
1844        let _ = std::fs::remove_file(path_str);
1845    }
1846
1847    #[test]
1848    fn test_hash_chain_concurrent_log_thread_safe() {
1849        use std::sync::Arc;
1850        use std::thread;
1851
1852        let auditor = Arc::new(HashChainAuditor::new());
1853        let mut handles = vec![];
1854        for i in 0..4 {
1855            let a = Arc::clone(&auditor);
1856            handles.push(thread::spawn(move || {
1857                for j in 0..25 {
1858                    a.log(&ctx(&format!("SELECT {}_{}", i, j), "u", j as i64));
1859                }
1860            }));
1861        }
1862        for h in handles {
1863            h.join().unwrap();
1864        }
1865
1866        // 4 线程 × 25 条 = 100 条
1867        assert_eq!(auditor.len(), 100);
1868        // 并发写入后链仍应完整
1869        assert!(auditor.verify().is_ok());
1870    }
1871
1872    #[test]
1873    fn test_genesis_hash_constant_is_64_zeros() {
1874        // 验证 GENESIS_HASH 为 64 字符全零(SHA-256 输出长度)
1875        assert_eq!(GENESIS_HASH.len(), 64);
1876        assert!(GENESIS_HASH.chars().all(|c| c == '0'));
1877    }
1878}