Skip to main content

sz_orm_masking/
audit.rs

1//! 增强脱敏审计:带时间戳的详细审计日志、统计分析、合规报告。
2//!
3//! - [`MaskingAuditEntry`] — 单条审计记录(字段、原始长度、脱敏后长度、时间戳、操作者)
4//! - [`MaskingAuditLog`] — 审计日志集合,支持查询、统计、导出
5//! - [`MaskingReport`] — 脱敏合规报告生成器
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::MaskingRule;
12
13// ============================================================================
14// 增强审计条目
15// ============================================================================
16
17/// 脱敏审计条目:记录单次脱敏操作的完整信息。
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MaskingAuditEntry {
20    field: String,
21    rule: String,
22    original_len: usize,
23    masked_len: usize,
24    timestamp: u64,
25    operator: String,
26    session_id: String,
27}
28
29impl MaskingAuditEntry {
30    /// 创建审计条目
31    pub fn new(
32        field: &str,
33        rule: &str,
34        original_len: usize,
35        masked_len: usize,
36        timestamp: u64,
37    ) -> Self {
38        Self {
39            field: field.to_string(),
40            rule: rule.to_string(),
41            original_len,
42            masked_len,
43            timestamp,
44            operator: String::new(),
45            session_id: String::new(),
46        }
47    }
48
49    /// 设置操作者(链式)
50    pub fn with_operator(mut self, operator: &str) -> Self {
51        self.operator = operator.to_string();
52        self
53    }
54
55    /// 设置会话 ID(链式)
56    pub fn with_session(mut self, session_id: &str) -> Self {
57        self.session_id = session_id.to_string();
58        self
59    }
60
61    /// 字段名
62    pub fn field(&self) -> &str {
63        &self.field
64    }
65
66    /// 脱敏规则名
67    pub fn rule(&self) -> &str {
68        &self.rule
69    }
70
71    /// 原始值长度
72    pub fn original_len(&self) -> usize {
73        self.original_len
74    }
75
76    /// 脱敏后长度
77    pub fn masked_len(&self) -> usize {
78        self.masked_len
79    }
80
81    /// 时间戳
82    pub fn timestamp(&self) -> u64 {
83        self.timestamp
84    }
85
86    /// 操作者
87    pub fn operator(&self) -> &str {
88        &self.operator
89    }
90
91    /// 会话 ID
92    pub fn session_id(&self) -> &str {
93        &self.session_id
94    }
95
96    /// 被掩码的字符数
97    pub fn masked_chars(&self) -> usize {
98        self.original_len.saturating_sub(self.masked_len)
99    }
100
101    /// 是否被截断
102    pub fn was_truncated(&self) -> bool {
103        self.masked_len < self.original_len
104    }
105
106    /// 是否被扩展(脱敏后更长)
107    pub fn was_extended(&self) -> bool {
108        self.masked_len > self.original_len
109    }
110}
111
112// ============================================================================
113// 增强审计日志
114// ============================================================================
115
116/// 脱敏审计日志:记录所有脱敏操作,支持查询、统计、导出。
117#[derive(Debug, Clone, Default)]
118pub struct MaskingAuditLog {
119    entries: Vec<MaskingAuditEntry>,
120    max_entries: usize,
121}
122
123impl MaskingAuditLog {
124    /// 创建空审计日志(无上限)
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// 创建有上限的审计日志(超出时丢弃最旧条目)
130    pub fn with_capacity(max_entries: usize) -> Self {
131        Self {
132            entries: Vec::with_capacity(max_entries),
133            max_entries,
134        }
135    }
136
137    /// 记录一次脱敏操作
138    pub fn log(&mut self, entry: MaskingAuditEntry) {
139        if self.max_entries > 0 && self.entries.len() >= self.max_entries {
140            self.entries.remove(0);
141        }
142        self.entries.push(entry);
143    }
144
145    /// 便捷记录:字段名 + 规则 + 原始值 + 脱敏值 + 时间戳
146    pub fn log_simple(
147        &mut self,
148        field: &str,
149        rule: &str,
150        original: &str,
151        masked: &str,
152        timestamp: u64,
153    ) {
154        self.log(MaskingAuditEntry::new(
155            field,
156            rule,
157            original.chars().count(),
158            masked.chars().count(),
159            timestamp,
160        ));
161    }
162
163    /// 条目数
164    pub fn entry_count(&self) -> usize {
165        self.entries.len()
166    }
167
168    /// 所有条目
169    pub fn entries(&self) -> &[MaskingAuditEntry] {
170        &self.entries
171    }
172
173    /// 清空日志
174    pub fn clear(&mut self) {
175        self.entries.clear();
176    }
177
178    /// 按字段名查询条目
179    pub fn find_by_field(&self, field: &str) -> Vec<&MaskingAuditEntry> {
180        self.entries.iter().filter(|e| e.field() == field).collect()
181    }
182
183    /// 按时间范围查询条目
184    pub fn find_by_time_range(&self, start: u64, end: u64) -> Vec<&MaskingAuditEntry> {
185        self.entries
186            .iter()
187            .filter(|e| (start..=end).contains(&e.timestamp()))
188            .collect()
189    }
190
191    /// 按操作者查询条目
192    pub fn find_by_operator(&self, operator: &str) -> Vec<&MaskingAuditEntry> {
193        self.entries
194            .iter()
195            .filter(|e| e.operator() == operator)
196            .collect()
197    }
198
199    /// 被掩码的字符总数
200    pub fn total_masked_chars(&self) -> usize {
201        self.entries.iter().map(|e| e.masked_chars()).sum()
202    }
203
204    /// 涉及的不同字段数
205    pub fn unique_field_count(&self) -> usize {
206        let mut fields: Vec<&str> = self.entries.iter().map(|e| e.field()).collect();
207        fields.sort_unstable();
208        fields.dedup();
209        fields.len()
210    }
211
212    /// 各字段的脱敏次数
213    pub fn field_stats(&self) -> HashMap<String, usize> {
214        let mut stats = HashMap::new();
215        for entry in &self.entries {
216            *stats.entry(entry.field().to_string()).or_insert(0) += 1;
217        }
218        stats
219    }
220
221    /// 各脱敏规则的使用次数
222    pub fn rule_stats(&self) -> HashMap<String, usize> {
223        let mut stats = HashMap::new();
224        for entry in &self.entries {
225            *stats.entry(entry.rule().to_string()).or_insert(0) += 1;
226        }
227        stats
228    }
229
230    /// 导出为 JSON 字符串
231    pub fn to_json(&self) -> String {
232        serde_json::to_string(&self.entries).unwrap_or_else(|_| "[]".to_string())
233    }
234}
235
236// ============================================================================
237// 脱敏合规报告
238// ============================================================================
239
240/// 脱敏合规报告:汇总审计日志,生成统计报告。
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct MaskingReport {
243    total_operations: usize,
244    total_masked_chars: usize,
245    unique_fields: usize,
246    field_breakdown: Vec<FieldReport>,
247    rule_breakdown: Vec<RuleReport>,
248    truncated_count: usize,
249    extended_count: usize,
250}
251
252/// 单字段的报告
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct FieldReport {
255    field: String,
256    count: usize,
257    masked_chars: usize,
258}
259
260/// 单规则的报告
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct RuleReport {
263    rule: String,
264    count: usize,
265}
266
267impl MaskingReport {
268    /// 从审计日志生成报告
269    pub fn from_audit_log(log: &MaskingAuditLog) -> Self {
270        let entries = log.entries();
271        let total_operations = entries.len();
272        let total_masked_chars = entries.iter().map(|e| e.masked_chars()).sum();
273        let truncated_count = entries.iter().filter(|e| e.was_truncated()).count();
274        let extended_count = entries.iter().filter(|e| e.was_extended()).count();
275
276        let mut field_map: HashMap<String, (usize, usize)> = HashMap::new();
277        for entry in entries {
278            let slot = field_map.entry(entry.field().to_string()).or_insert((0, 0));
279            slot.0 += 1;
280            slot.1 += entry.masked_chars();
281        }
282        let mut field_breakdown: Vec<FieldReport> = field_map
283            .into_iter()
284            .map(|(field, (count, masked_chars))| FieldReport {
285                field,
286                count,
287                masked_chars,
288            })
289            .collect();
290        field_breakdown.sort_by_key(|b| std::cmp::Reverse(b.count));
291
292        let mut rule_map: HashMap<String, usize> = HashMap::new();
293        for entry in entries {
294            *rule_map.entry(entry.rule().to_string()).or_insert(0) += 1;
295        }
296        let mut rule_breakdown: Vec<RuleReport> = rule_map
297            .into_iter()
298            .map(|(rule, count)| RuleReport { rule, count })
299            .collect();
300        rule_breakdown.sort_by_key(|b| std::cmp::Reverse(b.count));
301
302        let unique_fields = field_breakdown.len();
303
304        Self {
305            total_operations,
306            total_masked_chars,
307            unique_fields,
308            field_breakdown,
309            rule_breakdown,
310            truncated_count,
311            extended_count,
312        }
313    }
314
315    /// 总操作数
316    pub fn total_operations(&self) -> usize {
317        self.total_operations
318    }
319
320    /// 被掩码字符总数
321    pub fn total_masked_chars(&self) -> usize {
322        self.total_masked_chars
323    }
324
325    /// 涉及字段数
326    pub fn unique_fields(&self) -> usize {
327        self.unique_fields
328    }
329
330    /// 被截断的条目数
331    pub fn truncated_count(&self) -> usize {
332        self.truncated_count
333    }
334
335    /// 被扩展的条目数
336    pub fn extended_count(&self) -> usize {
337        self.extended_count
338    }
339
340    /// 字段报告
341    pub fn field_breakdown(&self) -> &[FieldReport] {
342        &self.field_breakdown
343    }
344
345    /// 规则报告
346    pub fn rule_breakdown(&self) -> &[RuleReport] {
347        &self.rule_breakdown
348    }
349
350    /// 导出为 JSON
351    pub fn to_json(&self) -> String {
352        serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
353    }
354}
355
356impl FieldReport {
357    /// 字段名
358    pub fn field(&self) -> &str {
359        &self.field
360    }
361
362    /// 脱敏次数
363    pub fn count(&self) -> usize {
364        self.count
365    }
366
367    /// 掩码字符数
368    pub fn masked_chars(&self) -> usize {
369        self.masked_chars
370    }
371}
372
373impl RuleReport {
374    /// 规则名
375    pub fn rule(&self) -> &str {
376        &self.rule
377    }
378
379    /// 使用次数
380    pub fn count(&self) -> usize {
381        self.count
382    }
383}
384
385// ============================================================================
386// 规则名辅助
387// ============================================================================
388
389/// 获取脱敏规则的可读名称
390pub fn rule_name(rule: &MaskingRule) -> &'static str {
391    match rule {
392        MaskingRule::Phone => "phone",
393        MaskingRule::Email => "email",
394        MaskingRule::IdCard => "idcard",
395        MaskingRule::BankCard => "bankcard",
396        MaskingRule::Name => "name",
397        MaskingRule::Address => "address",
398        MaskingRule::Ip => "ip",
399        MaskingRule::Imei => "imei",
400        MaskingRule::Plate => "plate",
401        MaskingRule::Custom(_) => "custom",
402        MaskingRule::Password => "password",
403        MaskingRule::ApiKey => "apikey",
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    // ----- MaskingAuditEntry -----
412
413    #[test]
414    fn audit_entry_new() {
415        let e = MaskingAuditEntry::new("phone", "phone", 11, 11, 1000);
416        assert_eq!(e.field(), "phone");
417        assert_eq!(e.rule(), "phone");
418        assert_eq!(e.original_len(), 11);
419        assert_eq!(e.masked_len(), 11);
420        assert_eq!(e.timestamp(), 1000);
421    }
422
423    #[test]
424    fn audit_entry_with_operator() {
425        let e = MaskingAuditEntry::new("phone", "phone", 11, 11, 0).with_operator("admin");
426        assert_eq!(e.operator(), "admin");
427    }
428
429    #[test]
430    fn audit_entry_with_session() {
431        let e = MaskingAuditEntry::new("phone", "phone", 11, 11, 0).with_session("sess123");
432        assert_eq!(e.session_id(), "sess123");
433    }
434
435    #[test]
436    fn audit_entry_masked_chars() {
437        let e = MaskingAuditEntry::new("phone", "phone", 11, 7, 0);
438        assert_eq!(e.masked_chars(), 4);
439    }
440
441    #[test]
442    fn audit_entry_was_truncated() {
443        let e = MaskingAuditEntry::new("phone", "phone", 11, 7, 0);
444        assert!(e.was_truncated());
445    }
446
447    #[test]
448    fn audit_entry_was_extended() {
449        let e = MaskingAuditEntry::new("phone", "phone", 5, 15, 0);
450        assert!(e.was_extended());
451    }
452
453    #[test]
454    fn audit_entry_not_truncated_not_extended() {
455        let e = MaskingAuditEntry::new("phone", "phone", 11, 11, 0);
456        assert!(!e.was_truncated());
457        assert!(!e.was_extended());
458    }
459
460    // ----- MaskingAuditLog -----
461
462    #[test]
463    fn audit_log_new_empty() {
464        let log = MaskingAuditLog::new();
465        assert_eq!(log.entry_count(), 0);
466    }
467
468    #[test]
469    fn audit_log_log_entry() {
470        let mut log = MaskingAuditLog::new();
471        log.log(MaskingAuditEntry::new("phone", "phone", 11, 11, 0));
472        assert_eq!(log.entry_count(), 1);
473    }
474
475    #[test]
476    fn audit_log_log_simple() {
477        let mut log = MaskingAuditLog::new();
478        log.log_simple("phone", "phone", "13812345678", "138****5678", 0);
479        assert_eq!(log.entry_count(), 1);
480    }
481
482    #[test]
483    fn audit_log_clear() {
484        let mut log = MaskingAuditLog::new();
485        log.log(MaskingAuditEntry::new("phone", "phone", 11, 11, 0));
486        log.clear();
487        assert_eq!(log.entry_count(), 0);
488    }
489
490    #[test]
491    fn audit_log_find_by_field() {
492        let mut log = MaskingAuditLog::new();
493        log.log(MaskingAuditEntry::new("phone", "phone", 11, 11, 0));
494        log.log(MaskingAuditEntry::new("email", "email", 15, 15, 0));
495        log.log(MaskingAuditEntry::new("phone", "phone", 11, 11, 1));
496        let results = log.find_by_field("phone");
497        assert_eq!(results.len(), 2);
498    }
499
500    #[test]
501    fn audit_log_find_by_time_range() {
502        let mut log = MaskingAuditLog::new();
503        log.log(MaskingAuditEntry::new("a", "r", 1, 1, 100));
504        log.log(MaskingAuditEntry::new("b", "r", 1, 1, 200));
505        log.log(MaskingAuditEntry::new("c", "r", 1, 1, 300));
506        let results = log.find_by_time_range(150, 250);
507        assert_eq!(results.len(), 1);
508    }
509
510    #[test]
511    fn audit_log_find_by_operator() {
512        let mut log = MaskingAuditLog::new();
513        log.log(MaskingAuditEntry::new("a", "r", 1, 1, 0).with_operator("admin"));
514        log.log(MaskingAuditEntry::new("b", "r", 1, 1, 0).with_operator("user"));
515        let results = log.find_by_operator("admin");
516        assert_eq!(results.len(), 1);
517    }
518
519    #[test]
520    fn audit_log_total_masked_chars() {
521        let mut log = MaskingAuditLog::new();
522        log.log(MaskingAuditEntry::new("a", "r", 10, 6, 0));
523        log.log(MaskingAuditEntry::new("b", "r", 8, 4, 0));
524        assert_eq!(log.total_masked_chars(), 8);
525    }
526
527    #[test]
528    fn audit_log_unique_field_count() {
529        let mut log = MaskingAuditLog::new();
530        log.log(MaskingAuditEntry::new("phone", "r", 1, 1, 0));
531        log.log(MaskingAuditEntry::new("email", "r", 1, 1, 0));
532        log.log(MaskingAuditEntry::new("phone", "r", 1, 1, 0));
533        assert_eq!(log.unique_field_count(), 2);
534    }
535
536    #[test]
537    fn audit_log_field_stats() {
538        let mut log = MaskingAuditLog::new();
539        log.log(MaskingAuditEntry::new("phone", "r", 1, 1, 0));
540        log.log(MaskingAuditEntry::new("phone", "r", 1, 1, 0));
541        log.log(MaskingAuditEntry::new("email", "r", 1, 1, 0));
542        let stats = log.field_stats();
543        assert_eq!(stats["phone"], 2);
544        assert_eq!(stats["email"], 1);
545    }
546
547    #[test]
548    fn audit_log_rule_stats() {
549        let mut log = MaskingAuditLog::new();
550        log.log(MaskingAuditEntry::new("a", "phone", 1, 1, 0));
551        log.log(MaskingAuditEntry::new("b", "email", 1, 1, 0));
552        log.log(MaskingAuditEntry::new("c", "phone", 1, 1, 0));
553        let stats = log.rule_stats();
554        assert_eq!(stats["phone"], 2);
555        assert_eq!(stats["email"], 1);
556    }
557
558    #[test]
559    fn audit_log_to_json() {
560        let mut log = MaskingAuditLog::new();
561        log.log(MaskingAuditEntry::new("phone", "phone", 11, 11, 0));
562        let json = log.to_json();
563        assert!(json.contains("phone"));
564    }
565
566    #[test]
567    fn audit_log_capacity_eviction() {
568        let mut log = MaskingAuditLog::with_capacity(2);
569        log.log(MaskingAuditEntry::new("a", "r", 1, 1, 0));
570        log.log(MaskingAuditEntry::new("b", "r", 1, 1, 0));
571        log.log(MaskingAuditEntry::new("c", "r", 1, 1, 0));
572        assert_eq!(log.entry_count(), 2);
573        assert_eq!(log.entries()[0].field(), "b");
574    }
575
576    // ----- MaskingReport -----
577
578    #[test]
579    fn report_empty_log() {
580        let log = MaskingAuditLog::new();
581        let report = MaskingReport::from_audit_log(&log);
582        assert_eq!(report.total_operations(), 0);
583        assert_eq!(report.total_masked_chars(), 0);
584        assert_eq!(report.unique_fields(), 0);
585    }
586
587    #[test]
588    fn report_total_operations() {
589        let mut log = MaskingAuditLog::new();
590        log.log(MaskingAuditEntry::new("a", "r", 10, 6, 0));
591        log.log(MaskingAuditEntry::new("b", "r", 8, 4, 0));
592        let report = MaskingReport::from_audit_log(&log);
593        assert_eq!(report.total_operations(), 2);
594        assert_eq!(report.total_masked_chars(), 8);
595    }
596
597    #[test]
598    fn report_unique_fields() {
599        let mut log = MaskingAuditLog::new();
600        log.log(MaskingAuditEntry::new("phone", "r", 1, 1, 0));
601        log.log(MaskingAuditEntry::new("email", "r", 1, 1, 0));
602        let report = MaskingReport::from_audit_log(&log);
603        assert_eq!(report.unique_fields(), 2);
604    }
605
606    #[test]
607    fn report_truncated_count() {
608        let mut log = MaskingAuditLog::new();
609        log.log(MaskingAuditEntry::new("a", "r", 10, 6, 0));
610        log.log(MaskingAuditEntry::new("b", "r", 5, 5, 0));
611        let report = MaskingReport::from_audit_log(&log);
612        assert_eq!(report.truncated_count(), 1);
613    }
614
615    #[test]
616    fn report_extended_count() {
617        let mut log = MaskingAuditLog::new();
618        log.log(MaskingAuditEntry::new("a", "r", 5, 15, 0));
619        log.log(MaskingAuditEntry::new("b", "r", 5, 5, 0));
620        let report = MaskingReport::from_audit_log(&log);
621        assert_eq!(report.extended_count(), 1);
622    }
623
624    #[test]
625    fn report_field_breakdown() {
626        let mut log = MaskingAuditLog::new();
627        log.log(MaskingAuditEntry::new("phone", "r", 10, 6, 0));
628        log.log(MaskingAuditEntry::new("phone", "r", 10, 6, 0));
629        log.log(MaskingAuditEntry::new("email", "r", 15, 13, 0));
630        let report = MaskingReport::from_audit_log(&log);
631        let fields = report.field_breakdown();
632        assert_eq!(fields.len(), 2);
633        // phone 有 2 次,排第一
634        assert_eq!(fields[0].field(), "phone");
635        assert_eq!(fields[0].count(), 2);
636    }
637
638    #[test]
639    fn report_rule_breakdown() {
640        let mut log = MaskingAuditLog::new();
641        log.log(MaskingAuditEntry::new("a", "phone", 1, 1, 0));
642        log.log(MaskingAuditEntry::new("b", "phone", 1, 1, 0));
643        log.log(MaskingAuditEntry::new("c", "email", 1, 1, 0));
644        let report = MaskingReport::from_audit_log(&log);
645        let rules = report.rule_breakdown();
646        assert_eq!(rules.len(), 2);
647        assert_eq!(rules[0].rule(), "phone");
648        assert_eq!(rules[0].count(), 2);
649    }
650
651    #[test]
652    fn report_to_json() {
653        let mut log = MaskingAuditLog::new();
654        log.log(MaskingAuditEntry::new("phone", "phone", 11, 11, 0));
655        let report = MaskingReport::from_audit_log(&log);
656        let json = report.to_json();
657        assert!(json.contains("total_operations"));
658    }
659
660    // ----- rule_name -----
661
662    #[test]
663    fn rule_name_all_variants() {
664        assert_eq!(rule_name(&MaskingRule::Phone), "phone");
665        assert_eq!(rule_name(&MaskingRule::Email), "email");
666        assert_eq!(rule_name(&MaskingRule::IdCard), "idcard");
667        assert_eq!(rule_name(&MaskingRule::BankCard), "bankcard");
668        assert_eq!(rule_name(&MaskingRule::Name), "name");
669        assert_eq!(rule_name(&MaskingRule::Address), "address");
670        assert_eq!(rule_name(&MaskingRule::Ip), "ip");
671        assert_eq!(rule_name(&MaskingRule::Imei), "imei");
672        assert_eq!(rule_name(&MaskingRule::Plate), "plate");
673        assert_eq!(rule_name(&MaskingRule::Custom("3,2".into())), "custom");
674        assert_eq!(rule_name(&MaskingRule::Password), "password");
675        assert_eq!(rule_name(&MaskingRule::ApiKey), "apikey");
676    }
677}