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.
2 changed files with 23 additions and 135 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue