Skip to main content

sz_orm_masking/
config.rs

1//! 脱敏配置管理与敏感字段自动检测。
2//!
3//! - [`MaskingConfigManager`] — 管理多套脱敏配置档案(按场景/租户隔离)
4//! - [`MaskingProfile`] — 单套脱敏配置档案
5//! - [`SensitiveFieldDetector`] — 基于字段名模式自动检测敏感字段并推荐脱敏规则
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::MaskingRule;
12
13// ============================================================================
14// 脱敏配置档案
15// ============================================================================
16
17/// 脱敏配置档案:一套命名的字段脱敏规则集合。
18///
19/// 用于多租户/多场景隔离,例如 `default`、`strict`、`gdpr` 等不同脱敏强度。
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct MaskingProfile {
22    name: String,
23    description: String,
24    rules: HashMap<String, MaskingRule>,
25    enabled: bool,
26}
27
28impl MaskingProfile {
29    /// 创建空档案
30    pub fn new(name: &str) -> Self {
31        Self {
32            name: name.to_string(),
33            description: String::new(),
34            rules: HashMap::new(),
35            enabled: true,
36        }
37    }
38
39    /// 创建档案并设置描述
40    pub fn with_description(name: &str, description: &str) -> Self {
41        Self {
42            name: name.to_string(),
43            description: description.to_string(),
44            rules: HashMap::new(),
45            enabled: true,
46        }
47    }
48
49    /// 档案名
50    pub fn name(&self) -> &str {
51        &self.name
52    }
53
54    /// 描述
55    pub fn description(&self) -> &str {
56        &self.description
57    }
58
59    /// 是否启用
60    pub fn is_enabled(&self) -> bool {
61        self.enabled
62    }
63
64    /// 设置启用状态(链式)
65    pub fn set_enabled(mut self, enabled: bool) -> Self {
66        self.enabled = enabled;
67        self
68    }
69
70    /// 添加字段规则(链式)
71    pub fn with_rule(mut self, field: &str, rule: MaskingRule) -> Self {
72        self.rules.insert(field.to_string(), rule);
73        self
74    }
75
76    /// 添加多条规则(链式)
77    pub fn with_rules(mut self, rules: HashMap<String, MaskingRule>) -> Self {
78        self.rules.extend(rules);
79        self
80    }
81
82    /// 规则数
83    pub fn rule_count(&self) -> usize {
84        self.rules.len()
85    }
86
87    /// 获取字段规则
88    pub fn get_rule(&self, field: &str) -> Option<&MaskingRule> {
89        self.rules.get(field)
90    }
91
92    /// 移除字段规则
93    pub fn remove_rule(&mut self, field: &str) -> Option<MaskingRule> {
94        self.rules.remove(field)
95    }
96
97    /// 所有规则引用
98    pub fn rules(&self) -> &HashMap<String, MaskingRule> {
99        &self.rules
100    }
101
102    /// 对 HashMap 应用本档案脱敏
103    pub fn apply_to_map(&self, data: &HashMap<String, String>) -> HashMap<String, String> {
104        if !self.enabled {
105            return data.clone();
106        }
107        crate::DataMasker::mask_map(&self.rules, data)
108    }
109
110    /// 对 JSON 应用本档案脱敏
111    pub fn apply_to_json(&self, json: &str) -> String {
112        if !self.enabled {
113            return json.to_string();
114        }
115        crate::DataMasker::mask_json(&self.rules, json)
116    }
117}
118
119// ============================================================================
120// 脱敏配置管理器
121// ============================================================================
122
123/// 脱敏配置管理器:管理多套脱敏档案,按名称切换。
124///
125/// 支持默认档案、按名称获取档案、合并档案等操作。
126#[derive(Debug, Clone, Default)]
127pub struct MaskingConfigManager {
128    profiles: HashMap<String, MaskingProfile>,
129    default_profile: String,
130}
131
132impl MaskingConfigManager {
133    /// 创建空管理器
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// 注册档案(链式)
139    pub fn add_profile(mut self, profile: MaskingProfile) -> Self {
140        let name = profile.name().to_string();
141        if self.profiles.is_empty() {
142            self.default_profile = name.clone();
143        }
144        self.profiles.insert(name, profile);
145        self
146    }
147
148    /// 设置默认档案名
149    pub fn set_default(&mut self, name: &str) -> bool {
150        if self.profiles.contains_key(name) {
151            self.default_profile = name.to_string();
152            true
153        } else {
154            false
155        }
156    }
157
158    /// 默认档案名
159    pub fn default_profile_name(&self) -> &str {
160        &self.default_profile
161    }
162
163    /// 获取档案
164    pub fn get_profile(&self, name: &str) -> Option<&MaskingProfile> {
165        self.profiles.get(name)
166    }
167
168    /// 获取默认档案
169    pub fn default_profile(&self) -> Option<&MaskingProfile> {
170        self.profiles.get(&self.default_profile)
171    }
172
173    /// 档案数
174    pub fn profile_count(&self) -> usize {
175        self.profiles.len()
176    }
177
178    /// 所有档案名
179    pub fn profile_names(&self) -> Vec<&str> {
180        self.profiles.keys().map(|s| s.as_str()).collect()
181    }
182
183    /// 移除档案
184    pub fn remove_profile(&mut self, name: &str) -> Option<MaskingProfile> {
185        let removed = self.profiles.remove(name);
186        if removed.is_some() && self.default_profile == name {
187            self.default_profile = self.profiles.keys().next().cloned().unwrap_or_default();
188        }
189        removed
190    }
191
192    /// 用指定档案脱敏 HashMap
193    pub fn apply_to_map(
194        &self,
195        profile_name: &str,
196        data: &HashMap<String, String>,
197    ) -> Option<HashMap<String, String>> {
198        self.profiles
199            .get(profile_name)
200            .map(|p| p.apply_to_map(data))
201    }
202
203    /// 用默认档案脱敏 HashMap
204    pub fn apply_with_default(
205        &self,
206        data: &HashMap<String, String>,
207    ) -> Option<HashMap<String, String>> {
208        self.default_profile().map(|p| p.apply_to_map(data))
209    }
210
211    /// 用指定档案脱敏 JSON
212    pub fn apply_to_json(&self, profile_name: &str, json: &str) -> Option<String> {
213        self.profiles
214            .get(profile_name)
215            .map(|p| p.apply_to_json(json))
216    }
217
218    /// 合并两个档案:将 `source` 的规则补充到 `target`(不覆盖已有规则)
219    pub fn merge_profiles(&self, target: &str, source: &str) -> Option<MaskingProfile> {
220        let target_profile = self.profiles.get(target)?;
221        let source_profile = self.profiles.get(source)?;
222        let mut merged = target_profile.clone();
223        for (field, rule) in source_profile.rules() {
224            if !merged.rules().contains_key(field) {
225                merged = merged.with_rule(field, rule.clone());
226            }
227        }
228        Some(merged)
229    }
230}
231
232// ============================================================================
233// 敏感字段检测器
234// ============================================================================
235
236/// 字段名匹配模式:关键词列表 + 对应脱敏规则
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct FieldPattern {
239    keywords: Vec<String>,
240    rule: MaskingRule,
241}
242
243impl FieldPattern {
244    /// 创建字段模式
245    pub fn new(keywords: Vec<String>, rule: MaskingRule) -> Self {
246        Self { keywords, rule }
247    }
248
249    /// 关键词列表
250    pub fn keywords(&self) -> &[String] {
251        &self.keywords
252    }
253
254    /// 脱敏规则
255    pub fn rule(&self) -> &MaskingRule {
256        &self.rule
257    }
258
259    /// 检查字段名是否匹配本模式(大小写不敏感,包含任一关键词即匹配)
260    pub fn matches(&self, field: &str) -> bool {
261        let field_lower = field.to_lowercase();
262        self.keywords
263            .iter()
264            .any(|kw| field_lower.contains(&kw.to_lowercase()))
265    }
266}
267
268/// 敏感字段检测器:基于字段名模式自动检测敏感字段并推荐脱敏规则。
269///
270/// 内置常见敏感字段模式(phone/mobile/tel、email/mail、idcard/identity、
271/// bankcard/card、password/pwd、name、address/addr、ip 等),
272/// 也支持自定义模式。
273#[derive(Debug, Clone, Default)]
274pub struct SensitiveFieldDetector {
275    patterns: Vec<FieldPattern>,
276}
277
278impl SensitiveFieldDetector {
279    /// 创建包含内置模式的检测器
280    pub fn new() -> Self {
281        Self {
282            patterns: Self::builtin_patterns(),
283        }
284    }
285
286    /// 创建空检测器
287    pub fn empty() -> Self {
288        Self::default()
289    }
290
291    /// 添加自定义模式(链式)
292    pub fn add_pattern(mut self, pattern: FieldPattern) -> Self {
293        self.patterns.push(pattern);
294        self
295    }
296
297    /// 模式数
298    pub fn pattern_count(&self) -> usize {
299        self.patterns.len()
300    }
301
302    /// 检测字段名对应的脱敏规则
303    pub fn detect(&self, field: &str) -> Option<&MaskingRule> {
304        self.patterns
305            .iter()
306            .find(|p| p.matches(field))
307            .map(|p| p.rule())
308    }
309
310    /// 批量检测字段名,返回 (字段名, 规则) 列表
311    pub fn detect_fields(&self, fields: &[String]) -> Vec<(String, &MaskingRule)> {
312        fields
313            .iter()
314            .filter_map(|f| self.detect(f).map(|r| (f.clone(), r)))
315            .collect()
316    }
317
318    /// 为字段列表自动生成脱敏规则 HashMap
319    pub fn auto_rules(&self, fields: &[String]) -> HashMap<String, MaskingRule> {
320        fields
321            .iter()
322            .filter_map(|f| self.detect(f).map(|r| (f.clone(), r.clone())))
323            .collect()
324    }
325
326    /// 对 HashMap 自动检测并脱敏
327    pub fn auto_mask(&self, data: &HashMap<String, String>) -> HashMap<String, String> {
328        let fields: Vec<String> = data.keys().cloned().collect();
329        let rules = self.auto_rules(&fields);
330        crate::DataMasker::mask_map(&rules, data)
331    }
332
333    /// 内置敏感字段模式
334    fn builtin_patterns() -> Vec<FieldPattern> {
335        vec![
336            FieldPattern::new(
337                vec![
338                    "phone".to_string(),
339                    "mobile".to_string(),
340                    "tel".to_string(),
341                    "telephone".to_string(),
342                ],
343                MaskingRule::Phone,
344            ),
345            FieldPattern::new(
346                vec![
347                    "email".to_string(),
348                    "mail".to_string(),
349                    "email_addr".to_string(),
350                ],
351                MaskingRule::Email,
352            ),
353            FieldPattern::new(
354                vec![
355                    "idcard".to_string(),
356                    "identity".to_string(),
357                    "id_number".to_string(),
358                    "id_card".to_string(),
359                ],
360                MaskingRule::IdCard,
361            ),
362            FieldPattern::new(
363                vec![
364                    "bankcard".to_string(),
365                    "bank_card".to_string(),
366                    "card_number".to_string(),
367                    "cardno".to_string(),
368                ],
369                MaskingRule::BankCard,
370            ),
371            FieldPattern::new(
372                vec![
373                    "password".to_string(),
374                    "pwd".to_string(),
375                    "passwd".to_string(),
376                    "secret".to_string(),
377                ],
378                MaskingRule::Password,
379            ),
380            FieldPattern::new(
381                vec![
382                    "apikey".to_string(),
383                    "api_key".to_string(),
384                    "access_token".to_string(),
385                    "auth_token".to_string(),
386                ],
387                MaskingRule::ApiKey,
388            ),
389            FieldPattern::new(
390                vec![
391                    "name".to_string(),
392                    "username".to_string(),
393                    "fullname".to_string(),
394                ],
395                MaskingRule::Name,
396            ),
397            FieldPattern::new(
398                vec![
399                    "ip".to_string(),
400                    "ip_addr".to_string(),
401                    "ipaddress".to_string(),
402                ],
403                MaskingRule::Ip,
404            ),
405            FieldPattern::new(
406                vec![
407                    "address".to_string(),
408                    "addr".to_string(),
409                    "home_addr".to_string(),
410                ],
411                MaskingRule::Address,
412            ),
413            FieldPattern::new(
414                vec!["imei".to_string(), "device_imei".to_string()],
415                MaskingRule::Imei,
416            ),
417            FieldPattern::new(
418                vec![
419                    "plate".to_string(),
420                    "license_plate".to_string(),
421                    "car_plate".to_string(),
422                ],
423                MaskingRule::Plate,
424            ),
425        ]
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    // ----- MaskingProfile -----
434
435    #[test]
436    fn profile_new() {
437        let p = MaskingProfile::new("default");
438        assert_eq!(p.name(), "default");
439        assert_eq!(p.description(), "");
440        assert!(p.is_enabled());
441        assert_eq!(p.rule_count(), 0);
442    }
443
444    #[test]
445    fn profile_with_description() {
446        let p = MaskingProfile::with_description("strict", "Strict GDPR masking");
447        assert_eq!(p.description(), "Strict GDPR masking");
448    }
449
450    #[test]
451    fn profile_with_rule() {
452        let p = MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone);
453        assert_eq!(p.rule_count(), 1);
454        assert!(p.get_rule("phone").is_some());
455    }
456
457    #[test]
458    fn profile_with_rules_batch() {
459        let mut rules = HashMap::new();
460        rules.insert("phone".to_string(), MaskingRule::Phone);
461        rules.insert("email".to_string(), MaskingRule::Email);
462        let p = MaskingProfile::new("default").with_rules(rules);
463        assert_eq!(p.rule_count(), 2);
464    }
465
466    #[test]
467    fn profile_set_disabled() {
468        let p = MaskingProfile::new("default").set_enabled(false);
469        assert!(!p.is_enabled());
470    }
471
472    #[test]
473    fn profile_remove_rule() {
474        let mut p = MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone);
475        let removed = p.remove_rule("phone");
476        assert!(removed.is_some());
477        assert_eq!(p.rule_count(), 0);
478    }
479
480    #[test]
481    fn profile_apply_to_map() {
482        let p = MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone);
483        let mut data = HashMap::new();
484        data.insert("phone".to_string(), "13812345678".to_string());
485        let result = p.apply_to_map(&data);
486        assert_eq!(result["phone"], "138****5678");
487    }
488
489    #[test]
490    fn profile_apply_to_map_disabled() {
491        let p = MaskingProfile::new("default")
492            .with_rule("phone", MaskingRule::Phone)
493            .set_enabled(false);
494        let mut data = HashMap::new();
495        data.insert("phone".to_string(), "13812345678".to_string());
496        let result = p.apply_to_map(&data);
497        assert_eq!(result["phone"], "13812345678");
498    }
499
500    #[test]
501    fn profile_apply_to_json() {
502        let p = MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone);
503        let json = r#"{"phone":"13812345678"}"#;
504        let result = p.apply_to_json(json);
505        assert!(result.contains("138****5678"));
506    }
507
508    #[test]
509    fn profile_apply_to_json_disabled() {
510        let p = MaskingProfile::new("default")
511            .with_rule("phone", MaskingRule::Phone)
512            .set_enabled(false);
513        let json = r#"{"phone":"13812345678"}"#;
514        assert_eq!(p.apply_to_json(json), json);
515    }
516
517    // ----- MaskingConfigManager -----
518
519    #[test]
520    fn config_manager_default_empty() {
521        let m = MaskingConfigManager::new();
522        assert_eq!(m.profile_count(), 0);
523    }
524
525    #[test]
526    fn config_manager_add_profile() {
527        let m = MaskingConfigManager::new().add_profile(MaskingProfile::new("default"));
528        assert_eq!(m.profile_count(), 1);
529        assert_eq!(m.default_profile_name(), "default");
530    }
531
532    #[test]
533    fn config_manager_first_profile_is_default() {
534        let m = MaskingConfigManager::new()
535            .add_profile(MaskingProfile::new("first"))
536            .add_profile(MaskingProfile::new("second"));
537        assert_eq!(m.default_profile_name(), "first");
538    }
539
540    #[test]
541    fn config_manager_set_default() {
542        let mut m = MaskingConfigManager::new()
543            .add_profile(MaskingProfile::new("a"))
544            .add_profile(MaskingProfile::new("b"));
545        assert!(m.set_default("b"));
546        assert_eq!(m.default_profile_name(), "b");
547    }
548
549    #[test]
550    fn config_manager_set_default_nonexistent() {
551        let mut m = MaskingConfigManager::new().add_profile(MaskingProfile::new("a"));
552        assert!(!m.set_default("nonexistent"));
553    }
554
555    #[test]
556    fn config_manager_get_profile() {
557        let m = MaskingConfigManager::new().add_profile(MaskingProfile::new("default"));
558        assert!(m.get_profile("default").is_some());
559        assert!(m.get_profile("nonexistent").is_none());
560    }
561
562    #[test]
563    fn config_manager_remove_profile() {
564        let mut m = MaskingConfigManager::new()
565            .add_profile(MaskingProfile::new("a"))
566            .add_profile(MaskingProfile::new("b"));
567        let removed = m.remove_profile("a");
568        assert!(removed.is_some());
569        assert_eq!(m.profile_count(), 1);
570        // 默认切换到剩余档案
571        assert_eq!(m.default_profile_name(), "b");
572    }
573
574    #[test]
575    fn config_manager_apply_to_map() {
576        let m = MaskingConfigManager::new()
577            .add_profile(MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone));
578        let mut data = HashMap::new();
579        data.insert("phone".to_string(), "13812345678".to_string());
580        let result = m.apply_to_map("default", &data).unwrap();
581        assert_eq!(result["phone"], "138****5678");
582    }
583
584    #[test]
585    fn config_manager_apply_with_default() {
586        let m = MaskingConfigManager::new()
587            .add_profile(MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone));
588        let mut data = HashMap::new();
589        data.insert("phone".to_string(), "13812345678".to_string());
590        let result = m.apply_with_default(&data).unwrap();
591        assert_eq!(result["phone"], "138****5678");
592    }
593
594    #[test]
595    fn config_manager_apply_to_json() {
596        let m = MaskingConfigManager::new()
597            .add_profile(MaskingProfile::new("default").with_rule("phone", MaskingRule::Phone));
598        let json = r#"{"phone":"13812345678"}"#;
599        let result = m.apply_to_json("default", json).unwrap();
600        assert!(result.contains("138****5678"));
601    }
602
603    #[test]
604    fn config_manager_merge_profiles() {
605        let m = MaskingConfigManager::new()
606            .add_profile(MaskingProfile::new("base").with_rule("phone", MaskingRule::Phone))
607            .add_profile(MaskingProfile::new("extra").with_rule("email", MaskingRule::Email));
608        let merged = m.merge_profiles("base", "extra").unwrap();
609        assert_eq!(merged.rule_count(), 2);
610    }
611
612    #[test]
613    fn config_manager_profile_names() {
614        let m = MaskingConfigManager::new()
615            .add_profile(MaskingProfile::new("a"))
616            .add_profile(MaskingProfile::new("b"));
617        let names = m.profile_names();
618        assert_eq!(names.len(), 2);
619    }
620
621    // ----- FieldPattern -----
622
623    #[test]
624    fn field_pattern_matches() {
625        let p = FieldPattern::new(
626            vec!["phone".to_string(), "mobile".to_string()],
627            MaskingRule::Phone,
628        );
629        assert!(p.matches("phone_number"));
630        assert!(p.matches("user_mobile"));
631        assert!(!p.matches("email"));
632    }
633
634    #[test]
635    fn field_pattern_case_insensitive() {
636        let p = FieldPattern::new(vec!["phone".to_string()], MaskingRule::Phone);
637        assert!(p.matches("PHONE"));
638        assert!(p.matches("Phone"));
639    }
640
641    #[test]
642    fn field_pattern_keywords() {
643        let p = FieldPattern::new(
644            vec!["phone".to_string(), "tel".to_string()],
645            MaskingRule::Phone,
646        );
647        assert_eq!(p.keywords().len(), 2);
648    }
649
650    // ----- SensitiveFieldDetector -----
651
652    #[test]
653    fn detector_new_has_builtin_patterns() {
654        let d = SensitiveFieldDetector::new();
655        assert!(d.pattern_count() > 0);
656    }
657
658    #[test]
659    fn detector_empty() {
660        let d = SensitiveFieldDetector::empty();
661        assert_eq!(d.pattern_count(), 0);
662    }
663
664    #[test]
665    fn detector_add_pattern() {
666        let d = SensitiveFieldDetector::empty().add_pattern(FieldPattern::new(
667            vec!["custom".to_string()],
668            MaskingRule::Password,
669        ));
670        assert_eq!(d.pattern_count(), 1);
671    }
672
673    #[test]
674    fn detector_phone() {
675        let d = SensitiveFieldDetector::new();
676        let rule = d.detect("phone_number").unwrap();
677        assert_eq!(rule, &MaskingRule::Phone);
678    }
679
680    #[test]
681    fn detector_mobile() {
682        let d = SensitiveFieldDetector::new();
683        assert!(d.detect("user_mobile").is_some());
684    }
685
686    #[test]
687    fn detector_email() {
688        let d = SensitiveFieldDetector::new();
689        let rule = d.detect("email_addr").unwrap();
690        assert_eq!(rule, &MaskingRule::Email);
691    }
692
693    #[test]
694    fn detector_password() {
695        let d = SensitiveFieldDetector::new();
696        let rule = d.detect("user_password").unwrap();
697        assert_eq!(rule, &MaskingRule::Password);
698    }
699
700    #[test]
701    fn detector_idcard() {
702        let d = SensitiveFieldDetector::new();
703        let rule = d.detect("id_card").unwrap();
704        assert_eq!(rule, &MaskingRule::IdCard);
705    }
706
707    #[test]
708    fn detector_bankcard() {
709        let d = SensitiveFieldDetector::new();
710        let rule = d.detect("bank_card_no").unwrap();
711        assert_eq!(rule, &MaskingRule::BankCard);
712    }
713
714    #[test]
715    fn detector_apikey() {
716        let d = SensitiveFieldDetector::new();
717        let rule = d.detect("api_key").unwrap();
718        assert_eq!(rule, &MaskingRule::ApiKey);
719    }
720
721    #[test]
722    fn detector_name() {
723        let d = SensitiveFieldDetector::new();
724        let rule = d.detect("username").unwrap();
725        assert_eq!(rule, &MaskingRule::Name);
726    }
727
728    #[test]
729    fn detector_address() {
730        let d = SensitiveFieldDetector::new();
731        let rule = d.detect("home_address").unwrap();
732        assert_eq!(rule, &MaskingRule::Address);
733    }
734
735    #[test]
736    fn detector_ip() {
737        let d = SensitiveFieldDetector::new();
738        let rule = d.detect("ip_addr").unwrap();
739        assert_eq!(rule, &MaskingRule::Ip);
740    }
741
742    #[test]
743    fn detector_imei() {
744        let d = SensitiveFieldDetector::new();
745        let rule = d.detect("device_imei").unwrap();
746        assert_eq!(rule, &MaskingRule::Imei);
747    }
748
749    #[test]
750    fn detector_plate() {
751        let d = SensitiveFieldDetector::new();
752        let rule = d.detect("license_plate").unwrap();
753        assert_eq!(rule, &MaskingRule::Plate);
754    }
755
756    #[test]
757    fn detector_non_sensitive() {
758        let d = SensitiveFieldDetector::new();
759        assert!(d.detect("created_at").is_none());
760        assert!(d.detect("order_id").is_none());
761    }
762
763    #[test]
764    fn detector_detect_fields() {
765        let d = SensitiveFieldDetector::new();
766        let fields = vec![
767            "phone".to_string(),
768            "email".to_string(),
769            "created_at".to_string(),
770        ];
771        let detected = d.detect_fields(&fields);
772        assert_eq!(detected.len(), 2);
773    }
774
775    #[test]
776    fn detector_auto_rules() {
777        let d = SensitiveFieldDetector::new();
778        let fields = vec![
779            "phone".to_string(),
780            "email".to_string(),
781            "created_at".to_string(),
782        ];
783        let rules = d.auto_rules(&fields);
784        assert_eq!(rules.len(), 2);
785        assert!(rules.contains_key("phone"));
786        assert!(rules.contains_key("email"));
787    }
788
789    #[test]
790    fn detector_auto_mask() {
791        let d = SensitiveFieldDetector::new();
792        let mut data = HashMap::new();
793        data.insert("phone".to_string(), "13812345678".to_string());
794        data.insert("email".to_string(), "test@example.com".to_string());
795        data.insert("created_at".to_string(), "2024-01-01".to_string());
796        let result = d.auto_mask(&data);
797        assert_eq!(result["phone"], "138****5678");
798        assert_eq!(result["email"], "t***@example.com");
799        assert_eq!(result["created_at"], "2024-01-01");
800    }
801}