]> arthur.barton.de Git - netdata.git/blob - python.d/fail2ban.chart.py
d6177d0e254e0cf5a20ec4b46792d524f984d1d5
[netdata.git] / python.d / fail2ban.chart.py
1 # -*- coding: utf-8 -*-
2 # Description: fail2ban log netdata python.d module
3 # Author: l2isbad
4
5 from base import LogService
6 from re import compile as r_compile
7
8 try:
9     from itertools import filterfalse
10 except ImportError:
11     from itertools import ifilterfalse as filterfalse
12 from os import access as is_accessible, R_OK
13 from os.path import isdir
14 from glob import glob
15 import bisect
16
17 priority = 60000
18 retries = 60
19 REGEX_JAILS = r_compile(r'\[([A-Za-z-_]+)][^\[\]]*?(?<!# )enabled = true')
20 REGEX_DATA = r_compile(r'\[(?P<jail>[a-z]+)\] (?P<ban>[A-Z])[a-z]+ (?P<ipaddr>\d{1,3}(?:\.\d{1,3}){3})')
21 ORDER = ['jails_bans', 'jails_in_jail']
22
23
24 class Service(LogService):
25     def __init__(self, configuration=None, name=None):
26         LogService.__init__(self, configuration=configuration, name=name)
27         self.order = ORDER
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', '')
31         self.bans = dict()
32         try:
33             self.exclude = self.configuration['exclude'].split()
34         except (KeyError, AttributeError):
35             self.exclude = list()
36
37     def _get_data(self):
38         """
39         Parse new log lines
40         :return: dict
41         """
42         raw = self._get_raw_data()
43         if raw is None:
44             return None
45         elif not raw:
46             return self.data
47
48         # Fail2ban logs looks like
49         # 2016-12-25 12:36:04,711 fail2ban.actions[2455]: WARNING [ssh] Ban 178.156.32.231
50         for row in raw:
51             match = REGEX_DATA.search(row)
52             if match:
53                 match_dict = match.groupdict()
54                 jail, ban, ipaddr = match_dict['jail'], match_dict['ban'], match_dict['ipaddr']
55                 if jail in self.jails_list:
56                     if ban == 'B':
57                         self.data[jail] += 1
58                         if address_not_in_jail(self.bans[jail], ipaddr, self.data[jail + '_in_jail']):
59                            self.data[jail + '_in_jail'] += 1
60                     else:
61                         if ipaddr in self.bans[jail]:
62                             self.bans[jail].remove(ipaddr)
63                             self.data[jail + '_in_jail'] -= 1
64
65         return self.data
66
67     def check(self):
68
69         # Check "log_path" is accessible.
70         # If NOT STOP plugin
71         if not is_accessible(self.log_path, R_OK):
72             self.error('Cannot access file %s' % self.log_path)
73             return False
74         jails_list = list()
75
76         if self.conf_dir:
77             dir_jails, error = parse_conf_dir(self.conf_dir)
78             jails_list.extend(dir_jails)
79             if not dir_jails:
80                 self.error(error)
81
82         if self.conf_path:
83             path_jails, error = parse_conf_path(self.conf_path)
84             jails_list.extend(path_jails)
85             if not path_jails:
86                 self.error(error)
87
88         # If for some reason parse failed we still can START with default jails_list.
89         self.jails_list = list(set(jails_list) - set(self.exclude)) or ['ssh']
90
91         self.data = dict([(jail, 0) for jail in self.jails_list])
92         self.data.update(dict([(jail + '_in_jail', 0) for jail in self.jails_list]))
93         self.bans = dict([(jail, list()) for jail in self.jails_list])
94
95         self.create_dimensions()
96         self.info('Plugin successfully started. Jails: %s' % self.jails_list)
97         return True
98
99     def create_dimensions(self):
100         self.definitions = {
101             'jails_bans': {'options': [None, "Jails Ban Statistics", "bans/s", 'bans', 'jail.bans', 'line'],
102                             'lines': []},
103             'jails_in_jail': {'options': [None, "Currently In Jail", "ip addresses", 'in jail', 'jail.in_jail', 'line'],
104                             'lines': []},
105                            }
106         for jail in self.jails_list:
107             self.definitions['jails_bans']['lines'].append([jail, jail, 'incremental'])
108             self.definitions['jails_in_jail']['lines'].append([jail + '_in_jail', jail, 'absolute'])
109
110
111 def parse_conf_dir(conf_dir):
112     if not isdir(conf_dir):
113         return list(), '%s is not a directory' % conf_dir
114
115     jail_local = list(filter(lambda local: is_accessible(local, R_OK), glob(conf_dir + '/*.local')))
116     jail_conf = list(filter(lambda conf: is_accessible(conf, R_OK), glob(conf_dir + '/*.conf')))
117
118     if not (jail_local or jail_conf):
119         return list(), '%s is empty or not readable' % conf_dir
120
121     # According "man jail.conf" files could be *.local AND *.conf
122     # *.conf files parsed first. Changes in *.local overrides configuration in *.conf
123     if jail_conf:
124         jail_local.extend([conf for conf in jail_conf if conf[:-5] not in [local[:-6] for local in jail_local]])
125     jails_list = list()
126     for conf in jail_local:
127         with open(conf, 'rt') as f:
128             raw_data = f.read()
129
130         data = ' '.join(raw_data.split())
131         jails_list.extend(REGEX_JAILS.findall(data))
132     jails_list = list(set(jails_list))
133
134     return jails_list, 'can\'t locate any jails in %s. Default jail is [\'ssh\']' % conf_dir
135
136
137 def parse_conf_path(conf_path):
138     if not is_accessible(conf_path, R_OK):
139         return list(), '%s is not readable' % conf_path
140
141     with open(conf_path, 'rt') as jails_conf:
142         raw_data = jails_conf.read()
143
144     data = raw_data.split()
145     jails_list = REGEX_JAILS.findall(' '.join(data))
146     return jails_list, 'can\'t locate any jails in %s. Default jail is  [\'ssh\']' % conf_path
147
148
149 def address_not_in_jail(pool, address, pool_size):
150     index = bisect.bisect_left(pool, address)
151     if index < pool_size:
152         if pool[index] == address:
153             return False
154         else:
155             bisect.insort_left(pool, address)
156             return True
157     else:
158         bisect.insort_left(pool, address)
159         return True