Coverage for src/lexigram/admin/lib/security.py: 25%

24 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:39 +0800

1"""Security utilities for lexigram-admin.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from typing import Any 

7 

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) 

36 

37 

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. 

44 

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 

49 

50 Returns: 

51 Masked data with same structure 

52 

53 Example: 

54 >>> mask_sensitive_data({"username": "john", "password": "secret123"}) 

55 {'username': 'john', 'password': '****'} 

56 """ 

57 keys_to_check = sensitive_keys or SENSITIVE_KEYS 

58 

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 

75 

76 

77def mask_string_secrets(text: str, mask: str = "****") -> str: 

78 """Mask common secret patterns in strings. 

79 

80 Args: 

81 text: Text to mask 

82 mask: Mask string to use 

83 

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)