maskinfly 0.1.1__tar.gz

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.
@@ -0,0 +1,7 @@
1
+ Copyright <2026> <MordantAcid>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ include requirements.txt
5
+ recursive-include maskify *.py
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: maskinfly
3
+ Version: 0.1.1
4
+ Summary: Библиотека для маскировки чувствительных данных (пароли, токены, и т.д.) в строках, словарях и списках
5
+ Author-email: MordantAcid <cagej7517@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/MordantAcid/maskifly
8
+ Project-URL: Repository, https://github.com/MordantAcid/maskifly.git
9
+ Keywords: masking,data-masking,privacy,security,pii,secrets
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.7
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: pydantic
25
+ Requires-Dist: pydantic>=2.0.0; extra == "pydantic"
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=7.0.0; extra == "test"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pydantic>=2.0.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # maskinfly
34
+
35
+ **maskinfly** — это легковесная библиотека для рекурсивной маскировки чувствительных данных (паролей, токенов, email, номеров карт, SSN, IP-адресов и т.д.) в Python-структурах. Она автоматически обнаруживает и заменяет конфиденциальную информацию в строках, словарях и списках, а также поддерживает аудит всех произведённых замен.
36
+
37
+ ## Возможности
38
+
39
+ - **Рекурсивная маскировка** — работает с вложенными словарями, списками и другими коллекциями.
40
+ - **Встроенные паттерны** — пароли, JWT, email, номера кредитных карт (регулярное выражение ошибочно, но оставлено для совместимости), SSN, IP-адреса, токены.
41
+ - **Маскировка по имени переменной** — если в коде переменная называется `password`, `api_key` и т.п., её значение будет замаскировано даже без явного паттерна.
42
+ - **Аудит** — логирование причины замены (`pattern`, `varname`, `type`) и пути к значению.
43
+ - **Поддержка `pydantic.SecretStr`** — если установлен Pydantic, объекты `SecretStr` маскируются автоматически.
44
+ - **Простой интерфейс** — функция `mask()` для быстрой маскировки или класс `Masker` для тонкой настройки.
45
+
46
+ ## Установка
47
+
48
+ ```bash
49
+ pip install maskify
50
+
51
+ git clone https://github.com/MordantAcid/maskifly.git
52
+
53
+ Для поддержки pydantic.SecretStr установите дополнительную зависимость:
54
+
55
+ pip install .[pydantic]
56
+
57
+ Быстрый старт
58
+
59
+ from maskify import mask
60
+
61
+ # Маскировка в словаре
62
+ data = {
63
+ "user": "john",
64
+ "password": "secret123",
65
+ "email": "john@example.com"
66
+ }
67
+ masked = mask(data)
68
+ print(masked)
69
+ # {'user': 'john', 'password': '***', 'email': 'john@example.com'}
70
+
71
+ # Маскировка в строке
72
+ text = "My token is abc123xyz"
73
+ print(mask(text))
74
+ # 'My token is ***'
75
+
76
+ # Включение аудита (лог будет выведен в stderr)
77
+ mask(data, audit_enabled=True)
78
+ # Вывод в лог: 2025-01-01 12:00:00 - MASKIFY_AUDIT - Значение маски 'password' | reason=pattern | type=str
79
+
80
+ Использование
81
+ Функция mask()
82
+ Самый простой способ — импортировать mask и передать данные:
83
+
84
+ result = mask(data, audit_enabled=False, audit_logger=None)
85
+
86
+ - data — любые данные (str, dict, list и т.д.).
87
+ - audit_enabled — если True, включает логирование аудита.
88
+ - audit_logger — собственный экземпляр AuditLogger (опционально).
89
+
90
+ Класс Masker
91
+ Для более гибкого управления создайте экземпляр Masker:
92
+
93
+ from maskify import Masker
94
+
95
+ masker = Masker(audit_enabled=True)
96
+ masked_data = masker.mask(data)
97
+
98
+ Параметры конструктора:
99
+
100
+ - audit_enabled: bool = False
101
+ - audit_logger: Optional[AuditLogger] = None
102
+
103
+ Метод mask(data, path="") принимает произвольные данные и опциональный строковый путь (используется для аудита).
104
+
105
+ Аудит: AuditLogger
106
+ По умолчанию аудит пишет в logging.getLogger("maskify.audit") с уровнем INFO и форматированием '%(asctime)s - MASKIFY_AUDIT - %(message)s'. Вы можете передать свой логгер:
107
+
108
+ import logging
109
+ from maskify import AuditLogger
110
+
111
+ custom_logger = logging.getLogger("my_audit")
112
+ audit = AuditLogger(logger=custom_logger)
113
+ masker = Masker(audit_enabled=True, audit_logger=audit)
114
+ masker.mask({"secret": "value"})
115
+
116
+ Маскировка по имени переменной
117
+ Если значение не подошло ни под один паттерн, библиотека пытается определить имя переменной, в которой оно хранится (с помощью inspect). Если имя входит в набор SENSITIVE_VAR_NAMES, значение заменяется на ***.
118
+
119
+ Чувствительные имена по умолчанию:
120
+
121
+ password, passwd, pwd, secret, token, api_key, apikey,
122
+ credit_card, creditcard, card_number, ssn, social_security,
123
+ pin, auth, bearer, private_key
124
+
125
+ Пример:
126
+
127
+ secret_token = "abc123xyz"
128
+ mask(secret_token) # -> "***" (переменная называется secret_token)
129
+
130
+ Работа с pydantic.SecretStr
131
+ Если установлен Pydantic, объекты SecretStr маскируются вне зависимости от содержимого:
132
+
133
+ from pydantic import SecretStr
134
+ from maskify import mask
135
+
136
+ secret = SecretStr("real_password")
137
+ masked = mask(secret) # -> "***"
138
+
139
+ Требования
140
+ Python ≥ 3.7
141
+
142
+ Для опциональной поддержки SecretStr: pydantic >= 2.0.0
@@ -0,0 +1,110 @@
1
+ # maskinfly
2
+
3
+ **maskinfly** — это легковесная библиотека для рекурсивной маскировки чувствительных данных (паролей, токенов, email, номеров карт, SSN, IP-адресов и т.д.) в Python-структурах. Она автоматически обнаруживает и заменяет конфиденциальную информацию в строках, словарях и списках, а также поддерживает аудит всех произведённых замен.
4
+
5
+ ## Возможности
6
+
7
+ - **Рекурсивная маскировка** — работает с вложенными словарями, списками и другими коллекциями.
8
+ - **Встроенные паттерны** — пароли, JWT, email, номера кредитных карт (регулярное выражение ошибочно, но оставлено для совместимости), SSN, IP-адреса, токены.
9
+ - **Маскировка по имени переменной** — если в коде переменная называется `password`, `api_key` и т.п., её значение будет замаскировано даже без явного паттерна.
10
+ - **Аудит** — логирование причины замены (`pattern`, `varname`, `type`) и пути к значению.
11
+ - **Поддержка `pydantic.SecretStr`** — если установлен Pydantic, объекты `SecretStr` маскируются автоматически.
12
+ - **Простой интерфейс** — функция `mask()` для быстрой маскировки или класс `Masker` для тонкой настройки.
13
+
14
+ ## Установка
15
+
16
+ ```bash
17
+ pip install maskify
18
+
19
+ git clone https://github.com/MordantAcid/maskifly.git
20
+
21
+ Для поддержки pydantic.SecretStr установите дополнительную зависимость:
22
+
23
+ pip install .[pydantic]
24
+
25
+ Быстрый старт
26
+
27
+ from maskify import mask
28
+
29
+ # Маскировка в словаре
30
+ data = {
31
+ "user": "john",
32
+ "password": "secret123",
33
+ "email": "john@example.com"
34
+ }
35
+ masked = mask(data)
36
+ print(masked)
37
+ # {'user': 'john', 'password': '***', 'email': 'john@example.com'}
38
+
39
+ # Маскировка в строке
40
+ text = "My token is abc123xyz"
41
+ print(mask(text))
42
+ # 'My token is ***'
43
+
44
+ # Включение аудита (лог будет выведен в stderr)
45
+ mask(data, audit_enabled=True)
46
+ # Вывод в лог: 2025-01-01 12:00:00 - MASKIFY_AUDIT - Значение маски 'password' | reason=pattern | type=str
47
+
48
+ Использование
49
+ Функция mask()
50
+ Самый простой способ — импортировать mask и передать данные:
51
+
52
+ result = mask(data, audit_enabled=False, audit_logger=None)
53
+
54
+ - data — любые данные (str, dict, list и т.д.).
55
+ - audit_enabled — если True, включает логирование аудита.
56
+ - audit_logger — собственный экземпляр AuditLogger (опционально).
57
+
58
+ Класс Masker
59
+ Для более гибкого управления создайте экземпляр Masker:
60
+
61
+ from maskify import Masker
62
+
63
+ masker = Masker(audit_enabled=True)
64
+ masked_data = masker.mask(data)
65
+
66
+ Параметры конструктора:
67
+
68
+ - audit_enabled: bool = False
69
+ - audit_logger: Optional[AuditLogger] = None
70
+
71
+ Метод mask(data, path="") принимает произвольные данные и опциональный строковый путь (используется для аудита).
72
+
73
+ Аудит: AuditLogger
74
+ По умолчанию аудит пишет в logging.getLogger("maskify.audit") с уровнем INFO и форматированием '%(asctime)s - MASKIFY_AUDIT - %(message)s'. Вы можете передать свой логгер:
75
+
76
+ import logging
77
+ from maskify import AuditLogger
78
+
79
+ custom_logger = logging.getLogger("my_audit")
80
+ audit = AuditLogger(logger=custom_logger)
81
+ masker = Masker(audit_enabled=True, audit_logger=audit)
82
+ masker.mask({"secret": "value"})
83
+
84
+ Маскировка по имени переменной
85
+ Если значение не подошло ни под один паттерн, библиотека пытается определить имя переменной, в которой оно хранится (с помощью inspect). Если имя входит в набор SENSITIVE_VAR_NAMES, значение заменяется на ***.
86
+
87
+ Чувствительные имена по умолчанию:
88
+
89
+ password, passwd, pwd, secret, token, api_key, apikey,
90
+ credit_card, creditcard, card_number, ssn, social_security,
91
+ pin, auth, bearer, private_key
92
+
93
+ Пример:
94
+
95
+ secret_token = "abc123xyz"
96
+ mask(secret_token) # -> "***" (переменная называется secret_token)
97
+
98
+ Работа с pydantic.SecretStr
99
+ Если установлен Pydantic, объекты SecretStr маскируются вне зависимости от содержимого:
100
+
101
+ from pydantic import SecretStr
102
+ from maskify import mask
103
+
104
+ secret = SecretStr("real_password")
105
+ masked = mask(secret) # -> "***"
106
+
107
+ Требования
108
+ Python ≥ 3.7
109
+
110
+ Для опциональной поддержки SecretStr: pydantic >= 2.0.0
@@ -0,0 +1,30 @@
1
+ from maskinfly.masker import Masker
2
+ from maskinfly.audit import AuditLogger
3
+ from typing import Optional
4
+
5
+ __all__ = [
6
+ "mask", "AuditLogger", "Masker"
7
+ ]
8
+
9
+ __version__ = "0.1.1"
10
+
11
+ def mask(data, audit_enabled: bool = False, audit_logger: Optional[AuditLogger] = None):
12
+ """Основной удобный интерфейс для маскировки данных.
13
+
14
+ Примеры:
15
+ >>> mask({"user": "john", "password", "secret123"})
16
+ {'user': 'john', 'password': '***'}
17
+
18
+ >>> mask("My token is abc123xyz", audit_enabled=True)
19
+ 'My token is ***'
20
+
21
+ Args:
22
+ data: любые данные (строка, dict, список и т.д.)
23
+ audit_enabled: включить логирование аудита
24
+ audit_logger: свой экземпляр AuditLogger (опционально)
25
+
26
+ Returns:
27
+ замаскированная копия данных
28
+ """
29
+ masker = Masker(audit_enabled=audit_enabled, audit_logger=audit_logger)
30
+ return masker.mask(data)
@@ -0,0 +1,18 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ class AuditLogger:
5
+ def __init__(self, logger: Optional[logging.Logger] = None):
6
+ if logger is None:
7
+ self.logger = logging.getLogger("maskify.audit")
8
+ if not self.logger.handlers:
9
+ handler = logging.StreamHandler()
10
+ formatter = logging.Formatter('%(asctime)s - MASKIFY_AUDIT - %(message)s')
11
+ handler.setFormatter(formatter)
12
+ self.logger.addHandler(handler)
13
+ self.logger.setLevel(logging.INFO)
14
+ else:
15
+ self.logger = logger
16
+
17
+ def log(self, path: str, reason: str, value_type: str) -> None:
18
+ self.logger.info(f"Значение маски '{path}' | reason={reason} | type={value_type}")
@@ -0,0 +1,97 @@
1
+ import re
2
+ from typing import Any, Dict, List, Tuple, Optional, Union
3
+ from collections.abc import Mapping, Sequence
4
+ from maskinfly.patterns import PATTERNS, DEFAULT_MASK
5
+ from maskinfly.audit import AuditLogger
6
+ from maskinfly.utils import find_variable_name, SENSITIVE_VAR_NAMES
7
+
8
+ try:
9
+ from pydantic import SecretStr
10
+ HAS_PYDANTIC = True
11
+ except ImportError:
12
+ HAS_PYDANTIC = False
13
+ SecretStr = None
14
+
15
+ class Masker:
16
+ def __init__(self, audit_enabled: bool = False, audit_logger: Optional[AuditLogger] = None):
17
+ self.audit_enabled = audit_enabled
18
+ self.audit = audit_logger or AuditLogger() if audit_enabled else None
19
+ self.patterns = PATTERNS.copy()
20
+ self.mask_str = DEFAULT_MASK
21
+
22
+ def mask(self, data: Any, path: str = "") -> Any:
23
+ if data is None:
24
+ return None
25
+
26
+ # Маскировка по типу SecretStr
27
+ if HAS_PYDANTIC and SecretStr is not None and isinstance(data, SecretStr):
28
+ if self.audit_enabled and self.audit:
29
+ self.audit.log(path or "root", "type", "SecretStr")
30
+ return self.mask_str
31
+
32
+ # Маскировка строк
33
+ if isinstance(data, str):
34
+ return self.mask_string(data, path)
35
+
36
+ # Маскировка словарей
37
+ if isinstance(data, Mapping):
38
+ new_dict = {}
39
+ for key, val in data.items():
40
+ new_path = f"{path}.{key}" if path else key
41
+ # Если ключ чувствительный, маскируем значение независимо от его типа
42
+ if key.lower() in SENSITIVE_VAR_NAMES:
43
+ # Маскируем только если это строка или SecretStr
44
+ if isinstance(val, str):
45
+ masked_val = self.mask_str
46
+ if self.audit_enabled and self.audit:
47
+ self.audit.log(new_path, "varname", "str")
48
+ elif HAS_PYDANTIC and SecretStr is not None and isinstance(val, SecretStr):
49
+ masked_val = self.mask_str
50
+ if self.audit_enabled and self.audit:
51
+ self.audit.log(new_path, "varname", "SecretStr")
52
+ else:
53
+ # Для нестроковых значений просто рекурсивно маскируем
54
+ masked_val = self.mask(val, new_path)
55
+ new_dict[key] = masked_val
56
+ else:
57
+ new_dict[key] = self.mask(val, new_path)
58
+ return new_dict
59
+
60
+ # Маскировка последовательностей (кроме строк)
61
+ if isinstance(data, Sequence) and not isinstance(data, (str, bytes)):
62
+ return [self.mask(item, f"{path}[i]") for i, item in enumerate(data)]
63
+
64
+ # Остальные типы не маскируем
65
+ return data
66
+
67
+ def mask_string(self, value: str, path: str) -> str:
68
+ masked = value
69
+ reason = None
70
+
71
+ # 1. Паттерны
72
+ for pattern_name, regex in self.patterns.items():
73
+ if regex.search(masked):
74
+ masked = regex.sub(self._replacer, masked)
75
+ reason = "pattern"
76
+ break
77
+
78
+ # 2. По имени переменной (если еще не замаскировано)
79
+ if reason is None:
80
+ var_name = find_variable_name(value, frame_depth=3)
81
+ if var_name and var_name.lower() in SENSITIVE_VAR_NAMES:
82
+ masked = self.mask_str
83
+ reason = "varname"
84
+
85
+ # Аудит
86
+ if self.audit_enabled and reason and masked != value and self.audit:
87
+ self.audit.log(path, reason, "str")
88
+
89
+ return masked
90
+
91
+ @staticmethod
92
+ def _replacer(match: re.Match) -> str:
93
+ groups = match.groups()
94
+ if groups:
95
+ last_group = groups[-1]
96
+ return match.string[:match.start(len(groups))] + DEFAULT_MASK + match.string[match.end():]
97
+ return DEFAULT_MASK
@@ -0,0 +1,13 @@
1
+ import re
2
+
3
+ PATTERNS = {
4
+ "password": re.compile(r'(?i)(password|passwd|pwd)(\s*[:=]\s*)(\S+)'),
5
+ "token": re.compile(r'(?i)(token|api_key|apikey)(\s*[:=]\s*)(\S+)'),
6
+ "credit_card": re.compile(r'\b[\w\.-]+@[\w\.-]+\.\w+\b'),
7
+ "email": re.compile(r'\b[\w\.-]+@[\w\.-]+\.\w+\b'),
8
+ "jwt": re.compile(r'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'),
9
+ "ip": re.compile(r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b'),
10
+ "ssn": re.compile(r'\b\d{3}-\d{2}-\d{4}\b'),
11
+ }
12
+
13
+ DEFAULT_MASK = "***"
@@ -0,0 +1,31 @@
1
+ import inspect
2
+ from typing import Any, Optional
3
+
4
+ def find_variable_name(value: Any, frame_depth: int = 2) -> Optional[str]:
5
+ frame = inspect.currentframe()
6
+ try:
7
+ for _ in range(frame_depth):
8
+ if frame is None:
9
+ return None
10
+ frame = frame.f_back
11
+ if frame is None:
12
+ return None
13
+
14
+ value_id = id(value)
15
+ # Сначала ищем в locals
16
+ for name, val in frame.f_locals.items():
17
+ if id(val) == value_id:
18
+ return name
19
+ # Потом в globals
20
+ for name, val in frame.f_globals.items():
21
+ if id(val) == value_id:
22
+ return name
23
+ finally:
24
+ del frame
25
+ return None
26
+
27
+ SENSITIVE_VAR_NAMES = {
28
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
29
+ 'credit_card', 'creditcard', 'card_number', 'ssn', 'social_security',
30
+ 'pin', 'auth', 'bearer', 'private_key'
31
+ }
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: maskinfly
3
+ Version: 0.1.1
4
+ Summary: Библиотека для маскировки чувствительных данных (пароли, токены, и т.д.) в строках, словарях и списках
5
+ Author-email: MordantAcid <cagej7517@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/MordantAcid/maskifly
8
+ Project-URL: Repository, https://github.com/MordantAcid/maskifly.git
9
+ Keywords: masking,data-masking,privacy,security,pii,secrets
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.7
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: pydantic
25
+ Requires-Dist: pydantic>=2.0.0; extra == "pydantic"
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=7.0.0; extra == "test"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pydantic>=2.0.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # maskinfly
34
+
35
+ **maskinfly** — это легковесная библиотека для рекурсивной маскировки чувствительных данных (паролей, токенов, email, номеров карт, SSN, IP-адресов и т.д.) в Python-структурах. Она автоматически обнаруживает и заменяет конфиденциальную информацию в строках, словарях и списках, а также поддерживает аудит всех произведённых замен.
36
+
37
+ ## Возможности
38
+
39
+ - **Рекурсивная маскировка** — работает с вложенными словарями, списками и другими коллекциями.
40
+ - **Встроенные паттерны** — пароли, JWT, email, номера кредитных карт (регулярное выражение ошибочно, но оставлено для совместимости), SSN, IP-адреса, токены.
41
+ - **Маскировка по имени переменной** — если в коде переменная называется `password`, `api_key` и т.п., её значение будет замаскировано даже без явного паттерна.
42
+ - **Аудит** — логирование причины замены (`pattern`, `varname`, `type`) и пути к значению.
43
+ - **Поддержка `pydantic.SecretStr`** — если установлен Pydantic, объекты `SecretStr` маскируются автоматически.
44
+ - **Простой интерфейс** — функция `mask()` для быстрой маскировки или класс `Masker` для тонкой настройки.
45
+
46
+ ## Установка
47
+
48
+ ```bash
49
+ pip install maskify
50
+
51
+ git clone https://github.com/MordantAcid/maskifly.git
52
+
53
+ Для поддержки pydantic.SecretStr установите дополнительную зависимость:
54
+
55
+ pip install .[pydantic]
56
+
57
+ Быстрый старт
58
+
59
+ from maskify import mask
60
+
61
+ # Маскировка в словаре
62
+ data = {
63
+ "user": "john",
64
+ "password": "secret123",
65
+ "email": "john@example.com"
66
+ }
67
+ masked = mask(data)
68
+ print(masked)
69
+ # {'user': 'john', 'password': '***', 'email': 'john@example.com'}
70
+
71
+ # Маскировка в строке
72
+ text = "My token is abc123xyz"
73
+ print(mask(text))
74
+ # 'My token is ***'
75
+
76
+ # Включение аудита (лог будет выведен в stderr)
77
+ mask(data, audit_enabled=True)
78
+ # Вывод в лог: 2025-01-01 12:00:00 - MASKIFY_AUDIT - Значение маски 'password' | reason=pattern | type=str
79
+
80
+ Использование
81
+ Функция mask()
82
+ Самый простой способ — импортировать mask и передать данные:
83
+
84
+ result = mask(data, audit_enabled=False, audit_logger=None)
85
+
86
+ - data — любые данные (str, dict, list и т.д.).
87
+ - audit_enabled — если True, включает логирование аудита.
88
+ - audit_logger — собственный экземпляр AuditLogger (опционально).
89
+
90
+ Класс Masker
91
+ Для более гибкого управления создайте экземпляр Masker:
92
+
93
+ from maskify import Masker
94
+
95
+ masker = Masker(audit_enabled=True)
96
+ masked_data = masker.mask(data)
97
+
98
+ Параметры конструктора:
99
+
100
+ - audit_enabled: bool = False
101
+ - audit_logger: Optional[AuditLogger] = None
102
+
103
+ Метод mask(data, path="") принимает произвольные данные и опциональный строковый путь (используется для аудита).
104
+
105
+ Аудит: AuditLogger
106
+ По умолчанию аудит пишет в logging.getLogger("maskify.audit") с уровнем INFO и форматированием '%(asctime)s - MASKIFY_AUDIT - %(message)s'. Вы можете передать свой логгер:
107
+
108
+ import logging
109
+ from maskify import AuditLogger
110
+
111
+ custom_logger = logging.getLogger("my_audit")
112
+ audit = AuditLogger(logger=custom_logger)
113
+ masker = Masker(audit_enabled=True, audit_logger=audit)
114
+ masker.mask({"secret": "value"})
115
+
116
+ Маскировка по имени переменной
117
+ Если значение не подошло ни под один паттерн, библиотека пытается определить имя переменной, в которой оно хранится (с помощью inspect). Если имя входит в набор SENSITIVE_VAR_NAMES, значение заменяется на ***.
118
+
119
+ Чувствительные имена по умолчанию:
120
+
121
+ password, passwd, pwd, secret, token, api_key, apikey,
122
+ credit_card, creditcard, card_number, ssn, social_security,
123
+ pin, auth, bearer, private_key
124
+
125
+ Пример:
126
+
127
+ secret_token = "abc123xyz"
128
+ mask(secret_token) # -> "***" (переменная называется secret_token)
129
+
130
+ Работа с pydantic.SecretStr
131
+ Если установлен Pydantic, объекты SecretStr маскируются вне зависимости от содержимого:
132
+
133
+ from pydantic import SecretStr
134
+ from maskify import mask
135
+
136
+ secret = SecretStr("real_password")
137
+ masked = mask(secret) # -> "***"
138
+
139
+ Требования
140
+ Python ≥ 3.7
141
+
142
+ Для опциональной поддержки SecretStr: pydantic >= 2.0.0
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ requirements.txt
6
+ maskinfly/__init__.py
7
+ maskinfly/audit.py
8
+ maskinfly/masker.py
9
+ maskinfly/patterns.py
10
+ maskinfly/utils.py
11
+ maskinfly.egg-info/PKG-INFO
12
+ maskinfly.egg-info/SOURCES.txt
13
+ maskinfly.egg-info/dependency_links.txt
14
+ maskinfly.egg-info/requires.txt
15
+ maskinfly.egg-info/top_level.txt
16
+ tests/test_audit.py
17
+ tests/test_init.py
18
+ tests/test_masker.py
19
+ tests/test_patterns.py
20
+ tests/test_utils.py
@@ -0,0 +1,10 @@
1
+
2
+ [dev]
3
+ pytest>=7.0.0
4
+ pydantic>=2.0.0
5
+
6
+ [pydantic]
7
+ pydantic>=2.0.0
8
+
9
+ [test]
10
+ pytest>=7.0.0
@@ -0,0 +1 @@
1
+ maskinfly
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "maskinfly"
7
+ version = "0.1.1"
8
+ description = "Библиотека для маскировки чувствительных данных (пароли, токены, и т.д.) в строках, словарях и списках"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "MordantAcid", email = "cagej7517@gmail.com"},
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.7",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Topic :: Security",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Typing :: Typed",
27
+ ]
28
+ keywords = ["masking", "data-masking", "privacy", "security", "pii", "secrets"]
29
+
30
+ [project.optional-dependencies]
31
+ pydantic = ["pydantic>=2.0.0"]
32
+ test = ["pytest>=7.0.0"]
33
+ dev = ["pytest>=7.0.0", "pydantic>=2.0.0"]
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["."]
37
+ include = ["maskinfly*"]
38
+
39
+ [tool.pytest.ini_options]
40
+ minversion = "7.0"
41
+ testpaths = ["tests"]
42
+ python_files = "test_*.py"
43
+ python_classes = "Test*"
44
+ python_functions = "test_*"
45
+
46
+ [project.urls]
47
+
48
+ Homepage = "https://github.com/MordantAcid/maskifly"
49
+ Repository = "https://github.com/MordantAcid/maskifly.git"
@@ -0,0 +1,5 @@
1
+ # Для дополнительной поддержки SecretStr установите:
2
+ pydantic>=2.0.0
3
+
4
+ # Для Тестирования
5
+ pytest>=7.0.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ import logging
2
+ import pytest
3
+ from maskinfly.audit import AuditLogger
4
+
5
+ def test_audit_logger_creates_handler():
6
+ logger = AuditLogger()
7
+ assert len(logger.logger.handlers) == 1
8
+ assert isinstance(logger.logger.handlers[0], logging.StreamHandler)
9
+
10
+ def test_audit_logger_respects_provided_logger():
11
+ custom_logger = logging.getLogger("custom")
12
+ custom_logger.handlers.clear()
13
+ logger = AuditLogger(logger=custom_logger)
14
+ assert logger.logger is custom_logger
15
+ assert len(logger.logger.handlers) == 0
16
+
17
+ def test_audit_logger_log(caplog):
18
+ caplog.set_level(logging.INFO, logger="maskify.audit")
19
+ logger = AuditLogger()
20
+ logger.log("user.password", "pattern", "str")
21
+
22
+ assert len(caplog.records) == 1
23
+ record = caplog.records[0]
24
+ assert "Значение маски 'user.password'" in record.message
25
+ assert "reason=pattern" in record.message
26
+ assert "type=str" in record.message
@@ -0,0 +1,28 @@
1
+ import pytest
2
+ from unittest.mock import patch
3
+ from maskinfly import mask, Masker, AuditLogger, __version__
4
+
5
+ def test_mask_function_with_dict():
6
+ result = mask({"user": "alice", "password": "pass123"})
7
+ assert result["password"] == "***"
8
+ assert result["user"] == "alice"
9
+
10
+ def test_mask_function_with_string():
11
+ jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
12
+ result = mask(f"My token is {jwt}")
13
+ assert result == "My token is ***"
14
+
15
+ def test_mask_function_with_audit(caplog):
16
+ caplog.set_level("INFO", logger="maskify.audit")
17
+ mask({"secret": "abc"}, audit_enabled=True)
18
+ assert len(caplog.records) > 0
19
+ record = caplog.records[0]
20
+ assert "Значение маски 'secret'" in record.message
21
+ assert "reason=varname" in record.message
22
+ assert "type=str" in record.message
23
+
24
+ def test_mask_function_custom_audit_logger():
25
+ custom_logger = AuditLogger()
26
+ with patch.object(custom_logger, "log") as mock_log:
27
+ mask("token=xyz", audit_enabled=True, audit_logger=custom_logger)
28
+ mock_log.assert_called()
@@ -0,0 +1,65 @@
1
+ import pytest
2
+ from unittest.mock import patch
3
+ from maskinfly.masker import Masker, HAS_PYDANTIC
4
+
5
+ pytestmark = pytest.mark.skipif(not HAS_PYDANTIC, reason="pydantic not installed")
6
+
7
+ def test_mask_string_by_pattern(masker_no_audit):
8
+ result = masker_no_audit.mask_string("password=12345", "test")
9
+ assert result == "password=***"
10
+
11
+ def test_mask_string_by_varname(masker_no_audit):
12
+ with patch("maskinfly.masker.find_variable_name", return_value="password"):
13
+ result = masker_no_audit.mask_string("my_secret", "some.path")
14
+ assert result == "***"
15
+
16
+ def test_mask_string_no_match(masker_no_audit):
17
+ with patch("maskinfly.masker.find_variable_name", return_value=None):
18
+ result = masker_no_audit.mask_string("hello world!", "path")
19
+ assert result == "hello world!"
20
+
21
+ def test_mask_string_audit(masker_with_audit, audit_logger):
22
+ with patch("maskinfly.masker.find_variable_name", return_value="password"):
23
+ # Подменим audit логгер для проверки вызова
24
+ masker_with_audit.audit = audit_logger
25
+ result = masker_with_audit.mask_string("secret_value", "user.pwd")
26
+ assert result == "***"
27
+ # Проверим, что audit.log был вызван
28
+ with patch.object(audit_logger, "log") as mock_log:
29
+ masker_with_audit.mask_string("secret", "path")
30
+ mock_log.assert_called_once_with("path", "varname", "str")
31
+
32
+ def test_mask_dict(masker_no_audit):
33
+ data = {
34
+ "user": "john",
35
+ "password": "secret123",
36
+ "nested": {"api_key": "abc123"}
37
+ }
38
+ masked = masker_no_audit.mask(data)
39
+ assert masked["password"] == "***"
40
+ assert masked["nested"]["api_key"] == "***"
41
+ assert masked["user"] == "john"
42
+
43
+ def test_mask_list(masker_no_audit):
44
+ data = ["token=xyz", "safe", {"pwd": "pass"}]
45
+ masked = masker_no_audit.mask(data)
46
+ assert masked[0] == "token=***"
47
+ assert masked[1] == "safe"
48
+ assert masked[2]["pwd"] == "***"
49
+
50
+ def test_mask_secret_str(masker_no_audit):
51
+ from pydantic import SecretStr
52
+ secret = SecretStr("real_secret")
53
+ masked = masker_no_audit.mask(secret, path="secret_field")
54
+ assert masked == "***"
55
+
56
+ def test_mask_other_type(masker_no_audit):
57
+ assert masker_no_audit.mask(42) == 42
58
+ assert masker_no_audit.mask(True) is True
59
+ assert masker_no_audit.mask(None) is None
60
+
61
+ def test_mask_replacer(masker_with_audit):
62
+ pattern = masker_with_audit.patterns["password"]
63
+ match = pattern.search("password=12345")
64
+ replaced = masker_with_audit._replacer(match)
65
+ assert replaced == "password=***"
@@ -0,0 +1,37 @@
1
+ import re
2
+ from maskinfly.patterns import PATTERNS, DEFAULT_MASK
3
+
4
+ def test_patterns_are_compile():
5
+ for name, pattern in PATTERNS.items():
6
+ assert isinstance(pattern, re.Pattern), f"Паттерн {name} не скомпилирован"
7
+
8
+ def test_password_pattern():
9
+ pattern = PATTERNS["password"]
10
+ match = pattern.search("password=12345")
11
+ assert match is not None
12
+ groups = match.groups()
13
+ assert len(groups) == 3
14
+ assert groups[2] == "12345"
15
+
16
+ def test_token_pattern():
17
+ pattern = PATTERNS["token"]
18
+ match = pattern.search("token=abc123")
19
+ assert match is not None
20
+ groups = match.groups()
21
+ assert groups[2] == "abc123"
22
+
23
+ def test_email_pattern():
24
+ pattern = PATTERNS["email"]
25
+ assert pattern.search("user@example.com") is not None
26
+ assert pattern.search("Недействительный") is None
27
+
28
+ def test_jwt_pattern():
29
+ pattern = PATTERNS["jwt"]
30
+ jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
31
+ assert pattern.search(jwt) is not None
32
+
33
+ def test_ip_pattern():
34
+ pattern = PATTERNS["ip"]
35
+ assert pattern.search("192.168.1.1") is not None
36
+ assert pattern.search("999.999.999.999") is None
37
+ assert pattern.search("256.0.0.1") is None
@@ -0,0 +1,20 @@
1
+ import inspect
2
+ import pytest
3
+ from maskinfly.utils import find_variable_name, SENSITIVE_VAR_NAMES
4
+
5
+ def test_sensitive_var_names():
6
+ assert "password" in SENSITIVE_VAR_NAMES
7
+ assert "token" in SENSITIVE_VAR_NAMES
8
+ assert "api_key" in SENSITIVE_VAR_NAMES
9
+
10
+ def test_find_variable_name_from_local():
11
+ test_value = "secret123"
12
+ def inner():
13
+ return find_variable_name(test_value, frame_depth=2)
14
+ name = inner()
15
+ assert name == "test_value"
16
+
17
+ def test_find_variable_name_not_found():
18
+ # Временный объект без имени
19
+ result = find_variable_name(object(), frame_depth=1)
20
+ assert result is None