Thuban
2018-12-01 54613da01ddf0f14527822ced5b9bd750b825fb6
exemples en +
3 files added
4 files modified
270 ■■■■ changed files
LICENSE 21 ●●●●● patch | view | raw | blame | history
Makefile 12 ●●●● patch | view | raw | blame | history
vilain 2 ●●● patch | view | raw | blame | history
vilain.conf 29 ●●●●● patch | view | raw | blame | history
vilain.py 64 ●●●● patch | view | raw | blame | history
vilainreport 11 ●●●●● patch | view | raw | blame | history
vilainreport.py 131 ●●●●● patch | view | raw | blame | history
LICENSE
New file
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Thuban
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Makefile
@@ -2,21 +2,25 @@
# See LICENSE file for copyright and license details.
#
# vilain version
VERSION = 0.3
VERSION = 0.7
# Customize below to fit your system
# paths
PREFIX = /usr/local
MANPREFIX = ${PREFIX}/man/man1/
install:
install:
    @echo installing executable file to ${DESTDIR}${PREFIX}/bin
    @mkdir -p ${DESTDIR}${PREFIX}/bin
    @cp -f vilain ${DESTDIR}${PREFIX}/bin
    @cp -f vilainreport ${DESTDIR}${PREFIX}/bin
    @echo installing script file to ${DESTDIR}${PREFIX}/sbin
    @cp -f vilain.py ${DESTDIR}${PREFIX}/sbin
    @cp -f vilainreport.py ${DESTDIR}${PREFIX}/sbin
    @chmod 755 ${DESTDIR}${PREFIX}/bin/vilain
    @chmod 755 ${DESTDIR}${PREFIX}/bin/vilainreport
    @chmod 644 ${DESTDIR}${PREFIX}/sbin/vilain.py
    @chmod 644 ${DESTDIR}${PREFIX}/sbin/vilainreport.py
    @echo installing init script in /etc/rc.d
    @cp -f vilain.rc /etc/rc.d/vilain
    @chmod 755 /etc/rc.d/vilain
@@ -29,8 +33,10 @@
uninstall:
    @echo removing executable file from ${DESTDIR}${PREFIX}/bin
    @rm -f ${DESTDIR}${PREFIX}/bin/vilain
    @rm -f ${DESTDIR}${PREFIX}/bin/vilainreport
    @rm -f ${DESTDIR}${PREFIX}/sbin/vilain.py
    @rm -f ${DESTDIR}${PREFIX}/sbin/vilainreport.py
    @echo removing manual page to ${DESTDIR}${MANPREFIX}/
    @rm -f ${DESTDIR}${MANPREFIX}/vilain.1
.PHONY: install uninstall
.PHONY: install uninstall
vilain
@@ -1,5 +1,5 @@
#!/bin/sh
# script to launch vilain with the latest python3 version avaiable
# script to launch vilain with the latest python3 version available
PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1)
PYTHON="/usr/local/bin/python$PYTHONVERSION"
vilain.conf
@@ -1,13 +1,16 @@
[DEFAULT]
# 24h + 5min
# Time to keep banned a bad ip
watch_while = 86700
watch_while = 86700
# Max tries before being banned
maxtries = 3
# pf table to keep bad IP.
# remember to clean the table with this command in a cron job :
#     /sbin/pfctl -t vilain_bruteforce -T expire 86400
vilain_table = vilain_bruteforce
# vilain log file
vilain_log = /var/log/daemon
# duration before each checks on the different log files
sleeptime = 3.0
@@ -30,6 +33,14 @@
[ssh2]
logfile = /var/log/authlog
regex = .* Connection closed by ([\S]+) .*
[ssh3]
logfile = /var/log/authlog
regex = .* Invalid user \w+ ([\S]+) .*
[ssh4]
logfile = /var/log/authlog
regex = .* Disconnected from authenticating user root ([\S]+) .*
#[http404]
#logfile = /var/www/logs/access.log
@@ -56,3 +67,19 @@
logfile = /var/www/logs/access.log
regex = (?:\S+\s){1}(\S+).*wp-login.php.*
# Nextcloud: login page
# Nextcloud 12 brings protection against brute-force attacks
# but 1/ not yet tested so far 2/ system protection is probably more efficient
[nextcloud]
logfile = /var/www/htdocs/datacloud/nextcloud.log
regex = .*Bruteforce attempt from \\"(.*)\\" detected
# Nextcloud: public shares protected by password
# regex is compliant with NginX log format:
#     /etc/nginx/nginx.conf:
#        log_format main '$remote_addr - $remote_user [$time_local] "$request" '
#                        '$status $body_bytes_sent "$http_referer" '
#                        '"$http_user_agent" "$http_x_forwarded_for"';
[nextcloud-share]
logfile = /var/www/logs/access-nextcloud.log
regex = (\d+\.\d+\.\d+\.\d+) \-.*POST /s/\w+/authenticate HTTP/1.1\" 200
vilain.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
# -*- coding:Utf-8 -*-
# -*- coding:Utf-8 -*-
"""
Author :      thuban <thuban@yeuxdelibad.net>
Author :      thuban <thuban@yeuxdelibad.net>
              Vincent <vincent.delft@gmail.com>
Licence :     MIT
Require : python >= 3.5
@@ -11,11 +11,11 @@
Description : Mimic fail2ban with pf for OpenBSD.
              Inspired from http://www.vincentdelft.be/post/post_20161106
              In pf.conf, add :
              In pf.conf, add :
                    table <vilain_bruteforce> persist
                    block quick from <vilain_bruteforce>
                    block quick from <vilain_bruteforce>
              To see banned IP :
              To see banned IP :
                    pfctl -t vilain_bruteforce -T show
"""
@@ -30,7 +30,7 @@
import time
CONFIGFILE = "/etc/vilain.conf"
VERSION = "0.5"
VERSION = "0.7"
vilain_table = "vilain_bruteforce"
LOGFILE = "/var/log/daemon"
@@ -38,15 +38,19 @@
    print("Only root can use this tool")
    sys.exit(1)
# Configure logging
log_handler = logging.handlers.WatchedFileHandler(LOGFILE)
formatter = logging.Formatter(
        '%(asctime)s %(module)s:%(funcName)s:%(message)s',
        '%b %d %H:%M:%S')
log_handler.setFormatter(formatter)
# declare logger
logger = logging.getLogger(__name__)
logger.addHandler(log_handler)
logger.setLevel(logging.INFO)
def configure_logging():
    print('Log file : {}'.format(LOGFILE))
    log_handler = logging.handlers.WatchedFileHandler(LOGFILE)
    formatter = logging.Formatter(
            '%(asctime)s %(module)s:%(funcName)s:%(message)s',
            '%Y-%m-%d %H:%M:%S')
    log_handler.setFormatter(formatter)
    logger.addHandler(log_handler)
    logger.setLevel(logging.INFO)
# functions
def readconfig():
@@ -57,11 +61,9 @@
    config = configparser.ConfigParser()
    config.read(CONFIGFILE)
    return(config)
    return (config, config.defaults())
def load_config():
    c = readconfig()
    d = c.defaults()
def load_config(c, d):
    watch_while = int(d['watch_while'])
    VILAIN_TABLE = d['vilain_table']
    default_maxtries = int(d['maxtries'])
@@ -72,8 +74,7 @@
        ignoreips = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()]
    return(watch_while, default_maxtries, vilain_table, ignoreips, sleeptime)
def load_sections():
    c = readconfig()
def load_sections(c):
    for s in c.sections():
        if c.has_option(s,'logfile'):
            LOGFILE = c.get(s,'logfile')
@@ -87,15 +88,15 @@
            yield d
class Vilain():
    def __init__(self):
    def __init__(self, config, config_dict):
        logger.info('Start vilain version {}'.format(VERSION))
        self.loop = asyncio.get_event_loop()
        self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config()
        self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config(config, config_dict)
        self.ip_seen_at = {}
        self.load_bad_ips()
        self.bad_ip_queue = asyncio.Queue(loop=self.loop)
        for entry in load_sections():
        for entry in load_sections(config):
            logger.info("Start vilain for {}".format(entry))
            asyncio.ensure_future(self.check_logs(entry['logfile'], entry['maxtries'], entry['regex'], entry['name']))
@@ -111,7 +112,7 @@
        for res in ret.split():
            ip = res.strip().decode('utf-8')
            logger.info('Add existing banned IPs in your pf table: {}'.format(ip))
            #we assign the counter to 1, but for sure we don't know the real value
            #we assign the counter to 1, but for sure we don't know the real value
            self.ip_seen_at[ip]={'time':time.time(),'count':1}
@@ -181,7 +182,7 @@
            logger.info("{} detected, reason {}, count: {}, maxtries: {}".format(ip, reason, n_ip, maxtries))
            if n_ip >= maxtries:
                ret = subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip])
                logger.info("Blacklisting {}, return code:{}".format(ip, ret))
                logger.info("Blacklisting {}, reason {}, return code:{}".format(ip, reason, ret))
            #for debugging, this line allow us to see if the script run until here
            logger.debug('ban_ips end:{}'.format(self.ip_seen_at))
@@ -206,9 +207,9 @@
def main():
def main(config, config_dict):
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    v = Vilain()
    v = Vilain(config, config_dict)
    v.start()
    return 0
@@ -229,11 +230,16 @@
    if args.version:
        print("Version: ", VERSION)
        sys.exit(0)
    main()
    # read config
    config, config_dict = readconfig()
    logfile = config_dict.get('vilain_log', None)
    if logfile:
        LOGFILE = logfile
    configure_logging()
    main(config, config_dict)
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
vilainreport
New file
@@ -0,0 +1,11 @@
#!/bin/sh
# script to launch vilainreport with the latest python3 version available
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/vilainreport.py
else
    echo "Error : no python3 executable found"
fi
exit
vilainreport.py
New file
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# -*- coding:Utf-8 -*-
"""
Author : Yax https://blogduyax.madyanne.fr/
"""
import re
import sys
import socket
pattern = '(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+).*Blacklisting (\d+\.\d+\.\d+\.\d+), reason (.*), return'
regex = re.compile(pattern)
class CounterDict:
    def __init__(self):
        self._counters = dict()
    def inc(self, k):
        v = self._counters.get(k, 0) + 1
        self._counters[k] = v
    def get(self, k):
        return self._counters.get(k, 0)
    def keys(self):
        return self._counters.keys()
    def reset(self):
        self._counters = dict()
    def topitems(self):
        return sorted(self._counters.items(), key=lambda x: x[1], reverse=True)
class Value:
    def __init__(self):
        self._value = ""
    def __str__(self):
        return self._value
    def __eq__(self, other):
        return str(self._value) == str(other)
    def set(self, value):
        self._value = value
last_day = Value()
# daily counters: key is reason
dcounters = CounterDict()
# global counters: key is reason
gcounters = CounterDict()
# hourly counters: key is hour
hcounters = CounterDict()
# top counters: key is IP
tcounters = CounterDict()
def plural(noun, count):
    if count > 1:
        return noun + "s"
    else:
        return noun
def process(m):
    current_day = m.group(1) + "-" + m.group(2) + "-" + m.group(3)
    current_hour = m.group(4)
    full_time = m.group(4) + ":" + m.group(5) + ":" + m.group(6)
    ip = m.group(7)
    reason = m.group(8)
    # new day
    #print("({})-({}) => {}".format(last_day, current_day, last_day == current_day))
    if last_day != current_day:
        # display day counters
        sys.stdout.write("\n")
        for reason in dcounters.keys():
            count = dcounters.get(reason)
            sys.stdout.write("Probe '{}': {} {}\n".format(reason, count, plural("attack", count)))
        last_day.set(current_day)
        dcounters.reset()
        sys.stdout.write("\n### Date {}\n".format(current_day))
    # output current line
    sys.stdout.write("{} blacklist IP {} ({})\n".format(full_time, ip, reason))
    # increment counters
    dcounters.inc(reason)
    gcounters.inc(reason)
    hcounters.inc(current_hour)
    tcounters.inc(ip)
# parse stdin
for line in sys.stdin:
    match = regex.match(line)
    if match:
        process(match)
# output counters
sys.stdout.write("\n")
for reason in dcounters.keys():
    sys.stdout.write("Probe '{}' : {} attacks\n".format(reason, dcounters.get(reason)))
sys.stdout.write("\n### Attacks per probe\n")
for k in gcounters.keys():
    count = gcounters.get(k)
    sys.stdout.write("Probe '{}': {} {} \n".format(k, count, plural("attack", count)))
sys.stdout.write("\n### Hourly repartition\n")
for k in sorted(hcounters.keys()):
    sys.stdout.write("Hour {} - {:02d}: {}\n".format(k, int(k) + 1, hcounters.get(k)))
sys.stdout.write("\n### Top attackers\n")
for k, v in tcounters.topitems():
    if v < 2:
        break
    try:
        ns = socket.gethostbyaddr(k.strip())[0]
    except:
        ns = '?'
    sys.stdout.write("IP {:16}: {} - {}\n".format(k, v, ns))