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.
- miso_client/__init__.py +104 -84
- miso_client/errors.py +30 -4
- miso_client/models/__init__.py +4 -0
- miso_client/models/config.py +56 -35
- miso_client/models/error_response.py +41 -0
- miso_client/services/__init__.py +5 -5
- miso_client/services/auth.py +65 -48
- miso_client/services/cache.py +42 -41
- miso_client/services/encryption.py +18 -17
- miso_client/services/logger.py +115 -100
- miso_client/services/permission.py +27 -36
- miso_client/services/redis.py +17 -15
- miso_client/services/role.py +25 -36
- miso_client/utils/__init__.py +3 -3
- miso_client/utils/config_loader.py +24 -16
- miso_client/utils/data_masker.py +104 -33
- miso_client/utils/http_client.py +462 -254
- miso_client/utils/internal_http_client.py +471 -0
- miso_client/utils/jwt_tools.py +14 -17
- miso_client/utils/sensitive_fields_loader.py +116 -0
- {miso_client-0.1.0.dist-info → miso_client-0.4.0.dist-info}/METADATA +165 -3
- miso_client-0.4.0.dist-info/RECORD +26 -0
- miso_client-0.1.0.dist-info/RECORD +0 -23
- {miso_client-0.1.0.dist-info → miso_client-0.4.0.dist-info}/WHEEL +0 -0
- {miso_client-0.1.0.dist-info → miso_client-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.1.0.dist-info → miso_client-0.4.0.dist-info}/top_level.txt +0 -0
miso_client/utils/data_masker.py
CHANGED
|
@@ -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
|
-
#
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|