safe-logs 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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
@@ -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,4 @@
1
+ safe_logs/__init__.py,sha256=MEs8SsYZxdCCbKPHHA7HyiI2fiEOsGWLfY9h6-_JJk0,6370
2
+ safe_logs-0.1.0.dist-info/METADATA,sha256=EXFUsIEOqP-Guv2OqeTIDy99h2b8NTN4FnOGuf1Zszs,3696
3
+ safe_logs-0.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
4
+ safe_logs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any