Skip to content

Commit

Permalink
Merge pull request #4 from zabbix/release/1.0.3
Browse files Browse the repository at this point in the history
Release v1.0.3
  • Loading branch information
aiantsen authored Jan 9, 2024
2 parents a591e06 + 1b2e268 commit a1eeeb6
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 51 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ jobs:
strategy:
matrix:
python-version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
platform:
- ubuntu-latest
- macos-latest
Expand All @@ -31,15 +31,15 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ./requirements.txt
pip install -r ./requirements-dev.txt
pip install coverage
python -m pip install --upgrade pip
pip install -r ./requirements.txt
pip install -r ./requirements-dev.txt
pip install coverage
- name: Lint with flake8
run: |
flake8 ./zabbix_utils/ --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 ./zabbix_utils/ --count --exit-zero --max-complexity=20 --max-line-length=127 --statistics
- name: Test with unittest
run: |
python -m unittest discover -s ./tests -p 'test_*.py'
python -m unittest discover -s ./tests -p 'test_*.py'
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## [1.0.3](https://github.com/zabbix/python-zabbix-utils/compare/v1.0.2...v1.0.3) (2024-01-09)

### Documentation

- added support for Python 3.12
- discontinued support for Python 3.7

### Bug fixes:

- fixed issue with hiding private (sensitive) information in the log.
- fixed small bugs and flaws.

## [1.0.2](https://github.com/zabbix/python-zabbix-utils/compare/v1.0.1...v1.0.2) (2023-12-15)

### Bug fixes:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Supported versions:

Tested on:

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

## Documentation

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
packages=["zabbix_utils"],
tests_require=["unittest"],
install_requires=["typing_extensions>=4.0.0;python_version<'3.11'"],
python_requires='>=3.7',
python_requires='>=3.8',
project_urls={
'Zabbix': 'https://www.zabbix.com/documentation/current',
'Source': 'https://github.com/zabbix/python-zabbix-utils',
Expand Down
48 changes: 38 additions & 10 deletions tests/test_zabbix_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,24 +478,52 @@ def test_hide_private(self):

test_cases = [
{
'input': json.dumps({"auth": "q2BTIw85kqmjtXl3","token": "jZAC51wHuWdwvQnx"}),
'output': json.dumps({"auth": mask, "token": mask})
'input': {"auth": "q2BTIw85kqmjtXl3","token": "jZAC51wHuWdwvQnx"},
'output': {"auth": mask, "token": mask}
},
{
'input': json.dumps({"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"}),
'output': json.dumps({"token": f"jZAC{mask}R2uW"})
'input': {"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"},
'output': {"token": f"jZAC{mask}R2uW"}
},
{
'input': json.dumps({"auth": "q2BTIw85kqmjtXl3zCgSSR26gwCGVFMK"}),
'output': json.dumps({"auth": f"q2BT{mask}VFMK"})
'input': {"auth": "q2BTIw85kqmjtXl3zCgSSR26gwCGVFMK"},
'output': {"auth": f"q2BT{mask}VFMK"}
},
{
'input': json.dumps({"sessionid": "p1xqXSf2HhYWa2ml6R5R2uWwbP2T55vh"}),
'output': json.dumps({"sessionid": f"p1xq{mask}55vh"})
'input': {"sessionid": "p1xqXSf2HhYWa2ml6R5R2uWwbP2T55vh"},
'output': {"sessionid": f"p1xq{mask}55vh"}
},
{
'input': json.dumps({"password": "HlphkcKgQKvofQHP"}),
'output': json.dumps({"password": mask})
'input': {"password": "HlphkcKgQKvofQHP"},
'output': {"password": mask}
},
{
'input': {"result": "p1xqXSf2HhYWa2ml6R5R2uWwbP2T55vh"},
'output': {"result": f"p1xq{mask}55vh"}
},
{
'input': {"result": "6.0.0"},
'output': {"result": "6.0.0"}
},
{
'input': {"result": ["10"]},
'output': {"result": ["10"]}
},
{
'input': {"result": [{"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"}]},
'output': {"result": [{"token": f"jZAC{mask}R2uW"}]}
},
{
'input': {"result": [["10"],["15"]]},
'output': {"result": [["10"],["15"]]}
},
{
'input': {"result": [[{"token": "jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"}]]},
'output': {"result": [[{"token": f"jZAC{mask}R2uW"}]]}
},
{
'input': {"result": ["jZAC51wHuWdwvQnxwbP2T55vh6R5R2uW"]},
'output': {"result": [f"jZAC{mask}R2uW"]}
}
]

Expand Down
10 changes: 5 additions & 5 deletions zabbix_utils/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ def send_api_request(self, method: str, params: Union[dict, None] = None,
headers["Authorization"] = f"Basic {self.__basic_cred}"

log.debug(
"Sending request to %s with body:%s",
"Sending request to %s with body: %s",
self.url,
json.dumps(request_json)
request_json
)

req = ul.Request(
Expand Down Expand Up @@ -457,20 +457,20 @@ def send_api_request(self, method: str, params: Union[dict, None] = None,
if method not in ModuleUtils.FILES_METHODS:
log.debug(
"Received response body: %s",
json.dumps(resp_json, indent=4, separators=(',', ': '))
resp_json
)
else:
debug_json = resp_json.copy()
if debug_json.get('result'):
debug_json['result'] = shorten(debug_json['result'], 200, placeholder='...')
log.debug(
"Received response body (short): %s",
"Received response body (clipped): %s",
json.dumps(debug_json, indent=4, separators=(',', ': '))
)

if 'error' in resp_json:
err = resp_json['error'].copy()
err['body'] = json.dumps(request_json)
err['body'] = request_json.copy()
raise APIRequestError(err)

return resp_json
Expand Down
69 changes: 51 additions & 18 deletions zabbix_utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ class ModuleUtils():

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

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

@classmethod
def hide_private(cls, message: str, fields: dict = None) -> str:
def hide_private(cls, input_data: dict, fields: dict = None) -> dict:
"""Hide private data Zabbix info (e.g. token, password)
Args:
message (str): Message text with private data.
fields (dict): Dictionary of private fields and their seeking regexps.
input_data (dict): Input dictionary with private fields.
fields (dict): Dictionary of private fields and their filtering regexps.
Returns:
str: Message text without private data.
dict: Result dictionary without private data.
"""

private_fields = fields if fields else cls.PRIVATE_FIELDS

if not isinstance(input_data, dict):
raise TypeError(f"Unsupported data type '{type(input_data).__name__}', \
only 'dict' is expected")

def gen_repl(match: Match):
return cls.mask_secret(match.group(0))

pattern = re.compile(
r"|".join([rf"(?<=\"{f}\":\s\"){r}" for f, r in private_fields.items()])
)

return re.sub(pattern, gen_repl, message)
def hide_str(k, v):
return re.sub(private_fields[k], gen_repl, v)

def hide_dict(v):
return cls.hide_private(v)

def hide_list(v):
result = []
for item in v:
if isinstance(item, dict):
result.append(hide_dict(item))
continue
if isinstance(item, list):
result.append(hide_list(item))
continue
if isinstance(item, str):
if 'result' in private_fields:
result.append(hide_str('result', item))
continue
result.append(item)
return result

result_data = input_data.copy()

for key, value in result_data.items():
if isinstance(value, str):
if key in private_fields:
result_data[key] = hide_str(key, value)
if isinstance(value, dict):
result_data[key] = hide_dict(value)
if isinstance(value, list):
result_data[key] = hide_list(value)

return result_data


class ZabbixProtocol():
Expand All @@ -126,22 +159,22 @@ class ZabbixProtocol():
HEADER_SIZE = 13

@classmethod
def __prepare_request(cls, data: Union[bytes, str, dict]) -> bytes:
def __prepare_request(cls, data: Union[bytes, str, list, dict]) -> bytes:
if isinstance(data, bytes):
return data
if isinstance(data, str):
return data.encode("utf-8")
if isinstance(data, list) or isinstance(data, dict):
return json.dumps(data, ensure_ascii=False).encode("utf-8")
raise TypeError("Unsupported data type, only 'bytes', 'str' or 'dict' is expected")
raise TypeError("Unsupported data type, only 'bytes', 'str', 'list' or 'dict' is expected")

@classmethod
def create_packet(cls, payload: Union[bytes, str, dict],
def create_packet(cls, payload: Union[bytes, str, list, dict],
log: Logger, compression: bool = False) -> bytes:
"""Create a packet for sending via the Zabbix protocol.
Args:
payload (Union[bytes, str, dict]): Payload of the future packet
payload (Union[bytes, str, list, dict]): Payload of the future packet
log (Logger): Logger object
compression (bool, optional): Compression use flag. Defaults to `False`.
Expand Down
7 changes: 4 additions & 3 deletions zabbix_utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

from typing import Union

from .common import ModuleUtils


Expand All @@ -33,13 +35,12 @@ class APIRequestError(ModuleBaseException):
"""Exception class when Zabbix API returns error by request.
Args:
api_error (str): Raw error message from Zabbix API.
api_error (Union[str, dict]): Raw error message from Zabbix API.
"""
def __init__(self, api_error: str):
def __init__(self, api_error: Union[str, dict]):
if isinstance(api_error, dict):
api_error['body'] = ModuleUtils.hide_private(api_error['body'])
super().__init__("{message} {data}".format(**api_error))
self.error = api_error
for key, value in api_error.items():
setattr(self, key, value)
else:
Expand Down
14 changes: 9 additions & 5 deletions zabbix_utils/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

import json
import logging
from .common import ModuleUtils

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

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.hide_data = ModuleUtils.hide_private

def __hide_data(self, raw_data):
return json.dumps(ModuleUtils.hide_private(raw_data), indent=4, separators=(',', ': '))

def filter(self, record):
record.msg = self.hide_data(record.msg)
if record.args:
record.args = tuple(self.hide_data(arg) if isinstance(arg, str)
else arg for arg in record.args)
if isinstance(record.args, tuple):
record.args = tuple(self.__hide_data(arg)
if isinstance(arg, dict) else arg for arg in record.args)
if isinstance(record.args, dict):
record.args = self.__hide_data(record.args)

return 1
2 changes: 1 addition & 1 deletion zabbix_utils/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

__version__ = "1.0.2"
__version__ = "1.0.3"

__min_supported__ = 5.0
__max_supported__ = 7.0

0 comments on commit a1eeeb6

Please sign in to comment.