claw_guard/masking/
mod.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::error::GuardResult;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8pub enum MaskType {
9 Redact,
11 HashBlake3,
13 Truncate { n: usize },
15 EmailMask,
17 JsonFieldMask { pattern: String },
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct MaskDirective {
24 pub field_pattern: String,
26 pub mask_type: MaskType,
28}
29
30#[derive(Debug, Clone, Default)]
32pub struct MaskingEngine;
33
34impl MaskingEngine {
35 pub fn new() -> Self {
37 Self
38 }
39
40 pub fn apply(value: &mut Value, masks: &[MaskDirective]) -> GuardResult<()> {
42 apply_recursive(value, "$", masks)
43 }
44}
45
46fn apply_recursive(value: &mut Value, path: &str, masks: &[MaskDirective]) -> GuardResult<()> {
47 for directive in masks {
48 if matches_pattern(&directive.field_pattern, path) {
49 apply_mask(value, &directive.mask_type)?;
50 }
51 }
52
53 match value {
54 Value::Object(map) => {
55 for (key, child) in map.iter_mut() {
56 let child_path = format!("{path}.{key}");
57 apply_recursive(child, &child_path, masks)?;
58 }
59 }
60 Value::Array(items) => {
61 for (index, child) in items.iter_mut().enumerate() {
62 let child_path = format!("{path}[{index}]");
63 apply_recursive(child, &child_path, masks)?;
64 }
65 }
66 _ => {}
67 }
68
69 Ok(())
70}
71
72fn apply_mask(value: &mut Value, mask_type: &MaskType) -> GuardResult<()> {
73 match mask_type {
74 MaskType::Redact => *value = Value::String("[REDACTED]".to_owned()),
75 MaskType::HashBlake3 => {
76 let raw = match value {
77 Value::String(inner) => inner.clone(),
78 _ => serde_json::to_string(value)?,
79 };
80 *value = Value::String(blake3::hash(raw.as_bytes()).to_hex().to_string());
81 }
82 MaskType::Truncate { n } => {
83 if let Some(inner) = value.as_str() {
84 *value = Value::String(inner.chars().take(*n).collect());
85 }
86 }
87 MaskType::EmailMask => {
88 if let Some(inner) = value.as_str() {
89 *value = Value::String(mask_email(inner));
90 }
91 }
92 MaskType::JsonFieldMask { pattern } => {
93 redact_nested_fields(value, pattern);
94 }
95 }
96
97 Ok(())
98}
99
100fn redact_nested_fields(value: &mut Value, pattern: &str) {
101 match value {
102 Value::Object(map) => {
103 for (key, child) in map.iter_mut() {
104 if matches_pattern(pattern, key) {
105 *child = Value::String("[REDACTED]".to_owned());
106 } else {
107 redact_nested_fields(child, pattern);
108 }
109 }
110 }
111 Value::Array(items) => {
112 for item in items {
113 redact_nested_fields(item, pattern);
114 }
115 }
116 _ => {}
117 }
118}
119
120fn mask_email(value: &str) -> String {
121 match value.split_once('@') {
122 Some((local, domain)) => {
123 let first = local.chars().next().unwrap_or('*');
124 format!("{first}***@{domain}")
125 }
126 None => "[REDACTED]".to_owned(),
127 }
128}
129
130fn matches_pattern(pattern: &str, value: &str) -> bool {
131 if pattern == "*" || pattern == value {
132 return true;
133 }
134
135 let mut remainder = value;
136 let parts = pattern.split('*').filter(|part| !part.is_empty());
137 let anchored_start = !pattern.starts_with('*');
138 let anchored_end = !pattern.ends_with('*');
139 let mut first = true;
140
141 for part in parts {
142 if first && anchored_start {
143 if !remainder.starts_with(part) {
144 return false;
145 }
146 remainder = &remainder[part.len()..];
147 } else if let Some(index) = remainder.find(part) {
148 remainder = &remainder[index + part.len()..];
149 } else {
150 return false;
151 }
152 first = false;
153 }
154
155 !anchored_end || remainder.is_empty()
156}