From owner-freebsd-questions@FreeBSD.ORG Fri Dec 18 01:35:55 2009 Return-Path: Delivered-To: freebsd-questions@freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2001:4f8:fff6::34]) by hub.freebsd.org (Postfix) with ESMTP id A904A106568B for ; Fri, 18 Dec 2009 01:35:55 +0000 (UTC) (envelope-from lostlogic@lostlogicx.com) Received: from erudite.lostlogicx.com (erudite.lostlogicx.com [74.208.67.179]) by mx1.freebsd.org (Postfix) with ESMTP id 6D30F8FC0C for ; Fri, 18 Dec 2009 01:35:55 +0000 (UTC) Received: by erudite.lostlogicx.com (Postfix, from userid 1001) id 1A9C1279E5; Thu, 17 Dec 2009 17:35:55 -0800 (PST) Date: Thu, 17 Dec 2009 17:35:55 -0800 From: Brandon Low To: freebsd-questions@freebsd.org Message-ID: <20091218013555.GJ73162@lostlogicx.com> References: <20091218013422.GI73162@lostlogicx.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20091218013422.GI73162@lostlogicx.com> X-Operating-System: FreeBSD 8.0-RELEASE amd64 User-Agent: Mutt/1.5.20 (2009-06-14) Subject: Re: RFC: Fam/Python based script for bruteforce blocking X-BeenThere: freebsd-questions@freebsd.org X-Mailman-Version: 2.1.5 Precedence: list List-Id: User questions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 18 Dec 2009 01:35:55 -0000 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()