fail2ban.chart.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # -*- coding: utf-8 -*-
  2. # Description: fail2ban log netdata python.d module
  3. # Author: l2isbad
  4. import bisect
  5. from glob import glob
  6. from re import compile as r_compile
  7. from os import access as is_accessible, R_OK
  8. from os.path import isdir, getsize
  9. from bases.FrameworkServices.LogService import LogService
  10. priority = 60000
  11. retries = 60
  12. REGEX_JAILS = r_compile(r'\[([a-zA-Z0-9_-]+)\][^\[\]]+?enabled\s+= (true|false)')
  13. REGEX_DATA = r_compile(r'\[(?P<jail>[A-Za-z-_0-9]+)\] (?P<action>U|B)[a-z]+ (?P<ipaddr>\d{1,3}(?:\.\d{1,3}){3})')
  14. ORDER = ['jails_bans', 'jails_in_jail']
  15. class Service(LogService):
  16. """
  17. fail2ban log class
  18. Reads logs line by line
  19. Jail auto detection included
  20. It produces following charts:
  21. * Bans per second for every jail
  22. * Banned IPs for every jail (since the last restart of netdata)
  23. """
  24. def __init__(self, configuration=None, name=None):
  25. LogService.__init__(self, configuration=configuration, name=name)
  26. self.order = ORDER
  27. self.definitions = dict()
  28. self.log_path = self.configuration.get('log_path', '/var/log/fail2ban.log')
  29. self.conf_path = self.configuration.get('conf_path', '/etc/fail2ban/jail.local')
  30. self.conf_dir = self.configuration.get('conf_dir', '/etc/fail2ban/jail.d/')
  31. self.exclude = self.configuration.get('exclude')
  32. def _get_data(self):
  33. """
  34. Parse new log lines
  35. :return: dict
  36. """
  37. raw = self._get_raw_data()
  38. if raw is None:
  39. return None
  40. elif not raw:
  41. return self.to_netdata
  42. # Fail2ban logs looks like
  43. # 2016-12-25 12:36:04,711 fail2ban.actions[2455]: WARNING [ssh] Ban 178.156.32.231
  44. for row in raw:
  45. match = REGEX_DATA.search(row)
  46. if match:
  47. match_dict = match.groupdict()
  48. jail, action, ipaddr = match_dict['jail'], match_dict['action'], match_dict['ipaddr']
  49. if jail in self.jails_list:
  50. if action == 'B':
  51. self.to_netdata[jail] += 1
  52. if address_not_in_jail(self.banned_ips[jail], ipaddr, self.to_netdata[jail + '_in_jail']):
  53. self.to_netdata[jail + '_in_jail'] += 1
  54. else:
  55. if ipaddr in self.banned_ips[jail]:
  56. self.banned_ips[jail].remove(ipaddr)
  57. self.to_netdata[jail + '_in_jail'] -= 1
  58. return self.to_netdata
  59. def check(self):
  60. """
  61. :return: bool
  62. Check if the "log_path" is not empty and readable
  63. """
  64. if not (is_accessible(self.log_path, R_OK) and getsize(self.log_path) != 0):
  65. self.error('%s is not readable or empty' % self.log_path)
  66. return False
  67. self.jails_list, self.to_netdata, self.banned_ips = self.jails_auto_detection_()
  68. self.definitions = create_definitions_(self.jails_list)
  69. self.info('Jails: %s' % self.jails_list)
  70. return True
  71. def jails_auto_detection_(self):
  72. """
  73. return: <tuple>
  74. * jails_list - list of enabled jails (['ssh', 'apache', ...])
  75. * to_netdata - dict ({'ssh': 0, 'ssh_in_jail': 0, ...})
  76. * banned_ips - here will be stored all the banned ips ({'ssh': ['1.2.3.4', '5.6.7.8', ...], ...})
  77. """
  78. raw_jails_list = list()
  79. jails_list = list()
  80. for raw_jail in parse_configuration_files_(self.conf_path, self.conf_dir, self.error):
  81. raw_jails_list.extend(raw_jail)
  82. for jail, status in raw_jails_list:
  83. if status == 'true' and jail not in jails_list:
  84. jails_list.append(jail)
  85. elif status == 'false' and jail in jails_list:
  86. jails_list.remove(jail)
  87. # If for some reason parse failed we still can START with default jails_list.
  88. jails_list = list(set(jails_list) - set(self.exclude.split()
  89. if isinstance(self.exclude, str) else list())) or ['ssh']
  90. to_netdata = dict([(jail, 0) for jail in jails_list])
  91. to_netdata.update(dict([(jail + '_in_jail', 0) for jail in jails_list]))
  92. banned_ips = dict([(jail, list()) for jail in jails_list])
  93. return jails_list, to_netdata, banned_ips
  94. def create_definitions_(jails_list):
  95. """
  96. Chart definitions creating
  97. """
  98. definitions = {
  99. 'jails_bans': {'options': [None, 'Jails Ban Statistics', 'bans/s', 'bans', 'jail.bans', 'line'],
  100. 'lines': []},
  101. 'jails_in_jail': {'options': [None, 'Banned IPs (since the last restart of netdata)', 'IPs',
  102. 'in jail', 'jail.in_jail', 'line'],
  103. 'lines': []}}
  104. for jail in jails_list:
  105. definitions['jails_bans']['lines'].append([jail, jail, 'incremental'])
  106. definitions['jails_in_jail']['lines'].append([jail + '_in_jail', jail, 'absolute'])
  107. return definitions
  108. def parse_configuration_files_(jails_conf_path, jails_conf_dir, print_error):
  109. """
  110. :param jails_conf_path: <str>
  111. :param jails_conf_dir: <str>
  112. :param print_error: <function>
  113. :return: <tuple>
  114. Uses "find_jails_in_files" function to find all jails in the "jails_conf_dir" directory
  115. and in the "jails_conf_path"
  116. All files must endswith ".local" or ".conf"
  117. Return order is important.
  118. According man jail.conf it should be
  119. * jail.conf
  120. * jail.d/*.conf (in alphabetical order)
  121. * jail.local
  122. * jail.d/*.local (in alphabetical order)
  123. """
  124. path_conf, path_local, dir_conf, dir_local = list(), list(), list(), list()
  125. # Parse files in the directory
  126. if not (isinstance(jails_conf_dir, str) and isdir(jails_conf_dir)):
  127. print_error('%s is not a directory' % jails_conf_dir)
  128. else:
  129. dir_conf = list(filter(lambda conf: is_accessible(conf, R_OK), glob(jails_conf_dir + '/*.conf')))
  130. dir_local = list(filter(lambda local: is_accessible(local, R_OK), glob(jails_conf_dir + '/*.local')))
  131. if not (dir_conf or dir_local):
  132. print_error('%s is empty or not readable' % jails_conf_dir)
  133. else:
  134. dir_conf, dir_local = (find_jails_in_files(dir_conf, print_error),
  135. find_jails_in_files(dir_local, print_error))
  136. # Parse .conf and .local files
  137. if isinstance(jails_conf_path, str) and jails_conf_path.endswith(('.local', '.conf')):
  138. path_conf, path_local = (find_jails_in_files([jails_conf_path.split('.')[0] + '.conf'], print_error),
  139. find_jails_in_files([jails_conf_path.split('.')[0] + '.local'], print_error))
  140. return path_conf, dir_conf, path_local, dir_local
  141. def find_jails_in_files(list_of_files, print_error):
  142. """
  143. :param list_of_files: <list>
  144. :param print_error: <function>
  145. :return: <list>
  146. Open a file and parse it to find all (enabled and disabled) jails
  147. The output is a list of tuples:
  148. [('ssh', 'true'), ('apache', 'false'), ...]
  149. """
  150. jails_list = list()
  151. for conf in list_of_files:
  152. if is_accessible(conf, R_OK):
  153. with open(conf, 'rt') as f:
  154. raw_data = f.readlines()
  155. data = ' '.join(line for line in raw_data if line.startswith(('[', 'enabled')))
  156. jails_list.extend(REGEX_JAILS.findall(data))
  157. else:
  158. print_error('%s is not readable or not exist' % conf)
  159. return jails_list
  160. def address_not_in_jail(pool, address, pool_size):
  161. """
  162. :param pool: <list>
  163. :param address: <str>
  164. :param pool_size: <int>
  165. :return: bool
  166. Checks if the address is in the pool.
  167. If not address will be added
  168. """
  169. index = bisect.bisect_left(pool, address)
  170. if index < pool_size:
  171. if pool[index] == address:
  172. return False
  173. bisect.insort_left(pool, address)
  174. return True
  175. else:
  176. bisect.insort_left(pool, address)
  177. return True