diff --git a/.secrets.baseline b/.secrets.baseline index 4e41319f..82424789 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -861,22 +861,6 @@ "line_number": 39 } ], - "tests/unit/test_structured_logging.py": [ - { - "type": "Base64 High Entropy String", - "filename": "tests/unit/test_structured_logging.py", - "hashed_secret": "0cb62954c5feaf5379bfb79b8e0087953ec49b9c", - "is_verified": false, - "line_number": 45 - }, - { - "type": "JSON Web Token", - "filename": "tests/unit/test_structured_logging.py", - "hashed_secret": "d6b66ddd9ea7dbe760114bfe9a97352a5e139134", - "is_verified": false, - "line_number": 50 - } - ], "tests/unit/test_tenant_context.py": [ { "type": "Hex High Entropy String", @@ -887,5 +871,5 @@ } ] }, - "generated_at": "2026-09-14T08:02:35Z" + "generated_at": "2026-09-17T05:12:11Z" } diff --git a/src/cachekit/logging.py b/src/cachekit/logging.py index ad4ea278..18d9d149 100644 --- a/src/cachekit/logging.py +++ b/src/cachekit/logging.py @@ -42,7 +42,6 @@ def _get_logging_config(): # Performance and health thresholds HIGH_UTILIZATION_THRESHOLD = 0.9 # When to warn about high utilization -LONG_TOKEN_LENGTH_THRESHOLD = 30 # Minimum length to abbreviate tokens @dataclass @@ -159,13 +158,12 @@ class UltraOptimizedStructuredLogger: - Lock-free ring buffer - Sampling (10% default) - Async batch writes - - Smart PII masking + - PII key-name masking (password/token/secret/key/auth kwargs) - Near-zero overhead when not sampled """ - def __init__(self, name: str, mask_sensitive: bool = True): + def __init__(self, name: str): self.name = name - self.mask_sensitive = mask_sensitive self.buffer = LockFreeRingBuffer() self.writer = AsyncLogWriter(self.buffer) self.writer.start() @@ -481,29 +479,25 @@ def __exit__(self, exc_type, exc_val, exc_tb): _logger_lock = threading.Lock() -def get_structured_logger(name: str, mask_sensitive: bool = True) -> UltraOptimizedStructuredLogger: +def get_structured_logger(name: str) -> UltraOptimizedStructuredLogger: """Get or create a structured logger instance. Args: name: Logger name (usually __name__) - mask_sensitive: Whether to mask sensitive data Returns: Ultra-optimized structured logger instance """ - # Create cache key including mask_sensitive setting - cache_key = f"{name}:{mask_sensitive}" - # Fast path - check if already exists - if cache_key in _logger_instances: - return _logger_instances[cache_key] + if name in _logger_instances: + return _logger_instances[name] # Slow path - create new instance with _logger_lock: # Double-check pattern - if cache_key not in _logger_instances: - _logger_instances[cache_key] = UltraOptimizedStructuredLogger(name, mask_sensitive) - return _logger_instances[cache_key] + if name not in _logger_instances: + _logger_instances[name] = UltraOptimizedStructuredLogger(name) + return _logger_instances[name] # Alias @@ -534,38 +528,3 @@ def format(self, record): log_data["exception"] = "".join(traceback.format_exception(*record.exc_info)) return json.dumps(log_data, separators=(",", ":")) - - -# Additional compatibility functions -def mask_sensitive_patterns(data: str) -> str: - """Mask sensitive patterns in data.""" - if data is None: - return None - - import re - - # SSN patterns - data = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "XXX-XX-XXXX", data) - data = re.sub(r"\b\d{9}\b", "XXXXXXXXX", data) - - # Credit card patterns - data = re.sub(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b", "XXXX-XXXX-XXXX-XXXX", data) - - # Email addresses - data = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "XXX@XXX.XXX", data) - - # Phone numbers - data = re.sub(r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b", "XXX-XXX-XXXX", data) - data = re.sub(r"\(\d{3}\)\s?\d{3}[-.\s]?\d{4}\b", "(XXX) XXX-XXXX", data) - - # JWT tokens (must be done before general API keys) - data = re.sub(r"\b[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\b", "XXX.XXX.XXX", data) - - # API keys and tokens (20+ chars) - data = re.sub( - r"\b[A-Za-z0-9_-]{20,}\b", - lambda m: "XXXXX...XXXXX" if len(m.group()) > LONG_TOKEN_LENGTH_THRESHOLD else "XXX", - data, - ) - - return data diff --git a/src/cachekit/reliability/profiles.py b/src/cachekit/reliability/profiles.py index a9b96a71..bb190b90 100644 --- a/src/cachekit/reliability/profiles.py +++ b/src/cachekit/reliability/profiles.py @@ -67,15 +67,11 @@ class ProfileConfig: metrics_batch_size: int = 100 metrics_flush_interval: float = 0.1 - # Security features - mask_sensitive_data: bool = True - # Backpressure settings backpressure_read_operations: bool = False # Skip backpressure for reads backpressure_timeout: float = 0.1 # Shorter timeout for cache ops # Logging optimization - lazy_pii_masking: bool = True log_level_threshold: str = "INFO" @@ -94,10 +90,7 @@ class ProfileConfig: health_cache_duration=1.0, # Cache longer for performance # Performance optimized max_concurrent_requests=1000, # Higher limit - # Security still enabled - mask_sensitive_data=True, # Minimal logging - lazy_pii_masking=True, log_level_threshold="WARNING", # Only log warnings and errors ), ReliabilityProfile.BALANCED: ProfileConfig( @@ -115,13 +108,10 @@ class ProfileConfig: max_concurrent_requests=100, metrics_batch_size=100, metrics_flush_interval=0.1, - # Security enabled - mask_sensitive_data=True, # Optimized backpressure backpressure_read_operations=False, # Skip reads backpressure_timeout=0.1, # Optimized logging - lazy_pii_masking=True, log_level_threshold="INFO", ), ReliabilityProfile.FULL: ProfileConfig( @@ -139,13 +129,10 @@ class ProfileConfig: max_concurrent_requests=50, metrics_batch_size=50, # Smaller batches for lower latency metrics_flush_interval=0.05, # More frequent flushes - # Full security - mask_sensitive_data=True, # Full backpressure (including reads) backpressure_read_operations=True, backpressure_timeout=0.5, # Longer timeout for reliability # Full logging - lazy_pii_masking=True, log_level_threshold="DEBUG", ), } @@ -234,7 +221,6 @@ def create_optimized_decorator_config(profile: ReliabilityProfile = ReliabilityP "_use_async_metrics": config.async_metrics, "_use_lightweight_health": True, "_health_check_level": config.health_check_level.value, - "_lazy_pii_masking": config.lazy_pii_masking, "_backpressure_read_ops": config.backpressure_read_operations, "_metrics_batch_size": config.metrics_batch_size, "_metrics_flush_interval": config.metrics_flush_interval, diff --git a/tests/unit/test_structured_logging.py b/tests/unit/test_structured_logging.py index 5d46ee2e..9d7d2f61 100644 --- a/tests/unit/test_structured_logging.py +++ b/tests/unit/test_structured_logging.py @@ -12,72 +12,19 @@ JsonFormatter, StructuredRedisLogger, get_structured_logger, - mask_sensitive_patterns, ) -class TestSensitiveDataMasking: - """Test PII masking functionality.""" - - def test_mask_ssn_patterns(self): - """Test SSN masking.""" - assert mask_sensitive_patterns("My SSN is 123-45-6789") == "My SSN is XXX-XX-XXXX" - assert mask_sensitive_patterns("SSN: 123456789") == "SSN: XXXXXXXXX" - - def test_mask_credit_card_patterns(self): - """Test credit card masking.""" - assert mask_sensitive_patterns("Card: 1234-5678-9012-3456") == "Card: XXXX-XXXX-XXXX-XXXX" - assert mask_sensitive_patterns("Card: 1234567890123456") == "Card: XXXX-XXXX-XXXX-XXXX" - - def test_mask_email_addresses(self): - """Test email masking.""" - assert mask_sensitive_patterns("Email: user@example.com") == "Email: XXX@XXX.XXX" - assert mask_sensitive_patterns("Contact: john.doe+tag@company.co.uk") == "Contact: XXX@XXX.XXX" - - def test_mask_phone_numbers(self): - """Test phone number masking.""" - assert mask_sensitive_patterns("Call: 123-456-7890") == "Call: XXX-XXX-XXXX" - assert mask_sensitive_patterns("Phone: (123) 456-7890") == "Phone: (XXX) XXX-XXXX" - assert mask_sensitive_patterns("Tel: 123.456.7890") == "Tel: XXX-XXX-XXXX" - - def test_mask_api_keys(self): - """Test API key masking.""" - long_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" - assert mask_sensitive_patterns(f"API Key: {long_key}") == "API Key: XXXXX...XXXXX" - - def test_mask_jwt_tokens(self): - """Test JWT token masking.""" - jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" - assert mask_sensitive_patterns(f"Token: {jwt}") == "Token: XXX.XXX.XXX" - - def test_mask_multiple_patterns(self): - """Test masking multiple patterns in one string.""" - text = "User email@test.com with SSN 123-45-6789 called from 555-123-4567" - expected = "User XXX@XXX.XXX with SSN XXX-XX-XXXX called from XXX-XXX-XXXX" - assert mask_sensitive_patterns(text) == expected - - def test_empty_string(self): - """Test masking empty string.""" - assert mask_sensitive_patterns("") == "" - assert mask_sensitive_patterns(None) is None - - class TestStructuredRedisLogger: """Test StructuredRedisLogger functionality.""" @pytest.fixture def logger(self): """Create a test logger instance.""" - return StructuredRedisLogger("test_logger", mask_sensitive=True) - - @pytest.fixture - def logger_no_mask(self): - """Create a test logger without masking.""" - return StructuredRedisLogger("test_logger_no_mask", mask_sensitive=False) + return StructuredRedisLogger("test_logger") def test_logger_initialization(self, logger): """Test logger initialization.""" - assert logger.mask_sensitive is True assert hasattr(logger, "_context") assert isinstance(logger._context, threading.local) @@ -106,19 +53,18 @@ def test_get_context(self, logger): context = logger._get_context() assert context["trace_id"] == trace_id - def test_cache_key_always_redacted(self, logger, logger_no_mask): - """cache_operation redacts the key regardless of mask_sensitive (CWE-532, LAB-304).""" + def test_cache_key_always_redacted(self, logger): + """cache_operation always redacts the key via digest (CWE-532, LAB-304).""" from unittest.mock import patch as _patch from cachekit.hash_utils import redact_cache_key sensitive = "ns:tenant-42:func:app.f:args:email@test.com:v1" - for lg in (logger, logger_no_mask): - with _patch("cachekit.logging.logging.Logger.log") as mock_log: - lg.cache_operation("get", sensitive, hit=True) - extra = mock_log.call_args[1]["extra"]["structured"] - assert extra["cache_key"] == redact_cache_key(sensitive) - assert sensitive not in str(extra) + with _patch("cachekit.logging.logging.Logger.log") as mock_log: + logger.cache_operation("get", sensitive, hit=True) + extra = mock_log.call_args[1]["extra"]["structured"] + assert extra["cache_key"] == redact_cache_key(sensitive) + assert sensitive not in str(extra) @patch("cachekit.logging.logging.Logger.log") def test_cache_operation_logging(self, mock_log, logger): @@ -322,11 +268,9 @@ class TestFactoryFunction: """Test factory function.""" def test_get_structured_logger(self): - """Test get_structured_logger factory.""" + """Test get_structured_logger factory returns one cached instance per name.""" logger1 = get_structured_logger("test1") assert isinstance(logger1, StructuredRedisLogger) - assert logger1.mask_sensitive is True - logger2 = get_structured_logger("test2", mask_sensitive=False) - assert isinstance(logger2, StructuredRedisLogger) - assert logger2.mask_sensitive is False + logger1_again = get_structured_logger("test1") + assert logger1_again is logger1