Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / node.d / fail2ban_.in @ c91df8cc

Historique | Voir | Annoter | Télécharger (9,04 ko)

1
# #!@@PYTHON@@
2
# -*- python -*-
3

    
4
"""
5
=head1 NAME
6

    
7
fail2ban_ - Wildcard plugin to monitor fail2ban blacklists
8

    
9
=head1 ABOUT
10

    
11
Requires Python 2.7
12
Requires fail2ban 0.9.2
13

    
14
=head1 AUTHOR
15

    
16
Copyright (c) 2015 Lee Clemens
17

    
18
Inspired by fail2ban plugin written by Stig Sandbeck Mathisen
19

    
20
=head1 CONFIGURATION
21

    
22
fail2ban-client needs to be run as root.
23

    
24
Add the following to your @@CONFDIR@@/munin-node:
25

    
26
  [fail2ban_*]
27
    user root
28

    
29
=head1 LICENSE
30

    
31
GNU GPLv2 or any later version
32

    
33
=begin comment
34

    
35
This program is free software; you can redistribute it and/or modify
36
it under the terms of the GNU General Public License as published by
37
the Free Software Foundation; either version 2 of the License, or (at
38
your option) any later version.
39

    
40
This program is distributed in the hope that it will be useful, but
41
WITHOUT ANY WARRANTY; without even the implied warranty of
42
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
43
General Public License for more details
44

    
45
You should have received a copy of the GNU General Public License along
46
with this program; if not, write to the Free Software Foundation, Inc.,
47
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
48

    
49
=end comment
50

    
51
=head1 BUGS
52

    
53
Transient values (particularly ASNs) come and go...
54
Better error handling (Popen), logging
55
Optimize loops and parsing in __get_jail_status() and parse_fail2ban_status()
56
Cymru ASNs aren't displayed in numerical order (internal name has alpha-prefix)
57
Use JSON status once fail2ban exposes JSON status data
58

    
59
=head1 MAGIC MARKERS
60

    
61
 #%# family=auto
62
 #%# capabilities=autoconf suggest
63

    
64
=cut
65
"""
66

    
67
from collections import Counter
68
from os import path, stat, access, X_OK, environ
69
from subprocess import Popen, PIPE
70
from time import time
71
import re
72
import sys
73

    
74

    
75
PLUGIN_BASE = "fail2ban_"
76

    
77
CACHE_DIR = environ['MUNIN_PLUGSTATE']
78
CACHE_MAX_AGE = 120
79

    
80
STATUS_FLAVORS_FIELDS = {
81
    "basic": ["jail"],
82
    "cymru": ["asn", "country", "rir"]
83
}
84

    
85

    
86
def __parse_plugin_name():
87
    if path.basename(__file__).count("_") == 1:
88
        return path.basename(__file__)[len(PLUGIN_BASE):], ""
89
    else:
90
        return (path.basename(__file__)[len(PLUGIN_BASE):].split("_")[0],
91
                path.basename(__file__)[len(PLUGIN_BASE):].split("_")[1])
92

    
93

    
94
def __get_jails_cache_file():
95
    return "%s/%s.state" % (CACHE_DIR, path.basename(__file__))
96

    
97

    
98
def __get_jail_status_cache_file(jail_name):
99
    return "%s/%s__%s.state" % (CACHE_DIR, path.basename(__file__), jail_name)
100

    
101

    
102
def __parse_jail_names(jails_data):
103
    """
104
    Parse the jails returned by `fail2ban-client status`:
105

    
106
    Status
107
    |- Number of jail:	3
108
    `- Jail list:	apache-badbots, dovecot, sshd
109
    """
110
    jails = []
111
    for line in jails_data.splitlines()[1:]:
112
        if line.startswith("`- Jail list:"):
113
            return [jail.strip(" ,\t") for jail in
114
                    line.split(":", 1)[1].split(" ")]
115
    return jails
116

    
117

    
118
def __get_jail_names():
119
    """
120
    Read jails from cache or execute `fail2ban-client status`
121
     and pass stdout to __parse_jail_names
122
    """
123
    cache_filename = __get_jails_cache_file()
124
    try:
125
        mtime = stat(cache_filename).st_mtime
126
    except OSError:
127
        mtime = 0
128
    if time() - mtime > CACHE_MAX_AGE:
129
        p = Popen(["fail2ban-client", "status"], shell=False, stdout=PIPE)
130
        jails_data = p.communicate()[0]
131
        with open(cache_filename, 'w') as f:
132
            f.write(jails_data)
133
    else:
134
        with open(cache_filename, 'r') as f:
135
            jails_data = f.read()
136
    return __parse_jail_names(jails_data)
137

    
138

    
139
def autoconf():
140
    """
141
    Attempt to find fail2ban-client in path (using `which`) and ping the client
142
    """
143
    p_which = Popen(["which", "fail2ban-client"], shell=False, stdout=PIPE,
144
                    stderr=PIPE)
145
    stdout, stderr = p_which.communicate()
146
    if len(stdout) > 0:
147
        client_path = stdout.strip()
148
        if access(client_path, X_OK):
149
            p_ping = Popen([client_path, "ping"], shell=False)
150
            p_ping.communicate()
151
            if p_ping.returncode == 0:
152
                print("yes")
153
            else:
154
                print("no (fail2ban-server does not respond to ping)")
155
        else:
156
            print("no (fail2ban-client is not executable)")
157
    else:
158
        import os
159

    
160
        print("no (fail2ban-client not found in path: %s)" %
161
              os.environ["PATH"])
162

    
163

    
164
def suggest():
165
    """
166
    Iterate all defined flavors (source of data) and fields (graph to display)
167
    """
168
    # Just use basic for autoconf/suggest
169
    flavor = "basic"
170
    for field in STATUS_FLAVORS_FIELDS[flavor]:
171
        print("%s_%s" % (flavor, field if len(flavor) > 0 else flavor))
172

    
173

    
174
def __get_jail_status(jail, flavor):
175
    """
176
    Return cache or execute `fail2ban-client status <jail> <flavor>`
177
     and save to cache and return
178
    """
179
    cache_filename = __get_jail_status_cache_file(jail)
180
    try:
181
        mtime = stat(cache_filename).st_mtime
182
    except OSError:
183
        mtime = 0
184
    if time() - mtime > CACHE_MAX_AGE:
185
        p = Popen(["fail2ban-client", "status", jail, flavor], shell=False,
186
                  stdout=PIPE)
187
        jail_status_data = p.communicate()[0]
188
        with open(cache_filename, 'w') as f:
189
            f.write(jail_status_data)
190
    else:
191
        with open(cache_filename, 'r') as f:
192
            jail_status_data = f.read()
193
    return jail_status_data
194

    
195

    
196
def __normalize(name):
197
    name = re.sub("[^a-z0-9A-Z]", "_", name)
198
    return name
199

    
200

    
201
def __count_groups(value_str):
202
    """
203
    Helper method to count unique values in the space-delimited value_str
204
    """
205
    return Counter([key for key in value_str.split(" ") if key])
206

    
207

    
208
def config(flavor, field):
209
    """
210
    Print config data (e.g. munin-run config), including possible labels
211
     by parsing real status data
212
    """
213
    print("graph_title fail2ban %s %s" % (flavor, field))
214
    print("graph_args --base 1000 -l 0")
215
    print("graph_vlabel Hosts banned")
216
    print("graph_category security")
217
    print("graph_info"
218
          " Number of hosts banned using status flavor %s and field %s" %
219
          (flavor, field))
220
    print("graph_total total")
221
    munin_fields, field_labels, values = parse_fail2ban_status(flavor, field)
222
    for munin_field in munin_fields:
223
        print("%s.label %s" % (munin_field, field_labels[munin_field]))
224

    
225

    
226
def run(flavor, field):
227
    """
228
    Parse the status data and print all values for a given flavor and field
229
    """
230
    munin_fields, field_labels, values = parse_fail2ban_status(flavor, field)
231
    for munin_field in munin_fields:
232
        print("%s.value %s" % (munin_field, values[munin_field]))
233

    
234

    
235
def parse_fail2ban_status(flavor, field):
236
    """
237
    Shared method to parse jail status output and determine field names
238
     and aggregate counts
239
    """
240
    field_labels = dict()
241
    values = dict()
242
    for jail in __get_jail_names():
243
        jail_status = __get_jail_status(jail, flavor)
244
        for line in jail_status.splitlines()[1:]:
245
            if flavor == "basic":
246
                if field == "jail":
247
                    if line.startswith("   |- Currently banned:"):
248
                        internal_name = __normalize(jail)
249
                        field_labels[internal_name] = jail
250
                        values[internal_name] = line.split(":", 1)[1].strip()
251
                else:
252
                    raise Exception(
253
                        "Undefined field %s for flavor %s for jail %s" %
254
                        (field, flavor, jail))
255
            elif flavor == "cymru":
256
                # Determine which line of output we care about
257
                if field == "asn":
258
                    search_string = "   |- Banned ASN list:"
259
                elif field == "country":
260
                    search_string = "   |- Banned Country list:"
261
                elif field == "rir":
262
                    search_string = "   `- Banned RIR list:"
263
                else:
264
                    raise Exception(
265
                        "Undefined field %s for flavor %s for jail %s" %
266
                        (field, flavor, jail))
267
                if line.startswith(search_string):
268
                    prefix = "%s_%s" % (flavor, field)
269
                    # Now process/aggregate the counts
270
                    counts_dict = __count_groups(line.split(":", 1)[1].strip())
271
                    for key in counts_dict:
272
                        internal_name = "%s_%s" % (prefix, __normalize(key))
273
                        if internal_name in field_labels:
274
                            values[internal_name] += counts_dict[key]
275
                        else:
276
                            field_labels[internal_name] = key
277
                            values[internal_name] = counts_dict[key]
278
            else:
279
                raise Exception("Undefined flavor: %s for jail %s" %
280
                                (flavor, jail))
281
    return sorted(field_labels.keys()), field_labels, values
282

    
283

    
284
if __name__ == "__main__":
285
    if len(sys.argv) > 1:
286
        command = sys.argv[1]
287
    else:
288
        command = ""
289
    if command == "autoconf":
290
        autoconf()
291
    elif command == "suggest":
292
        suggest()
293
    elif command == 'config':
294
        flavor_, field_ = __parse_plugin_name()
295
        config(flavor_, field_)
296
    else:
297
        flavor_, field_ = __parse_plugin_name()
298
        run(flavor_, field_)