Added FailUser to GitHub
Please welcome failUser to your Enigma 1/2 system today.master
commit
b14489a8cd
|
@ -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.
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
Loading…
Reference in New Issue