RFC: Fam/Python based script for bruteforce blocking
Brandon Low
lostlogic at lostlogicx.com
Fri Dec 18 01:35:55 UTC 2009
Not sure why this didn't attach the first time.
#!/usr/bin/env python
import errno
import logging
import optparse
import os
import re
import select
import signal
import subprocess
import sys
import time
import datetime
import _fam
def getUpdateBlocks(pfctl, expire_seconds, blacklist_filename, table, limit_n):
expire=str(expire_seconds)
blacklist=blacklist_filename
limit=limit_n
baseArgs=(pfctl, '-t', table, '-T')
def callAndLog(*args, **kwargs):
c=subprocess.Popen(baseArgs + args, stderr=subprocess.PIPE,
stdout=kwargs.get('stdout',subprocess.PIPE))
stdout,stderr=c.communicate()
if stdout: logging.info(stdout)
for line in (stderr if stderr else '').split('\n'):
if not line: continue
getattr(logging,'info' if line.find('ALTQ') < 0 else 'debug')(line)
reParts=('(.*) erudite sshd\[[0-9]+\]: ',
'(?:', '|'.join(('Invalid user .* from',
'Did not receive identification string from',
'error: PAM: authentication error for root from')), ') ',
'(', '(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}',
'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)', ')\n?')
r=re.compile(''.join(reParts))
df='%b %d %H:%M:%S'
oneDay=datetime.timedelta(days=1)
def processFile(now, ips, filename):
with open(filename, 'r') as f:
for line in f:
m=r.match(line)
if not m: continue
d=datetime.datetime.strptime(m.group(1),df).replace(now.year)
if d > now: d=d.replace(now.year-1)
if now-d < oneDay: ips[m.group(2)]=ips.get(m.group(2),0) + 1
def updateBlocks(filename):
logging.info("Updating blacklist...")
ips={}
now=datetime.datetime.now()
processFile(now, ips, filename)
logging.debug("Found %s IPs", len(ips))
logging.debug("Adding ips to pf table")
callAndLog('add', *tuple(k for k,v in ips.iteritems() if v >= limit))
logging.debug("Expiring ips from pf table")
callAndLog('expire', expire)
logging.debug("Saving table state to file")
with open(blacklist,'w') as blacklistFile:
callAndLog('show', stdout=blacklistFile)
logging.debug("Done")
return updateBlocks
def main():
parser=optparse.OptionParser()
parser.add_option("-d", "--debug",
action="store_true", help="Enable debug logging")
parser.add_option("-a", "--auth_log",
default="/var/log/auth.log", help="Authentication log filename")
parser.add_option("-b", "--blacklist",
default="/var/db/blacklist", help="Blacklist filename")
parser.add_option("-l", "--log_file",
default="/var/log/bruteforce.log", help="Log filename")
parser.add_option("-p", "--pfctl",
default="/sbin/pfctl", help="pfctl binary")
parser.add_option("-e", "--expire", type="int",
default=604800, help="Seconds to hold a grudge")
parser.add_option("-t", "--table",
default="bruteforce", help="Name of pf table to work on")
parser.add_option("-i", "--limit", type="int",
default=2, help="Number of invalid logins to get blacklisted")
(opts, args)=parser.parse_args()
if args: optparse.error("No non-option arguments expected")
logging.basicConfig(filename=opts.log_file,
level=logging.DEBUG if opts.debug else logging.INFO)
fc=_fam.open()
p=select.poll()
p.register(fc, select.POLLIN|select.POLLPRI)
fr=fc.monitorFile(opts.auth_log, None)
updateBlocks=getUpdateBlocks(
opts.pfctl, opts.expire, opts.blacklist, opts.table, opts.limit)
while True:
p.poll(60)
update=False
while fc.pending():
fe=fc.nextEvent()
if fe.code in (_fam.Exists,_fam.Changed,_fam.Created): update=True
if not fe.filename==opts.auth_log: raise "FAM event: wrong file"
if update: updateBlocks(fe.filename)
if __name__ == "__main__": main()
More information about the freebsd-questions
mailing list