Added FailUser to GitHub

Please welcome failUser to your Enigma 1/2 system today.
master
david 2021-04-08 13:33:13 -04:00
commit b14489a8cd
4 changed files with 299 additions and 0 deletions

58
README.md Normal file
View File

@ -0,0 +1,58 @@
# failUser
This Python3 script allows you to block via IP tables bad user's who attempt to login to Enigma 1/2.
## Setup
1. Get the code, `git clone https://github.com/Beanzilla/failUser`
2. Initalize a Python Virtual Environment, `python3 -m venv env`
3. Activate Python venv `. env/bin/activate`
4. Installing what Python needs, `pip install -r require.txt`
5. Run `failUser.py` to create `failUser.cfg` (Then stop it)
6. Edit `failUser.cfg` with target Enigma 1/2 directory path, block_time in hours to block an IP.
7. Run it in the background `nohup ./failUser.py &` (Now you can close the connection yet still have failUser running)
(You can use the first step in Stopping it to tell if it's running)
## Configuration
The config `failuser.cfg` is in JSON, so just edit with your favorite text editor.
### target
This is the path to your enigma-bbs.log file. (It can be relative, but an exact is prefered)
Target the latests one not one that has .# at the very end.
### block_time
This is a whole number for how many hours to "block" the IP in the IP tables.
### last_unblock
Periodically the script will check a file it creates `blocks.json` to see if any blocked IPs need to be removed.
Please don't touch that, just let it be.
### bad_users
By default I include some common names users will wnat to try to login in as, use Enigma 1/2's configuration to flag usernames as invalid... this script will detect them and update it's config automatically.
See [here](https://nuskooler.github.io/enigma-bbs/configuration/config-hjson.html) for the Enigma 1/2's documentation on that. (Or the direct github reference within the default config [here](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config_default.js#L52))
## Running after Setup
1. Activate Python venv, `. env/bin/activate`
2. Run it in the background `nohup ./failUser.py &` (Now you can close the connection yet still have failUser running)
(You can use the first step in Stopping it to tell if it's running)
## Stopping it
1. Obtain it's Process ID (PID) via, `ps x | grep failUser`, (Your looking for the line with `python3` in it, that's failUser)
2. Issue `kill PID` Where PID is obtained from previous step (If all you see is `grep` in the line then failUser was not running)
## Debugging a crash
1. Look at `nohup.out` and or `failUser.log`
2. If needed make an issue here at this repo.

114
config.py Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env python3
from logging import basicConfig, DEBUG, INFO, WARN, ERROR, CRITICAL, getLogger
from logging.handlers import TimedRotatingFileHandler
from os.path import exists, join, dirname, abspath
from json import loads, dumps
from json.decoder import JSONDecodeError
import pendulum
# Get the full path for this file
currentdir = dirname(abspath(__file__))
# Target log file
TARGET = join("bbs", join("logs", "enigma-bbs.log"))
# Setup logging
# DEBUG, INFO, WARN, ERROR, CRITICAL
basicConfig(
level=DEBUG,
format="%(asctime)s - %(filename)s (%(lineno)d) - %(name)s - %(levelname)s - %(message)s",
handlers=[
TimedRotatingFileHandler(
filename=join(currentdir, "failUser.log"),
when="midnight",
backupCount=1,
),
#logging.StreamHandler(stream=sys.stdout),
],
)
log = getLogger("failUser")
# Config JSON
def save_config(con):
with open("failUser.cfg", "w") as f:
f.write(dumps(con, indent=4, sort_keys=False))
def load_config():
if not exists("failUser.cfg"):
now = pendulum.now().to_datetime_string()
defaults = {
# Target enigma logs
"target": "bbs/logs/enigma-bbs.log",
# block_time in hours
"block_time": 4,
# Last unblock
"last_unblock": now,
# List of bad users to detect and block
"bad_users": [
"root",
"postgres",
"mysql",
"apache",
"nginx",
],
}
save_config(defaults)
return defaults
else:
with open("failUser.cfg", "r") as f:
config = loads(f.read())
return config
# blocks in json
def add_block(ip, time):
# first load in all blocks
try:
with open("blocks.json", "r") as f:
blocks = loads(f.read())
except FileNotFoundError:
blocks = {}
pass
# add ip and time
#log.debug("Added {0} in blocks.json".format(ip))
blocks[ip] = time
# update blocks
with open("blocks.json", "w") as f:
f.write(dumps(blocks))
def rm_block(ip):
# first load all blocks
try:
with open("blocks.json", "r") as f:
blocks = loads(f.read())
except FileNotFoundError:
return
try:
if blocks[ip]:
#log.debug("Removed {0} in blocks.json".format(ip))
del blocks[ip]
# update blocks
with open("blocks.json", "w") as f:
f.write(dumps(blocks))
except KeyError:
log.debug("Unable to unblock '{0}'".format(ip))
def check_blocks():
# return a list of ips exceeding block_time in config
result = []
conf = load_config()
# load in blocks
try:
with open("blocks.json", "r") as f:
blocks = loads(f.read())
except FileNotFoundError:
return
now = pendulum.now()
for ip in blocks:
dt = pendulum.parse(blocks[ip])
#log.debug("IP={0} TIME_LEFT={1}".format(ip, abs(now.diff(dt, False).in_hours())))
if now.diff(dt).in_hours() > conf["block_time"]:
# Oops, this ip needs to be unblocked
result.append(ip)
if result:
return result

121
failUser.py Executable file
View File

@ -0,0 +1,121 @@
#!/usr/bin/env python3
from json import loads, dumps
from json.decoder import JSONDecodeError
import pendulum
from subprocess import run, PIPE
from os.path import exists, join
from pyinotify import WatchManager, Notifier, ProcessEvent
from pyinotify import IN_MODIFY, IN_DELETE, IN_MOVE_SELF, IN_CREATE
import sys
# https://github.com/manos/python-inotify-tail_example/blob/master/tail-F_inotify.py
# Branch off the logging into a seperate file
from config import log, load_config, save_config, add_block, rm_block, check_blocks
myConfig = load_config()
myfile = myConfig["target"]
last_run = myConfig["last_unblock"]
bad_users = myConfig["bad_users"]
target = open(myfile, 'r')
target.seek(0,2)
WM = WatchManager()
dirmask = IN_MODIFY | IN_DELETE | IN_MOVE_SELF | IN_CREATE
def blocker(ip):
# Utility function to block given ip as string
#run(["iptables", "-I", "DOCKER-USER", "-i", "eth0", "-s", ip, "-j", "DROP"], stdout=PIPE, check=True)
print("iptables -I DOCKER-USER -i eth0 -s {0} -j DROP".format(ip))
def unblocker(ip):
# Utility function to unblock given ip as string
#run(["iptables", "-D", "DOCKER-USER", "-i", "eth0", "-s", ip, "-j", "DROP"], stdout=PIPE, check=True)
print("iptables -D DOCKER-USER -i eth0 -s {0} -j DROP".format(ip))
def is_bad(line):
# Given line, attempt to parse... then is there a issue with it
# Returns a python dict with ip and time in log
if line: # Do we actually have something?
try:
j = loads(line)
#if j["msg"] == "Attempt to login with banned username":
if j["username"] in bad_users:
r = {}
r["ip"] = "{0}".format(j["ip"][7:])
r["time"] = j["time"]
return r
except JSONDecodeError:
log.error("Failed to decode line, '{0}'".format(line))
def checkup():
# Check all our blocks
unblocks = check_blocks()
if unblocks:
for ip in unblocks:
log.info("Unblocked {0}".format(ip))
unblocker(ip)
rm_block(ip)
class EventHandler(ProcessEvent):
def process_IN_MODIFY(self, event):
if myfile not in join(event.path, event.name):
return
else:
#luser = is_bad(target.readline().rstrip())
for line in target.readlines():
luser = is_bad(line.rstrip())
if(luser):
blocker(luser["ip"])
now = pendulum.now().to_atom_string()
log.info("Blocked {0} at {1}".format(luser["ip"], now))
add_block(luser["ip"], now)
def process_IN_MOVE_SELF(self, event):
log.debug("Log file moved... continuing read on stale log!")
def process_IN_CREATE(self, event):
global target
if myfile in join(event.path, event.name):
target.close()
target = open(myfile, 'r')
log.debug("Log file created... Catching up!")
for line in target.readlines():
luser = is_bad(line.rstrip())
if(luser):
blocker(luser["ip"])
now = pendulum.now().to_atom_string()
log.info("Blocked {0} at {1}".format(luser["ip"], now))
add_block(luser["ip"], now)
target.seek(0,2)
return
notifier = Notifier(WM, EventHandler())
index = myfile.rfind("/")
WM.add_watch(myfile[:index], dirmask)
last = pendulum.parse(last_run)
while True:
try:
now = pendulum.now()
if now.diff(last).in_hours() > 1:
last = now
checkup()
notifier.process_events()
if notifier.check_events():
notifier.read_events()
except KeyboardInterrupt:
break
# Issue stop on event system
notifier.stop()
target.close()
# Update config
myConfig["last_unblock"] = last.to_atom_string()
save_config(myConfig)
exit(0)

6
require.txt Normal file
View File

@ -0,0 +1,6 @@
pendulum==2.1.2
pkg-resources==0.0.0
pyinotify==0.9.6
python-dateutil==2.8.1
pytzdata==2020.1
six==1.15.0