Coverage for src/lexigram/admin/lib/security.py: 100%
24 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Security utilities for lexigram-admin."""
3from __future__ import annotations
5import re
6from typing import Any
8# Patterns for detecting sensitive data
9SENSITIVE_KEYS = frozenset(
10 {
11 "password",
12 "passwd",
13 "pwd",
14 "secret",
15 "token",
16 "api_key",
17 "apikey",
18 "access_token",
19 "refresh_token",
20 "bearer",
21 "authorization",
22 "auth",
23 "credential",
24 "private_key",
25 "privatekey",
26 "session_id",
27 "sessionid",
28 "cookie",
29 "csrf",
30 "ssn",
31 "credit_card",
32 "creditcard",
33 "card_number",
34 },
35)
38def mask_sensitive_data(
39 data: Any,
40 mask: str = "****",
41 sensitive_keys: frozenset[str] | None = None,
42) -> Any:
43 """Mask sensitive data in a dictionary or nested structure.
45 Args:
46 data: Data to mask (dict, list, or primitive)
47 mask: Mask string to use
48 sensitive_keys: Custom set of sensitive key names
50 Returns:
51 Masked data with same structure
53 Example:
54 >>> mask_sensitive_data({"username": "john", "password": "secret123"})
55 {'username': 'john', 'password': '****'}
56 """
57 keys_to_check = sensitive_keys or SENSITIVE_KEYS
59 if isinstance(data, dict):
60 result = {}
61 for key, value in data.items():
62 key_lower = str(key).lower()
63 # Check if key matches any sensitive pattern
64 is_sensitive = any(s in key_lower for s in keys_to_check)
65 if is_sensitive:
66 result[key] = mask
67 else:
68 result[key] = mask_sensitive_data(value, mask, keys_to_check)
69 return result
70 if isinstance(data, list):
71 return [mask_sensitive_data(item, mask, keys_to_check) for item in data]
72 if isinstance(data, tuple):
73 return tuple(mask_sensitive_data(item, mask, keys_to_check) for item in data)
74 return data
77def mask_string_secrets(text: str, mask: str = "****") -> str:
78 """Mask common secret patterns in strings.
80 Args:
81 text: Text to mask
82 mask: Mask string to use
84 Returns:
85 Masked text
86 """
87 # Bearer tokens
88 text = re.sub(r"Bearer\s+[A-Za-z0-9_-]+", f"Bearer {mask}", text)
89 # API keys (common format)
90 text = re.sub(
91 r'api[_-]?key["\']?\s*[:=]\s*["\']?[A-Za-z0-9_-]+',
92 f"api_key={mask}",
93 text,
94 flags=re.I,
95 )
96 # Password in URLs
97 return re.sub(r"://([^:]+):([^@]+)@", f"://\\1:{mask}@", text)