File size: 3,729 Bytes
b4f2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""Module for scrub."""

from __future__ import annotations

import hashlib
import re
from typing import Any

_PATTERNS = [
    re.compile(r"(sk-[A-Za-z0-9]{12,})"),
    re.compile(r"(?i)(bearer\s+)([A-Za-z0-9._\-]{12,})"),
    re.compile(r"(?i)(api[_-]?key\s*[:=]\s*)(\S+)"),
    re.compile(r"(?i)(token\s*[:=]\s*)(\S+)"),
    re.compile(r"(?i)(authorization\s*[:=]\s*)(\S+)"),
    re.compile(r"(?i)(password\s*[:=]\s*)(\S+)"),
    re.compile(r"(?i)(secret\s*[:=]\s*)(\S+)"),
]

_LIKELY_TOKEN_PATTERNS = [
    re.compile(r"^(sk-[A-Za-z0-9]{12,})$"),
    re.compile(r"^[A-Za-z0-9._\-]{24,}$"),
]

_SENSITIVE_ATTR_NAMES = {
    "token",
    "access_token",
    "refresh_token",
    "api_key",
    "apikey",
    "authorization",
    "password",
    "secret",
    "app_secret",
    "client_secret",
    "account",
}

_SENSITIVE_TOKENS = {
    "token",
    "password",
    "secret",
    "credential",
    "credentials",
    "authorization",
    "auth",
}

_KEY_PREFIX_TOKENS = {
    "api",
    "app",
    "access",
    "secret",
    "client",
    "private",
    "public",
    "auth",
    "authorization",
}

_CAMEL_BOUNDARY = re.compile(r"([a-z0-9])([A-Z])")
_TOKEN_SPLIT = re.compile(r"[^a-z0-9]+")


def _mask_match(match: re.Match[str]) -> str:
    groups = match.groups()
    if len(groups) == 2:
        return groups[0] + mask_secret(groups[1])
    return mask_secret(groups[0])


def is_sensitive_key_name(name: str) -> bool:
    normalized = _CAMEL_BOUNDARY.sub(r"\1_\2", str(name or "")).lower().strip()
    if not normalized:
        return False
    if normalized in _SENSITIVE_ATTR_NAMES:
        return True

    tokens = [item for item in _TOKEN_SPLIT.split(normalized) if item]
    if not tokens:
        return False

    if any(token in _SENSITIVE_TOKENS for token in tokens):
        return True

    if "key" in tokens:
        return True

    for token in tokens:
        if token.endswith("key") and token[:-3] in _KEY_PREFIX_TOKENS:
            return True
    return False


def looks_like_token(value: str) -> bool:
    return any(p.match(value) for p in _LIKELY_TOKEN_PATTERNS)


def mask_value_for_key(key: str, value: Any) -> Any:
    if isinstance(value, str):
        return mask_secret(value)
    return value


def fingerprint_sha256(value: str) -> str:
    v = (value or "").encode("utf-8")
    return hashlib.sha256(v).hexdigest()


def mask_secret(secret: str) -> str:
    s = str(secret)
    if len(s) <= 6:
        return "***"
    return s[:2] + "***" + s[-2:]


def scrub_text(text: str) -> str:
    value = text
    for pattern in _PATTERNS:
        value = pattern.sub(_mask_match, value)
    return value


def scrub_obj(obj: Any, *, key_hint: str = "", depth: int = 0) -> Any:
    if depth >= 6:
        return obj

    if obj is None:
        return obj

    if isinstance(obj, str):
        if looks_like_token(obj) or is_sensitive_key_name(key_hint):
            return mask_secret(obj)
        return scrub_text(obj)

    if isinstance(obj, dict):
        scrubbed: dict[Any, Any] = {}
        for k, v in obj.items():
            key_name = str(k).lower() if k is not None else ""
            if key_name in _SENSITIVE_ATTR_NAMES or is_sensitive_key_name(key_name):
                scrubbed[k] = mask_value_for_key(key_name, v)
            else:
                scrubbed[k] = scrub_obj(v, key_hint=key_name, depth=depth + 1)
        return scrubbed

    if isinstance(obj, list):
        return [scrub_obj(v, key_hint=key_hint, depth=depth + 1) for v in obj]

    if isinstance(obj, tuple):
        return tuple(scrub_obj(v, key_hint=key_hint, depth=depth + 1) for v in obj)

    return obj


def scrub_for_storage(value: Any) -> Any:
    return scrub_obj(value, key_hint="", depth=0)