summaryrefslogtreecommitdiffstats
path: root/python.d/fail2ban.chart.py
diff options
context:
space:
mode:
Diffstat (limited to 'python.d/fail2ban.chart.py')
-rw-r--r--python.d/fail2ban.chart.py267
1 files changed, 173 insertions, 94 deletions
diff --git a/python.d/fail2ban.chart.py b/python.d/fail2ban.chart.py
index c7d24e8c..35761e89 100644
--- a/python.d/fail2ban.chart.py
+++ b/python.d/fail2ban.chart.py
@@ -2,131 +2,210 @@
# Description: fail2ban log netdata python.d module
# 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 os.path import isdir, getsize
from glob import glob
+import bisect
+from base import LogService
priority = 60000
retries = 60
-REGEX = compile(r'\[([A-Za-z-_]+)][^\[\]]*?(?<!# )enabled = true')
-ORDER = ['jails_group']
+REGEX_JAILS = r_compile(r'\[([A-Za-z-_0-9]+)][^\[\]]*?(?<!# )enabled = (?:(true|false))')
+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})')
+ORDER = ['jails_bans', 'jails_in_jail']
class Service(LogService):
+ """
+ fail2ban log class
+ Reads logs line by line
+ Jail auto detection included
+ It produces following charts:
+ * Bans per second for every jail
+ * Banned IPs for every jail (since the last restart of netdata)
+ """
def __init__(self, configuration=None, name=None):
LogService.__init__(self, configuration=configuration, name=name)
self.order = ORDER
+ self.definitions = dict()
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.conf_dir = self.configuration.get('conf_dir', '')
- try:
- self.exclude = self.configuration['exclude'].split()
- except (KeyError, AttributeError):
- self.exclude = []
+ self.conf_dir = self.configuration.get('conf_dir', '/etc/fail2ban/jail.d/')
+ self.exclude = self.configuration.get('exclude')
def _get_data(self):
"""
Parse new log lines
:return: dict
"""
- 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.to_netdata
# Fail2ban logs looks like
# 2016-12-25 12:36:04,711 fail2ban.actions[2455]: WARNING [ssh] Ban 178.156.32.231
- data = dict(
- zip(
- self.jails_list,
- [len(list(filterfalse(lambda line: (jail + '] Ban') not in line, raw))) for jail in self.jails_list]
- ))
-
- for jail in data:
- self.data[jail] += data[jail]
-
- return self.data
+ for row in raw:
+ match = REGEX_DATA.search(row)
+ if match:
+ match_dict = match.groupdict()
+ jail, action, ipaddr = match_dict['jail'], match_dict['action'], match_dict['ipaddr']
+ if jail in self.jails_list:
+ if action == 'B':
+ self.to_netdata[jail] += 1
+ if address_not_in_jail(self.banned_ips[jail], ipaddr, self.to_netdata[jail + '_in_jail']):
+ self.to_netdata[jail + '_in_jail'] += 1
+ else:
+ if ipaddr in self.banned_ips[jail]:
+ self.banned_ips[jail].remove(ipaddr)
+ self.to_netdata[jail + '_in_jail'] -= 1
+
+ return self.to_netdata
def check(self):
+ """
+ :return: bool
- # 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)
- 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 if the "log_path" is not empty and readable
+ """
- # If for some reason parse failed we still can START with default jails_list.
- self.jails_list = list(set(jails_list) - set(self.exclude)) or ['ssh']
- self.data = dict([(jail, 0) for jail in self.jails_list])
- self.create_dimensions()
- self.info('Plugin successfully started. Jails: %s' % self.jails_list)
+ if not (is_accessible(self.log_path, R_OK) and getsize(self.log_path) != 0):
+ self.error('%s is not readable or empty' % self.log_path)
+ return False
+ self.jails_list, self.to_netdata, self.banned_ips = self.jails_auto_detection_()
+ self.definitions = create_definitions_(self.jails_list)
+ self.info('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': []}}
- for jail in self.jails_list:
- self.definitions['jails_group']['lines'].append([jail, jail, 'incremental'])
-
+ def jails_auto_detection_(self):
+ """
+ return: <tuple>
-def parse_conf_dir(conf_dir):
- if not isdir(conf_dir):
- return list(), '%s is not a directory' % conf_dir
+ * jails_list - list of enabled jails (['ssh', 'apache', ...])
+ * to_netdata - dict ({'ssh': 0, 'ssh_in_jail': 0, ...})
+ * banned_ips - here will be stored all the banned ips ({'ssh': ['1.2.3.4', '5.6.7.8', ...], ...})
+ """
+ raw_jails_list = list()
+ jails_list = list()
- 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')))
+ for raw_jail in parse_configuration_files_(self.conf_path, self.conf_dir, self.error):
+ raw_jails_list.extend(raw_jail)
- if not (jail_local or jail_conf):
- return list(), '%s is empty or not readable' % conf_dir
+ for jail, status in raw_jails_list:
+ if status == 'true' and jail not in jails_list:
+ jails_list.append(jail)
+ elif status == 'false' and jail in jails_list:
+ jails_list.remove(jail)
- # 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]])
+ # If for some reason parse failed we still can START with default jails_list.
+ jails_list = list(set(jails_list) - set(self.exclude.split()
+ if isinstance(self.exclude, str) else list())) or ['ssh']
+
+ to_netdata = dict([(jail, 0) for jail in jails_list])
+ to_netdata.update(dict([(jail + '_in_jail', 0) for jail in jails_list]))
+ banned_ips = dict([(jail, list()) for jail in jails_list])
+
+ return jails_list, to_netdata, banned_ips
+
+
+def create_definitions_(jails_list):
+ """
+ Chart definitions creating
+ """
+
+ 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 jails_list:
+ definitions['jails_bans']['lines'].append([jail, jail, 'incremental'])
+ definitions['jails_in_jail']['lines'].append([jail + '_in_jail', jail, 'absolute'])
+
+ return definitions
+
+
+def parse_configuration_files_(jails_conf_path, jails_conf_dir, print_error):
+ """
+ :param jails_conf_path: <str>
+ :param jails_conf_dir: <str>
+ :param print_error: <function>
+ :return: <tuple>
+
+ Uses "find_jails_in_files" function to find all jails in the "jails_conf_dir" directory
+ and in the "jails_conf_path"
+
+ All files must endswith ".local" or ".conf"
+ Return order is important.
+ According man jail.conf it should be
+ * jail.conf
+ * jail.d/*.conf (in alphabetical order)
+ * jail.local
+ * jail.d/*.local (in alphabetical order)
+ """
+ path_conf, path_local, dir_conf, dir_local = list(), list(), list(), list()
+
+ # Parse files in the directory
+ if not (isinstance(jails_conf_dir, str) and isdir(jails_conf_dir)):
+ print_error('%s is not a directory' % jails_conf_dir)
+ else:
+ dir_conf = list(filter(lambda conf: is_accessible(conf, R_OK), glob(jails_conf_dir + '/*.conf')))
+ dir_local = list(filter(lambda local: is_accessible(local, R_OK), glob(jails_conf_dir + '/*.local')))
+ if not (dir_conf or dir_local):
+ print_error('%s is empty or not readable' % jails_conf_dir)
+ else:
+ dir_conf, dir_local = (find_jails_in_files(dir_conf, print_error),
+ find_jails_in_files(dir_local, print_error))
+
+ # Parse .conf and .local files
+ if isinstance(jails_conf_path, str) and jails_conf_path.endswith(('.local', '.conf')):
+ path_conf, path_local = (find_jails_in_files([jails_conf_path.split('.')[0] + '.conf'], print_error),
+ find_jails_in_files([jails_conf_path.split('.')[0] + '.local'], print_error))
+
+ return path_conf, dir_conf, path_local, dir_local
+
+
+def find_jails_in_files(list_of_files, print_error):
+ """
+ :param list_of_files: <list>
+ :param print_error: <function>
+ :return: <list>
+
+ Open a file and parse it to find all (enabled and disabled) jails
+ The output is a list of tuples:
+ [('ssh', 'true'), ('apache', 'false'), ...]
+ """
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.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.findall(' '.join(data))
- return jails_list, 'can\'t locate any jails in %s. Default jail is [\'ssh\']' % conf_path
+ for conf in list_of_files:
+ if is_accessible(conf, R_OK):
+ with open(conf, 'rt') as conf:
+ raw_data = conf.read()
+ data = ' '.join(raw_data.split())
+ jails_list.extend(REGEX_JAILS.findall(data))
+ else:
+ print_error('%s is not readable or not exist' % conf)
+ return jails_list
+
+
+def address_not_in_jail(pool, address, pool_size):
+ """
+ :param pool: <list>
+ :param address: <str>
+ :param pool_size: <int>
+ :return: bool
+
+ Checks if the address is in the pool.
+ If not address will be added
+ """
+ index = bisect.bisect_left(pool, address)
+ if index < pool_size:
+ if pool[index] == address:
+ return False
+ bisect.insort_left(pool, address)
+ return True
+ else:
+ bisect.insort_left(pool, address)
+ return True