Files
cmbuyer/client/src/cmbuyer_client/logging_policy.py
T

81 lines
2.7 KiB
Python

"""采购工具日志的最小脱敏策略。"""
from __future__ import annotations
import logging
from pathlib import Path
import re
from .runtime import RuntimePaths
LOGGER_NAME = "cmbuyer_client"
REDACTED = "[已隐藏]"
_SENSITIVE_KEY_PATTERN = (
r"token|authorization|password|secret|api[_-]?key|"
r"address|phone|mobile|payment|pay|card|bank[_-]?account|"
r"令牌|授权|密码|密钥|地址|手机号|电话|支付|银行卡"
)
_KEY_VALUE_PATTERN = re.compile(
rf"(?P<key>{_SENSITIVE_KEY_PATTERN})\s*(?P<separator>[:=])\s*"
r"(?P<value>\"[^\"]*\"|'[^']*'|[^\s,;]+)",
flags=re.IGNORECASE,
)
_PHONE_PATTERN = re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)")
_BEARER_PATTERN = re.compile(r"(?i)\bBearer\s+[0-9a-f]{64}\b")
_BARE_TOKEN_PATTERN = re.compile(r"(?<![0-9a-fA-F])[0-9a-fA-F]{64}(?![0-9a-fA-F])")
def redact_text(message: str) -> str:
"""移除日志文本中的凭据、地址、手机号和支付字段值。"""
def replace_key_value(match: re.Match[str]) -> str:
return f"{match.group('key')}{match.group('separator')}{REDACTED}"
redacted = _BEARER_PATTERN.sub("Bearer " + REDACTED, message)
redacted = _KEY_VALUE_PATTERN.sub(replace_key_value, redacted)
redacted = _BARE_TOKEN_PATTERN.sub(REDACTED, redacted)
return _PHONE_PATTERN.sub(REDACTED, redacted)
class SensitiveDataFilter(logging.Filter):
"""在任何 handler 格式化记录前,清除敏感字段。
该过滤器在日志写入前替换 ``msg`` 与 ``args``,确保文件 handler 不会得到原始值。
"""
def filter(self, record: logging.LogRecord) -> bool:
record.msg = redact_text(record.getMessage())
record.args = ()
return True
class RedactingFormatter(logging.Formatter):
"""再次处理完整格式化文本,覆盖异常 traceback 中的敏感值。"""
def format(self, record: logging.LogRecord) -> str:
return redact_text(super().format(record))
def formatException(self, exc_info: tuple[type[BaseException], BaseException, object]) -> str:
return redact_text(super().formatException(exc_info))
def configure_application_logger(paths: RuntimePaths) -> logging.Logger:
"""配置唯一的 UTF-8 文件日志,并确保其先经过脱敏过滤。"""
paths.ensure_exists()
logger = logging.getLogger(LOGGER_NAME)
logger.setLevel(logging.INFO)
logger.propagate = False
for handler in tuple(logger.handlers):
logger.removeHandler(handler)
handler.close()
handler = logging.FileHandler(Path(paths.logs) / "client.log", encoding="utf-8")
handler.addFilter(SensitiveDataFilter())
handler.setFormatter(RedactingFormatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
return logger