"""Log redaction: token shapes + every secret value the agent has seen. The agent's journal must never carry a bearer, a deploy grant or a `.env` value. `Redactor` is a `logging.Filter` installed on every handler; `add()` registers a value the moment it is read from OpenBao (or minted by OpenBao), and `scrub()` is also applied to the `log_tail` sent to tenancy. Secret NAMES are fine; values never.""" from __future__ import annotations import logging import re import threading # Token / credential shapes redacted even before a value is known to us. _SHAPES = [ re.compile(r"\bhv[sbr]\.[A-Za-z0-9_\-]{20,}"), # OpenBao/Vault service, batch, recovery re.compile(r"\bs\.[A-Za-z0-9]{24,}\b"), # legacy vault token re.compile(r"\beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}"), # JWT re.compile(r"(?i)(authorization:\s*bearer\s+)\S+"), re.compile(r"(?i)(x-vault-token:\s*)\S+"), ] MASK = "[REDACTED]" class Redactor(logging.Filter): def __init__(self) -> None: super().__init__("monky-deployd.redactor") self._values: set[str] = set() self._lock = threading.Lock() def add(self, value: str | None) -> None: """Register a secret value (min 4 chars — shorter ones would shred the log).""" if value and len(value) >= 4: with self._lock: self._values.add(value) def forget_all(self) -> None: with self._lock: self._values.clear() def scrub(self, text: str) -> str: if not text: return text for pat in _SHAPES: if pat.groups: text = pat.sub(lambda m: m.group(1) + MASK, text) else: text = pat.sub(MASK, text) with self._lock: values = sorted(self._values, key=len, reverse=True) for v in values: if v in text: text = text.replace(v, MASK) return text # logging.Filter def filter(self, record: logging.LogRecord) -> bool: try: msg = record.getMessage() except Exception: # pragma: no cover - a broken format string is not our problem msg = str(record.msg) record.msg = self.scrub(msg) record.args = () return True REDACTOR = Redactor() def install(logger: logging.Logger | None = None) -> Redactor: """Attach the singleton to every handler of `logger` (root by default).""" logger = logger or logging.getLogger() for h in logger.handlers: if REDACTOR not in h.filters: h.addFilter(REDACTOR) return REDACTOR