Módulo VPSManager para WHMCS con configuración externa, acceso seguro al panel y métricas Graphite para LX.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

104 行
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.')