fail2ban.chart.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # -*- coding: utf-8 -*-
  2. # Description: fail2ban log netdata python.d module
  3. # Author: ilyam8
  4. # SPDX-License-Identifier: GPL-3.0-or-later
  5. import re
  6. import os
  7. from collections import defaultdict
  8. from glob import glob
  9. from bases.FrameworkServices.LogService import LogService
  10. ORDER = [
  11. 'jails_bans',
  12. 'jails_in_jail',
  13. ]
  14. def charts(jails):
  15. """
  16. Chart definitions creating
  17. """
  18. ch = {
  19. ORDER[0]: {
  20. 'options': [None, 'Jails Ban Rate', 'bans/s', 'bans', 'jail.bans', 'line'],
  21. 'lines': []
  22. },
  23. ORDER[1]: {
  24. 'options': [None, 'Banned IPs (since the last restart of netdata)', 'IPs', 'in jail',
  25. 'jail.in_jail', 'line'],
  26. 'lines': []
  27. },
  28. }
  29. for jail in jails:
  30. dim = [
  31. jail,
  32. jail,
  33. 'incremental',
  34. ]
  35. ch[ORDER[0]]['lines'].append(dim)
  36. dim = [
  37. '{0}_in_jail'.format(jail),
  38. jail,
  39. 'absolute',
  40. ]
  41. ch[ORDER[1]]['lines'].append(dim)
  42. return ch
  43. RE_JAILS = re.compile(r'\[([a-zA-Z0-9_-]+)\][^\[\]]+?enabled\s+= (true|false)')
  44. # Example:
  45. # 2018-09-12 11:45:53,715 fail2ban.actions[25029]: WARNING [ssh] Unban 195.201.88.33
  46. # 2018-09-12 11:45:58,727 fail2ban.actions[25029]: WARNING [ssh] Ban 217.59.246.27
  47. # 2018-09-12 11:45:58,727 fail2ban.actions[25029]: WARNING [ssh] Restore Ban 217.59.246.27
  48. RE_DATA = re.compile(r'\[(?P<jail>[A-Za-z-_0-9]+)\] (?P<action>Unban|Ban|Restore Ban) (?P<ip>[a-f0-9.:]+)')
  49. DEFAULT_JAILS = [
  50. 'ssh',
  51. ]
  52. class Service(LogService):
  53. def __init__(self, configuration=None, name=None):
  54. LogService.__init__(self, configuration=configuration, name=name)
  55. self.order = ORDER
  56. self.definitions = dict()
  57. self.log_path = self.configuration.get('log_path', '/var/log/fail2ban.log')
  58. self.conf_path = self.configuration.get('conf_path', '/etc/fail2ban/jail.local')
  59. self.conf_dir = self.configuration.get('conf_dir', '/etc/fail2ban/jail.d/')
  60. self.exclude = self.configuration.get('exclude', str())
  61. self.monitoring_jails = list()
  62. self.banned_ips = defaultdict(set)
  63. self.data = dict()
  64. def check(self):
  65. """
  66. :return: bool
  67. """
  68. if not self.conf_path.endswith(('.conf', '.local')):
  69. self.error('{0} is a wrong conf path name, must be *.conf or *.local'.format(self.conf_path))
  70. return False
  71. if not os.access(self.log_path, os.R_OK):
  72. self.error('{0} is not readable'.format(self.log_path))
  73. return False
  74. if os.path.getsize(self.log_path) == 0:
  75. self.error('{0} is empty'.format(self.log_path))
  76. return False
  77. self.monitoring_jails = self.jails_auto_detection()
  78. for jail in self.monitoring_jails:
  79. self.data[jail] = 0
  80. self.data['{0}_in_jail'.format(jail)] = 0
  81. self.definitions = charts(self.monitoring_jails)
  82. self.info('monitoring jails: {0}'.format(self.monitoring_jails))
  83. return True
  84. def get_data(self):
  85. """
  86. :return: dict
  87. """
  88. raw = self._get_raw_data()
  89. if not raw:
  90. return None if raw is None else self.data
  91. for row in raw:
  92. match = RE_DATA.search(row)
  93. if not match:
  94. continue
  95. match = match.groupdict()
  96. if match['jail'] not in self.monitoring_jails:
  97. continue
  98. jail, action, ip = match['jail'], match['action'], match['ip']
  99. if action == 'Ban' or action == 'Restore Ban':
  100. self.data[jail] += 1
  101. if ip not in self.banned_ips[jail]:
  102. self.banned_ips[jail].add(ip)
  103. self.data['{0}_in_jail'.format(jail)] += 1
  104. else:
  105. if ip in self.banned_ips[jail]:
  106. self.banned_ips[jail].remove(ip)
  107. self.data['{0}_in_jail'.format(jail)] -= 1
  108. return self.data
  109. def get_files_from_dir(self, dir_path, suffix):
  110. """
  111. :return: list
  112. """
  113. if not os.path.isdir(dir_path):
  114. self.error('{0} is not a directory'.format(dir_path))
  115. return list()
  116. return glob('{0}/*.{1}'.format(self.conf_dir, suffix))
  117. def get_jails_from_file(self, file_path):
  118. """
  119. :return: list
  120. """
  121. if not os.access(file_path, os.R_OK):
  122. self.error('{0} is not readable or not exist'.format(file_path))
  123. return list()
  124. with open(file_path, 'rt') as f:
  125. lines = f.readlines()
  126. raw = ' '.join(line for line in lines if line.startswith(('[', 'enabled')))
  127. match = RE_JAILS.findall(raw)
  128. # Result: [('ssh', 'true'), ('dropbear', 'true'), ('pam-generic', 'true'), ...]
  129. if not match:
  130. self.debug('{0} parse failed'.format(file_path))
  131. return list()
  132. return match
  133. def jails_auto_detection(self):
  134. """
  135. :return: list
  136. Parses jail configuration files. Returns list of enabled jails.
  137. According man jail.conf parse order must be
  138. * jail.conf
  139. * jail.d/*.conf (in alphabetical order)
  140. * jail.local
  141. * jail.d/*.local (in alphabetical order)
  142. """
  143. jails_files, all_jails, active_jails = list(), list(), list()
  144. jails_files.append('{0}.conf'.format(self.conf_path.rsplit('.')[0]))
  145. jails_files.extend(self.get_files_from_dir(self.conf_dir, 'conf'))
  146. jails_files.append('{0}.local'.format(self.conf_path.rsplit('.')[0]))
  147. jails_files.extend(self.get_files_from_dir(self.conf_dir, 'local'))
  148. self.debug('config files to parse: {0}'.format(jails_files))
  149. for f in jails_files:
  150. all_jails.extend(self.get_jails_from_file(f))
  151. exclude = self.exclude.split()
  152. for name, status in all_jails:
  153. if name in exclude:
  154. continue
  155. if status == 'true' and name not in active_jails:
  156. active_jails.append(name)
  157. elif status == 'false' and name in active_jails:
  158. active_jails.remove(name)
  159. return active_jails or DEFAULT_JAILS