blindlog 1.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.
blindlog-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: blindlog
3
+ Version: 1.1.0
4
+ Summary: Deterministic privacy-preserving logger for Python.
5
+ Project-URL: Repository, https://github.com/A-P-Shukla/Blind-Log
6
+ Project-URL: Bug Tracker, https://github.com/A-P-Shukla/Blind-Log/issues
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Provides-Extra: fastapi
11
+ Requires-Dist: fastapi; extra == "fastapi"
12
+ Requires-Dist: starlette; extra == "fastapi"
13
+ Dynamic: license-file
14
+
15
+ # BlindLog v1.1
16
+
17
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-181717.svg?style=for-the-badge&logo=github)](https://github.com/A-P-Shukla/Blind-Log)
18
+
19
+ BlindLog is a **zero-dependency, production-ready Privacy-Preserving Observability SDK** for Python.
20
+
21
+ It solves the fundamental conflict in backend engineering: The developer needs to see everything to fix bugs, but compliance constraints (GDPR/HIPAA/SOC2) dictate that you cannot see anything personal.
22
+
23
+ By replacing raw Personal Identifiable Information (PII) with consistent, structure-preserving deterministic hashes, developers retain perfect system observability without leaking actual identities into application logs.
24
+
25
+ ---
26
+
27
+ ## 💡 The "Why": Why Use BlindLog?
28
+
29
+ ### The Problem with Redaction
30
+ Legacy redaction tools look for emails or credit cards and replace them with static text like `*****` or `[REDACTED]`. The fatal flaw here is **context destruction**. If your logs show `[REDACTED] failed to purchase order [REDACTED]`, you cannot trace a specific user's journey through your microservices when every trace of their identity maps to the exact same generic string.
31
+
32
+ ### The BlindLog Solution: Deterministic Pseudonymization
33
+ BlindLog uses natively-keyed **BLAKE2b cryptography** to consistently map data:
34
+ - `user1@gmail.com` **always** logs as `blnd_ref_8ax92bfac000...@masked.com`.
35
+ - `user2@gmail.com` **always** logs as `blnd_ref_1c89f81ba000...@masked.com`.
36
+
37
+ You instantly know if the *same* user triggered 50 errors across 4 microservices over a week, while remaining legally compliant because the raw identity is cryptographically destroyed.
38
+
39
+ ---
40
+
41
+ ## 🚀 Installation
42
+
43
+ BlindLog has zero external dependencies and runs natively on Python 3.8+.
44
+
45
+ ```bash
46
+ pip install blindlog
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 🛠️ Exactly How to Use It
52
+
53
+ BlindLog is built on an extensible architecture that operates automatically once plugged into your existing application. It intercepts data, traverses JSON payloads recursively, and masks strings without breaking schema.
54
+
55
+ ### 1. Mandatory Security Configuration
56
+ BlindLog operates on keyed hashes. To prevent rainbow-table reverse-engineering, you **must** supply a cryptographic secret.
57
+
58
+ Set the following environment variables on your production servers:
59
+ ```bash
60
+ export BLINDLOG_SECRET="your-super-strong-random-secret-key"
61
+ export BLINDLOG_SALT="optional-additional-salt"
62
+ ```
63
+
64
+ > **Warning:** If `BLINDLOG_SECRET` is missing, BlindLog will violently crash on boot to protect your system from generating reversible, unkeyed hashes. For local development, you can set `export BLINDLOG_DEBUG="true"` to bypass this crash.
65
+
66
+ ---
67
+
68
+ ### 2. Standard Python Logging Interception
69
+
70
+ BlindLog ships with a `logging.Formatter` that hooks directly into Python's native `logging` module. It intercepts all string messages, dictionary `args`, and even `Exception` tracebacks to scrub PII before it hits your terminal or logging aggregator (like Datadog/Elasticsearch).
71
+
72
+ ```python
73
+ import logging
74
+ from blindlog.formatters import BlindLogFormatter
75
+
76
+ # 1. Initialize your logger
77
+ logger = logging.getLogger("my_application")
78
+ logger.setLevel(logging.INFO)
79
+
80
+ # 2. Create a handler
81
+ handler = logging.StreamHandler()
82
+
83
+ # 3. Attach the BlindLogFormatter!
84
+ handler.setFormatter(BlindLogFormatter())
85
+ logger.addHandler(handler)
86
+
87
+ # Usage A: Standard Strings (Slow Path - Regex Scanning)
88
+ logger.info("Failed login for akhand@gmail.com on card 4111-2222-3333-4444")
89
+ # Output: Failed login for blnd_ref_8a9df2c00000...@masked.com on card 4111-c918a2-f8b1c4-4444
90
+
91
+ # Usage B: Dictionary Arguments (Fast Path - Key Matching)
92
+ # BlindLog detects keys like 'password' or 'email' instantly.
93
+ logger.info("User created", {"email": "ceo@corp.com", "password": "super-secret"})
94
+ # Output: User created {'email': 'blnd_ref_9bf... masked', 'password': 'blind:838ab...'}
95
+
96
+ # Usage C: Safe Exceptions
97
+ try:
98
+ raise ValueError("User akhand@gmail.com exhausted their API quota")
99
+ except ValueError:
100
+ logger.exception("A system error occurred")
101
+ # Output: The stack trace is fully processed, and akhand@gmail.com is masked inside the Traceback!
102
+ ```
103
+
104
+ ---
105
+
106
+ ### 3. FastAPI & Starlette Middleware
107
+
108
+ BlindLog acts as a **Pure ASGI Middleware**. It intercepts raw HTTP traffic *before* it hits your application routers. It safely buffers HTTP payloads (up to 5MB to prevent OOM DOS) and logs masked Request Bodies, Response Bodies, and Headers.
109
+
110
+ ```python
111
+ from fastapi import FastAPI
112
+ from blindlog.integrations.fastapi import BlindLogFastAPIMiddleware
113
+
114
+ app = FastAPI()
115
+
116
+ # Attach the middleware
117
+ app.add_middleware(BlindLogFastAPIMiddleware)
118
+
119
+ @app.post("/checkout")
120
+ async def checkout(payload: dict):
121
+ # If the user sends {"credit_card": "4111-...", "cookie": "session_123"},
122
+ # The middleware automatically logs the sanitized payload to standard out.
123
+ return {"status": "success"}
124
+ ```
125
+
126
+ **What the Middleware handles automatically:**
127
+ - **Request/Response Bodies:** Deeply nested JSON is recursively traversed and masked.
128
+ - **HTTP Headers:** Sensitive context headers (like `Authorization`, `Cookie`, `X-API-Key`) are extracted and encrypted without losing duplicate associations.
129
+ - **Streaming Protections:** Safe passage for WebSockets and SSE pipelines.
130
+
131
+ ---
132
+
133
+ ### 4. Customizing the Configuration (`BlindLogConfig`)
134
+
135
+ You can tune BlindLog's rules by defining a `BlindLogConfig`. Once created, the config is `frozen` (immutable) to prevent runtime tampering.
136
+
137
+ ```python
138
+ from blindlog.core import BlindLogger
139
+ from blindlog.config import BlindLogConfig
140
+
141
+ # 1. Define custom sensitive keys.
142
+ # Note: This overwrites the defaults, so add your specific database fields.
143
+ custom_keys = frozenset({"internal_db_id", "auth_token", "email"})
144
+
145
+ config = BlindLogConfig(
146
+ secret_key="my-custom-key", # Will fall back to ENV var if omitted
147
+ sensitive_keys=custom_keys,
148
+ debug_mode=False
149
+ )
150
+
151
+ logger = BlindLogger(config=config)
152
+ ```
153
+
154
+ **Key Matching Engine (Fast Path):**
155
+ BlindLog checks dictionary keys via:
156
+ 1. **Exact Match:** e.g., `"email"` == `"email"`.
157
+ 2. **Suffix Match:** e.g., `"user_password"` ends with `"_password"`.
158
+ 3. **Normalization:** Hyphens (`x-api-key`) and camelCase (`APIKey`) are normalized to `snake_case` before matching, ensuring maximum coverage across varying schemas.
159
+
160
+ ---
161
+
162
+ ### 5. Custom Format Registration (Adding Regex)
163
+
164
+ BlindLog implements a **Registry Pattern**. If you have custom internal tokens (e.g., specific AWS KMS IDs or internal Employee IDs) you can teach BlindLog to find and format them dynamically in free-text.
165
+
166
+ ```python
167
+ import re
168
+ from blindlog.core import BlindLogger
169
+
170
+ logger = BlindLogger()
171
+
172
+ # 1. Define a regex pattern
173
+ aws_pattern = re.compile(r"AWS-KMS-\d{6}")
174
+
175
+ # 2. Define a callback function that takes the matched string and returns a safe string
176
+ # Note: You can also hash it dynamically inside the callback if you wish!
177
+ def mask_aws(matched_string: str) -> str:
178
+ return "blnd_aws_TOKEN_REDACTED"
179
+
180
+ # 3. Register the rule
181
+ logger.registry.register(aws_pattern, mask_aws)
182
+
183
+ masked = logger.mask("Exception: AWS-KMS-123456 failed to load.")
184
+ # Output: "Exception: blnd_aws_TOKEN_REDACTED failed to load."
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 🛡️ Default Out-Of-The-Box Protections
190
+
191
+ BlindLog actively protects the following data types automatically via RegEx and Key-discovery:
192
+
193
+ - **Emails:** Truncated and hashed (`blnd_ref_HASH...@masked.com`)
194
+ - **Credit Cards:** Preserves major industry format (`4111-HASH-HASH-1234`)
195
+ - **API Keys & Tokens:** Covers OpenAI, Stripe, AWS, Slack, and GitHub PATs natively.
196
+ - **Phone Numbers:** International and NANP routing.
197
+ - **Social Security Numbers (SSN):** US Formats.
198
+ - **IPv4 Addresses:** Validated octet arrays.
199
+ - **HTTP/Web Standard Keys:** `cookie`, `set_cookie`, `authorization`, `password`, `secret`, `private_key`, `credentials`.
200
+
201
+ ---
202
+
203
+ ## 🧮 Idempotency & System Guarantees
204
+
205
+ - **Idempotent Masking Guarantees:** BlindLog uses strict structural RegEx evaluations (`MASKED_PATTERN`). If you pass already-masked data into the engine multiple times, it skips it instantly. You will never double-hash a log.
206
+ - **ReDoS Mitigation:** Slow-path Regex execution cuts off after 10,000 characters, averting CPU exhaustion DOS attacks.
207
+ - **OOM Prevention:** Middlewares strictly cap at 5MB buffer payloads.
208
+ - **Type Sabotage Checks:** Gracefully handles `None`, `True`, and circular nested dictionary references without crashing or returning unmasked memory addresses.
209
+
210
+ For further exploration, please review our [Architecture Guide](./ARCHITECTURE.md) and the `CHANGELOG.md`!
@@ -0,0 +1,196 @@
1
+ # BlindLog v1.1
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-181717.svg?style=for-the-badge&logo=github)](https://github.com/A-P-Shukla/Blind-Log)
4
+
5
+ BlindLog is a **zero-dependency, production-ready Privacy-Preserving Observability SDK** for Python.
6
+
7
+ It solves the fundamental conflict in backend engineering: The developer needs to see everything to fix bugs, but compliance constraints (GDPR/HIPAA/SOC2) dictate that you cannot see anything personal.
8
+
9
+ By replacing raw Personal Identifiable Information (PII) with consistent, structure-preserving deterministic hashes, developers retain perfect system observability without leaking actual identities into application logs.
10
+
11
+ ---
12
+
13
+ ## 💡 The "Why": Why Use BlindLog?
14
+
15
+ ### The Problem with Redaction
16
+ Legacy redaction tools look for emails or credit cards and replace them with static text like `*****` or `[REDACTED]`. The fatal flaw here is **context destruction**. If your logs show `[REDACTED] failed to purchase order [REDACTED]`, you cannot trace a specific user's journey through your microservices when every trace of their identity maps to the exact same generic string.
17
+
18
+ ### The BlindLog Solution: Deterministic Pseudonymization
19
+ BlindLog uses natively-keyed **BLAKE2b cryptography** to consistently map data:
20
+ - `user1@gmail.com` **always** logs as `blnd_ref_8ax92bfac000...@masked.com`.
21
+ - `user2@gmail.com` **always** logs as `blnd_ref_1c89f81ba000...@masked.com`.
22
+
23
+ You instantly know if the *same* user triggered 50 errors across 4 microservices over a week, while remaining legally compliant because the raw identity is cryptographically destroyed.
24
+
25
+ ---
26
+
27
+ ## 🚀 Installation
28
+
29
+ BlindLog has zero external dependencies and runs natively on Python 3.8+.
30
+
31
+ ```bash
32
+ pip install blindlog
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🛠️ Exactly How to Use It
38
+
39
+ BlindLog is built on an extensible architecture that operates automatically once plugged into your existing application. It intercepts data, traverses JSON payloads recursively, and masks strings without breaking schema.
40
+
41
+ ### 1. Mandatory Security Configuration
42
+ BlindLog operates on keyed hashes. To prevent rainbow-table reverse-engineering, you **must** supply a cryptographic secret.
43
+
44
+ Set the following environment variables on your production servers:
45
+ ```bash
46
+ export BLINDLOG_SECRET="your-super-strong-random-secret-key"
47
+ export BLINDLOG_SALT="optional-additional-salt"
48
+ ```
49
+
50
+ > **Warning:** If `BLINDLOG_SECRET` is missing, BlindLog will violently crash on boot to protect your system from generating reversible, unkeyed hashes. For local development, you can set `export BLINDLOG_DEBUG="true"` to bypass this crash.
51
+
52
+ ---
53
+
54
+ ### 2. Standard Python Logging Interception
55
+
56
+ BlindLog ships with a `logging.Formatter` that hooks directly into Python's native `logging` module. It intercepts all string messages, dictionary `args`, and even `Exception` tracebacks to scrub PII before it hits your terminal or logging aggregator (like Datadog/Elasticsearch).
57
+
58
+ ```python
59
+ import logging
60
+ from blindlog.formatters import BlindLogFormatter
61
+
62
+ # 1. Initialize your logger
63
+ logger = logging.getLogger("my_application")
64
+ logger.setLevel(logging.INFO)
65
+
66
+ # 2. Create a handler
67
+ handler = logging.StreamHandler()
68
+
69
+ # 3. Attach the BlindLogFormatter!
70
+ handler.setFormatter(BlindLogFormatter())
71
+ logger.addHandler(handler)
72
+
73
+ # Usage A: Standard Strings (Slow Path - Regex Scanning)
74
+ logger.info("Failed login for akhand@gmail.com on card 4111-2222-3333-4444")
75
+ # Output: Failed login for blnd_ref_8a9df2c00000...@masked.com on card 4111-c918a2-f8b1c4-4444
76
+
77
+ # Usage B: Dictionary Arguments (Fast Path - Key Matching)
78
+ # BlindLog detects keys like 'password' or 'email' instantly.
79
+ logger.info("User created", {"email": "ceo@corp.com", "password": "super-secret"})
80
+ # Output: User created {'email': 'blnd_ref_9bf... masked', 'password': 'blind:838ab...'}
81
+
82
+ # Usage C: Safe Exceptions
83
+ try:
84
+ raise ValueError("User akhand@gmail.com exhausted their API quota")
85
+ except ValueError:
86
+ logger.exception("A system error occurred")
87
+ # Output: The stack trace is fully processed, and akhand@gmail.com is masked inside the Traceback!
88
+ ```
89
+
90
+ ---
91
+
92
+ ### 3. FastAPI & Starlette Middleware
93
+
94
+ BlindLog acts as a **Pure ASGI Middleware**. It intercepts raw HTTP traffic *before* it hits your application routers. It safely buffers HTTP payloads (up to 5MB to prevent OOM DOS) and logs masked Request Bodies, Response Bodies, and Headers.
95
+
96
+ ```python
97
+ from fastapi import FastAPI
98
+ from blindlog.integrations.fastapi import BlindLogFastAPIMiddleware
99
+
100
+ app = FastAPI()
101
+
102
+ # Attach the middleware
103
+ app.add_middleware(BlindLogFastAPIMiddleware)
104
+
105
+ @app.post("/checkout")
106
+ async def checkout(payload: dict):
107
+ # If the user sends {"credit_card": "4111-...", "cookie": "session_123"},
108
+ # The middleware automatically logs the sanitized payload to standard out.
109
+ return {"status": "success"}
110
+ ```
111
+
112
+ **What the Middleware handles automatically:**
113
+ - **Request/Response Bodies:** Deeply nested JSON is recursively traversed and masked.
114
+ - **HTTP Headers:** Sensitive context headers (like `Authorization`, `Cookie`, `X-API-Key`) are extracted and encrypted without losing duplicate associations.
115
+ - **Streaming Protections:** Safe passage for WebSockets and SSE pipelines.
116
+
117
+ ---
118
+
119
+ ### 4. Customizing the Configuration (`BlindLogConfig`)
120
+
121
+ You can tune BlindLog's rules by defining a `BlindLogConfig`. Once created, the config is `frozen` (immutable) to prevent runtime tampering.
122
+
123
+ ```python
124
+ from blindlog.core import BlindLogger
125
+ from blindlog.config import BlindLogConfig
126
+
127
+ # 1. Define custom sensitive keys.
128
+ # Note: This overwrites the defaults, so add your specific database fields.
129
+ custom_keys = frozenset({"internal_db_id", "auth_token", "email"})
130
+
131
+ config = BlindLogConfig(
132
+ secret_key="my-custom-key", # Will fall back to ENV var if omitted
133
+ sensitive_keys=custom_keys,
134
+ debug_mode=False
135
+ )
136
+
137
+ logger = BlindLogger(config=config)
138
+ ```
139
+
140
+ **Key Matching Engine (Fast Path):**
141
+ BlindLog checks dictionary keys via:
142
+ 1. **Exact Match:** e.g., `"email"` == `"email"`.
143
+ 2. **Suffix Match:** e.g., `"user_password"` ends with `"_password"`.
144
+ 3. **Normalization:** Hyphens (`x-api-key`) and camelCase (`APIKey`) are normalized to `snake_case` before matching, ensuring maximum coverage across varying schemas.
145
+
146
+ ---
147
+
148
+ ### 5. Custom Format Registration (Adding Regex)
149
+
150
+ BlindLog implements a **Registry Pattern**. If you have custom internal tokens (e.g., specific AWS KMS IDs or internal Employee IDs) you can teach BlindLog to find and format them dynamically in free-text.
151
+
152
+ ```python
153
+ import re
154
+ from blindlog.core import BlindLogger
155
+
156
+ logger = BlindLogger()
157
+
158
+ # 1. Define a regex pattern
159
+ aws_pattern = re.compile(r"AWS-KMS-\d{6}")
160
+
161
+ # 2. Define a callback function that takes the matched string and returns a safe string
162
+ # Note: You can also hash it dynamically inside the callback if you wish!
163
+ def mask_aws(matched_string: str) -> str:
164
+ return "blnd_aws_TOKEN_REDACTED"
165
+
166
+ # 3. Register the rule
167
+ logger.registry.register(aws_pattern, mask_aws)
168
+
169
+ masked = logger.mask("Exception: AWS-KMS-123456 failed to load.")
170
+ # Output: "Exception: blnd_aws_TOKEN_REDACTED failed to load."
171
+ ```
172
+
173
+ ---
174
+
175
+ ## 🛡️ Default Out-Of-The-Box Protections
176
+
177
+ BlindLog actively protects the following data types automatically via RegEx and Key-discovery:
178
+
179
+ - **Emails:** Truncated and hashed (`blnd_ref_HASH...@masked.com`)
180
+ - **Credit Cards:** Preserves major industry format (`4111-HASH-HASH-1234`)
181
+ - **API Keys & Tokens:** Covers OpenAI, Stripe, AWS, Slack, and GitHub PATs natively.
182
+ - **Phone Numbers:** International and NANP routing.
183
+ - **Social Security Numbers (SSN):** US Formats.
184
+ - **IPv4 Addresses:** Validated octet arrays.
185
+ - **HTTP/Web Standard Keys:** `cookie`, `set_cookie`, `authorization`, `password`, `secret`, `private_key`, `credentials`.
186
+
187
+ ---
188
+
189
+ ## 🧮 Idempotency & System Guarantees
190
+
191
+ - **Idempotent Masking Guarantees:** BlindLog uses strict structural RegEx evaluations (`MASKED_PATTERN`). If you pass already-masked data into the engine multiple times, it skips it instantly. You will never double-hash a log.
192
+ - **ReDoS Mitigation:** Slow-path Regex execution cuts off after 10,000 characters, averting CPU exhaustion DOS attacks.
193
+ - **OOM Prevention:** Middlewares strictly cap at 5MB buffer payloads.
194
+ - **Type Sabotage Checks:** Gracefully handles `None`, `True`, and circular nested dictionary references without crashing or returning unmasked memory addresses.
195
+
196
+ For further exploration, please review our [Architecture Guide](./ARCHITECTURE.md) and the `CHANGELOG.md`!
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "blindlog"
7
+ version = "1.1.0"
8
+ description = "Deterministic privacy-preserving logger for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ # Zero dependencies for core!
13
+ # We use standard lib: json, logging, hashlib, os, re
14
+ ]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/A-P-Shukla/Blind-Log"
18
+ "Bug Tracker" = "https://github.com/A-P-Shukla/Blind-Log/issues"
19
+
20
+ [project.optional-dependencies]
21
+ fastapi = ["fastapi", "starlette"] # Users only install this if they need it
22
+
23
+ [tool.setuptools]
24
+ package-dir = {"" = "src"}
25
+ packages = ["blindlog", "blindlog.integrations"]
26
+
27
+ [tool.pytest.ini_options]
28
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ # Fix #15: Export all public-facing classes for clean DI
2
+ from blindlog.core import BlindLogger
3
+ from blindlog.config import BlindLogConfig
4
+ from blindlog.interfaces import MaskingEngine
5
+
6
+ __all__ = ["BlindLogger", "BlindLogConfig", "MaskingEngine"]
@@ -0,0 +1,58 @@
1
+ import os
2
+ import warnings
3
+ import hashlib
4
+ from dataclasses import dataclass, field
5
+ from typing import FrozenSet, Optional
6
+ from blindlog.rules import DEFAULT_SENSITIVE_KEYS
7
+
8
+ # Fix #3: Use frozenset for sensitive_keys to prevent mutation.
9
+ # Fix #16: Note — `object.__setattr__` is used in __post_init__ to initialize
10
+ # computed fields on a frozen dataclass. This is a standard Python pattern.
11
+ # Any external code can also use `object.__setattr__` to bypass frozen protection —
12
+ # this is a Python language limitation, not a runtime-enforceable security boundary.
13
+ @dataclass(frozen=True)
14
+ class BlindLogConfig:
15
+ secret_key: str = ""
16
+ salt: str = ""
17
+ sensitive_keys: FrozenSet[str] = field(default_factory=lambda: frozenset(DEFAULT_SENSITIVE_KEYS))
18
+ debug_mode: bool = False
19
+
20
+ def __post_init__(self):
21
+ if not self.secret_key:
22
+ object.__setattr__(self, 'secret_key', os.environ.get("BLINDLOG_SECRET", ""))
23
+ if not self.salt:
24
+ object.__setattr__(self, 'salt', os.environ.get("BLINDLOG_SALT", ""))
25
+
26
+ env_debug = os.environ.get("BLINDLOG_DEBUG", "").lower() in ("true", "1", "yes")
27
+ if env_debug and not self.debug_mode:
28
+ object.__setattr__(self, 'debug_mode', True)
29
+
30
+ if not self.secret_key and not self.debug_mode:
31
+ raise ValueError("BlindLog: BLINDLOG_SECRET is not set. Refusing to run in non-debug mode to prevent cryptographic bypass.")
32
+
33
+ object.__setattr__(self, '_blake_key_cache', self._derive_blake_key())
34
+
35
+ def _derive_blake_key(self) -> bytes:
36
+ safe_secret = self.secret_key or ""
37
+ safe_salt = self.salt or ""
38
+ raw = (safe_secret + safe_salt).encode('utf-8')
39
+
40
+ if len(raw) > 64:
41
+ warnings.warn(
42
+ f"BlindLog: Combined secret+salt is {len(raw)} bytes, exceeding BLAKE2b's 64-byte key limit. "
43
+ "Key will be derived via BLAKE2b digest to avoid silent truncation.",
44
+ stacklevel=5
45
+ )
46
+ raw = hashlib.blake2b(raw, digest_size=64).digest()
47
+
48
+ if len(raw) == 0:
49
+ warnings.warn(
50
+ "BlindLog: blake_key is empty. Hashes will be unkeyed and trivially reversible.",
51
+ stacklevel=5
52
+ )
53
+
54
+ return raw
55
+
56
+ @property
57
+ def blake_key(self) -> bytes:
58
+ return self._blake_key_cache
@@ -0,0 +1,85 @@
1
+ import hashlib
2
+ import logging
3
+ from typing import Any, Dict, List, Optional
4
+ from blindlog.rules import EMAIL_REGEX, CREDIT_CARD_REGEX, IPV4_REGEX, API_KEY_REGEX, PHONE_REGEX, SSN_REGEX
5
+ from blindlog.utils import walk_and_mask
6
+ from blindlog.config import BlindLogConfig
7
+ from blindlog.registry import RuleRegistry
8
+ from blindlog.interfaces import MaskingEngine, Payload
9
+
10
+ _log = logging.getLogger("blindlog.core")
11
+
12
+ class BlindLogger(MaskingEngine):
13
+ """
14
+ Core engine for Deterministic Pseudonymization and Structure-Preserving Masking.
15
+ """
16
+ def __init__(
17
+ self,
18
+ config: Optional[BlindLogConfig] = None,
19
+ **kwargs
20
+ ):
21
+ self.config = config if config else BlindLogConfig(**kwargs)
22
+
23
+ self.registry = RuleRegistry(keyed_hash_fn=lambda v: f"blind:{self._hash(v, length=10)}")
24
+ self.registry.register(EMAIL_REGEX, self._mask_email)
25
+ self.registry.register(CREDIT_CARD_REGEX, self._mask_cc)
26
+ self.registry.register(IPV4_REGEX, lambda v: f"blnd_ip_{self._hash(v, length=10)}")
27
+ self.registry.register(API_KEY_REGEX, lambda v: f"blnd_key_{self._hash(v, length=10)}")
28
+ self.registry.register(PHONE_REGEX, lambda v: f"blnd_ph_{self._hash(v, length=10)}")
29
+ self.registry.register(SSN_REGEX, lambda v: f"blnd_ssn_{self._hash(v, length=10)}")
30
+
31
+ def _hash(self, value: str, length: int = 12) -> str:
32
+ if length < 1:
33
+ raise ValueError(f"BlindLog: hash length must be >= 1, got {length}")
34
+
35
+ if not isinstance(value, str):
36
+ value = str(value)
37
+
38
+ digest_bytes = max(1, (length + 1) // 2)
39
+ h = hashlib.blake2b(value.encode('utf-8'), key=self.config.blake_key, digest_size=digest_bytes)
40
+
41
+ return h.hexdigest()[:length]
42
+
43
+ def _mask_email(self, email: str) -> str:
44
+ """Structure-preserving mask for emails: blnd_ref_HASH...@masked.com"""
45
+ try:
46
+ local, domain = email.rsplit('@', 1)
47
+ hash_val = self._hash(email, length=12)
48
+ return f"blnd_ref_{hash_val}...@masked.com"
49
+ except ValueError:
50
+ return f"blnd_ref_{self._hash(email)}...@masked.com"
51
+
52
+ def _mask_cc(self, cc: str) -> str:
53
+ """Structure-preserving mask for credit cards: 4111-abcdef-abcdef-1234"""
54
+ clean_cc = cc.replace("-", "").replace(" ", "")
55
+ if len(clean_cc) >= 12:
56
+ first_4 = clean_cc[:4]
57
+ last_4 = clean_cc[-4:]
58
+ hash_val = self._hash(clean_cc, length=12)
59
+ return f"{first_4}-{hash_val[:6]}-{hash_val[6:12]}-{last_4}"
60
+ return self._hash(cc)
61
+
62
+ def _mask_value(self, value: str) -> str:
63
+ if len(value) > 10000:
64
+ return "<Skipped due to length: ReDoS Protection>"
65
+
66
+ fallback_hash = lambda v: f"blind:{self._hash(v, length=10)}"
67
+ return self.registry.process_value(value, fallback_hash)
68
+
69
+ def _mask_slow_path(self, text: str) -> str:
70
+ if len(text) > 10000:
71
+ return "<Skipped due to length: ReDoS Protection>"
72
+
73
+ return self.registry.process_text(text)
74
+
75
+ def mask(self, payload: Payload) -> Payload:
76
+ """Public endpoint to mask a payload."""
77
+ if self.config.debug_mode:
78
+ return payload
79
+
80
+ return walk_and_mask(
81
+ data=payload,
82
+ sensitive_keys=self.config.sensitive_keys,
83
+ mask_value_fn=self._mask_value,
84
+ mask_text_fn=self._mask_slow_path
85
+ )
@@ -0,0 +1,65 @@
1
+ import logging
2
+ import copy
3
+ from typing import Optional
4
+ from blindlog.interfaces import MaskingEngine
5
+
6
+ _log = logging.getLogger("blindlog.formatter")
7
+
8
+ class BlindLogFormatter(logging.Formatter):
9
+ """
10
+ A custom logging formatter that intercepts and pseudonymizes sensitive data
11
+ before it is written to the log stream.
12
+ """
13
+ def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None, style: str = '%', blind_logger: Optional[MaskingEngine] = None):
14
+ super().__init__(fmt, datefmt, style)
15
+ self._blind_logger = blind_logger
16
+ self._initialized = blind_logger is not None
17
+
18
+ @property
19
+ def blind_logger(self):
20
+ if not self._initialized:
21
+ from blindlog.core import BlindLogger
22
+ self._blind_logger = BlindLogger()
23
+ self._initialized = True
24
+ return self._blind_logger
25
+
26
+ def format(self, record: logging.LogRecord) -> str:
27
+ record_copy = copy.copy(record)
28
+
29
+ try:
30
+ # Mask the message body
31
+ if isinstance(record_copy.msg, (dict, list)):
32
+ record_copy.msg = self.blind_logger.mask(copy.deepcopy(record_copy.msg))
33
+ elif isinstance(record_copy.msg, str):
34
+ record_copy.msg = self.blind_logger.mask(record_copy.msg)
35
+
36
+ # Fix #1: Handle both tuple and dict-style log args
37
+ if record_copy.args:
38
+ args_copy = copy.deepcopy(record_copy.args)
39
+ if isinstance(args_copy, dict):
40
+ # Mapping style: logger.info("%(name)s logged in", {"name": "user@example.com"})
41
+ record_copy.args = {
42
+ k: self.blind_logger.mask(v) if isinstance(v, (str, dict, list)) else v
43
+ for k, v in args_copy.items()
44
+ }
45
+ elif isinstance(args_copy, tuple):
46
+ # Positional style: logger.info("Hello %s", "user@example.com")
47
+ record_copy.args = tuple(
48
+ self.blind_logger.mask(arg) if isinstance(arg, (str, dict, list)) else arg
49
+ for arg in args_copy
50
+ )
51
+
52
+ # Fix #2: Mask exception info to prevent PII leaks in stack traces
53
+ if record_copy.exc_info and record_copy.exc_info[0] is not None:
54
+ exc_text = self.formatException(record_copy.exc_info)
55
+ record_copy.exc_text = self.blind_logger.mask(exc_text)
56
+ record_copy.exc_info = None # Prevent super() from re-formatting unmasked
57
+
58
+ except Exception as e:
59
+ _log.error("BlindLog: Masking failed. Reason: %s", e)
60
+ record_copy.msg = "[BLINDLOG MASKING FAILED - RECORD SUPPRESSED]"
61
+ record_copy.args = None
62
+ record_copy.exc_info = None
63
+ record_copy.exc_text = None
64
+
65
+ return super().format(record_copy)
File without changes