1 # -*- coding: utf-8 -*-
2 # Description: fail2ban log netdata python.d module
5 from base import LogService
6 from re import compile as r_compile
7 from os import access as is_accessible, R_OK
8 from os.path import isdir
14 REGEX_JAILS = r_compile(r'\[([A-Za-z-_]+)][^\[\]]*?(?<!# )enabled = true')
15 REGEX_DATA = r_compile(r'\[(?P<jail>[a-z]+)\] (?P<ban>[A-Z])[a-z]+ (?P<ipaddr>\d{1,3}(?:\.\d{1,3}){3})')
16 ORDER = ['jails_bans', 'jails_in_jail']
19 class Service(LogService):
20 def __init__(self, configuration=None, name=None):
21 LogService.__init__(self, configuration=configuration, name=name)
23 self.log_path = self.configuration.get('log_path', '/var/log/fail2ban.log')
24 self.conf_path = self.configuration.get('conf_path', '/etc/fail2ban/jail.local')
25 self.conf_dir = self.configuration.get('conf_dir', '')
28 self.exclude = self.configuration['exclude'].split()
29 except (KeyError, AttributeError):
37 raw = self._get_raw_data()
43 # Fail2ban logs looks like
44 # 2016-12-25 12:36:04,711 fail2ban.actions[2455]: WARNING [ssh] Ban 178.156.32.231
46 match = REGEX_DATA.search(row)
48 match_dict = match.groupdict()
49 jail, ban, ipaddr = match_dict['jail'], match_dict['ban'], match_dict['ipaddr']
50 if jail in self.jails_list:
53 if address_not_in_jail(self.bans[jail], ipaddr, self.data[jail + '_in_jail']):
54 self.data[jail + '_in_jail'] += 1
56 if ipaddr in self.bans[jail]:
57 self.bans[jail].remove(ipaddr)
58 self.data[jail + '_in_jail'] -= 1
64 # Check "log_path" is accessible.
66 if not is_accessible(self.log_path, R_OK):
67 self.error('Cannot access file %s' % self.log_path)
72 dir_jails, error = parse_conf_dir(self.conf_dir)
73 jails_list.extend(dir_jails)
78 path_jails, error = parse_conf_path(self.conf_path)
79 jails_list.extend(path_jails)
83 # If for some reason parse failed we still can START with default jails_list.
84 self.jails_list = list(set(jails_list) - set(self.exclude)) or ['ssh']
86 self.data = dict([(jail, 0) for jail in self.jails_list])
87 self.data.update(dict([(jail + '_in_jail', 0) for jail in self.jails_list]))
88 self.bans = dict([(jail, list()) for jail in self.jails_list])
90 self.create_dimensions()
91 self.info('Plugin successfully started. Jails: %s' % self.jails_list)
94 def create_dimensions(self):
96 'jails_bans': {'options': [None, "Jails Ban Statistics", "bans/s", 'bans', 'jail.bans', 'line'],
98 'jails_in_jail': {'options': [None, "Currently In Jail", "ip addresses", 'in jail', 'jail.in_jail', 'line'],
101 for jail in self.jails_list:
102 self.definitions['jails_bans']['lines'].append([jail, jail, 'incremental'])
103 self.definitions['jails_in_jail']['lines'].append([jail + '_in_jail', jail, 'absolute'])
106 def parse_conf_dir(conf_dir):
107 if not isdir(conf_dir):
108 return list(), '%s is not a directory' % conf_dir
110 jail_local = list(filter(lambda local: is_accessible(local, R_OK), glob(conf_dir + '/*.local')))
111 jail_conf = list(filter(lambda conf: is_accessible(conf, R_OK), glob(conf_dir + '/*.conf')))
113 if not (jail_local or jail_conf):
114 return list(), '%s is empty or not readable' % conf_dir
116 # According "man jail.conf" files could be *.local AND *.conf
117 # *.conf files parsed first. Changes in *.local overrides configuration in *.conf
119 jail_local.extend([conf for conf in jail_conf if conf[:-5] not in [local[:-6] for local in jail_local]])
121 for conf in jail_local:
122 with open(conf, 'rt') as f:
125 data = ' '.join(raw_data.split())
126 jails_list.extend(REGEX_JAILS.findall(data))
127 jails_list = list(set(jails_list))
129 return jails_list, 'can\'t locate any jails in %s. Default jail is [\'ssh\']' % conf_dir
132 def parse_conf_path(conf_path):
133 if not is_accessible(conf_path, R_OK):
134 return list(), '%s is not readable' % conf_path
136 with open(conf_path, 'rt') as jails_conf:
137 raw_data = jails_conf.read()
139 data = raw_data.split()
140 jails_list = REGEX_JAILS.findall(' '.join(data))
141 return jails_list, 'can\'t locate any jails in %s. Default jail is [\'ssh\']' % conf_path
144 def address_not_in_jail(pool, address, pool_size):
145 index = bisect.bisect_left(pool, address)
146 if index < pool_size:
147 if pool[index] == address:
150 bisect.insort_left(pool, address)
153 bisect.insort_left(pool, address)