Source code for pain001.logging_schema.redaction

# Copyright (C) 2023-2026 Pain001. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# Licensed under either of the Apache License, Version 2.0 or the MIT
# License, at your option. You may not use this file except in
# compliance with one of those licences. Copies are provided in
# LICENSE-APACHE and LICENSE-MIT.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licences is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the applicable Licence for the specific language
# governing permissions and limitations.

"""PII redaction and log-injection sanitization (GDPR/PCI-DSS, CWE-117)."""

from typing import Any


[docs] def mask_sensitive_data(value: str, visible_chars: int = 4) -> str: """Mask sensitive data for logging. Args: value: The sensitive value to mask. visible_chars: Number of characters to show at start and end. Returns: Masked string showing only first and last visible_chars. Examples: >>> mask_sensitive_data("GB29NWBK60161331926819", 4) 'GB29****6819' >>> mask_sensitive_data("Short", 4) '****' """ if len(value) <= visible_chars * 2: return "****" masked_length = len(value) - (visible_chars * 2) return ( f"{value[:visible_chars]}{'*' * masked_length}{value[-visible_chars:]}" )
def _sanitize_value(value: Any) -> Any: """Sanitize value to prevent log injection (remove newlines).""" if isinstance(value, str): return value.replace("\n", "").replace("\r", "") return value def _redact_pii_from_dict(data: dict[str, Any]) -> dict[str, Any]: """Redact PII from dictionary fields recursively. This function implements GDPR/PCI-DSS compliant logging by automatically masking sensitive fields before they reach log aggregation systems. It also sanitizes all string values to prevent log injection (CWE-117). Redacted fields: - *iban* (any key containing 'iban'): Shows first 4 + last 4 chars - *bic* (any key containing 'bic'): Shows first 4 + last 2 chars - *name* (any key containing 'name'): Replaced with [REDACTED] - *account* (any key containing 'account'): Shows first 4 + last 4 chars Args: data: Dictionary that may contain PII fields. Returns: New dictionary with PII fields redacted and strings sanitized. Example: >>> _redact_pii_from_dict({"debtor_iban": "GB29NWBK60161331926819"}) {'debtor_iban': 'GB29****6819'} """ redacted: dict[str, Any] = {} for key, value in data.items(): key_lower = key.lower() if isinstance(value, dict): redacted[key] = _redact_pii_from_dict(value) elif isinstance(value, list): redacted[key] = [ ( _redact_pii_from_dict(item) if isinstance(item, dict) else _sanitize_value(item) ) for item in value ] elif "iban" in key_lower and isinstance(value, str): redacted[key] = mask_sensitive_data( _sanitize_value(value), visible_chars=4 ) elif "bic" in key_lower and isinstance(value, str): val = _sanitize_value(value) redacted[key] = ( f"{val[:4]}**{val[-2:]}" if len(val) > 6 else "****" ) elif "name" in key_lower and isinstance(value, str): redacted[key] = "[REDACTED]" elif "account" in key_lower and isinstance(value, str): redacted[key] = mask_sensitive_data( _sanitize_value(value), visible_chars=4 ) else: redacted[key] = _sanitize_value(value) return redacted