1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::MaskingRule;
12
13#[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 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 pub fn with_operator(mut self, operator: &str) -> Self {
51 self.operator = operator.to_string();
52 self
53 }
54
55 pub fn with_session(mut self, session_id: &str) -> Self {
57 self.session_id = session_id.to_string();
58 self
59 }
60
61 pub fn field(&self) -> &str {
63 &self.field
64 }
65
66 pub fn rule(&self) -> &str {
68 &self.rule
69 }
70
71 pub fn original_len(&self) -> usize {
73 self.original_len
74 }
75
76 pub fn masked_len(&self) -> usize {
78 self.masked_len
79 }
80
81 pub fn timestamp(&self) -> u64 {
83 self.timestamp
84 }
85
86 pub fn operator(&self) -> &str {
88 &self.operator
89 }
90
91 pub fn session_id(&self) -> &str {
93 &self.session_id
94 }
95
96 pub fn masked_chars(&self) -> usize {
98 self.original_len.saturating_sub(self.masked_len)
99 }
100
101 pub fn was_truncated(&self) -> bool {
103 self.masked_len < self.original_len
104 }
105
106 pub fn was_extended(&self) -> bool {
108 self.masked_len > self.original_len
109 }
110}
111
112#[derive(Debug, Clone, Default)]
118pub struct MaskingAuditLog {
119 entries: Vec<MaskingAuditEntry>,
120 max_entries: usize,
121}
122
123impl MaskingAuditLog {
124 pub fn new() -> Self {
126 Self::default()
127 }
128
129 pub fn with_capacity(max_entries: usize) -> Self {
131 Self {
132 entries: Vec::with_capacity(max_entries),
133 max_entries,
134 }
135 }
136
137 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 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 pub fn entry_count(&self) -> usize {
165 self.entries.len()
166 }
167
168 pub fn entries(&self) -> &[MaskingAuditEntry] {
170 &self.entries
171 }
172
173 pub fn clear(&mut self) {
175 self.entries.clear();
176 }
177
178 pub fn find_by_field(&self, field: &str) -> Vec<&MaskingAuditEntry> {
180 self.entries.iter().filter(|e| e.field() == field).collect()
181 }
182
183 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 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 pub fn total_masked_chars(&self) -> usize {
201 self.entries.iter().map(|e| e.masked_chars()).sum()
202 }
203
204 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 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 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 pub fn to_json(&self) -> String {
232 serde_json::to_string(&self.entries).unwrap_or_else(|_| "[]".to_string())
233 }
234}
235
236#[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#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct FieldReport {
255 field: String,
256 count: usize,
257 masked_chars: usize,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct RuleReport {
263 rule: String,
264 count: usize,
265}
266
267impl MaskingReport {
268 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(|a, b| b.count.cmp(&a.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(|a, b| b.count.cmp(&a.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 pub fn total_operations(&self) -> usize {
317 self.total_operations
318 }
319
320 pub fn total_masked_chars(&self) -> usize {
322 self.total_masked_chars
323 }
324
325 pub fn unique_fields(&self) -> usize {
327 self.unique_fields
328 }
329
330 pub fn truncated_count(&self) -> usize {
332 self.truncated_count
333 }
334
335 pub fn extended_count(&self) -> usize {
337 self.extended_count
338 }
339
340 pub fn field_breakdown(&self) -> &[FieldReport] {
342 &self.field_breakdown
343 }
344
345 pub fn rule_breakdown(&self) -> &[RuleReport] {
347 &self.rule_breakdown
348 }
349
350 pub fn to_json(&self) -> String {
352 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
353 }
354}
355
356impl FieldReport {
357 pub fn field(&self) -> &str {
359 &self.field
360 }
361
362 pub fn count(&self) -> usize {
364 self.count
365 }
366
367 pub fn masked_chars(&self) -> usize {
369 self.masked_chars
370 }
371}
372
373impl RuleReport {
374 pub fn rule(&self) -> &str {
376 &self.rule
377 }
378
379 pub fn count(&self) -> usize {
381 self.count
382 }
383}
384
385pub 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 #[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 #[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 #[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 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 #[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}