Skip to main content

sz_orm_audit/
lib.rs

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