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.
- maskinfly-0.1.1/LICENSE +7 -0
- maskinfly-0.1.1/MANIFEST.in +5 -0
- maskinfly-0.1.1/PKG-INFO +142 -0
- maskinfly-0.1.1/README.md +110 -0
- maskinfly-0.1.1/maskinfly/__init__.py +30 -0
- maskinfly-0.1.1/maskinfly/audit.py +18 -0
- maskinfly-0.1.1/maskinfly/masker.py +97 -0
- maskinfly-0.1.1/maskinfly/patterns.py +13 -0
- maskinfly-0.1.1/maskinfly/utils.py +31 -0
- maskinfly-0.1.1/maskinfly.egg-info/PKG-INFO +142 -0
- maskinfly-0.1.1/maskinfly.egg-info/SOURCES.txt +20 -0
- maskinfly-0.1.1/maskinfly.egg-info/dependency_links.txt +1 -0
- maskinfly-0.1.1/maskinfly.egg-info/requires.txt +10 -0
- maskinfly-0.1.1/maskinfly.egg-info/top_level.txt +1 -0
- maskinfly-0.1.1/pyproject.toml +49 -0
- maskinfly-0.1.1/requirements.txt +5 -0
- maskinfly-0.1.1/setup.cfg +4 -0
- maskinfly-0.1.1/tests/test_audit.py +26 -0
- maskinfly-0.1.1/tests/test_init.py +28 -0
- maskinfly-0.1.1/tests/test_masker.py +65 -0
- maskinfly-0.1.1/tests/test_patterns.py +37 -0
- maskinfly-0.1.1/tests/test_utils.py +20 -0
maskinfly-0.1.1/LICENSE
ADDED
|
@@ -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.
|
maskinfly-0.1.1/PKG-INFO
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|