Projet

Général

Profil

Révision a11e32ff

IDa11e32ff40402a76426911505a6170f1cf369363
Parent eeaf47c6
Enfant c91df8cc

Ajouté par Lee Clemens il y a plus de 10 ans

Add fail2ban_ wildcard plugin for extended status

Voir les différences:

plugins/node.d/fail2ban_.in
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
    munin_fields, field_labels, values = parse_fail2ban_status(flavor, field)
221
    for munin_field in munin_fields:
222
        print("%s.label %s" % (munin_field, field_labels[munin_field]))
223

  
224

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

  
233

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

  
282

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

Formats disponibles : Unified diff