Skip to content

Commit a1eeeb6

Browse files
authored
Merge pull request #4 from zabbix/release/1.0.3
Release v1.0.3
2 parents a591e06 + 1b2e268 commit a1eeeb6

File tree

10 files changed

+129
-51
lines changed

10 files changed

+129
-51
lines changed

.github/workflows/tests.yaml

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ jobs:
1313
strategy:
1414
matrix:
1515
python-version:
16-
- "3.7"
1716
- "3.8"
1817
- "3.9"
1918
- "3.10"
2019
- "3.11"
20+
- "3.12"
2121
platform:
2222
- ubuntu-latest
2323
- macos-latest
@@ -31,15 +31,15 @@ jobs:
3131
python-version: ${{ matrix.python-version }}
3232
- name: Install dependencies
3333
run: |
34-
python -m pip install --upgrade pip
35-
pip install -r ./requirements.txt
36-
pip install -r ./requirements-dev.txt
37-
pip install coverage
34+
python -m pip install --upgrade pip
35+
pip install -r ./requirements.txt
36+
pip install -r ./requirements-dev.txt
37+
pip install coverage
3838
- name: Lint with flake8
3939
run: |
4040
flake8 ./zabbix_utils/ --count --select=E9,F63,F7,F82 --show-source --statistics
4141
flake8 ./zabbix_utils/ --count --exit-zero --max-complexity=20 --max-line-length=127 --statistics
4242
- name: Test with unittest
4343
run: |
44-
python -m unittest discover -s ./tests -p 'test_*.py'
44+
python -m unittest discover -s ./tests -p 'test_*.py'
4545

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## [1.0.3](https://github.com/zabbix/python-zabbix-utils/compare/v1.0.2...v1.0.3) (2024-01-09)
2+
3+
### Documentation
4+
5+
- added support for Python 3.12
6+
- discontinued support for Python 3.7
7+
8+
### Bug fixes:
9+
10+
- fixed issue with hiding private (sensitive) information in the log.
11+
- fixed small bugs and flaws.
12+
113
## [1.0.2](https://github.com/zabbix/python-zabbix-utils/compare/v1.0.1...v1.0.2) (2023-12-15)
214

315
### Bug fixes:

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ Supported versions:
2727

2828
Tested on:
2929

30-
* Zabbix 5.0, 6.0, 6.4 and 7.0.0 alpha 7
31-
* Python 3.8, 3.9, 3.10 and 3.11
30+
* Zabbix 5.0, 6.0, 6.4 and pre-7.0
31+
* Python 3.8, 3.9, 3.10, 3.11 and 3.12
3232

3333
## Documentation
3434

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
packages=["zabbix_utils"],
4747
tests_require=["unittest"],
4848
install_requires=["typing_extensions>=4.0.0;python_version<'3.11'"],
49-
python_requires='>=3.7',
49+
python_requires='>=3.8',
5050
project_urls={
5151
'Zabbix': 'https://www.zabbix.com/documentation/current',
5252
'Source': 'https://github.com/zabbix/python-zabbix-utils',

tests/test_zabbix_api.py

+38-10
Original file line numberDiff line numberDiff line change
@@ -478,24 +478,52 @@ def test_hide_private(self):
478478

479479
test_cases = [
480480
{
481-
'input': json.dumps({"auth": "q2BTIw85kqmjtXl3","token": "jZAC51wHuWdwvQnx"}),
482-
'output': json.dumps({"auth": mask, "token": mask})
481+
'input': {"auth": "q2BTIw85kqmjtXl3","token": "jZAC51wHuWdwvQnx"},
482+
'output': {"auth": mask, "token": mask}
483483
},
484484
{
485-
'input': json.dumps({"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"}),
486-
'output': json.dumps({"token": f"jZAC{mask}R2uW"})
485+
'input': {"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"},
486+
'output': {"token": f"jZAC{mask}R2uW"}
487487
},
488488
{
489-
'input': json.dumps({"auth": "q2BTIw85kqmjtXl3zCgSSR26gwCGVFMK"}),
490-
'output': json.dumps({"auth": f"q2BT{mask}VFMK"})
489+
'input': {"auth": "q2BTIw85kqmjtXl3zCgSSR26gwCGVFMK"},
490+
'output': {"auth": f"q2BT{mask}VFMK"}
491491
},
492492
{
493-
'input': json.dumps({"sessionid": "p1xqXSf2HhYWa2ml6R5R2uWwbP2T55vh"}),
494-
'output': json.dumps({"sessionid": f"p1xq{mask}55vh"})
493+
'input': {"sessionid": "p1xqXSf2HhYWa2ml6R5R2uWwbP2T55vh"},
494+
'output': {"sessionid": f"p1xq{mask}55vh"}
495495
},
496496
{
497-
'input': json.dumps({"password": "HlphkcKgQKvofQHP"}),
498-
'output': json.dumps({"password": mask})
497+
'input': {"password": "HlphkcKgQKvofQHP"},
498+
'output': {"password": mask}
499+
},
500+
{
501+
'input': {"result": "p1xqXSf2HhYWa2ml6R5R2uWwbP2T55vh"},
502+
'output': {"result": f"p1xq{mask}55vh"}
503+
},
504+
{
505+
'input': {"result": "6.0.0"},
506+
'output': {"result": "6.0.0"}
507+
},
508+
{
509+
'input': {"result": ["10"]},
510+
'output': {"result": ["10"]}
511+
},
512+
{
513+
'input': {"result": [{"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"}]},
514+
'output': {"result": [{"token": f"jZAC{mask}R2uW"}]}
515+
},
516+
{
517+
'input': {"result": [["10"],["15"]]},
518+
'output': {"result": [["10"],["15"]]}
519+
},
520+
{
521+
'input': {"result": [[{"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"}]]},
522+
'output': {"result": [[{"token": f"jZAC{mask}R2uW"}]]}
523+
},
524+
{
525+
'input': {"result": ["jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"]},
526+
'output': {"result": [f"jZAC{mask}R2uW"]}
499527
}
500528
]
501529

zabbix_utils/api.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -425,9 +425,9 @@ def send_api_request(self, method: str, params: Union[dict, None] = None,
425425
headers["Authorization"] = f"Basic {self.__basic_cred}"
426426

427427
log.debug(
428-
"Sending request to %s with body:%s",
428+
"Sending request to %s with body: %s",
429429
self.url,
430-
json.dumps(request_json)
430+
request_json
431431
)
432432

433433
req = ul.Request(
@@ -457,20 +457,20 @@ def send_api_request(self, method: str, params: Union[dict, None] = None,
457457
if method not in ModuleUtils.FILES_METHODS:
458458
log.debug(
459459
"Received response body: %s",
460-
json.dumps(resp_json, indent=4, separators=(',', ': '))
460+
resp_json
461461
)
462462
else:
463463
debug_json = resp_json.copy()
464464
if debug_json.get('result'):
465465
debug_json['result'] = shorten(debug_json['result'], 200, placeholder='...')
466466
log.debug(
467-
"Received response body (short): %s",
467+
"Received response body (clipped): %s",
468468
json.dumps(debug_json, indent=4, separators=(',', ': '))
469469
)
470470

471471
if 'error' in resp_json:
472472
err = resp_json['error'].copy()
473-
err['body'] = json.dumps(request_json)
473+
err['body'] = request_json.copy()
474474
raise APIRequestError(err)
475475

476476
return resp_json

zabbix_utils/common.py

+51-18
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ class ModuleUtils():
4848

4949
# List of private fields and regular expressions to hide them
5050
PRIVATE_FIELDS = {
51-
"token": "[A-Za-z0-9]+",
52-
"auth": "[A-Za-z0-9]+",
53-
"sessionid": "[A-Za-z0-9]+",
54-
"password": "[^'\"]+",
55-
"result": "(?!(zabbix_export|[0-9.]{5}))[A-Za-z0-9]+",
51+
"token": r"^.+$",
52+
"auth": r"^.+$",
53+
"sessionid": r"^.+$",
54+
"password": r"^.+$",
55+
"result": r"^[A-Za-z0-9]{32}$",
5656
}
5757

5858
@classmethod
@@ -96,27 +96,60 @@ def mask_secret(cls, string: str, show_len: int = 4) -> str:
9696
return f"{string[:show_len]}{cls.HIDING_MASK}{string[-show_len:]}"
9797

9898
@classmethod
99-
def hide_private(cls, message: str, fields: dict = None) -> str:
99+
def hide_private(cls, input_data: dict, fields: dict = None) -> dict:
100100
"""Hide private data Zabbix info (e.g. token, password)
101101
102102
Args:
103-
message (str): Message text with private data.
104-
fields (dict): Dictionary of private fields and their seeking regexps.
103+
input_data (dict): Input dictionary with private fields.
104+
fields (dict): Dictionary of private fields and their filtering regexps.
105105
106106
Returns:
107-
str: Message text without private data.
107+
dict: Result dictionary without private data.
108108
"""
109109

110110
private_fields = fields if fields else cls.PRIVATE_FIELDS
111111

112+
if not isinstance(input_data, dict):
113+
raise TypeError(f"Unsupported data type '{type(input_data).__name__}', \
114+
only 'dict' is expected")
115+
112116
def gen_repl(match: Match):
113117
return cls.mask_secret(match.group(0))
114118

115-
pattern = re.compile(
116-
r"|".join([rf"(?<=\"{f}\":\s\"){r}" for f, r in private_fields.items()])
117-
)
118-
119-
return re.sub(pattern, gen_repl, message)
119+
def hide_str(k, v):
120+
return re.sub(private_fields[k], gen_repl, v)
121+
122+
def hide_dict(v):
123+
return cls.hide_private(v)
124+
125+
def hide_list(v):
126+
result = []
127+
for item in v:
128+
if isinstance(item, dict):
129+
result.append(hide_dict(item))
130+
continue
131+
if isinstance(item, list):
132+
result.append(hide_list(item))
133+
continue
134+
if isinstance(item, str):
135+
if 'result' in private_fields:
136+
result.append(hide_str('result', item))
137+
continue
138+
result.append(item)
139+
return result
140+
141+
result_data = input_data.copy()
142+
143+
for key, value in result_data.items():
144+
if isinstance(value, str):
145+
if key in private_fields:
146+
result_data[key] = hide_str(key, value)
147+
if isinstance(value, dict):
148+
result_data[key] = hide_dict(value)
149+
if isinstance(value, list):
150+
result_data[key] = hide_list(value)
151+
152+
return result_data
120153

121154

122155
class ZabbixProtocol():
@@ -126,22 +159,22 @@ class ZabbixProtocol():
126159
HEADER_SIZE = 13
127160

128161
@classmethod
129-
def __prepare_request(cls, data: Union[bytes, str, dict]) -> bytes:
162+
def __prepare_request(cls, data: Union[bytes, str, list, dict]) -> bytes:
130163
if isinstance(data, bytes):
131164
return data
132165
if isinstance(data, str):
133166
return data.encode("utf-8")
134167
if isinstance(data, list) or isinstance(data, dict):
135168
return json.dumps(data, ensure_ascii=False).encode("utf-8")
136-
raise TypeError("Unsupported data type, only 'bytes', 'str' or 'dict' is expected")
169+
raise TypeError("Unsupported data type, only 'bytes', 'str', 'list' or 'dict' is expected")
137170

138171
@classmethod
139-
def create_packet(cls, payload: Union[bytes, str, dict],
172+
def create_packet(cls, payload: Union[bytes, str, list, dict],
140173
log: Logger, compression: bool = False) -> bytes:
141174
"""Create a packet for sending via the Zabbix protocol.
142175
143176
Args:
144-
payload (Union[bytes, str, dict]): Payload of the future packet
177+
payload (Union[bytes, str, list, dict]): Payload of the future packet
145178
log (Logger): Logger object
146179
compression (bool, optional): Compression use flag. Defaults to `False`.
147180

zabbix_utils/exceptions.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
2323
# OTHER DEALINGS IN THE SOFTWARE.
2424

25+
from typing import Union
26+
2527
from .common import ModuleUtils
2628

2729

@@ -33,13 +35,12 @@ class APIRequestError(ModuleBaseException):
3335
"""Exception class when Zabbix API returns error by request.
3436
3537
Args:
36-
api_error (str): Raw error message from Zabbix API.
38+
api_error (Union[str, dict]): Raw error message from Zabbix API.
3739
"""
38-
def __init__(self, api_error: str):
40+
def __init__(self, api_error: Union[str, dict]):
3941
if isinstance(api_error, dict):
4042
api_error['body'] = ModuleUtils.hide_private(api_error['body'])
4143
super().__init__("{message} {data}".format(**api_error))
42-
self.error = api_error
4344
for key, value in api_error.items():
4445
setattr(self, key, value)
4546
else:

zabbix_utils/logger.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
2323
# OTHER DEALINGS IN THE SOFTWARE.
2424

25+
import json
2526
import logging
2627
from .common import ModuleUtils
2728

@@ -38,12 +39,15 @@ class SensitiveFilter(logging.Filter):
3839

3940
def __init__(self, *args, **kwargs):
4041
super().__init__(*args, **kwargs)
41-
self.hide_data = ModuleUtils.hide_private
42+
43+
def __hide_data(self, raw_data):
44+
return json.dumps(ModuleUtils.hide_private(raw_data), indent=4, separators=(',', ': '))
4245

4346
def filter(self, record):
44-
record.msg = self.hide_data(record.msg)
45-
if record.args:
46-
record.args = tuple(self.hide_data(arg) if isinstance(arg, str)
47-
else arg for arg in record.args)
47+
if isinstance(record.args, tuple):
48+
record.args = tuple(self.__hide_data(arg)
49+
if isinstance(arg, dict) else arg for arg in record.args)
50+
if isinstance(record.args, dict):
51+
record.args = self.__hide_data(record.args)
4852

4953
return 1

zabbix_utils/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
2323
# OTHER DEALINGS IN THE SOFTWARE.
2424

25-
__version__ = "1.0.2"
25+
__version__ = "1.0.3"
2626

2727
__min_supported__ = 5.0
2828
__max_supported__ = 7.0

0 commit comments

Comments
 (0)