miso-client 0.1.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of miso-client might be problematic. Click here for more details.

@@ -5,16 +5,18 @@ Implements ISO 27001 data protection controls by masking sensitive fields
5
5
  in log entries and context data.
6
6
  """
7
7
 
8
- from typing import Any, Set
8
+ from typing import Any, Optional, Set
9
+
10
+ from .sensitive_fields_loader import get_sensitive_fields_array
9
11
 
10
12
 
11
13
  class DataMasker:
12
14
  """Static class for masking sensitive data."""
13
-
15
+
14
16
  MASKED_VALUE = "***MASKED***"
15
-
16
- # Set of sensitive field names (normalized)
17
- _sensitive_fields: Set[str] = {
17
+
18
+ # Hardcoded set of sensitive field names (normalized) - fallback if JSON cannot be loaded
19
+ _hardcoded_sensitive_fields: Set[str] = {
18
20
  "password",
19
21
  "passwd",
20
22
  "pwd",
@@ -37,58 +39,128 @@ class DataMasker:
37
39
  "privatekey",
38
40
  "secretkey",
39
41
  }
40
-
42
+
43
+ # Cached merged sensitive fields (loaded on first use)
44
+ _sensitive_fields: Optional[Set[str]] = None
45
+ _config_loaded: bool = False
46
+
47
+ @classmethod
48
+ def _load_config(cls, config_path: Optional[str] = None) -> None:
49
+ """
50
+ Load sensitive fields configuration from JSON and merge with hardcoded defaults.
51
+
52
+ This method is called automatically on first use. It loads JSON configuration
53
+ and merges it with hardcoded defaults, ensuring backward compatibility.
54
+
55
+ Args:
56
+ config_path: Optional custom path to JSON config file
57
+ """
58
+ if cls._config_loaded:
59
+ return
60
+
61
+ # Start with hardcoded fields as base
62
+ merged_fields = set(cls._hardcoded_sensitive_fields)
63
+
64
+ try:
65
+ # Try to load fields from JSON configuration
66
+ json_fields = get_sensitive_fields_array(config_path)
67
+ if json_fields:
68
+ # Normalize and add JSON fields (same normalization as hardcoded fields)
69
+ for field in json_fields:
70
+ if isinstance(field, str):
71
+ # Normalize: lowercase and remove underscores/hyphens
72
+ normalized = field.lower().replace("_", "").replace("-", "")
73
+ merged_fields.add(normalized)
74
+ except Exception:
75
+ # If JSON loading fails, fall back to hardcoded fields only
76
+ pass
77
+
78
+ cls._sensitive_fields = merged_fields
79
+ cls._config_loaded = True
80
+
81
+ @classmethod
82
+ def _get_sensitive_fields(cls) -> Set[str]:
83
+ """
84
+ Get the set of sensitive fields (loads config on first call).
85
+
86
+ Returns:
87
+ Set of normalized sensitive field names
88
+ """
89
+ if not cls._config_loaded:
90
+ cls._load_config()
91
+ assert cls._sensitive_fields is not None
92
+ return cls._sensitive_fields
93
+
94
+ @classmethod
95
+ def set_config_path(cls, config_path: str) -> None:
96
+ """
97
+ Set custom path for sensitive fields configuration.
98
+
99
+ Must be called before first use of DataMasker methods if custom path is needed.
100
+ Otherwise, default path or environment variable will be used.
101
+
102
+ Args:
103
+ config_path: Path to JSON configuration file
104
+ """
105
+ # Reset cache to force reload with new path
106
+ cls._config_loaded = False
107
+ cls._sensitive_fields = None
108
+ cls._load_config(config_path)
109
+
41
110
  @classmethod
42
111
  def is_sensitive_field(cls, key: str) -> bool:
43
112
  """
44
113
  Check if a field name indicates sensitive data.
45
-
114
+
46
115
  Args:
47
116
  key: Field name to check
48
-
117
+
49
118
  Returns:
50
119
  True if field is sensitive, False otherwise
51
120
  """
52
121
  # Normalize key: lowercase and remove underscores/hyphens
53
122
  normalized_key = key.lower().replace("_", "").replace("-", "")
54
-
123
+
124
+ # Get sensitive fields (loads config on first use)
125
+ sensitive_fields = cls._get_sensitive_fields()
126
+
55
127
  # Check exact match
56
- if normalized_key in cls._sensitive_fields:
128
+ if normalized_key in sensitive_fields:
57
129
  return True
58
-
130
+
59
131
  # Check if field contains sensitive keywords
60
- for sensitive_field in cls._sensitive_fields:
132
+ for sensitive_field in sensitive_fields:
61
133
  if sensitive_field in normalized_key:
62
134
  return True
63
-
135
+
64
136
  return False
65
-
137
+
66
138
  @classmethod
67
139
  def mask_sensitive_data(cls, data: Any) -> Any:
68
140
  """
69
141
  Mask sensitive data in objects, arrays, or primitives.
70
-
142
+
71
143
  Returns a masked copy without modifying the original.
72
144
  Recursively processes nested objects and arrays.
73
-
145
+
74
146
  Args:
75
147
  data: Data to mask (dict, list, or primitive)
76
-
148
+
77
149
  Returns:
78
150
  Masked copy of the data
79
151
  """
80
152
  # Handle null and undefined
81
153
  if data is None:
82
154
  return data
83
-
155
+
84
156
  # Handle primitives (string, number, boolean)
85
157
  if not isinstance(data, (dict, list)):
86
158
  return data
87
-
159
+
88
160
  # Handle arrays
89
161
  if isinstance(data, list):
90
162
  return [cls.mask_sensitive_data(item) for item in data]
91
-
163
+
92
164
  # Handle objects/dicts
93
165
  masked: dict[str, Any] = {}
94
166
  for key, value in data.items():
@@ -101,49 +173,49 @@ class DataMasker:
101
173
  else:
102
174
  # Keep non-sensitive value as-is
103
175
  masked[key] = value
104
-
176
+
105
177
  return masked
106
-
178
+
107
179
  @classmethod
108
180
  def mask_value(cls, value: str, show_first: int = 0, show_last: int = 0) -> str:
109
181
  """
110
182
  Mask specific value (useful for masking individual strings).
111
-
183
+
112
184
  Args:
113
185
  value: String value to mask
114
186
  show_first: Number of characters to show at the start
115
187
  show_last: Number of characters to show at the end
116
-
188
+
117
189
  Returns:
118
190
  Masked string value
119
191
  """
120
192
  if not value or len(value) <= show_first + show_last:
121
193
  return cls.MASKED_VALUE
122
-
194
+
123
195
  first = value[:show_first] if show_first > 0 else ""
124
196
  last = value[-show_last:] if show_last > 0 else ""
125
197
  masked_length = max(8, len(value) - show_first - show_last)
126
198
  masked = "*" * masked_length
127
-
199
+
128
200
  return f"{first}{masked}{last}"
129
-
201
+
130
202
  @classmethod
131
203
  def contains_sensitive_data(cls, data: Any) -> bool:
132
204
  """
133
205
  Check if data contains sensitive information.
134
-
206
+
135
207
  Args:
136
208
  data: Data to check
137
-
209
+
138
210
  Returns:
139
211
  True if data contains sensitive fields, False otherwise
140
212
  """
141
213
  if data is None or not isinstance(data, (dict, list)):
142
214
  return False
143
-
215
+
144
216
  if isinstance(data, list):
145
217
  return any(cls.contains_sensitive_data(item) for item in data)
146
-
218
+
147
219
  # Check object keys
148
220
  for key, value in data.items():
149
221
  if cls.is_sensitive_field(key):
@@ -151,6 +223,5 @@ class DataMasker:
151
223
  if isinstance(value, (dict, list)):
152
224
  if cls.contains_sensitive_data(value):
153
225
  return True
154
-
155
- return False
156
226
 
227
+ return False