Source code for pyisyox.redactor

"""Log redactor for sensitive fields in eisy responses.

The ``POST /api/login`` response leaks the PG3 MQTT TLS keypair under
``data.ssl`` (verified in HAR captures, 2026-05-06). Access and refresh
tokens are also sensitive. Apply :func:`redact_sensitive` to any
debug-level dump of a JSON response body before logging.

Redactor walks the payload recursively so nested envelopes
(``{"successful": true, "data": {...}}``) are still scrubbed.
"""

from __future__ import annotations

from copy import deepcopy
from typing import Any

#: Keys whose values are replaced with ``<redacted>`` regardless of their
#: position in the payload tree.
SENSITIVE_KEYS: frozenset[str] = frozenset(
    {
        "accessToken",
        "refreshToken",
        "ssl",
        "token",
        "clientToken",
        "password",
        "Authorization",
        "authorization",
        "Cookie",
        "cookie",
        "Set-Cookie",
        "setCookie",
    }
)

REDACTED = "<redacted>"


[docs] def redact_sensitive(payload: Any, *, sensitive_keys: frozenset[str] = SENSITIVE_KEYS) -> Any: """Return a deep copy of ``payload`` with sensitive values replaced. Walks dicts and lists recursively. Non-container values pass through unchanged. The original ``payload`` is not mutated. Args: payload: A JSON-shaped value (dict, list, or primitive). Most commonly a dict from an aiohttp response body. sensitive_keys: Override the default set of keys to redact. Use for tests or for redacting additional auth-domain keys. Returns: A deep copy with sensitive values replaced by :data:`REDACTED`. """ cloned = deepcopy(payload) _walk(cloned, sensitive_keys) return cloned
def _walk(node: Any, sensitive_keys: frozenset[str]) -> None: """In-place redact pass over ``node`` and its descendants.""" if isinstance(node, dict): for key in list(node.keys()): if key in sensitive_keys: node[key] = REDACTED else: _walk(node[key], sensitive_keys) elif isinstance(node, list): for item in node: _walk(item, sensitive_keys)