From 54613da01ddf0f14527822ced5b9bd750b825fb6 Mon Sep 17 00:00:00 2001
From: Thuban <thuban@yeuxdelibad.net>
Date: Sat, 01 Dec 2018 09:17:24 +0000
Subject: [PATCH] exemples en +

---
 vilain.conf     |   29 +++++
 vilain.py       |   64 +++++++-----
 Makefile        |   12 +
 vilainreport.py |  131 ++++++++++++++++++++++++++
 vilain          |    2 
 LICENSE         |   21 ++++
 vilainreport    |   11 ++
 7 files changed, 236 insertions(+), 34 deletions(-)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b663197
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Makefile b/Makefile
index e153e0b..92f620b 100644
--- a/Makefile
+++ b/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
diff --git a/vilain b/vilain
index d385066..9c2841c 100755
--- a/vilain
+++ b/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"
diff --git a/vilain.conf b/vilain.conf
index ecfec45..1e9a7e5 100644
--- a/vilain.conf
+++ b/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
diff --git a/vilain.py b/vilain.py
index d36c59f..082b15f 100755
--- a/vilain.py
+++ b/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
-
diff --git a/vilainreport b/vilainreport
new file mode 100644
index 0000000..7d6e7a8
--- /dev/null
+++ b/vilainreport
@@ -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
diff --git a/vilainreport.py b/vilainreport.py
new file mode 100644
index 0000000..11a974f
--- /dev/null
+++ b/vilainreport.py
@@ -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))

--
Gitblit v1.9.3