]> arthur.barton.de Git - netdata.git/blobdiff - python.d/fail2ban.chart.py
ab-debian 0.20170327.01-0ab1, upstream v1.6.0-42-gaa6b96fc
[netdata.git] / python.d / fail2ban.chart.py
index dbbc1946703837d54407636f748c771a9ae83521..85d0ade618006adb707a9d6b5ea20f1156b2be0a 100644 (file)
@@ -3,20 +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 = true')
-ORDER = ['jails_group']
+REGEX_JAILS = r_compile(r'\[([A-Za-z-_]+)][^\[\]]*?(?<!# )enabled = true')
+REGEX_DATA = r_compile(r'\[(?P<jail>[a-z]+)\] (?P<ban>[A-Z])[a-z]+ (?P<ipaddr>\d{1,3}(?:\.\d{1,3}){3})')
+ORDER = ['jails_bans', 'jails_in_jail']
 
 
 class Service(LogService):
@@ -26,35 +23,39 @@ class Service(LogService):
         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', '')
+        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
         """
-        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
-        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]
+        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
 
@@ -65,51 +66,89 @@ class Service(LogService):
         if not is_accessible(self.log_path, R_OK):
             self.error('Cannot access file %s' % self.log_path)
             return False
-        if not isdir(self.conf_dir):
-            self.conf_dir = None
-
-        # If "conf_dir" not specified (or not a dir) plugin will use "conf_path"
-        if not self.conf_dir:
-            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 = jails_list
-            else:
-                self.jails_list = list()
-                self.error('Cannot access jail configuration file %s.' % self.conf_path)
-        # If "conf_dir" is specified and "conf_dir" is dir plugin will use "conf_dir"
-        else:
-            dot_local = glob(self.conf_dir + '/*.local')  # *.local jail configurations files
-            dot_conf = glob(self.conf_dir + '/*.conf')  # *.conf jail configuration files
-
-            if not any([dot_local, dot_conf]):
-                self.error('%s is empty or not readable' % self.conf_dir)
-            # According "man jail.conf" files could be *.local AND *.conf
-            # *.conf files parsed first. Changes in *.local overrides configuration in *.conf
-            if dot_conf:
-                dot_local.extend([conf for conf in dot_conf if conf[:-5] not in [local[:-6] for local in dot_local]])
-            # Make sure all files are readable
-            dot_local = [conf for conf in dot_local if is_accessible(conf, R_OK)]
-            if dot_local:
-                enabled_jails = list()
-                for jail_conf in dot_local:
-                    with open(jail_conf, 'rt') as conf:
-                        enabled_jails.extend(REGEX.findall(' '.join(conf.read().split())))
-                self.jails_list = list(set(enabled_jails))
-            else:
-                self.jails_list = list()
-                self.error('Files in %s not readable' % self.conf_dir)
+        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)
 
         # If for some reason parse failed we still can START with default jails_list.
-        self.jails_list = list(set(self.jails_list) - set(self.exclude)) or ['ssh']
+        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 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': []}}
+            '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, 'incremental'])
+            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