Thuban
2019-04-28 e57a7b2c21f4f06c8deae99b1db89bbe0a8eb81d
vilain
old mode 100755 new mode 100644
@@ -1,179 +1,11 @@
#!/usr/bin/env python3.4
# -*- coding:Utf-8 -*-
#!/bin/sh
# script to launch vilain with the latest python3 version available
"""
Author :      thuban <thuban@yeuxdelibad.net>
Licence :     MIT
Description : Mimic fail2ban with pf for OpenBSD.
              Inspired from http://www.vincentdelft.be/post/post_20161106
              In pf.conf, add :
                    table <vilain_bruteforce> persist
                    block quick from <vilain_bruteforce>
              You might want to add a cron task to remove old banned IP. As example, to ban for one day max :
                    pfctl -t vilain_bruteforce -T expire 86400
              To see banned IP :
                    pfctl -t vilain_bruteforce -T show
"""
import sys
import os
import configparser
import re
import time
import logging
import subprocess
from multiprocessing import Process, Queue, TimeoutError
configfile = "/etc/vilain.conf"
version = "0.1"
vilain_table = "vilain_bruteforce"
logfile = "/var/log/daemon"
if os.geteuid() != 0:
    print("Only root can use this tool")
    sys.exit()
# Configure logging
logger = logging.getLogger(__name__)
logging.basicConfig(filename=logfile,
                    format='%(asctime)s %(module)s:%(funcName)s:%(message)s',
                    datefmt='%H:%M:%S')
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
logger.addHandler(ch)
# functions
def readconfig():
    if not os.path.isfile(configfile):
        logging.error("Can't read config file, exiting...")
        sys.exit(1)
    config = configparser.ConfigParser()
    config.read(configfile)
    return(config)
def load_config():
    c = readconfig()
    d = c.defaults()
    watch_while = int(d['watch_while'])
    maxtries = int(d['maxtries'])
    vilain_table = d['vilain_table']
    return(watch_while, maxtries, vilain_table)
def load_sections():
    c = readconfig()
    for s in c.sections():
        logfile = c.get(s,'logfile')
        regex = c.get(s,'regex')
        d = {'name' : s, 'logfile':logfile, 'regex':regex}
        yield d
def check_logs(logfile, regex, bad_ip_queue):
    """
    worker who put in bad_ip_queue bruteforce IP
    """
    if not os.path.isfile(logfile) :
        logger.warning("{} doesn't exist".format(logfile))
        return
    # Watch the file for changes
    stat = os.stat(logfile)
    size = stat.st_size
    mtime = stat.st_mtime
    RE = re.compile(regex)
    while True:
        time.sleep(0.5)
        stat = os.stat(logfile)
        if mtime < stat.st_mtime:
            mtime = stat.st_mtime
            with open(logfile, "rb") as f:
                f.seek(size)
                lines = f.readlines()
                ul = [ u.decode() for u in lines ]
                line = "".join(ul).strip()
                ret = RE.match(line)
                if ret:
                    bad_ip = ret.groups()[0]
                    bad_ip_queue.put(bad_ip)
            size = stat.st_size
def ban_ips(bad_ip_queue, watch_while, maxtries, vilain_table):
    """
    worker who ban IP on bad_ip_queue
    add IP in bad_ips_list
    record time when this IP has been seen in ip_seen_at = { ip:time }
    check number of occurence of the same ip in bad_ips_list
    if more than 3 : ban and clean of list
    check old ip in ip_seen_at : remove older than watch_while
    """
    bad_ips_list = []
    ip_seen_at = {}
    while True:
        try:
            ip = bad_ip_queue.get(0.5)
            logger.info("{} detected".format(ip))
            bad_ips_list.append(ip)
            ip_seen_at[ip] = time.time()
            n_ip = bad_ips_list.count(ip)
            if n_ip >= maxtries:
                logger.info("Blacklisting {}".format(ip))
                subprocess.call(["pfctl", "-t", vilain_table, "-T", "add", ip])
                ip_seen_at.pop(ip)
                while ip in bad_ips_list:
                    bad_ips_list.remove(ip)
            to_remove = []
            for recorded_ip, last_seen in ip_seen_at.items():
                if time.time() - last_seen >= watch_while:
                    logger.info("{} not seen since a long time, forgetting...".format(recorded_ip))
                    to_remove.append(recorded_ip)
            for i in to_remove:
                ip_seen_at.pop(i)
        except TimeoutError:
            pass
        except KeyboardInterrupt:
            sys.exit(0)
def main():
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    watch_while, maxtries, vilain_table = load_config()
    bad_ip_queue = Queue()
    workers = []
    for entry in load_sections():
        logger.info("Start vilain for {}".format(entry['name']))
        worker = Process(target=check_logs,
                        args=(entry['logfile'], entry['regex'], bad_ip_queue))
        workers.append(worker)
        worker.daemon = True
        worker.start()
    ban_worker = Process(target=ban_ips, args=(bad_ip_queue, watch_while, maxtries))
    workers.append(ban_worker)
    ban_worker.daemon = True
    ban_worker.start()
    for w in workers:
        w.join()
    return 0
if __name__ == '__main__':
   main()
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1)
PYTHON="/usr/local/bin/python$PYTHONVERSION"
if [ -x $PYTHON ]; then
   $PYTHON /usr/local/sbin/vilain.py >/dev/null 2>&1 &
else
   echo "Error : no python3 executable found"
fi
exit