1+ """Security utilities for handling sensitive data"""
2+ import re
3+ from typing import Any , Dict
4+
5+
6+ SENSITIVE_FIELDS = [
7+ "secretKey" ,
8+ "accessKey" ,
9+ "password" ,
10+ "passwd" ,
11+ "pwd" ,
12+ "secret" ,
13+ "token" ,
14+ "apiKey" ,
15+ "api_key" ,
16+ "access_key" ,
17+ "secret_key" ,
18+ ]
19+
20+ MASK_PATTERN = "**********"
21+
22+
23+ def mask_sensitive_value (value : str ) -> str :
24+ """Mask a single sensitive value"""
25+ return MASK_PATTERN
26+
27+
28+ def is_masked_value (value : str ) -> bool :
29+ """Check if a value is already masked"""
30+ return value == MASK_PATTERN
31+
32+
33+ def preserve_sensitive_values (
34+ new_config : Dict [str , Any ],
35+ original_config : Dict [str , Any ]
36+ ) -> Dict [str , Any ]:
37+ """
38+ Preserve original sensitive values if masked pattern is detected in update request.
39+
40+ When frontend receives masked values and sends them back in update request,
41+ this function detects masked values and replaces them with original values
42+ from database.
43+
44+ Args:
45+ new_config: Config from update request (may contain masked values)
46+ original_config: Original config from database (contains real values)
47+
48+ Returns:
49+ Config with original sensitive values preserved
50+ """
51+ if not isinstance (new_config , dict ) or not isinstance (original_config , dict ):
52+ return new_config
53+
54+ preserved_config = {}
55+ for key , new_value in new_config .items ():
56+ # Check if key is a sensitive field
57+ key_lower = key .lower ()
58+ is_sensitive = any (
59+ field .lower () == key_lower or field .lower () in key_lower
60+ for field in SENSITIVE_FIELDS
61+ )
62+
63+ # If value is masked and field is sensitive, use original value
64+ if is_sensitive and isinstance (new_value , str ) and is_masked_value (new_value ):
65+ original_value = original_config .get (key )
66+ preserved_config [key ] = original_value if original_value else new_value
67+ elif isinstance (new_value , dict ):
68+ # Recursively process nested dictionaries
69+ original_nested = original_config .get (key , {})
70+ preserved_config [key ] = preserve_sensitive_values (new_value , original_nested )
71+ elif isinstance (new_value , list ):
72+ # Process list items (preserve original if matching structure)
73+ original_list = original_config .get (key , [])
74+ preserved_config [key ] = [
75+ preserve_sensitive_values (new_item , orig_item )
76+ if isinstance (new_item , dict ) and isinstance (orig_item , dict )
77+ else new_item
78+ for new_item , orig_item in zip (new_value , original_list )
79+ ] if len (new_value ) == len (original_list ) else new_value
80+ else :
81+ # Keep non-sensitive/non-masked values
82+ preserved_config [key ] = new_value
83+
84+ return preserved_config
85+
86+
87+ def mask_sensitive_dict (data : Dict [str , Any ]) -> Dict [str , Any ]:
88+ """
89+ Recursively mask sensitive fields in a dictionary.
90+
91+ Args:
92+ data: Dictionary that may contain sensitive fields
93+
94+ Returns:
95+ Dictionary with sensitive values masked
96+ """
97+ if not isinstance (data , dict ):
98+ return data
99+
100+ masked_data = {}
101+ for key , value in data .items ():
102+ # Check if the key is a sensitive field (case-insensitive)
103+ key_lower = key .lower ()
104+ is_sensitive = any (
105+ field .lower () == key_lower or field .lower () in key_lower
106+ for field in SENSITIVE_FIELDS
107+ )
108+
109+ if is_sensitive and isinstance (value , str ):
110+ # Mask the sensitive value
111+ masked_data [key ] = MASK_PATTERN
112+ elif isinstance (value , dict ):
113+ # Recursively mask nested dictionaries
114+ masked_data [key ] = mask_sensitive_dict (value )
115+ elif isinstance (value , list ):
116+ # Process list items
117+ masked_data [key ] = [
118+ mask_sensitive_dict (item ) if isinstance (item , dict ) else item
119+ for item in value
120+ ]
121+ else :
122+ # Keep non-sensitive values as-is
123+ masked_data [key ] = value
124+
125+ return masked_data
126+
127+
128+ def mask_sensitive_info (text : str ) -> str :
129+ """
130+ Mask sensitive information in text by replacing values with **********
131+
132+ Args:
133+ text: Original text that may contain sensitive information
134+
135+ Returns:
136+ Text with sensitive values masked
137+ """
138+ masked_text = text
139+
140+ for field in SENSITIVE_FIELDS :
141+ patterns = [
142+ rf'"{ field } "\s*:\s*"[^"]*"' ,
143+ rf'"{ field } "\s*:\s*"[^"]*"' ,
144+ rf'{ field } \s*=\s*[^\s,\]]+' ,
145+ rf"'{ field } '\s*:\s*'[^']*'" ,
146+ ]
147+
148+ for pattern in patterns :
149+ if '"' in pattern :
150+ masked_text = re .sub (pattern , f'"{ field } ": "{ MASK_PATTERN } "' , masked_text )
151+ elif "'" in pattern :
152+ masked_text = re .sub (pattern , f"'{ field } ': '{ MASK_PATTERN } '" , masked_text )
153+ else :
154+ masked_text = re .sub (pattern , f'{ field } ={ MASK_PATTERN } ' , masked_text )
155+
156+ return masked_text
0 commit comments