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