Módulo VPSManager para WHMCS con configuración externa, acceso seguro al panel y métricas Graphite para LX.
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

104 linhas
4.8 KiB

  1. #!/usr/bin/env python3
  2. """Local secret review. Never print matched values. Use alongside manual review."""
  3. import argparse
  4. import hashlib
  5. import math
  6. from pathlib import Path
  7. import re
  8. import subprocess
  9. import sys
  10. from collections import Counter
  11. root = Path(__file__).resolve().parents[1]
  12. parser = argparse.ArgumentParser()
  13. parser.add_argument('--staged', action='store_true')
  14. parser.add_argument('--tracked', action='store_true')
  15. args = parser.parse_args()
  16. if args.staged and args.tracked:
  17. parser.error('Choose one Git mode')
  18. mode = 'staged' if args.staged else 'tracked' if args.tracked else 'tree'
  19. def git(*parts):
  20. return subprocess.check_output(['git', '-C', str(root), *parts])
  21. if mode != 'tree':
  22. files = [p.decode() for p in git('ls-files', '-z').split(b'\0') if p]
  23. else:
  24. files = [str(p.relative_to(root)) for p in root.rglob('*') if p.is_file() and '.git' not in p.relative_to(root).parts]
  25. # Only these two exact artifacts may bypass heuristic string/entropy inspection.
  26. # Their content is checked against audited digests; any change requires review.
  27. trusted = {}
  28. manifest = root / 'assets' / 'vendor-sha256.txt'
  29. for line in manifest.read_text().splitlines():
  30. digest, name = line.split(' ', 1)
  31. trusted['assets/' + name] = digest
  32. keywords = re.compile(r'password|passwd|secret|token|api_key|apikey|Authorization:|BEGIN (?:RSA )?PRIVATE KEY|cookie', re.I)
  33. private_key = re.compile(r'-{5}BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-{5}')
  34. url_auth = re.compile(r'https?://[^\s/\'"]+:[^\s/\'"]+@', re.I)
  35. credential = re.compile(r'''["']?(?:password|passwd|secret|token|api_key|apikey)["']?\s*(?:=>|=|:)\s*(["'])([^"'\n]+)\1''', re.I)
  36. known_token = re.compile(r'\b(?:AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|xox[baprs]-[A-Za-z0-9-]{15,})\b')
  37. private_ip = re.compile(r'\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b')
  38. quote = re.compile(r'''["']([A-Za-z0-9_+/=.-]{28,})["']''')
  39. findings = []
  40. review = []
  41. def flag(name, line, reason):
  42. findings.append(f'{name}:{line}: {reason}')
  43. for name in sorted(files):
  44. path = root / name
  45. if mode == 'tree' and path.is_symlink():
  46. flag(name, 0, 'unexpected symlink'); continue
  47. if mode == 'staged':
  48. content = git('show', ':' + name)
  49. elif mode == 'tracked':
  50. content = git('show', 'HEAD:' + name)
  51. else:
  52. content = path.read_bytes()
  53. leaf = Path(name).name.lower()
  54. if (leaf in ['config.php', '.env', 'id_rsa', 'id_ed25519'] and name != 'lib/Config.php') or leaf.startswith('.env.') or leaf.endswith(('.local.php', '.sql', '.sql.gz', '.bak', '.swp', '.swo', '~', '.zip', '.tar', '.tar.gz', '.key', '.p12', '.pfx')):
  55. flag(name, 0, 'private/configuration/backup/archive filename')
  56. if name in trusted:
  57. if hashlib.sha256(content).hexdigest() != trusted[name]:
  58. flag(name, 0, 'vendor digest mismatch')
  59. continue
  60. try:
  61. text = content.decode('utf-8')
  62. except UnicodeDecodeError:
  63. flag(name, 0, 'unreviewed binary file'); continue
  64. for number, line in enumerate(text.splitlines(), 1):
  65. if keywords.search(line):
  66. review.append(f'{name}:{number}')
  67. if private_key.search(line) or known_token.search(line):
  68. flag(name, number, 'private key or known credential pattern')
  69. if url_auth.search(line) and not (name in ('tests/run.php', 'tests/backups.php') and 'example.invalid' in line):
  70. flag(name, number, 'credential embedded in URL')
  71. if private_ip.search(line):
  72. flag(name, number, 'private IP literal')
  73. for match in credential.finditer(line):
  74. value = match.group(2)
  75. if name in ('tests/run.php', 'tests/backups.php') and value.startswith('FAKE_'):
  76. continue
  77. flag(name, number, 'nonempty credential-like literal')
  78. for match in quote.finditer(line):
  79. value = match.group(1)
  80. if name in ('tests/run.php', 'tests/backups.php') and value.startswith('FAKE_'):
  81. continue
  82. if 'example.invalid' in value or re.fullmatch(r'[0-9a-f-]{36}', value):
  83. continue
  84. # Code identifiers, paths and human phrases need manual keyword review;
  85. # look for random-looking mixed-case/digit string literals.
  86. counts = Counter(value)
  87. entropy = -sum((n/len(value))*math.log2(n/len(value)) for n in counts.values())
  88. if entropy > 4.5 and re.search('[a-z]', value) and re.search('[A-Z]', value) and re.search('[0-9]', value):
  89. flag(name, number, 'high-entropy literal requires review')
  90. print(f'Audit mode={mode}; files={len(files)}; keyword lines={len(review)}; flagged={len(findings)}')
  91. for item in findings:
  92. print(item)
  93. if findings:
  94. sys.exit(1)
  95. print('PASS heuristic scan. Keyword matches require the documented manual review; values are not printed.')