Coverage for lintro / ai / secrets.py: 100%

13 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Detect potential secrets in code before sending to AI.""" 

2 

3from __future__ import annotations 

4 

5import re 

6 

7# Common secret patterns 

8_SECRET_PATTERNS = [ 

9 re.compile( 

10 r"(?:api[_-]?key|apikey)\s*[=:]\s*[\"']?[A-Za-z0-9_\-]{20,}", 

11 re.I, 

12 ), 

13 re.compile( 

14 r"(?:secret|password|passwd|pwd)\s*[=:]\s*[\"']?[^\s\"']{8,}", 

15 re.I, 

16 ), 

17 re.compile( 

18 r"(?:token)\s*[=:]\s*[\"']?[A-Za-z0-9_\-\.]{20,}", 

19 re.I, 

20 ), 

21 re.compile( 

22 r"(?:aws_access_key_id|aws_secret_access_key)\s*[=:]\s*[\"']?[A-Za-z0-9/+=]{16,}", 

23 re.I, 

24 ), 

25 re.compile(r"ghp_[A-Za-z0-9]{36}"), # GitHub personal access token 

26 re.compile(r"sk-[A-Za-z0-9]{20,}"), # OpenAI/Anthropic API key 

27 re.compile( 

28 r"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----" 

29 r"[\s\S]*?" 

30 r"-----END (?:RSA |EC )?PRIVATE KEY-----", 

31 ), 

32] 

33 

34 

35def scan_for_secrets(text: str) -> list[str]: 

36 """Return list of detected secret pattern descriptions. 

37 

38 Scans the given text against a set of common secret patterns 

39 (API keys, passwords, tokens, private keys) and returns a 

40 human-readable description for each match found. 

41 

42 Args: 

43 text: The text to scan for secrets. 

44 

45 Returns: 

46 List of description strings for each detected secret pattern. 

47 """ 

48 found: list[str] = [] 

49 for pattern in _SECRET_PATTERNS: 

50 if pattern.search(text): 

51 found.append(f"Potential secret detected: {pattern.pattern[:40]}...") 

52 return found 

53 

54 

55def redact_secrets(text: str) -> str: 

56 """Redact detected secrets from text. 

57 

58 Replaces all matches of known secret patterns with ``[REDACTED]`` 

59 to prevent accidental leakage when sending text to external AI 

60 providers. 

61 

62 Args: 

63 text: The text to redact secrets from. 

64 

65 Returns: 

66 Text with all detected secrets replaced by ``[REDACTED]``. 

67 """ 

68 for pattern in _SECRET_PATTERNS: 

69 text = pattern.sub("[REDACTED]", text) 

70 return text