safe-logs 0.1.0__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.
- safe_logs-0.1.0/PKG-INFO +122 -0
- safe_logs-0.1.0/README.md +108 -0
- safe_logs-0.1.0/pyproject.toml +23 -0
- safe_logs-0.1.0/src/safe_logs/__init__.py +153 -0
safe_logs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: safe-logs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: alien1403
|
|
6
|
+
Author-email: hanghicelrazvanmihai@gmail.com
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# safe-logs
|
|
15
|
+
|
|
16
|
+
A lightweight, zero-dependency Python library that automatically scrubs sensitive information (PII, secrets, API keys) from your logs.
|
|
17
|
+
|
|
18
|
+
## Why use `safe-logs`?
|
|
19
|
+
|
|
20
|
+
Keeping sensitive information out of your logs is critical for security and compliance. Log files are often aggregated and stored in centralized systems where multiple developers or third-party services might have access to them. Leaking emails, phone numbers, credit cards, or API tokens can lead to severe security breaches. `safe-logs` acts as a safety net, ensuring these secrets are masked before they ever hit the output stream.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
You can install `safe-logs` via pip:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install safe-logs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or using poetry:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
poetry add safe-logs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### 1. The Quickest Way (Convenience Function)
|
|
39
|
+
|
|
40
|
+
The easiest way to start logging safely is to use the `get_safe_logger` function. This returns a standard Python `logging.Logger` with our scrubbing formatter already attached.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from safe_logs import get_safe_logger
|
|
44
|
+
|
|
45
|
+
logger = get_safe_logger("my_app")
|
|
46
|
+
|
|
47
|
+
# Outputs: 2026-06-12 12:00:00,000 - my_app - INFO - User [MASKED_EMAIL] has signed in.
|
|
48
|
+
logger.info("User test@example.com has signed in.")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Using with an Existing Logger (ScrubbingFormatter)
|
|
52
|
+
|
|
53
|
+
If you already have a complex logging setup, you can simply swap out your standard formatter for the `ScrubbingFormatter`.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import logging
|
|
57
|
+
from safe_logs import ScrubbingFormatter
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger("custom_app")
|
|
60
|
+
logger.setLevel(logging.DEBUG)
|
|
61
|
+
|
|
62
|
+
handler = logging.StreamHandler()
|
|
63
|
+
formatter = ScrubbingFormatter(fmt='%(levelname)s: %(message)s')
|
|
64
|
+
handler.setFormatter(formatter)
|
|
65
|
+
logger.addHandler(handler)
|
|
66
|
+
|
|
67
|
+
logger.debug("API request with token='super-secret-token'")
|
|
68
|
+
# Outputs: DEBUG: API request with token='[MASKED_SECRET]'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Direct String & Structured Data Scrubbing (LogScrubber)
|
|
72
|
+
|
|
73
|
+
If you just need to scrub data independent of the `logging` module, you can use the `LogScrubber` class directly. It recursively scrubs strings, lists, and dictionaries.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from safe_logs import LogScrubber
|
|
77
|
+
|
|
78
|
+
scrubber = LogScrubber()
|
|
79
|
+
|
|
80
|
+
data = {
|
|
81
|
+
"user_id": 123,
|
|
82
|
+
"email": "user@domain.com",
|
|
83
|
+
"password": "my_super_secret_password"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
clean_data = scrubber.scrub(data)
|
|
87
|
+
print(clean_data)
|
|
88
|
+
# Outputs: {'user_id': 123, 'email': '[MASKED_EMAIL]', 'password': '[MASKED]'}
|
|
89
|
+
```
|
|
90
|
+
*Note: Any dictionary key matching "password", "secret", "token", etc., is automatically scrubbed entirely, regardless of its value's format.*
|
|
91
|
+
|
|
92
|
+
## Advanced Features
|
|
93
|
+
|
|
94
|
+
### Partial Masking (Obfuscation)
|
|
95
|
+
Sometimes for debugging, you want to see the last 4 digits of a credit card or API key instead of replacing the entire string.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from safe_logs import LogScrubber, keep_last_n_chars
|
|
99
|
+
|
|
100
|
+
scrubber = LogScrubber(mask_templates={
|
|
101
|
+
"credit_card": keep_last_n_chars(4, '*'),
|
|
102
|
+
"api_key": keep_last_n_chars(4, '*')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# Outputs: "Card: ****************3456"
|
|
106
|
+
print(scrubber.scrub("Card: 1234-5678-9012-3456"))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Adding Custom Patterns
|
|
110
|
+
You can easily add your own regex patterns to the scrubber:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
scrubber = LogScrubber()
|
|
114
|
+
|
|
115
|
+
# Add a Social Security Number pattern
|
|
116
|
+
scrubber.add_pattern("ssn", r"\b\d{3}-\d{2}-\d{4}\b", "[MASKED_SSN]")
|
|
117
|
+
|
|
118
|
+
# Outputs: "SSN: [MASKED_SSN]"
|
|
119
|
+
print(scrubber.scrub("SSN: 123-45-6789"))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# safe-logs
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-dependency Python library that automatically scrubs sensitive information (PII, secrets, API keys) from your logs.
|
|
4
|
+
|
|
5
|
+
## Why use `safe-logs`?
|
|
6
|
+
|
|
7
|
+
Keeping sensitive information out of your logs is critical for security and compliance. Log files are often aggregated and stored in centralized systems where multiple developers or third-party services might have access to them. Leaking emails, phone numbers, credit cards, or API tokens can lead to severe security breaches. `safe-logs` acts as a safety net, ensuring these secrets are masked before they ever hit the output stream.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
You can install `safe-logs` via pip:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install safe-logs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or using poetry:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
poetry add safe-logs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### 1. The Quickest Way (Convenience Function)
|
|
26
|
+
|
|
27
|
+
The easiest way to start logging safely is to use the `get_safe_logger` function. This returns a standard Python `logging.Logger` with our scrubbing formatter already attached.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from safe_logs import get_safe_logger
|
|
31
|
+
|
|
32
|
+
logger = get_safe_logger("my_app")
|
|
33
|
+
|
|
34
|
+
# Outputs: 2026-06-12 12:00:00,000 - my_app - INFO - User [MASKED_EMAIL] has signed in.
|
|
35
|
+
logger.info("User test@example.com has signed in.")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Using with an Existing Logger (ScrubbingFormatter)
|
|
39
|
+
|
|
40
|
+
If you already have a complex logging setup, you can simply swap out your standard formatter for the `ScrubbingFormatter`.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import logging
|
|
44
|
+
from safe_logs import ScrubbingFormatter
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("custom_app")
|
|
47
|
+
logger.setLevel(logging.DEBUG)
|
|
48
|
+
|
|
49
|
+
handler = logging.StreamHandler()
|
|
50
|
+
formatter = ScrubbingFormatter(fmt='%(levelname)s: %(message)s')
|
|
51
|
+
handler.setFormatter(formatter)
|
|
52
|
+
logger.addHandler(handler)
|
|
53
|
+
|
|
54
|
+
logger.debug("API request with token='super-secret-token'")
|
|
55
|
+
# Outputs: DEBUG: API request with token='[MASKED_SECRET]'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Direct String & Structured Data Scrubbing (LogScrubber)
|
|
59
|
+
|
|
60
|
+
If you just need to scrub data independent of the `logging` module, you can use the `LogScrubber` class directly. It recursively scrubs strings, lists, and dictionaries.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from safe_logs import LogScrubber
|
|
64
|
+
|
|
65
|
+
scrubber = LogScrubber()
|
|
66
|
+
|
|
67
|
+
data = {
|
|
68
|
+
"user_id": 123,
|
|
69
|
+
"email": "user@domain.com",
|
|
70
|
+
"password": "my_super_secret_password"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
clean_data = scrubber.scrub(data)
|
|
74
|
+
print(clean_data)
|
|
75
|
+
# Outputs: {'user_id': 123, 'email': '[MASKED_EMAIL]', 'password': '[MASKED]'}
|
|
76
|
+
```
|
|
77
|
+
*Note: Any dictionary key matching "password", "secret", "token", etc., is automatically scrubbed entirely, regardless of its value's format.*
|
|
78
|
+
|
|
79
|
+
## Advanced Features
|
|
80
|
+
|
|
81
|
+
### Partial Masking (Obfuscation)
|
|
82
|
+
Sometimes for debugging, you want to see the last 4 digits of a credit card or API key instead of replacing the entire string.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from safe_logs import LogScrubber, keep_last_n_chars
|
|
86
|
+
|
|
87
|
+
scrubber = LogScrubber(mask_templates={
|
|
88
|
+
"credit_card": keep_last_n_chars(4, '*'),
|
|
89
|
+
"api_key": keep_last_n_chars(4, '*')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# Outputs: "Card: ****************3456"
|
|
93
|
+
print(scrubber.scrub("Card: 1234-5678-9012-3456"))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Adding Custom Patterns
|
|
97
|
+
You can easily add your own regex patterns to the scrubber:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
scrubber = LogScrubber()
|
|
101
|
+
|
|
102
|
+
# Add a Social Security Number pattern
|
|
103
|
+
scrubber.add_pattern("ssn", r"\b\d{3}-\d{2}-\d{4}\b", "[MASKED_SSN]")
|
|
104
|
+
|
|
105
|
+
# Outputs: "SSN: [MASKED_SSN]"
|
|
106
|
+
print(scrubber.scrub("SSN: 123-45-6789"))
|
|
107
|
+
```
|
|
108
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "safe-logs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "alien1403",email = "hanghicelrazvanmihai@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.poetry]
|
|
14
|
+
packages = [{include = "safe_logs", from = "src"}]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest (>=9.0.3,<10.0.0)"
|
|
23
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
safe_logs: A lightweight Python library to scrub sensitive information (PII, secrets) from logs.
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, Optional, Any, Callable, Union, Set, List
|
|
7
|
+
|
|
8
|
+
def keep_last_n_chars(n: int, mask_char: str = '*') -> Callable[[re.Match], str]:
|
|
9
|
+
"""
|
|
10
|
+
Returns a callable mask that replaces all but the last `n` characters of the matched string.
|
|
11
|
+
Useful for partial obfuscation of credit cards or API keys.
|
|
12
|
+
"""
|
|
13
|
+
def _masker(match: re.Match) -> str:
|
|
14
|
+
# If there's a capture group (like for api_key), we mask the group
|
|
15
|
+
text = match.group(1) if match.groups() else match.group(0)
|
|
16
|
+
|
|
17
|
+
if len(text) <= n:
|
|
18
|
+
masked = mask_char * len(text)
|
|
19
|
+
else:
|
|
20
|
+
masked = mask_char * (len(text) - n) + text[-n:]
|
|
21
|
+
|
|
22
|
+
if match.groups():
|
|
23
|
+
return match.group(0).replace(match.group(1), masked)
|
|
24
|
+
return masked
|
|
25
|
+
|
|
26
|
+
return _masker
|
|
27
|
+
|
|
28
|
+
class LogScrubber:
|
|
29
|
+
"""
|
|
30
|
+
A utility class that scrubs sensitive information from strings, lists, and dictionaries.
|
|
31
|
+
"""
|
|
32
|
+
DEFAULT_PATTERNS: Dict[str, re.Pattern] = {
|
|
33
|
+
"email": re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'),
|
|
34
|
+
"phone": re.compile(r'(?<!\d)(?:\+?\d{1,3}[-. ]?)?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}(?!\d)'),
|
|
35
|
+
"credit_card": re.compile(r'\b(?:\d{4}[- ]?){3}\d{4}\b'),
|
|
36
|
+
"api_key": re.compile(r'(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*["\']([^"\']+)["\']'),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def __init__(self, mask_templates: Optional[Dict[str, Union[str, Callable[[re.Match], str]]]] = None,
|
|
40
|
+
sensitive_keys: Optional[Set[str]] = None):
|
|
41
|
+
"""
|
|
42
|
+
Initializes the LogScrubber.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
mask_templates: A dictionary mapping pattern keys to their replacement strings or callable functions.
|
|
46
|
+
sensitive_keys: A set of string keys. If scrubbing a dictionary, values for these keys will be entirely masked.
|
|
47
|
+
"""
|
|
48
|
+
self.patterns = self.DEFAULT_PATTERNS.copy()
|
|
49
|
+
self.masks: Dict[str, Union[str, Callable[[re.Match], str]]] = {
|
|
50
|
+
"email": "[MASKED_EMAIL]",
|
|
51
|
+
"phone": "[MASKED_PHONE]",
|
|
52
|
+
"credit_card": "[MASKED_CARD]",
|
|
53
|
+
"api_key": "[MASKED_SECRET]"
|
|
54
|
+
}
|
|
55
|
+
if mask_templates:
|
|
56
|
+
self.masks.update(mask_templates)
|
|
57
|
+
self.sensitive_keys: Set[str] = sensitive_keys or {"password", "secret", "token", "api_key", "access_token"}
|
|
58
|
+
|
|
59
|
+
def add_pattern(self, name: str, pattern: Union[str, re.Pattern], mask: Union[str, Callable[[re.Match], str]]):
|
|
60
|
+
"""
|
|
61
|
+
Registers a new regular expression pattern and its corresponding mask.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: The key/name for this pattern.
|
|
65
|
+
pattern: The regex pattern (string or compiled re.Pattern).
|
|
66
|
+
mask: The replacement string or a callable taking an re.Match and returning a string.
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(pattern, str):
|
|
69
|
+
pattern = re.compile(pattern)
|
|
70
|
+
self.patterns[name] = pattern
|
|
71
|
+
self.masks[name] = mask
|
|
72
|
+
|
|
73
|
+
def _apply_mask(self, match: re.Match, mask: Union[str, Callable[[re.Match], str]]) -> str:
|
|
74
|
+
if callable(mask):
|
|
75
|
+
return mask(match)
|
|
76
|
+
if match.groups():
|
|
77
|
+
return match.group(0).replace(match.group(1), str(mask))
|
|
78
|
+
return str(mask)
|
|
79
|
+
|
|
80
|
+
def scrub(self, data: Any) -> Any:
|
|
81
|
+
"""
|
|
82
|
+
Recursively scrubs sensitive information from strings, dictionaries, or lists.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
data (Any): The input data to scrub.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Any: The scrubbed data.
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(data, str):
|
|
91
|
+
return self._scrub_string(data)
|
|
92
|
+
elif isinstance(data, dict):
|
|
93
|
+
return {k: ("[MASKED]" if str(k).lower() in self.sensitive_keys else self.scrub(v)) for k, v in data.items()}
|
|
94
|
+
elif isinstance(data, list):
|
|
95
|
+
return [self.scrub(item) for item in data]
|
|
96
|
+
return data
|
|
97
|
+
|
|
98
|
+
def _scrub_string(self, text: str) -> str:
|
|
99
|
+
for name, pattern in self.patterns.items():
|
|
100
|
+
mask = self.masks.get(name, "[MASKED]")
|
|
101
|
+
text = pattern.sub(lambda m: self._apply_mask(m, mask), text)
|
|
102
|
+
return text
|
|
103
|
+
|
|
104
|
+
class ScrubbingFormatter(logging.Formatter):
|
|
105
|
+
"""
|
|
106
|
+
A custom logging.Formatter that automatically scrubs sensitive information
|
|
107
|
+
from log messages before they are emitted.
|
|
108
|
+
"""
|
|
109
|
+
def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None,
|
|
110
|
+
style: str = '%', mask_templates: Optional[Dict[str, Union[str, Callable]]] = None,
|
|
111
|
+
sensitive_keys: Optional[Set[str]] = None):
|
|
112
|
+
"""
|
|
113
|
+
Initializes the ScrubbingFormatter.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
fmt: The format string for the log message.
|
|
117
|
+
datefmt: The format string for the date/time.
|
|
118
|
+
style: The format style ('%', '{', or '$').
|
|
119
|
+
mask_templates: Custom mask templates for the scrubber.
|
|
120
|
+
sensitive_keys: A set of dictionary keys that should be scrubbed when logging dicts/JSON.
|
|
121
|
+
"""
|
|
122
|
+
super().__init__(fmt, datefmt, style)
|
|
123
|
+
self.scrubber = LogScrubber(mask_templates, sensitive_keys)
|
|
124
|
+
|
|
125
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Formats the given log record and scrubs any sensitive information from the resulting string.
|
|
128
|
+
"""
|
|
129
|
+
original_msg = super().format(record)
|
|
130
|
+
return self.scrubber.scrub(original_msg)
|
|
131
|
+
|
|
132
|
+
def get_safe_logger(name: str, level: int = logging.INFO, **kwargs) -> logging.Logger:
|
|
133
|
+
"""
|
|
134
|
+
A convenience function to quickly instantiate a logger with the ScrubbingFormatter attached.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
name: The name of the logger.
|
|
138
|
+
level: The logging level. Defaults to logging.INFO.
|
|
139
|
+
**kwargs: Additional arguments to pass to ScrubbingFormatter (like mask_templates or sensitive_keys).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
logging.Logger: A configured logger instance ready to safely log messages.
|
|
143
|
+
"""
|
|
144
|
+
logger = logging.getLogger(name)
|
|
145
|
+
logger.setLevel(level)
|
|
146
|
+
|
|
147
|
+
if not logger.handlers:
|
|
148
|
+
handler = logging.StreamHandler()
|
|
149
|
+
formatter = ScrubbingFormatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', **kwargs)
|
|
150
|
+
handler.setFormatter(formatter)
|
|
151
|
+
logger.addHandler(handler)
|
|
152
|
+
|
|
153
|
+
return logger
|