4 files modified
3 files added
New file |
| | |
| | | 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. |
| | |
| | | # See LICENSE file for copyright and license details. |
| | | # |
| | | # vilain version |
| | | VERSION = 0.3 |
| | | VERSION = 0.7 |
| | | |
| | | # Customize below to fit your system |
| | | # paths |
| | |
| | | @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 |
| | |
| | | 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 |
| | | |
| | |
| | | #!/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" |
| | |
| | | # /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 |
| | | |
| | |
| | | [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 |
| | |
| | | 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 |
| | |
| | | import time |
| | | |
| | | CONFIGFILE = "/etc/vilain.conf" |
| | | VERSION = "0.5" |
| | | VERSION = "0.7" |
| | | vilain_table = "vilain_bruteforce" |
| | | LOGFILE = "/var/log/daemon" |
| | | |
| | |
| | | print("Only root can use this tool") |
| | | sys.exit(1) |
| | | |
| | | # Configure logging |
| | | # declare logger |
| | | logger = logging.getLogger(__name__) |
| | | |
| | | 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', |
| | | '%b %d %H:%M:%S') |
| | | '%Y-%m-%d %H:%M:%S') |
| | | log_handler.setFormatter(formatter) |
| | | logger = logging.getLogger(__name__) |
| | | logger.addHandler(log_handler) |
| | | logger.setLevel(logging.INFO) |
| | | |
| | | |
| | | # functions |
| | | def readconfig(): |
| | |
| | | |
| | | 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']) |
| | |
| | | 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') |
| | |
| | | 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'])) |
| | | |
| | |
| | | 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)) |
| | | |
| | |
| | | |
| | | |
| | | |
| | | 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 |
| | | |
| | |
| | | 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 |
| | | |
New file |
| | |
| | | #!/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 |
New file |
| | |
| | | #!/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))
|