From 538e38dbcbcb3995954d780f840440bb502c189f Mon Sep 17 00:00:00 2001 From: Enrico Tagliavini Date: Fri, 25 Aug 2017 10:26:07 +0200 Subject: [PATCH 01/11] Fix naming in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1aed365..2fb4002 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,12 @@ Manual Installation: License: -------- -Fail2Ban is free software; you can redistribute it and/or modify it under the +fail2ban-digest 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 Foundation; either version 2 of the License, or (at your option) any later version. -Fail2Ban is distributed in the hope that it will be useful, but WITHOUT ANY +fail2ban-digest 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 PARTICULAR PURPOSE. See the GNU General Public License for more details. From 25365b3a06143ea6f4408b00e791feb77492636d Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Sun, 24 Feb 2019 22:22:21 -0800 Subject: [PATCH 02/11] Added --quiet argument. --- bin/fail2ban_digest | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index 588077e..d16c408 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -145,12 +145,15 @@ def digest(db, delete): msg += '%3d event(s) for IP %-42s: %s\n' %(len(events), ip, ', '.join(events)) return (db_creation_date, msg) -def mail_digest(db, mail_to, mail_from, delete): +def mail_digest(db, mail_to, mail_from, delete, quiet): msg = EmailMessage() date_now = datetime.utcnow().strftime(db_date_format) creation_date, dgst = digest(db, delete) if dgst == '': - dgst = 'no ban event recorded for the named time frame' + if quiet: + return + else: + dgst = 'no ban event recorded for the named time frame' msg.set_content(default_mail_template.substitute( creation_date = creation_date, date_now = date_now, @@ -170,7 +173,7 @@ def main(args): elif args.cmd == 'digest': print(digest(args.database, args.delete)[1]) elif args.cmd == 'maildigest': - mail_digest(args.database, args.to, args.mail_from, args.delete) + mail_digest(args.database, args.to, args.mail_from, args.delete, args.quiet) elif args.cmd is None: print('No action specified') return @@ -241,6 +244,12 @@ if __name__ == '__main__': default = 'Fail2ban at {0} '.format(socket.gethostname()), help = 'Use FROM address for the email From header. Default: Fail2ban at {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( '--to', action = 'store', From 01beb565d165f5e807a3467b59f2541510de4b0b Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Sun, 24 Feb 2019 22:31:01 -0800 Subject: [PATCH 03/11] Converted dates in mail_digest to local timezone. --- bin/fail2ban_digest | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index d16c408..a2202e1 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -17,7 +17,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from dbm import gnu as dbm from email.message import EmailMessage from smtplib import SMTP @@ -37,13 +37,13 @@ db_location = '/var/lib/fail2ban/digest' db_creation_date_key = 'db_creation_date' db_date_format = '%Y-%m-%d %H:%M:%S' default_mail_template = Template('''Hi,\n - this is a digest email of banned IPs since ${creation_date} UTC and ${date_now} +This is a digest email of banned IPs since ${creation_date} and ${date_now}: ${digest} Regards, -Fail2ban digest +Fail2ban Digest ''') class store_yesno(argparse.Action): @@ -147,8 +147,13 @@ def digest(db, delete): def mail_digest(db, mail_to, mail_from, delete, quiet): msg = EmailMessage() - date_now = datetime.utcnow().strftime(db_date_format) + date_now = datetime.now().strftime(db_date_format) creation_date, dgst = digest(db, delete) + try: + # convert to local timezone + creation_date = datetime.strptime(creation_date, db_date_format).replace(tzinfo=timezone.utc).astimezone().strftime(db_date_format) + except ValueError: + pass # likely invalid date, continue. if dgst == '': if quiet: return From 7f3892f556f95f0b3a4831b48cbebfa311e2d24d Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Mon, 25 Feb 2019 13:02:14 -0800 Subject: [PATCH 04/11] Event dates are now stored in UTC, and printed/mailed using the local timezone. --- bin/fail2ban_digest | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index a2202e1..bb6737f 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -80,6 +80,12 @@ def ip_address(string): else: 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): db_fd.close() atexit.unregister(close_db) @@ -106,7 +112,7 @@ def add(db, ip): db = db_busy_open(db_location + '/' + db + '.dbm', 'c', 30) if db_creation_date_key not in db.keys(): db[db_creation_date_key] = datetime.utcnow().strftime(db_date_format).encode('UTF-8') - event_date = ('%s, ' % datetime.now().strftime(db_date_format)).encode('UTF-8') + event_date = ('%s, ' % datetime.utcnow().strftime(db_date_format)).encode('UTF-8') try: db[ip] += event_date except KeyError: @@ -142,25 +148,23 @@ def digest(db, delete): event_list.sort(key = lambda x: len(x[1]), reverse = True) msg = '' for ip, events in event_list: - msg += '%3d event(s) for IP %-42s: %s\n' %(len(events), ip, ', '.join(events)) + local_events = [] + for event in events: + local_events.append(utc_to_local(event)) + msg += '%3d event(s) for IP %-42s: %s\n' %(len(events), ip, ', '.join(local_events)) return (db_creation_date, msg) def mail_digest(db, mail_to, mail_from, delete, quiet): msg = EmailMessage() date_now = datetime.now().strftime(db_date_format) creation_date, dgst = digest(db, delete) - try: - # convert to local timezone - creation_date = datetime.strptime(creation_date, db_date_format).replace(tzinfo=timezone.utc).astimezone().strftime(db_date_format) - except ValueError: - pass # likely invalid date, continue. if dgst == '': if quiet: return else: dgst = 'no ban event recorded for the named time frame' msg.set_content(default_mail_template.substitute( - creation_date = creation_date, + creation_date = utc_to_local(creation_date), date_now = date_now, digest = dgst, )) From 7152766ca1f1b6b80a2da279db6d07a676493c73 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Tue, 5 Mar 2019 20:09:06 -0800 Subject: [PATCH 05/11] Fixed bug where db_creation_date_key was never found in the db keys. --- bin/fail2ban_digest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index bb6737f..8f41670 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -110,7 +110,7 @@ def db_busy_open(filename, flags, timeout): def add(db, ip): db = db_busy_open(db_location + '/' + db + '.dbm', 'c', 30) - if db_creation_date_key not in db.keys(): + if db_creation_date_key.encode('UTF-8') not in db.keys(): 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') try: From e5981669d88c0f85bc79e810891bbc7bddd6ee59 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Tue, 5 Mar 2019 23:46:50 -0800 Subject: [PATCH 06/11] Fixed sorting, added --sort argument. --- bin/fail2ban_digest | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index 8f41670..4bfcd99 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -45,6 +45,13 @@ Regards, Fail2ban Digest ''') +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): def __init__(self, option_strings, dest, nargs = None, **kwargs): @@ -136,23 +143,22 @@ def digest(db, delete): os.rename(new_db_file, db_file) try: - db_creation_date = db[db_creation_date_key].decode('UTF-8') + db_creation_date = utc_to_local(db[db_creation_date_key].decode('UTF-8')) except KeyError as e: db_creation_date = 'not found' - event_list = [] + events_list = [] for ip in db.keys(): if ip.decode('UTF-8') == db_creation_date_key: continue - event_list.append((ip.decode('UTF-8'), db[ip].decode('UTF-8').split(', ')[:-1])) + events_list.append(Ban(ip.decode('UTF-8'), db[ip].decode('UTF-8').split(', ')[:-1])) close_db(db) - event_list.sort(key = lambda x: len(x[1]), reverse = True) + events_list.sort(key=lambda x: x.events[0]) # sort by date + if sort: + events_list.sort(key=lambda x: len(x.events), reverse=True) msg = '' - for ip, events in event_list: - local_events = [] - for event in events: - local_events.append(utc_to_local(event)) - msg += '%3d event(s) for IP %-42s: %s\n' %(len(events), ip, ', '.join(local_events)) - return (db_creation_date, msg) + msg_html = '' + for ban in events_list: + msg += '%3d event(s) for IP %-42s: %s\n' %(len(ban.events), ban.ip, ', '.join(ban.events)) def mail_digest(db, mail_to, mail_from, delete, quiet): msg = EmailMessage() @@ -162,11 +168,11 @@ def mail_digest(db, mail_to, mail_from, delete, quiet): if quiet: return else: - dgst = 'no ban event recorded for the named time frame' + dgst = ' No ban event recorded for the named time frame.' msg.set_content(default_mail_template.substitute( - creation_date = utc_to_local(creation_date), - date_now = date_now, - digest = dgst, + creation_date = creation_date, + date_now = date_now, + digest = dgst )) msg['To'] = mail_to msg['From'] = mail_from @@ -231,6 +237,12 @@ if __name__ == '__main__': default = False, 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' subcommands[sc] = subparsers.add_parser( From e8eafc027b6a34f97957ab6a522ac9158f6811f2 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Tue, 5 Mar 2019 23:47:30 -0800 Subject: [PATCH 07/11] Added --html argument and templates. --- bin/fail2ban_digest | 88 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index 4bfcd99..dcf67b7 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -45,6 +45,63 @@ Regards, Fail2ban Digest ''') +default_html_template = Template(''' + + + + + +

Hi,

+

This is a digest email of banned IPs since ${creation_date} and ${date_now}:

+ + + + + + +${digest} +
#IPsWhen
+

Regards,

+

Fail2Ban Digest

+ + +''') +html_tr_template = Template(''' + ${count} + ${ip} + ${events} + +''') +html_error_template = Template(''' + ${error_msg} + ''') class Ban: def __init__(self, ip, events): self.ip = ip @@ -127,7 +184,7 @@ def add(db, ip): close_db(db) return -def digest(db, delete): +def digest(db, delete, sort): db_file = db_location + '/' + db + '.dbm' new_db_file = db_location + '/.' + db + '.dbm' try: @@ -158,22 +215,31 @@ def digest(db, delete): msg = '' msg_html = '' for ban in events_list: + msg_html += html_tr_template.substitute(count = len(ban.events), ip = ban.ip, events = '
'.join(ban.events)) msg += '%3d event(s) for IP %-42s: %s\n' %(len(ban.events), ban.ip, ', '.join(ban.events)) + return (db_creation_date, msg, msg_html) -def mail_digest(db, mail_to, mail_from, delete, quiet): +def mail_digest(db, mail_to, mail_from, delete, html, quiet, sort): msg = EmailMessage() date_now = datetime.now().strftime(db_date_format) - creation_date, dgst = digest(db, delete) + creation_date, dgst, dgst_html = digest(db, delete, sort) if dgst == '': if quiet: 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( creation_date = creation_date, date_now = date_now, digest = dgst )) + if html: + msg.add_alternative(default_html_template.substitute( + creation_date = creation_date, + date_now = date_now, + digest = dgst_html + ), subtype = 'html') msg['To'] = mail_to msg['From'] = mail_from msg['Subject'] = '[Fail2Ban] %s: digest for %s %s' % (db, socket.gethostname(), date_now) @@ -186,9 +252,9 @@ def main(args): if args.cmd == 'add': add(args.database, args.ip) elif args.cmd == 'digest': - print(digest(args.database, args.delete)[1]) + print(digest(args.database, args.delete, args.sort)[1]) elif args.cmd == 'maildigest': - mail_digest(args.database, args.to, args.mail_from, args.delete, args.quiet) + mail_digest(args.database, args.to, args.mail_from, args.html, args.delete, args.quiet, args.sort) elif args.cmd is None: print('No action specified') return @@ -259,6 +325,12 @@ if __name__ == '__main__': default = True, 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( '--mail-from', action = 'store', @@ -271,6 +343,12 @@ if __name__ == '__main__': 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( '--to', action = 'store', From f9d71346bae039fd0b2855a2e7f0d6987f324d0a Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Wed, 6 Mar 2019 00:02:46 -0800 Subject: [PATCH 08/11] Fixed HTML template spacing. --- bin/fail2ban_digest | 86 +++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index dcf67b7..622d7f2 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -48,60 +48,62 @@ Fail2ban Digest default_html_template = Template(''' - + th { + background-color: #dddddd; + } + -

Hi,

-

This is a digest email of banned IPs since ${creation_date} and ${date_now}:

- - - - - - +

Hi,

+

This is a digest email of banned IPs since ${creation_date} and ${date_now}:

+
#IPsWhen
+ + + + + ${digest} -
#IPsWhen
-

Regards,

-

Fail2Ban Digest

+ +

Regards,

+

Fail2Ban Digest

''') -html_tr_template = Template(''' - ${count} - ${ip} - ${events} - +html_tr_template = Template(''' + ${count} + ${ip} + ${events} + ''') -html_error_template = Template(''' - ${error_msg} - ''') +html_error_template = Template(''' + ${error_msg} + +''') + class Ban: def __init__(self, ip, events): self.ip = ip From ace1a9483442b374ffe2bf322d10db6199366b1c Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Wed, 6 Mar 2019 00:12:04 -0800 Subject: [PATCH 09/11] Fixed misplaced html argument in mail_digest call. --- bin/fail2ban_digest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index 622d7f2..136b597 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -256,7 +256,7 @@ def main(args): elif args.cmd == 'digest': print(digest(args.database, args.delete, args.sort)[1]) elif args.cmd == 'maildigest': - mail_digest(args.database, args.to, args.mail_from, args.html, args.delete, args.quiet, args.sort) + mail_digest(args.database, args.to, args.mail_from, args.delete, args.html, args.quiet, args.sort) elif args.cmd is None: print('No action specified') return From 6dd8a04a402bf10e4f32dbdee0e25d866c3204dd Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Wed, 6 Mar 2019 08:11:52 -0800 Subject: [PATCH 10/11] Added banned IPs count to digest. --- bin/fail2ban_digest | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index 136b597..00dba70 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -37,7 +37,7 @@ db_location = '/var/lib/fail2ban/digest' db_creation_date_key = 'db_creation_date' db_date_format = '%Y-%m-%d %H:%M:%S' default_mail_template = Template('''Hi,\n -This is a digest email of banned IPs since ${creation_date} and ${date_now}: +This is a digest email of the ${count} banned IPs between ${creation_date} and ${date_now}: ${digest} @@ -79,7 +79,7 @@ default_html_template = Template('''

Hi,

-

This is a digest email of banned IPs since ${creation_date} and ${date_now}:

+

This is a digest email of the ${count} banned IPs between ${creation_date} and ${date_now}:

@@ -219,12 +219,12 @@ def digest(db, delete, sort): for ban in events_list: msg_html += html_tr_template.substitute(count = len(ban.events), ip = ban.ip, events = '
'.join(ban.events)) msg += '%3d event(s) for IP %-42s: %s\n' %(len(ban.events), ban.ip, ', '.join(ban.events)) - return (db_creation_date, msg, msg_html) + return (len(events_list), db_creation_date, msg, msg_html) def mail_digest(db, mail_to, mail_from, delete, html, quiet, sort): msg = EmailMessage() date_now = datetime.now().strftime(db_date_format) - creation_date, dgst, dgst_html = digest(db, delete, sort) + count, creation_date, dgst, dgst_html = digest(db, delete, sort) if dgst == '': if quiet: return @@ -232,12 +232,14 @@ def mail_digest(db, mail_to, mail_from, delete, html, quiet, sort): 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( + 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, date_now = date_now, digest = dgst_html @@ -254,7 +256,7 @@ def main(args): if args.cmd == 'add': add(args.database, args.ip) elif args.cmd == 'digest': - print(digest(args.database, args.delete, args.sort)[1]) + print(digest(args.database, args.delete, args.sort)[2]) elif args.cmd == 'maildigest': mail_digest(args.database, args.to, args.mail_from, args.delete, args.html, args.quiet, args.sort) elif args.cmd is None: From adcb3bd91747c8e242aa9a2a369c5a8e837dd2fc Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Wed, 13 Mar 2019 09:47:24 -0700 Subject: [PATCH 11/11] UTF-8 encode the db_creation_date_key globally. --- bin/fail2ban_digest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fail2ban_digest b/bin/fail2ban_digest index 00dba70..2f7af0e 100755 --- a/bin/fail2ban_digest +++ b/bin/fail2ban_digest @@ -34,7 +34,7 @@ import re import sys db_location = '/var/lib/fail2ban/digest' -db_creation_date_key = 'db_creation_date' +db_creation_date_key = u'db_creation_date' db_date_format = '%Y-%m-%d %H:%M:%S' default_mail_template = Template('''Hi,\n This is a digest email of the ${count} banned IPs between ${creation_date} and ${date_now}: @@ -176,7 +176,7 @@ def db_busy_open(filename, flags, timeout): def add(db, ip): db = db_busy_open(db_location + '/' + db + '.dbm', 'c', 30) - if db_creation_date_key.encode('UTF-8') 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') event_date = ('%s, ' % datetime.utcnow().strftime(db_date_format)).encode('UTF-8') try:
#