1
0
Fork 0
mirror of https://github.com/ethauvin/fail2ban-digest.git synced 2025-04-26 10:58:12 -07:00

Compare commits

..

No commits in common. "master" and "v0.1" have entirely different histories.
master ... v0.1

2 changed files with 23 additions and 135 deletions

View file

@ -47,12 +47,12 @@ Manual Installation:
License: License:
-------- --------
fail2ban-digest is free software; you can redistribute it and/or modify it under the Fail2Ban is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later Foundation; either version 2 of the License, or (at your option) any later
version. version.
fail2ban-digest is distributed in the hope that it will be useful, but WITHOUT ANY Fail2Ban is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details. PARTICULAR PURPOSE. See the GNU General Public License for more details.

View file

@ -17,7 +17,7 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from dbm import gnu as dbm from dbm import gnu as dbm
from email.message import EmailMessage from email.message import EmailMessage
from smtplib import SMTP from smtplib import SMTP
@ -34,83 +34,17 @@ import re
import sys import sys
db_location = '/var/lib/fail2ban/digest' db_location = '/var/lib/fail2ban/digest'
db_creation_date_key = u'db_creation_date' db_creation_date_key = 'db_creation_date'
db_date_format = '%Y-%m-%d %H:%M:%S' db_date_format = '%Y-%m-%d %H:%M:%S'
default_mail_template = Template('''Hi,\n default_mail_template = Template('''Hi,\n
This is a digest email of the ${count} banned IPs between ${creation_date} and ${date_now}: this is a digest email of banned IPs since ${creation_date} UTC and ${date_now}
${digest} ${digest}
Regards, Regards,
Fail2ban Digest Fail2ban digest
''') ''')
default_html_template = Template('''<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;
}
table {
border-collapse: collapse;
}
td, th {
border: 1px solid darkgrey;
text-align: left;
padding: 6px;
}
td {
vertical-align: top;
}
td:last-child {
width: 1px;
white-space: nowrap;
}
th {
background-color: #dddddd;
}
</style>
</head>
<body>
<p>Hi,</p>
<p>This is a digest email of the <b>${count}</b> banned IPs between <b>${creation_date}</b> and <b>${date_now}</b>:</p>
<table>
<tr>
<th style="text-align: center">#</th>
<th style="text-align: center">IPs</th>
<th>When</th>
</tr>
${digest}
</table>
<p>Regards,</p>
<p><a href="https://github.com/enricotagliavini/fail2ban-digest">Fail2Ban Digest</a><p>
</body>
<html>
''')
html_tr_template = Template(''' <tr>
<td style="text-align: center">${count}</td>
<td style="text-align: right">${ip}</td>
<td>${events}</td>
</tr>
''')
html_error_template = Template(''' <tr>
<td colspan="3"><em>${error_msg}</em></td>
</tr>
''')
class Ban:
def __init__(self, ip, events):
self.ip = ip
self.events = []
for event in events:
self.events.append(utc_to_local(event))
self.events.sort
class store_yesno(argparse.Action): class store_yesno(argparse.Action):
def __init__(self, option_strings, dest, nargs = None, **kwargs): def __init__(self, option_strings, dest, nargs = None, **kwargs):
@ -146,12 +80,6 @@ def ip_address(string):
else: else:
raise argparse.ArgumentTypeError('%s is not a valid IPv4 or IPv6 address' % string) raise argparse.ArgumentTypeError('%s is not a valid IPv4 or IPv6 address' % string)
def utc_to_local(date_string):
try:
return datetime.strptime(date_string, db_date_format).replace(tzinfo=timezone.utc).astimezone().strftime(db_date_format)
except ValueError:
return date_string
def close_db(db_fd): def close_db(db_fd):
db_fd.close() db_fd.close()
atexit.unregister(close_db) atexit.unregister(close_db)
@ -178,7 +106,7 @@ def add(db, ip):
db = db_busy_open(db_location + '/' + db + '.dbm', 'c', 30) db = db_busy_open(db_location + '/' + db + '.dbm', 'c', 30)
if db_creation_date_key not in db.keys(): if db_creation_date_key not in db.keys():
db[db_creation_date_key] = datetime.utcnow().strftime(db_date_format).encode('UTF-8') db[db_creation_date_key] = datetime.utcnow().strftime(db_date_format).encode('UTF-8')
event_date = ('%s, ' % datetime.utcnow().strftime(db_date_format)).encode('UTF-8') event_date = ('%s, ' % datetime.now().strftime(db_date_format)).encode('UTF-8')
try: try:
db[ip] += event_date db[ip] += event_date
except KeyError: except KeyError:
@ -186,7 +114,7 @@ def add(db, ip):
close_db(db) close_db(db)
return return
def digest(db, delete, sort): def digest(db, delete):
db_file = db_location + '/' + db + '.dbm' db_file = db_location + '/' + db + '.dbm'
new_db_file = db_location + '/.' + db + '.dbm' new_db_file = db_location + '/.' + db + '.dbm'
try: try:
@ -202,48 +130,32 @@ def digest(db, delete, sort):
os.rename(new_db_file, db_file) os.rename(new_db_file, db_file)
try: try:
db_creation_date = utc_to_local(db[db_creation_date_key].decode('UTF-8')) db_creation_date = db[db_creation_date_key].decode('UTF-8')
except KeyError as e: except KeyError as e:
db_creation_date = 'not found' db_creation_date = 'not found'
events_list = [] event_list = []
for ip in db.keys(): for ip in db.keys():
if ip.decode('UTF-8') == db_creation_date_key: if ip.decode('UTF-8') == db_creation_date_key:
continue continue
events_list.append(Ban(ip.decode('UTF-8'), db[ip].decode('UTF-8').split(', ')[:-1])) event_list.append((ip.decode('UTF-8'), db[ip].decode('UTF-8').split(', ')[:-1]))
close_db(db) close_db(db)
events_list.sort(key=lambda x: x.events[0]) # sort by date event_list.sort(key = lambda x: len(x[1]), reverse = True)
if sort:
events_list.sort(key=lambda x: len(x.events), reverse=True)
msg = '' msg = ''
msg_html = '' for ip, events in event_list:
for ban in events_list: msg += '%3d event(s) for IP %-42s: %s\n' %(len(events), ip, ', '.join(events))
msg_html += html_tr_template.substitute(count = len(ban.events), ip = ban.ip, events = '<br>'.join(ban.events)) return (db_creation_date, msg)
msg += '%3d event(s) for IP %-42s: %s\n' %(len(ban.events), ban.ip, ', '.join(ban.events))
return (len(events_list), db_creation_date, msg, msg_html)
def mail_digest(db, mail_to, mail_from, delete, html, quiet, sort): def mail_digest(db, mail_to, mail_from, delete):
msg = EmailMessage() msg = EmailMessage()
date_now = datetime.now().strftime(db_date_format) date_now = datetime.utcnow().strftime(db_date_format)
count, creation_date, dgst, dgst_html = digest(db, delete, sort) creation_date, dgst = digest(db, delete)
if dgst == '': if dgst == '':
if quiet: dgst = 'no ban event recorded for the named time frame'
return
else:
dgst = ' No ban event recorded for the named time frame.'
dgst_html = html_error_template.substitute(error_msg = dgst)
msg.set_content(default_mail_template.substitute( msg.set_content(default_mail_template.substitute(
count = count,
creation_date = creation_date,
date_now = date_now,
digest = dgst
))
if html:
msg.add_alternative(default_html_template.substitute(
count = count,
creation_date = creation_date, creation_date = creation_date,
date_now = date_now, date_now = date_now,
digest = dgst_html digest = dgst,
), subtype = 'html') ))
msg['To'] = mail_to msg['To'] = mail_to
msg['From'] = mail_from msg['From'] = mail_from
msg['Subject'] = '[Fail2Ban] %s: digest for %s %s' % (db, socket.gethostname(), date_now) msg['Subject'] = '[Fail2Ban] %s: digest for %s %s' % (db, socket.gethostname(), date_now)
@ -256,9 +168,9 @@ def main(args):
if args.cmd == 'add': if args.cmd == 'add':
add(args.database, args.ip) add(args.database, args.ip)
elif args.cmd == 'digest': elif args.cmd == 'digest':
print(digest(args.database, args.delete, args.sort)[2]) print(digest(args.database, args.delete)[1])
elif args.cmd == 'maildigest': elif args.cmd == 'maildigest':
mail_digest(args.database, args.to, args.mail_from, args.delete, args.html, args.quiet, args.sort) mail_digest(args.database, args.to, args.mail_from, args.delete)
elif args.cmd is None: elif args.cmd is None:
print('No action specified') print('No action specified')
return return
@ -307,12 +219,6 @@ if __name__ == '__main__':
default = False, default = False,
help = 'do / don\'t delete current database, next call to add will create a new empty one' help = 'do / don\'t delete current database, next call to add will create a new empty one'
) )
subcommands[sc].add_argument(
'--sort', '--no-sort',
action = store_yesno,
default = True,
help = 'do / don\'t sort the digest by repeat event occurrences.'
)
sc = 'maildigest' sc = 'maildigest'
subcommands[sc] = subparsers.add_parser( subcommands[sc] = subparsers.add_parser(
@ -329,30 +235,12 @@ if __name__ == '__main__':
default = True, default = True,
help = 'do / don\'t delete current database, next call to add will create a new empty one' help = 'do / don\'t delete current database, next call to add will create a new empty one'
) )
subcommands[sc].add_argument(
'--html', '--no-html',
action = store_yesno,
default = False,
help = 'do / don\'t send the digest in HTML format.'
)
subcommands[sc].add_argument( subcommands[sc].add_argument(
'--mail-from', '--mail-from',
action = 'store', action = 'store',
default = 'Fail2ban at {0} <fail2ban@{0}>'.format(socket.gethostname()), default = 'Fail2ban at {0} <fail2ban@{0}>'.format(socket.gethostname()),
help = 'Use FROM address for the email From header. Default: Fail2ban at {0} <fail2ban@{0}> (automatically detected system hostname)'.format(socket.gethostname()) help = 'Use FROM address for the email From header. Default: Fail2ban at {0} <fail2ban@{0}> (automatically detected system hostname)'.format(socket.gethostname())
) )
subcommands[sc].add_argument(
'--quiet', '--no-quiet',
action = store_yesno,
default = False,
help = 'do / don\'t send digest if there are no ban events recorded for the named time frame'
)
subcommands[sc].add_argument(
'--sort', '--no-sort',
action = store_yesno,
default = True,
help = 'do / don\'t sort the digest by repeat event occurrences.'
)
subcommands[sc].add_argument( subcommands[sc].add_argument(
'--to', '--to',
action = 'store', action = 'store',