Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 17 Dec 2009 17:35:55 -0800
From:      Brandon Low <lostlogic@lostlogicx.com>
To:        freebsd-questions@freebsd.org
Subject:   Re: RFC: Fam/Python based script for bruteforce blocking
Message-ID:  <20091218013555.GJ73162@lostlogicx.com>
In-Reply-To: <20091218013422.GI73162@lostlogicx.com>
References:  <20091218013422.GI73162@lostlogicx.com>

next in thread | previous in thread | raw e-mail | index | archive | help
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()




Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20091218013555.GJ73162>