X-Git-Url: https://arthur.barton.de/gitweb/?a=blobdiff_plain;f=python.d%2Ffail2ban.chart.py;h=85d0ade618006adb707a9d6b5ea20f1156b2be0a;hb=a3ae351f1e4576e5a32166ddbc28c8699ce31991;hp=2d80282c68897f761b97495d9064eddb24abfb58;hpb=18c90c6d6c707600fddd6dcd7ab149b3e3b6d611;p=netdata.git diff --git a/python.d/fail2ban.chart.py b/python.d/fail2ban.chart.py index 2d80282c..85d0ade6 100644 --- a/python.d/fail2ban.chart.py +++ b/python.d/fail2ban.chart.py @@ -3,18 +3,17 @@ # Author: l2isbad from base import LogService -from re import compile -try: - from itertools import filterfalse -except ImportError: - from itertools import ifilterfalse as filterfalse +from re import compile as r_compile from os import access as is_accessible, R_OK +from os.path import isdir +from glob import glob +import bisect priority = 60000 retries = 60 -regex = compile(r'([A-Za-z-]+\]) enabled = ([a-z]+)') - -ORDER = ['jails_group'] +REGEX_JAILS = r_compile(r'\[([A-Za-z-_]+)][^\[\]]*?(?[a-z]+)\] (?P[A-Z])[a-z]+ (?P\d{1,3}(?:\.\d{1,3}){3})') +ORDER = ['jails_bans', 'jails_in_jail'] class Service(LogService): @@ -23,69 +22,133 @@ class Service(LogService): self.order = ORDER self.log_path = self.configuration.get('log_path', '/var/log/fail2ban.log') self.conf_path = self.configuration.get('conf_path', '/etc/fail2ban/jail.local') - self.default_jails = ['ssh'] + self.conf_dir = self.configuration.get('conf_dir', '') + self.bans = dict() try: self.exclude = self.configuration['exclude'].split() except (KeyError, AttributeError): - self.exclude = [] - + self.exclude = list() def _get_data(self): """ Parse new log lines :return: dict """ - - # If _get_raw_data returns empty list (no new lines in log file) we will send to Netdata this - self.data = {jail: 0 for jail in self.jails_list} - - try: - raw = self._get_raw_data() - if raw is None: - return None - elif not raw: - return self.data - except (ValueError, AttributeError): + raw = self._get_raw_data() + if raw is None: return None + elif not raw: + return self.data # Fail2ban logs looks like # 2016-12-25 12:36:04,711 fail2ban.actions[2455]: WARNING [ssh] Ban 178.156.32.231 - self.data = dict( - zip( - self.jails_list, - [len(list(filterfalse(lambda line: (jail + '] Ban') not in line, raw))) for jail in self.jails_list] - )) + for row in raw: + match = REGEX_DATA.search(row) + if match: + match_dict = match.groupdict() + jail, ban, ipaddr = match_dict['jail'], match_dict['ban'], match_dict['ipaddr'] + if jail in self.jails_list: + if ban == 'B': + self.data[jail] += 1 + if address_not_in_jail(self.bans[jail], ipaddr, self.data[jail + '_in_jail']): + self.data[jail + '_in_jail'] += 1 + else: + if ipaddr in self.bans[jail]: + self.bans[jail].remove(ipaddr) + self.data[jail + '_in_jail'] -= 1 return self.data def check(self): - + # Check "log_path" is accessible. # If NOT STOP plugin if not is_accessible(self.log_path, R_OK): - self.error('Cannot access file %s' % (self.log_path)) + self.error('Cannot access file %s' % self.log_path) return False + jails_list = list() + + if self.conf_dir: + dir_jails, error = parse_conf_dir(self.conf_dir) + jails_list.extend(dir_jails) + if not dir_jails: + self.error(error) + + if self.conf_path: + path_jails, error = parse_conf_path(self.conf_path) + jails_list.extend(path_jails) + if not path_jails: + self.error(error) - # Check "conf_path" is accessible. - # If "conf_path" is accesible try to parse it to find enabled jails - if is_accessible(self.conf_path, R_OK): - with open(self.conf_path, 'rt') as jails_conf: - jails_list = regex.findall(' '.join(jails_conf.read().split())) - self.jails_list = [jail[:-1] for jail, status in jails_list if status == 'true'] - else: - self.jails_list = [] - self.error('Cannot access jail.local file %s.' % (self.conf_path)) - # If for some reason parse failed we still can START with default jails_list. - self.jails_list = [jail for jail in self.jails_list if jail not in self.exclude]\ - if self.jails_list else self.default_jails + self.jails_list = list(set(jails_list) - set(self.exclude)) or ['ssh'] + + self.data = dict([(jail, 0) for jail in self.jails_list]) + self.data.update(dict([(jail + '_in_jail', 0) for jail in self.jails_list])) + self.bans = dict([(jail, list()) for jail in self.jails_list]) + self.create_dimensions() - self.info('Plugin succefully started. Jails: %s' % (self.jails_list)) + self.info('Plugin successfully started. Jails: %s' % self.jails_list) return True def create_dimensions(self): - self.definitions = {'jails_group': - {'options': - [None, "Jails ban statistics", "bans/s", 'Jails', 'jail.ban', 'line'], 'lines': []}} + self.definitions = { + 'jails_bans': {'options': [None, 'Jails Ban Statistics', "bans/s", 'bans', 'jail.bans', 'line'], + 'lines': []}, + 'jails_in_jail': {'options': [None, 'Banned IPs (since the last restart of netdata)', 'IPs', + 'in jail', 'jail.in_jail', 'line'], 'lines': []}, + } for jail in self.jails_list: - self.definitions['jails_group']['lines'].append([jail, jail, 'absolute']) + self.definitions['jails_bans']['lines'].append([jail, jail, 'incremental']) + self.definitions['jails_in_jail']['lines'].append([jail + '_in_jail', jail, 'absolute']) + + +def parse_conf_dir(conf_dir): + if not isdir(conf_dir): + return list(), '%s is not a directory' % conf_dir + + jail_local = list(filter(lambda local: is_accessible(local, R_OK), glob(conf_dir + '/*.local'))) + jail_conf = list(filter(lambda conf: is_accessible(conf, R_OK), glob(conf_dir + '/*.conf'))) + + if not (jail_local or jail_conf): + return list(), '%s is empty or not readable' % conf_dir + + # According "man jail.conf" files could be *.local AND *.conf + # *.conf files parsed first. Changes in *.local overrides configuration in *.conf + if jail_conf: + jail_local.extend([conf for conf in jail_conf if conf[:-5] not in [local[:-6] for local in jail_local]]) + jails_list = list() + for conf in jail_local: + with open(conf, 'rt') as f: + raw_data = f.read() + + data = ' '.join(raw_data.split()) + jails_list.extend(REGEX_JAILS.findall(data)) + jails_list = list(set(jails_list)) + + return jails_list, 'can\'t locate any jails in %s. Default jail is [\'ssh\']' % conf_dir + + +def parse_conf_path(conf_path): + if not is_accessible(conf_path, R_OK): + return list(), '%s is not readable' % conf_path + + with open(conf_path, 'rt') as jails_conf: + raw_data = jails_conf.read() + + data = raw_data.split() + jails_list = REGEX_JAILS.findall(' '.join(data)) + return jails_list, 'can\'t locate any jails in %s. Default jail is [\'ssh\']' % conf_path + + +def address_not_in_jail(pool, address, pool_size): + index = bisect.bisect_left(pool, address) + if index < pool_size: + if pool[index] == address: + return False + else: + bisect.insort_left(pool, address) + return True + else: + bisect.insort_left(pool, address) + return True