Ctrl K
ComunidadGuías…

butler/gateway/pii_scrub.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Outbound PII redaction for WeChat (LangChain PIIMiddleware subset)."""

from __future__ import annotations

import re
from typing import cast

from butler.utilities.env_parse import env_truthy

_PHONE = re.compile(r"(?<!\d)(?:\+?86[-\s]?)?1[3-9]\d{9}(?!\d)")
_ID18 = re.compile(r"(?<![0-9Xx])[1-9]\d{16}[\dXx](?![0-9Xx])")
_EMAIL = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")
# sk- / sk_ API keys, OpenAI/Anthropic style (>=20 trailing alnum)
_API_KEY = re.compile(r"\bsk[-_][A-Za-z0-9]{20,}\b")
# JWT: three base64url segments separated by dots, header always starts with eyJ
_JWT = re.compile(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b")
# IPv4 private ranges: 10/8, 172.16/12, 192.168/16, 127/8, link-local 169.254/16
_IPV4_PRIVATE = re.compile(
    r"\b(?:"
    r"10(?:\.\d{1,3}){3}"
    r"|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}"
    r"|192\.168(?:\.\d{1,3}){2}"
    r"|127(?:\.\d{1,3}){3}"
    r"|169\.254(?:\.\d{1,3}){2}"
    r")\b"
)
# Bank card: 13-19 digit run, Luhn-validated
_CARD_DIGITS = re.compile(r"(?<!\d)(\d{13,19})(?!\d)")
# AWS access key id, GitHub PAT, Bearer authorization header fragments
_AWS_ACCESS_KEY = re.compile(r"\bAKIA[0-9A-Z]{16}\b")
_GITHUB_PAT = re.compile(r"\b(?:ghp_|github_pat_)[A-Za-z0-9_]{20,}\b")
_BEARER = re.compile(r"\bBearer\s+[A-Za-z0-9._-]{20,}\b", re.IGNORECASE)


def _luhn_ok(digits: str) -> bool:
    total = 0
    for i, ch in enumerate(reversed(digits)):
        d = ord(ch) - 48
        if d < 0 or d > 9:
            return False
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0


def _scrub_card(match: re.Match[str]) -> str:
    if _luhn_ok(match.group(1)):
        return "[银行卡号已脱敏]"
    return match.group(0)


def outbound_pii_scrub_enabled() -> bool:
    return bool(env_truthy("BUTLER_OUTBOUND_PII_SCRUB", default=True))


def scrub_outbound_text(text: str) -> str:
    if not outbound_pii_scrub_enabled() or not text:
        return text
    out = _PHONE.sub("[手机号已脱敏]", text)
    out = _ID18.sub("[证件号已脱敏]", out)
    if env_truthy("BUTLER_OUTBOUND_PII_SCRUB_EMAIL", default=True):
        out = _EMAIL.sub("[邮箱已脱敏]", out)
    out = _API_KEY.sub("[API密钥已脱敏]", out)
    out = _JWT.sub("[JWT令牌已脱敏]", out)
    out = _BEARER.sub("[Bearer令牌已脱敏]", out)
    out = _AWS_ACCESS_KEY.sub("[云凭证已脱敏]", out)
    out = _GITHUB_PAT.sub("[GitHub令牌已脱敏]", out)
    out = _IPV4_PRIVATE.sub("[内网IP已脱敏]", out)
    out = _CARD_DIGITS.sub(_scrub_card, out)
    from butler.gateway.pii_scrub_ops import scrub_internal_leaks_safe

    return cast(str, scrub_internal_leaks_safe(out))