modules and filters POC

rust-rewrite
Y 2019-11-23 23:10:40 +01:00
parent 8aaa04389f
commit c99c3e111c
59 changed files with 261 additions and 2339 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true

11
.gitignore vendored
View File

@ -1 +1,10 @@
**/__pycache__/
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "pyruse"
version = "2.1.0"
authors = ["Y. <theYinYeti@yalis.fr>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4"
serde_json = "1.0"
serde_yaml = "0.8"
systemd = "0.4.0"

View File

@ -1,33 +0,0 @@
from distutils.core import setup
setup(
name='pyruse',
version='1.0',
license='GPL-3',
description='Route systemd-journal logs to filters and actions (ban, report…)',
long_description='''
================
Python peruser of systemd-journal
================
This program is intended to be used as a lightweight replacement for both epylog and fail2ban.
The wanted features are these:
* Peruse all log entries from systemds journal, and only those (ie: no log files).
* Passively wait on new entries; no active polling.
* Filter-out uninteresting log lines according to the settings.
* Act on matches in the journal, with some pre-defined actions.
* Create a daily report with 2 parts:
- events of interest (according to the settings),
- and other non-filtered-out log entries.
* Send an immediate email when something important happens (according to the settings).
''',
author='Yves G.',
author_email='theYinYeti@yalis.fr',
maintainer='Yves G.',
maintainer_email='theYinYeti@yalis.fr',
url='https://yalis.fr/git/yves/pyruse',
download_url='https://yalis.fr/git/yves/pyruse',
packages=['pyruse', 'pyruse.actions', 'pyruse.filters'],
)

View File

@ -1,22 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import datetime
from pyruse import base, counter
class Action(base.Action, counter.Counter):
def __init__(self, args):
base.Action.__init__(self)
counter.Counter.__init__(self, args["counter"])
self.keyName = args["for"]
self.save = args.get("save", None)
keepSeconds = args.get("keepSeconds", None)
if keepSeconds:
self.keepSeconds = datetime.timedelta(seconds = keepSeconds)
else:
self.keepSeconds = None
def act(self, entry):
count = self.augment(entry[self.keyName], self.keepSeconds)
if self.save:
entry[self.save] = count

View File

@ -1,22 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import datetime
from pyruse import base, counter
class Action(base.Action, counter.Counter):
def __init__(self, args):
base.Action.__init__(self)
counter.Counter.__init__(self, args["counter"])
self.keyName = args["for"]
self.save = args.get("save", None)
graceSeconds = args.get("graceSeconds", None)
if graceSeconds:
self.graceSeconds = datetime.timedelta(seconds = graceSeconds)
else:
self.graceSeconds = None
def act(self, entry):
self.reset(entry[self.keyName], self.graceSeconds)
if self.save:
entry[self.save] = 0

View File

@ -1,177 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import json
import os
import string
from collections import OrderedDict
from datetime import datetime
from enum import Enum, unique
from pyruse import base, config, email
@unique
class Details(Enum):
NONE = [ lambda l: [] ]
FIRST = [ lambda l: ["From : " + str(t) for t in l[:1]] ]
LAST = [ lambda l: ["Until: " + str(t) for t in l[-1:]] ]
FIRSTLAST = [ lambda l: ["From : " + str(l[0]), "Until: " + str(l[-1])] if len(l) > 1 else [str(t) for t in l] ]
ALL = [ lambda l: [str(t) for t in l] ]
def __init__(self, wrapper):
self.fn = wrapper[0]
def toAdoc(self, times):
return " +\n ".join(str(t) for t in self.fn(times))
def toHtml(self, times):
return "<br/>".join(str(t) for t in self.fn(times))
class Action(base.Action):
_storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \
+ "/" + os.path.basename(__file__) + ".journal"
_out = None
_hour = 0
_txtDocStart = '= Pyruse Report\n\n'
_txtHeadWarn = '== WARNING Messages\n\n'
_txtHeadInfo = '\n== Information Messages\n\n'
_txtHeadOther = '\n== Other log events\n\n'
_txtTableDelim = '|===============================================================================\n'
_txtTableHeader = '|Count|Message |Date+time for each occurrence\n'
_txtPreDelim = '----------\n'
_htmDocStart = '<html>\n<head><meta charset="utf-8"/><style type="text/css">td{vertical-align: top}</style></head>\n<body>\n<h1>Pyruse Report</h1>\n'
_htmDocStop = '</body></html>'
_htmHeadWarn = '<h2>WARNING Messages</h2>\n'
_htmHeadInfo = '<h2>Information Messages</h2>\n'
_htmHeadOther = '<h2>Other log events</h2>\n'
_htmTableStart = '<table border="1">\n<tr><th>Count</th><th>Message</th><th>Date+time for each occurrence</th></tr>\n'
_htmTableStop = '</table>\n'
_htmPreStart = '<pre>'
_htmPreStop = '</pre>\n'
def _closeJournal():
Action._out.write("{}]")
Action._out.close()
Action._out = None
def _openJournal():
if Action._out is None:
if os.path.exists(Action._storage):
Action._out = open(Action._storage, "a", 1)
else:
Action._out = open(Action._storage, "w", 1)
Action._out.write("[\n")
def __init__(self, args):
super().__init__()
l = args["level"]
if l == "WARN":
self.level = 1
elif l == "INFO":
self.level = 2
else:
self.level = 0
self.template = args["message"]
values = {}
for (_void, name, _void, _void) in string.Formatter().parse(self.template):
if name:
values[name] = None
self.values = values
ts = args.get("details", Details.ALL.name)
for e in Details:
if ts == e.name:
self.details = e
break
else:
self.details = Details.ALL
def act(self, entry):
for (name, _void) in self.values.items():
self.values[name] = entry.get(name, None)
msg = self.template.format_map(self.values)
json.dump(
OrderedDict(L = self.level, T = entry["__REALTIME_TIMESTAMP"].timestamp(), M = msg, D = self.details.name),
Action._out
)
Action._out.write(",\n")
thisHour = datetime.today().hour
if thisHour < Action._hour:
Action._closeJournal()
self._sendReport()
Action._openJournal()
Action._hour = thisHour
def _encode(self, text):
return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def _toAdoc(self, msg, times):
return "\n|{count:^5d}|{text}\n |{times}\n".format_map({
"count": sum(len(t) for (_void, t) in times.items()),
"text": msg,
"times": "\n +\n ".join(d.toAdoc(t) for (d,t) in times.items())
})
def _toHtml(self, msg, times):
return "<tr><td>{count}</td><td>{text}</td><td>{times}</td></tr>\n".format_map({
"count": sum(len(t) for (_void, t) in times.items()),
"text": self._encode(msg),
"times": "<br/><br/>".join(d.toHtml(t) for (d,t) in times.items())
})
def _sendReport(self):
messages = [[], {}, {}]
with open(Action._storage) as journal:
for e in json.load(journal):
if e != {}:
(L, T, M, D) = (e["L"], datetime.fromtimestamp(e["T"]), e["M"], e.get("D", Details.ALL.name))
if L == 0:
messages[0].append((T, M))
else:
dd = Details[D]
if M not in messages[L]:
messages[L][M] = {}
if dd not in messages[L][M]:
messages[L][M][dd] = []
messages[L][M][dd].append(T)
os.remove(Action._storage)
html = Action._htmDocStart + Action._htmHeadWarn
text = Action._txtDocStart + Action._txtHeadWarn
text += Action._txtTableDelim + Action._txtTableHeader
html += Action._htmTableStart
for (msg, times) in sorted(messages[1].items(), key = lambda i: i[0]):
text += self._toAdoc(msg, times)
html += self._toHtml(msg, times)
text += Action._txtTableDelim
html += Action._htmTableStop
text += Action._txtHeadInfo
html += Action._htmHeadInfo
text += Action._txtTableDelim + Action._txtTableHeader
html += Action._htmTableStart
for (msg, times) in sorted(messages[2].items(), key = lambda i: i[0]):
text += self._toAdoc(msg, times)
html += self._toHtml(msg, times)
text += Action._txtTableDelim
html += Action._htmTableStop
text += Action._txtHeadOther
html += Action._htmHeadOther
text += Action._txtPreDelim
html += Action._htmPreStart
for (time, msg) in messages[0]:
m = '%s: %s\n' % (time, msg)
text += m
html += self._encode(m)
text += Action._txtPreDelim
html += Action._htmPreStop
html += Action._htmDocStop
email.Mail(text, html).send()
Action._openJournal()

View File

@ -1,18 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base, dnat
class Action(base.Action, dnat.Mapper):
def __init__(self, args):
base.Action.__init__(self)
sa = (args ["saddr"], None )
sp = (args.get("sport", None), None )
a = (args.get("addr", None), args.get("addrValue", None))
p = (args.get("port", None), args.get("portValue", None))
da = (args.get("daddr", None), args.get("daddrValue", None))
dp = (args.get("dport", None), args.get("dportValue", None))
dnat.Mapper.__init__(self, sa, sp, a, p, da, dp, args.get("keepSeconds", 63))
def act(self, entry):
self.map(entry)

View File

@ -1,18 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base, dnat
class Action(base.Action, dnat.Matcher):
def __init__(self, args):
base.Action.__init__(self)
sa = args ["saddrInto"]
sp = args.get("sportInto", None)
a = args.get("addr", None)
p = args.get("port", None)
da = args.get("daddr", None)
dp = args.get("dport", None)
dnat.Matcher.__init__(self, a, p, da, dp, sa, sp)
def act(self, entry):
self.replace(entry)

View File

@ -1,22 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import string
from pyruse import base, email
class Action(base.Action):
def __init__(self, args):
super().__init__()
self.subject = args.get("subject", "Pyruse Notification")
self.template = args["message"]
values = {}
for (_void, name, _void, _void) in string.Formatter().parse(self.template):
if name:
values[name] = None
self.values = values
def act(self, entry):
for (name, _void) in self.values.items():
self.values[name] = entry.get(name, None)
msg = self.template.format_map(self.values)
email.Mail(msg).setSubject(self.subject).send()

View File

@ -1,37 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import os
import subprocess
from pyruse import ban, base, config
class Action(base.Action, ban.NetfilterBan):
_storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \
+ "/" + os.path.basename(__file__) + ".json"
_ipset = config.Config().asMap().get("ipsetBan", {}).get("ipset", ["/usr/bin/ipset", "-exist", "-quiet"])
def __init__(self, args):
base.Action.__init__(self)
ban.NetfilterBan.__init__(self, Action._storage)
if args is None:
return # on-boot configuration
ipv4Set = args["ipSetIPv4"]
ipv6Set = args["ipSetIPv6"]
field = args["IP"]
banSeconds = args.get("banSeconds", None)
self.initSelf(ipv4Set, ipv6Set, field, banSeconds)
def act(self, entry):
ban.NetfilterBan.act(self, entry)
def setBan(self, nfSet, ip, seconds):
cmd = list(Action._ipset)
cmd.extend(["add", nfSet, ip])
if seconds > 0:
cmd.extend(["timeout", str(seconds)])
subprocess.run(cmd)
def cancelBan(self, nfSet, ip):
cmd = list(Action._ipset)
cmd.extend(["del", nfSet, ip])
subprocess.run(cmd)

View File

@ -1,22 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import string
from pyruse import base, log
class Action(base.Action):
def __init__(self, args):
super().__init__()
self.level = log.Level[args.get("level", log.Level.INFO.name)]
self.template = args["message"]
values = {}
for (_void, name, _void, _void) in string.Formatter().parse(self.template):
if name:
values[name] = None
self.values = values
def act(self, entry):
for (name, _void) in self.values.items():
self.values[name] = entry.get(name, None)
msg = self.template.format_map(self.values)
log.log(self.level, msg)

View File

@ -1,39 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import os
import subprocess
from pyruse import ban, base, config
class Action(base.Action, ban.NetfilterBan):
_storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \
+ "/" + os.path.basename(__file__) + ".json"
_nft = config.Config().asMap().get("nftBan", {}).get("nft", ["/usr/bin/nft"])
def __init__(self, args):
base.Action.__init__(self)
ban.NetfilterBan.__init__(self, Action._storage)
if args is None:
return # on-boot configuration
ipv4Set = args["nftSetIPv4"]
ipv6Set = args["nftSetIPv6"]
field = args["IP"]
banSeconds = args.get("banSeconds", None)
self.initSelf(ipv4Set, ipv6Set, field, banSeconds)
def act(self, entry):
ban.NetfilterBan.act(self, entry)
def setBan(self, nfSet, ip, seconds):
if seconds == 0:
timeout = ""
else:
timeout = " timeout %ss" % seconds
cmd = list(Action._nft)
cmd.append("add element %s {%s%s}" % (nfSet, ip, timeout))
subprocess.run(cmd)
def cancelBan(self, nfSet, ip):
cmd = list(Action._nft)
cmd.append("delete element %s {%s}" % (nfSet, ip))
subprocess.run(cmd)

View File

@ -1,11 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base
class Action(base.Action):
def __init__(self, args):
super().__init__()
def act(self, entry):
pass

View File

@ -1,85 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import abc
import datetime
import json
class NetfilterBan(abc.ABC):
def __init__(self, storage):
self.storage = storage
def initSelf(self, ipv4Set, ipv6Set, ipField, banSeconds):
self.ipv4Set = ipv4Set
self.ipv6Set = ipv6Set
self.field = ipField
self.banSeconds = banSeconds
@abc.abstractmethod
def setBan(self, nfSet, ip, seconds):
pass
@abc.abstractmethod
def cancelBan(self, nfSet, ip):
pass
def act(self, entry):
ip = entry[self.field]
nfSet = self.ipv6Set if ":" in ip else self.ipv4Set
newBan = {"IP": ip, "nfSet": nfSet}
now = datetime.datetime.utcnow()
bans = []
previousTS = None
try:
with open(self.storage) as dataFile:
for ban in json.load(dataFile):
if ban["timestamp"] > 0 and ban["timestamp"] <= now.timestamp():
continue
elif {k: ban[k] for k in newBan.keys()} == newBan:
# should not happen, since the IP is banned…
previousTS = ban["timestamp"]
else:
bans.append(ban)
except IOError:
pass # new file
if previousTS is not None:
try:
self.cancelBan(nfSet, ip)
except Exception:
pass # too late: not a problem
if self.banSeconds:
until = now + datetime.timedelta(seconds = self.banSeconds)
newBan["timestamp"] = until.timestamp()
timeout = self.banSeconds
else:
newBan["timestamp"] = 0
timeout = 0
self.setBan(nfSet, ip, timeout)
bans.append(newBan)
with open(self.storage, "w") as dataFile:
json.dump(bans, dataFile)
def boot(self):
now = int(datetime.datetime.utcnow().timestamp())
bans = []
try:
with open(self.storage) as dataFile:
for ban in json.load(dataFile):
if ban["timestamp"] == 0:
self.setBan(ban["nfSet"], ban["IP"], 0)
bans.append(ban)
elif int(ban["timestamp"]) <= now:
continue
else:
timeout = int(ban["timestamp"]) - now
self.setBan(ban["nfSet"], ban["IP"], timeout)
bans.append(ban)
except IOError:
pass # no file
with open(self.storage, "w") as dataFile:
json.dump(bans, dataFile)

View File

@ -1,56 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import abc
from pyruse import log
class Step(abc.ABC):
def __init__(self):
self.nextStep = None
@abc.abstractmethod
def run(self, entry):
pass
def setNextStep(self, obj):
self.nextStep = obj
def setStepName(self, name):
self.stepName = name
class Filter(Step):
def __init__(self):
super().__init__()
self.altStep = None
def setAltStep(self, obj):
self.altStep = obj
@abc.abstractmethod
def filter(self, entry):
pass
def run(self, entry):
try:
nextStep = self.nextStep if self.filter(entry) else self.altStep
except Exception as e:
nextStep = self.altStep
log.error("Error while executing %s (%s): %s." % (type(self), self.stepName, str(e)))
return nextStep
class Action(Step):
def __init__(self):
super().__init__()
@abc.abstractmethod
def act(self, entry):
pass
def run(self, entry):
try:
self.act(entry)
nextStep = self.nextStep
except Exception as e:
nextStep = None
log.error("Error while executing %s (%s): %s." % (type(self), self.stepName, str(e)))
return nextStep

View File

@ -1,34 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import json
import os
from collections import OrderedDict
from pyruse import log
class Config:
CONF_NAME = "pyruse.json"
_paths = None
# __main__ must be the first to create a Config object, then paths are remembered
def __init__(self, paths = None):
if paths is None:
paths = Config._paths
Config._paths = paths
for p in paths:
confpath = os.path.join(p, Config.CONF_NAME)
try:
with open(confpath) as conffile:
conf = json.load(conffile, object_pairs_hook = OrderedDict)
self.conf = conf
break
except IOError:
log.debug("IOError while opening %s\n" % confpath)
except json.JSONDecodeError:
log.debug("JSONDecodeError while opening %s\n" % confpath)
else:
raise FileNotFoundError("File `%s` not found in either of %s." \
% (Config.CONF_NAME, str(paths)))
def asMap(self):
return self.conf

View File

@ -1,106 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import datetime
class _GraceAndTicks():
def __init__(self):
self.grace = None
self.ticks = []
class _CounterData():
def __init__(self):
self._keyVals = {}
def clean(self, refDT):
for k in list(self._keyVals.keys()):
v = self._keyVals[k]
if v.grace and v.grace <= refDT:
v.grace = None
if v.grace is None:
# None values (∞) are at the end of the list
for i in range(0, len(v.ticks)):
if v.ticks[i] and v.ticks[i] <= refDT:
continue
v.ticks = v.ticks[i:]
break
else:
del self._keyVals[k]
def graceActive(self, counterKey, refDT):
self.clean(refDT)
return counterKey in self._keyVals and self._keyVals[counterKey].grace
def augment(self, counterKey, refDT, until):
self.clean(refDT)
if counterKey in self._keyVals:
v = self._keyVals[counterKey]
if v.grace:
return 0
else:
v = _GraceAndTicks()
self._keyVals[counterKey] = v
l = len(v.ticks)
# chances are that until is greater than the last item
for i in range(l, 0, -1):
if until is None or (v.ticks[i - 1] and v.ticks[i - 1] < until):
v.ticks.insert(i, until)
break
else:
v.ticks.insert(0, until)
return l + 1
def lower(self, counterKey, refDT):
self.clean(refDT)
v = self._keyVals.get(counterKey, None)
if v is None or v.grace:
return 0
l = len(v.ticks)
if l == 1:
del self._keyVals[counterKey]
return 0
v.ticks.pop()
return l - 1
def reset(self, counterKey, refDT, graceUntil):
self.clean(refDT)
if graceUntil:
v = _GraceAndTicks()
v.grace = graceUntil
self._keyVals[counterKey] = v
elif counterKey in self._keyVals:
del self._keyVals[counterKey]
class Counter():
_counters = {}
def __init__(self, counter):
if counter not in Counter._counters:
Counter._counters[counter] = _CounterData()
self.counter = Counter._counters[counter]
def augment(self, counterKey, duration = None):
now = datetime.datetime.utcnow()
if self.counter.graceActive(counterKey, now):
return 0
else:
return self.counter.augment(counterKey, now, now + duration if duration else None)
def lower(self, counterKey):
now = datetime.datetime.utcnow()
if self.counter.graceActive(counterKey, now):
return 0
else:
return self.counter.lower(counterKey, now)
def reset(self, counterKey, graceDuration = None):
now = datetime.datetime.utcnow()
self.counter.reset(
counterKey, now,
now + graceDuration if graceDuration else None
)

View File

@ -1,97 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from datetime import datetime
_mappings = []
def _cleanMappings():
global _mappings
now = int(datetime.today().timestamp())
_mappings = [m for m in _mappings if (now >> m["bits"]) <= m["time"]]
def putMapping(mapping):
global _mappings
_cleanMappings()
_mappings.append(mapping)
def getMappings():
global _mappings
_cleanMappings()
return _mappings
def periodBits(keepSeconds):
seconds, bits = keepSeconds, 0
while seconds:
bits += 1
seconds = seconds >> 1
return bits # number of significant bits in keepSeconds
def valueFor(spec, entry):
return spec[1] if spec[0] is None else entry.get(spec[0], spec[1])
class Mapper():
def __init__(self, saddr, sport, addr, port, daddr, dport, keepSeconds):
for spec in [saddr, addr]:
if spec[0] is None and spec[1] is None:
raise ValueError("Neither field nor value was specified for address")
self.saddr = saddr
self.sport = sport
self.addr = addr
self.port = port
self.daddr = daddr
self.dport = dport
self.keepBits = periodBits(keepSeconds)
def map(self, entry):
saddr = valueFor(self.saddr, entry)
addr = valueFor(self.addr, entry)
if saddr is None or addr is None:
return
sport = valueFor(self.sport, entry)
port = valueFor(self.port, entry)
daddr = valueFor(self.daddr, entry)
dport = valueFor(self.dport, entry)
putMapping(dict(
bits = self.keepBits,
time = 1 + (int(entry["__REALTIME_TIMESTAMP"].timestamp()) >> self.keepBits),
saddr = saddr, sport = sport,
addr = addr, port = port,
daddr = daddr, dport = dport
))
class Matcher():
def __init__(self, addr, port, daddr, dport, saddr, sport):
if addr is None and port is None and daddr is None and dport is None:
raise ValueError("No field was provided on which to do the matching")
if saddr is None and sport is None:
raise ValueError("No field was provided in which to store the translated values")
matchers = []
updaters = []
if addr is not None:
matchers.append((addr, "addr"))
if port is not None:
matchers.append((port, "port"))
if daddr is not None:
matchers.append((daddr, "daddr"))
if dport is not None:
matchers.append((dport, "dport"))
if saddr is not None:
updaters.append((saddr, "saddr"))
if sport is not None:
updaters.append((sport, "sport"))
self.matchers = matchers
self.updaters = updaters
def replace(self, entry):
for field, _void in self.matchers:
if field not in entry:
return
for mapping in getMappings():
for field, mapEntry in self.matchers:
if entry[field] != mapping[mapEntry]:
break
else:
for field, mapEntry in self.updaters:
entry[field] = mapping[mapEntry]
return

View File

@ -1,37 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import subprocess
from email.headerregistry import Address
from email.message import EmailMessage
from pyruse import config
class Mail:
_mailConf = config.Config().asMap().get("email", {})
def __init__(self, text, html = None):
self.text = text
self.html = html
self.mailSubject = Mail._mailConf.get("subject", "Pyruse Report")
self.mailFrom = Mail._mailConf.get("from", "pyruse")
self.mailTo = Mail._mailConf.get("to", ["hostmaster"])
def setSubject(self, subject):
if subject:
self.mailSubject = subject
return self
def send(self):
message = EmailMessage()
message["Subject"] = self.mailSubject
message["From"] = Address(addr_spec = self.mailFrom)
message["To"] = (Address(addr_spec = a) for a in self.mailTo)
message.set_content(self.text, cte = "quoted-printable")
if self.html:
message.add_alternative(self.html, subtype = "html", cte = "quoted-printable")
subprocess.run(
Mail._mailConf.get("sendmail", ["/usr/bin/sendmail", "-t"]),
input = message.as_bytes()
)

View File

@ -1,13 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
self.value = args["value"]
def filter(self, entry):
return entry[self.field] == self.value if self.field in entry else False

View File

@ -1,13 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
self.value = args["value"]
def filter(self, entry):
return entry[self.field] >= self.value if self.field in entry else False

View File

@ -1,13 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
self.values = args["values"]
def filter(self, entry):
return entry.get(self.field, None) in self.values

View File

@ -1,50 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import socket
from functools import reduce
from pyruse import base
class Filter(base.Filter):
ipReducer = lambda bits, byte: bits<<8 | byte
def __init__(self, args):
super().__init__()
self.field = args["field"]
ip4Nets = []
ip6Nets = []
for net in args["nets"]:
if ":" in net:
ip6Nets.append(self._toNetAndMask(socket.AF_INET6, 128, net))
else:
ip4Nets.append(self._toNetAndMask(socket.AF_INET, 32, net))
self.ip4Nets = ip4Nets
self.ip6Nets = ip6Nets
def filter(self, entry):
if self.field not in entry:
return False
ip = entry[self.field]
if ":" in ip:
return self._filter(socket.AF_INET6, ip, self.ip6Nets)
else:
return self._filter(socket.AF_INET, ip, self.ip4Nets)
def _filter(self, family, ip, nets):
for (net, mask) in nets:
numericIP = self._numericIP(family, ip)
if numericIP & mask == net:
return True
return False
def _toNetAndMask(self, family, bits, net):
if "/" in net:
ip, mask = net.split("/")
else:
ip, mask = net, bits
numericMask = ((1<<int(mask))-1)<<(bits-int(mask))
numericIP = self._numericIP(family, ip)
return numericIP & numericMask, numericMask
def _numericIP(self, family, ipString):
return reduce(Filter.ipReducer, socket.inet_pton(family, ipString))

View File

@ -1,13 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
self.value = args["value"]
def filter(self, entry):
return entry[self.field] <= self.value if self.field in entry else False

View File

@ -1,21 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import re
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
self.re = re.compile(args["re"])
self.save = args.get("save", [])
def filter(self, entry):
match = self.re.search(entry.get(self.field, ""))
if match:
for group, name in enumerate(self.save, start = 1):
entry[name] = match.group(group)
for name, value in match.groupdict().items():
entry[name] = value
return match

View File

@ -1,23 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import re
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
reList = []
for item in args["re"]:
reList.append(re.compile(item))
self.reList = reList
def filter(self, entry):
for item in self.reList:
match = item.search(entry.get(self.field, ""))
if match:
for name, value in match.groupdict().items():
entry[name] = value
return True
return False

View File

@ -1,17 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import pwd
from pyruse import base
class Filter(base.Filter):
def __init__(self, args):
super().__init__()
self.field = args["field"]
def filter(self, entry):
try:
pwd.getpwnam(entry.get(self.field, ""))
return True
except KeyError:
return False

View File

@ -1,28 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from enum import Enum, unique
from systemd import journal
@unique
class Level(Enum):
EMERG = 0 # System is unusable.
ALERT = 1 # Action must be taken immediately.
CRIT = 2 # Critical conditions, such as hard device errors.
ERR = 3 # Error conditions.
WARNING = 4 # Warning conditions.
NOTICE = 5 # Normal but significant conditions.
INFO = 6 # Informational messages.
DEBUG = 7
def log(level, string):
journal.send(string, PRIORITY = level.value)
def debug(string):
log(Level.DEBUG, string)
def notice(string):
log(Level.NOTICE, string)
def error(string):
log(Level.ERR, string)

View File

@ -1,53 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import os
import sys
from systemd import journal
from pyruse import config, module, workflow
PYRUSE_ENVVAR = "PYRUSE_EXTRA"
PYRUSE_PATHS = []
def _setPyrusePaths():
global PYRUSE_ENVVAR, PYRUSE_PATHS
for p in "/etc/pyruse", os.environ.get(PYRUSE_ENVVAR):
if p and os.path.isdir(p):
PYRUSE_PATHS.insert(0, p)
sys.path.insert(1, p)
PYRUSE_PATHS.insert(0, os.curdir)
def _doForEachJournalEntry(workflow):
enc8b = config.Config().asMap().get("8bit-message-encoding", "iso-8859-1")
j = journal.Reader(journal.SYSTEM_ONLY)
j.seek_tail()
j.get_previous()
while True:
event = j.wait(None)
if event == journal.APPEND:
for entry in j:
m = entry['MESSAGE']
if not isinstance(m, str):
entry['MESSAGE'] = m.decode(enc8b)
step = workflow.firstStep
while step is not None:
step = step.run(entry)
def boot(modName):
_setPyrusePaths()
conf = config.Config(PYRUSE_PATHS)
if "action_" in modName:
module.get({"action": modName, "args": None}).module.boot()
elif "filter_" in modName:
module.get({"filter": modName, "args": None}).module.boot()
else:
raise ValueError("Neither “action_” nor “filter_” found in the module name; the `boot` feature cannot work for %s\n" % modName)
def main():
_setPyrusePaths()
conf = config.Config(PYRUSE_PATHS).asMap().get("actions", {})
wf = workflow.Workflow(conf)
_doForEachJournalEntry(wf)
if __name__ == '__main__':
main()

View File

@ -1,41 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import importlib
from pyruse import log
_modules = {}
class Module:
def __init__(self, isAction, module, thenRun, elseRun):
self.isAction = isAction
self.isFilter = not isAction
self.module = module
self.thenRun = thenRun
self.elseRun = elseRun
def get(moduleDesc):
if "filter" in moduleDesc:
isAction = False
mod = _getModule("pyruse.filters." + moduleDesc["filter"])
obj = mod.Filter(moduleDesc.get("args", {}))
elseRun = moduleDesc["else"] if "else" in moduleDesc else None
elif "action" in moduleDesc:
isAction = True
mod = _getModule("pyruse.actions." + moduleDesc["action"])
obj = mod.Action(moduleDesc.get("args", {}))
elseRun = None
else:
raise ValueError("Step is neither “filter” nor “action”: %s\n" % str(moduleDesc))
thenRun = moduleDesc["then"] if "then" in moduleDesc else None
return Module(isAction, obj, thenRun, elseRun)
def _getModule(modName):
if modName not in _modules:
try:
module = importlib.import_module(modName)
except ImportError as e:
log.error("Module %s not found.\n" % modName)
raise e
_modules[modName] = module
return _modules[modName]

View File

@ -1,73 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse import base, config, log, module
class Workflow:
def __init__(self, actions):
self._withDebug = config.Config().asMap().get("debug", False)
seen = {}
dangling = []
firstStep = None
for label in actions:
if not label in seen:
(entryPoint, seen, newDangling) = self._initChain(actions, label, seen, (label,))
if firstStep is None:
firstStep = entryPoint
elif len(dangling) > 0:
for setter in dangling:
setter(entryPoint)
dangling = newDangling
self.firstStep = firstStep
def _initChain(self, actions, label, seen, wholeChain):
dangling = []
previousSetter = None
firstStep = None
isPreviousDangling = False
isThenCalled = False
for stepNum, step in enumerate(actions[label]):
if isThenCalled:
break
mod = module.get(step)
obj = mod.module
if self._withDebug:
obj.setStepName(label + '[' + str(stepNum) + ']')
if mod.thenRun:
(seen, dangling) = \
self._branchToChain(
obj.setNextStep, mod.thenRun, wholeChain,
actions, seen, dangling)
isThenCalled = True
if mod.isFilter:
if mod.elseRun:
(seen, dangling) = \
self._branchToChain(
obj.setAltStep, mod.elseRun, wholeChain,
actions, seen, dangling)
else:
dangling.append(obj.setAltStep)
isPreviousDangling = mod.isFilter and not isThenCalled
if previousSetter:
previousSetter(obj)
else:
firstStep = obj
previousSetter = obj.setNextStep
if isPreviousDangling:
dangling.append(previousSetter)
seen[label] = firstStep if len(dangling) == 0 else None
return (firstStep, seen, dangling)
def _branchToChain(self, parentSetter, branchName, wholeChain, actions, seen, dangling):
if branchName in wholeChain:
raise RecursionError("Loop found in actions: %s\n" % str(wholeChain + (branchName,)))
elif branchName in seen and seen[branchName] is not None:
parentSetter(seen[branchName])
elif branchName in actions:
(entryPoint, seen, newDangling) = \
self._initChain(actions, branchName, seen, wholeChain + (branchName,))
parentSetter(entryPoint)
dangling.extend(newDangling)
else:
raise ValueError("Action chain not found: %s\n" % branchName)
return (seen, dangling)

17
src/actions/mod.rs Normal file
View File

@ -0,0 +1,17 @@
mod noop;
pub use self::noop::*;
/*
pub trait Action {
fn act(&self, record: &mut Record) -> Result<(), ()>;
}
impl<T: Action> Module for T {
fn run(&self, record: &mut Record) -> Result<bool, ()> {
match self.act(record) {
Ok(()) => Ok(true),
Err(()) => Err(())
}
}
}
*/

38
src/actions/noop.rs Normal file
View File

@ -0,0 +1,38 @@
use crate::modules::{Module,ModuleArgs};
use crate::common::Record;
#[derive(Debug)]
pub struct Noop {}
impl Noop {
pub fn from_args(mut _args: ModuleArgs) -> Noop {
Noop {}
}
}
impl Module for Noop {
fn run(&self, _record: &mut Record) -> Result<bool, ()> {
Ok(true)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::common::Record;
use crate::actions::Noop;
use crate::modules::{Module,ModuleArgs};
fn generate_empty_args_record() -> (ModuleArgs<'static>, Record<'static>) {
let args = HashMap::with_capacity(0);
let record = HashMap::with_capacity(0);
(args, record)
}
#[test]
fn noop_does_nothing() {
let (args, mut record) = generate_empty_args_record();
let action = Noop::from_args(args);
assert!(action.run(&mut record).unwrap());
}
}

12
src/common.rs Normal file
View File

@ -0,0 +1,12 @@
use chrono::DateTime;
use std::collections::HashMap;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Value {
Bool(bool),
Str(String),
Int(isize),
Date(DateTime<chrono::Utc>)
}
pub type Record<'a> = HashMap<&'a str, Value>;

18
src/config/mod.rs Normal file
View File

@ -0,0 +1,18 @@
use crate::common::Record;
use crate::modules::{ModuleArgs,ModuleType};
pub struct Config<'a> {
actions: Vec<Chain<'a>>,
options: Record<'a>
}
pub struct Chain<'a> {
name: String,
steps: Vec<Step<'a>>
}
pub struct Step<'a> {
module_name: String,
module_type: ModuleType,
args: ModuleArgs<'a>
}

99
src/filters/equals.rs Normal file
View File

@ -0,0 +1,99 @@
use crate::modules::{Module,ModuleArgs};
use crate::common::Record;
use crate::common::Value;
#[derive(Debug)]
pub struct Equals {
field: String,
value: Value
}
impl Equals {
pub fn from_args(mut args: ModuleArgs) -> Equals {
Equals {
field: match args.remove("field") {
Some(Value::Str(s)) => s,
_ => panic!("The Equals filter needs a field to filter in “field”")
},
value: args.remove("value").expect("The Equals filter needs a reference value in “value”")
}
}
}
impl Module for Equals {
fn run(&self, record: &mut Record) -> Result<bool, ()> {
match (record.get(&self.field.as_ref()), &self.value) {
(Some(ref v1), ref v2) => Ok(v1 == v2),
(None, _) => Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use chrono::Utc;
use std::collections::HashMap;
use crate::common::{Record,Value};
use crate::filters::Equals;
use crate::modules::{Module,ModuleArgs};
fn generate_args_record_equal<'a>(name: &'a str, value: Value) -> (ModuleArgs<'static>, Record<'a>) {
let mut args = HashMap::with_capacity(2);
args.insert("field", Value::Str(String::from(name)));
args.insert("value", value.clone());
let mut record = HashMap::with_capacity(1);
record.insert(name, value);
(args, record)
}
fn generate_args_record_custom<'a>(ref_name: &str, ref_value: Value, test_name: &'a str, test_value: Value) -> (ModuleArgs<'static>, Record<'a>) {
let mut args = HashMap::with_capacity(2);
args.insert("field", Value::Str(String::from(ref_name)));
args.insert("value", ref_value);
let mut record = HashMap::with_capacity(1);
record.insert(test_name, test_value);
(args, record)
}
#[test]
fn filter_equals_should_return_true() {
let (args, mut record) = generate_args_record_equal("a_boolean", Value::Bool(false));
let filter = Equals::from_args(args);
assert!(filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_equal("a_string", Value::Str(String::from("Hello!")));
let filter = Equals::from_args(args);
assert!(filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_equal("an_integer", Value::Int(2));
let filter = Equals::from_args(args);
assert!(filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_equal("a_date", Value::Date(Utc::now()));
let filter = Equals::from_args(args);
assert!(filter.run(&mut record).unwrap());
}
#[test]
fn filter_equals_should_return_false() {
let (args, mut record) = generate_args_record_custom("a_boolean", Value::Bool(true), "a_boolean", Value::Bool(false));
let filter = Equals::from_args(args);
assert!(! filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_custom("a_string", Value::Str(String::from("Hello!")), "a_string", Value::Str(String::from("World!")));
let filter = Equals::from_args(args);
assert!(! filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_custom("an_integer", Value::Int(2), "an_integer", Value::Int(3));
let filter = Equals::from_args(args);
assert!(! filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_custom("a_date", Value::Date(Utc::now()), "a_date", Value::Date(Utc::now()));
let filter = Equals::from_args(args);
assert!(! filter.run(&mut record).unwrap());
let (args, mut record) = generate_args_record_custom("first_one", Value::Int(1), "second_one", Value::Int(1));
let filter = Equals::from_args(args);
assert!(! filter.run(&mut record).unwrap());
}
}

14
src/filters/mod.rs Normal file
View File

@ -0,0 +1,14 @@
mod equals;
pub use self::equals::*;
/*
pub trait Filter {
fn filter(&self, record: &mut Record) -> bool;
}
impl<T: Filter> Module for T {
fn run(&self, record: &mut Record) -> Result<bool, ()> {
Ok(self.filter(record))
}
}
*/

9
src/main.rs Normal file
View File

@ -0,0 +1,9 @@
mod actions;
mod common;
mod config;
mod filters;
mod modules;
fn main() {
println!("Hello, world!");
}

23
src/modules.rs Normal file
View File

@ -0,0 +1,23 @@
use crate::common::Record;
use crate::{actions,filters};
struct Available {
name: &'static str,
cons: fn(ModuleArgs) -> Box<dyn Module>
}
const AVAILABLE: &[Available] = &[
Available { name: "action_noop", cons: move |a| Box::new(actions::Noop::from_args(a)) },
Available { name: "filter_equals", cons: move |a| Box::new(filters::Equals::from_args(a)) }
];
pub trait Module {
fn run(&self, record: &mut Record) -> Result<bool, ()>;
}
pub type ModuleArgs<'a> = Record<'a>;
pub enum ModuleType {
Filter,
Action
}

View File

@ -1,58 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import time
from pyruse.actions.action_counterRaise import Action
from pyruse.actions import action_counterReset
def whenNonExistingThenRaiseTo1():
entry = {"k": "raise#1"}
Action({"counter": "test", "for": "k", "save": "action_counterRaise1"}).act(entry)
assert entry["action_counterRaise1"] == 1
def whenKeepSecondsThenRaiseUntilTimeOut():
entry = {"k": "raise#2"}
action = Action({"counter": "test", "for": "k", "save": "action_counterRaise2", "keepSeconds": 3})
action.act(entry)
assert entry["action_counterRaise2"] == 1
time.sleep(2)
action.act(entry)
assert entry["action_counterRaise2"] == 2
time.sleep(2)
action.act(entry)
assert entry["action_counterRaise2"] == 2 # one tick timed out
def whenDifferentKeyThenDifferentCounter():
entry1 = {"k": "raise#3"}
entry2 = {"k": "raise#4"}
action = Action({"counter": "test", "for": "k", "save": "action_counterRaise3"})
action.act(entry1)
assert entry1["action_counterRaise3"] == 1
action.act(entry2)
assert entry2["action_counterRaise3"] == 1
action.act(entry2)
assert entry2["action_counterRaise3"] == 2
action.act(entry2)
assert entry2["action_counterRaise3"] == 3
action.act(entry1)
assert entry1["action_counterRaise3"] == 2
def whenGraceTimeThenCountIs0():
entry = {"k": "raise#5"}
raiseAct = Action({"counter": "test", "for": "k", "save": "action_counterRaise4"})
graceAct = action_counterReset.Action({"counter": "test", "for": "k", "graceSeconds": 1, "save": "action_counterRaise4"})
raiseAct.act(entry)
assert entry["action_counterRaise4"] == 1
graceAct.act(entry)
assert entry["action_counterRaise4"] == 0
raiseAct.act(entry)
assert entry["action_counterRaise4"] == 0
time.sleep(1)
raiseAct.act(entry)
assert entry["action_counterRaise4"] == 1
def unitTests():
whenNonExistingThenRaiseTo1()
whenKeepSecondsThenRaiseUntilTimeOut()
whenDifferentKeyThenDifferentCounter()
whenGraceTimeThenCountIs0()

View File

@ -1,57 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import time
from pyruse.actions.action_counterReset import Action
from pyruse.actions import action_counterRaise
def whenResetThenCountIs0():
entry = {"k": "reset#1"}
resetAct = Action({"counter": "test", "for": "k", "save": "action_counterReset1"})
raiseAct = action_counterRaise.Action({"counter": "test", "for": "k", "save": "action_counterReset1"})
raiseAct.act(entry)
assert entry["action_counterReset1"] == 1
resetAct.act(entry)
assert entry["action_counterReset1"] == 0
def whenNoGraceTimeThenRaiseWorks():
entry = {"k": "reset#2"}
resetAct = Action({"counter": "test", "for": "k", "save": "action_counterReset2"})
raiseAct = action_counterRaise.Action({"counter": "test", "for": "k", "save": "action_counterReset2"})
raiseAct.act(entry)
assert entry["action_counterReset2"] == 1
resetAct.act(entry)
assert entry["action_counterReset2"] == 0
raiseAct.act(entry)
assert entry["action_counterReset2"] == 1
def whenGraceTimeThenRaiseFails():
entry = {"k": "reset#3"}
resetAct = Action({"counter": "test", "for": "k", "save": "action_counterReset3", "graceSeconds": 1})
raiseAct = action_counterRaise.Action({"counter": "test", "for": "k", "save": "action_counterReset3"})
raiseAct.act(entry)
assert entry["action_counterReset3"] == 1
resetAct.act(entry)
assert entry["action_counterReset3"] == 0
raiseAct.act(entry)
assert entry["action_counterReset3"] == 0
def whenGraceTimeThenRaiseWorksAtGraceEnd():
entry = {"k": "reset#4"}
resetAct = Action({"counter": "test", "for": "k", "save": "action_counterReset4", "graceSeconds": 1})
raiseAct = action_counterRaise.Action({"counter": "test", "for": "k", "save": "action_counterReset4"})
raiseAct.act(entry)
assert entry["action_counterReset4"] == 1
resetAct.act(entry)
assert entry["action_counterReset4"] == 0
raiseAct.act(entry)
assert entry["action_counterReset4"] == 0
time.sleep(1)
raiseAct.act(entry)
assert entry["action_counterReset4"] == 1
def unitTests():
whenResetThenCountIs0()
whenNoGraceTimeThenRaiseWorks()
whenGraceTimeThenRaiseFails()
whenGraceTimeThenRaiseWorksAtGraceEnd()

View File

@ -1,159 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import os
import re
from datetime import datetime
from pyruse.actions.action_dailyReport import Action
from pyruse import config
mail_filename = "email.dump"
wAction = Action({"level": "WARN", "message": "WarnMsg {m}"})
iAction = Action({"level": "INFO", "message": "InfoMsg {m}"})
oAction = Action({"level": "OTHER", "message": "MiscMsg {m}"})
wActFirst = Action({"level": "WARN", "message": "WarnMsg {m}", "details": "FIRST"})
wActLast = Action({"level": "WARN", "message": "WarnMsg {m}", "details": "LAST"})
wActFL = Action({"level": "WARN", "message": "WarnMsg {m}", "details": "FIRSTLAST"})
wActNone = Action({"level": "WARN", "message": "WarnMsg {m}", "details": "NONE"})
def newEntry(m):
return {"__REALTIME_TIMESTAMP": datetime.utcnow(), "m": m}
def whenNewDayThenReport():
if os.path.exists(mail_filename):
os.remove(mail_filename)
oAction.act(newEntry("message1"))
assert not os.path.exists(mail_filename)
Action._hour = 25
oAction.act(newEntry("message2"))
assert os.path.exists(mail_filename)
os.remove(mail_filename)
def whenEmailThenCheck3Sections():
if os.path.exists(mail_filename):
os.remove(mail_filename)
wAction.act(newEntry("messageW"))
iAction.act(newEntry("messageI"))
Action._hour = 25
oAction.act(newEntry("messageO"))
assert os.path.exists(mail_filename)
conf = config.Config().asMap().get("email", {})
reSubject = re.compile(r"^Subject: (.*)")
reFrom = re.compile(r"^From: (.*)")
reTo = re.compile(r"^To: (.*)")
subjOK = False
fromOK = False
toOK = False
nbWarn = 0
nbInfo = 0
nbMisc = 0
with open(mail_filename, 'rt') as m:
for line in m:
match = reSubject.match(line)
if match:
subjOK = match.group(1) == conf.get("subject", "Pyruse Report")
match = reFrom.match(line)
if match:
fromOK = match.group(1) == conf.get("from", "pyruse")
match = reTo.match(line)
if match:
toOK = match.group(1).split(", ") == conf.get("to", ["hostmaster"])
if "WarnMsg" in line:
nbWarn += 1
if "InfoMsg" in line:
nbInfo += 1
if "MiscMsg" in line:
nbMisc += 1
assert subjOK
assert fromOK
assert toOK
assert nbWarn == 2
assert nbInfo == 2
assert nbMisc == 2
os.remove(mail_filename)
def _compareEmailWithExpected(expected):
assert os.path.exists(mail_filename)
reTime = re.compile(r"\d{4}(?:[- :.]\d{2}){6}\d{4}")
warnSeen = False
nbTimes = 0
nbFirst = 0
nbLast = 0
line = ""
with open(mail_filename, 'rt') as m:
for l in m:
if l != "" and l[-1:] == "=":
line += l[:-1]
continue
elif l == "" and warnSeen:
break
line += l
if "WarnMsg" in line:
warnSeen = True
elif not warnSeen:
line = ""
continue
nbTimes += len(reTime.findall(line))
if "From=C2=A0:" in line:
nbFirst += 1
if "Until:" in line:
nbLast += 1
if "</tr>" in line:
break
line = ""
seen = dict(warn = warnSeen, times = nbTimes, first = nbFirst, last = nbLast)
assert seen == expected, "Expected=" + str(expected) + " ≠ Seen=" + str(seen)
os.remove(mail_filename)
def whenEmailThenCheckTimes(warnAction, expected):
if os.path.exists(mail_filename):
os.remove(mail_filename)
warnAction.act(newEntry("messageW"))
warnAction.act(newEntry("messageW"))
Action._hour = 25
warnAction.act(newEntry("messageW"))
_compareEmailWithExpected(expected)
def whenSeveralDetailsModesThenOnlyOneWarn():
if os.path.exists(mail_filename):
os.remove(mail_filename)
wAction.act(newEntry("messageW"))
wAction.act(newEntry("messageW"))
wAction.act(newEntry("messageW"))
wAction.act(newEntry("messageW"))
wAction.act(newEntry("messageW"))
wActFirst.act(newEntry("messageW"))
wActFirst.act(newEntry("messageW"))
wActFirst.act(newEntry("messageW"))
wActLast.act(newEntry("messageW"))
wActLast.act(newEntry("messageW"))
wActLast.act(newEntry("messageW"))
wActFL.act(newEntry("messageW"))
wActFL.act(newEntry("messageW"))
wActFL.act(newEntry("messageW"))
wActFL.act(newEntry("messageW"))
wActNone.act(newEntry("messageW"))
wActNone.act(newEntry("messageW"))
wActNone.act(newEntry("messageW"))
Action._hour = 25
wActNone.act(newEntry("messageW"))
_compareEmailWithExpected(dict(warn = True, times = 9, first = 2, last = 2))
def whenReportThenNewSetOfMessages():
if os.path.exists(mail_filename):
os.remove(mail_filename)
Action._hour = 25
oAction.act(newEntry("message3"))
assert os.path.exists(mail_filename)
os.remove(mail_filename)
whenEmailThenCheck3Sections()
def unitTests():
whenNewDayThenReport()
whenEmailThenCheck3Sections()
whenEmailThenCheckTimes(wActFirst, dict(warn = True, times = 1, first = 1, last = 0))
whenEmailThenCheckTimes(wActLast, dict(warn = True, times = 1, first = 0, last = 1))
whenEmailThenCheckTimes(wActFL, dict(warn = True, times = 2, first = 1, last = 1))
whenEmailThenCheckTimes(wActNone, dict(warn = True, times = 0, first = 0, last = 0))
whenSeveralDetailsModesThenOnlyOneWarn()
whenReportThenNewSetOfMessages()

View File

@ -1,93 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from datetime import datetime
from pyruse import dnat
from pyruse.actions.action_dnatCapture import Action
def whenNoSaddrThenError():
try:
Action(dict(addr=1))
except Exception:
return
assert False, "An exception should be raised when saddr is absent"
def whenNoAddrNorAddrvalueThenError():
try:
Action(dict(saddr=1))
except Exception:
return
assert False, "An exception should be raised when addr and addrValue are absent"
def whenNoAddrButAddrvalueThenNoError():
Action(dict(saddr=1, addrValue=1))
def whenNoAddrvalueButAddrThenNoError():
Action(dict(saddr=1, addr=1))
def whenNoKeepsecondsThen6bits():
a = Action(dict(saddr=1, addr=1))
assert a.keepBits == 6, "Default keepSeconds (63) should be on 6 bits, not " + str(a.keepBits)
def whenKeepsecondsIs150Then8bits():
a = Action(dict(saddr=1, addr=1, keepSeconds=150))
assert a.keepBits == 8, "150 for keepSeconds should be on 8 bits, not " + str(a.keepBits)
def whenInsufficientEntryThenNoMapping():
dnat._mappings = []
Action({"saddr": "sa", "addrValue": "x"}).act({"__REALTIME_TIMESTAMP": datetime(2018,1,1)})
assert dnat._mappings == [], "Got:\n" + str(dnat._mappings) + "\ninstead of []"
def whenFieldAndOrValueThenCheckMapping(spec, entryWithAddr, entryWithDAddr, expect):
dnat._mappings = []
# specify the Action
spec.update({"saddr": "sa"})
# prepare the entry
entry = {
"__REALTIME_TIMESTAMP": datetime(2018,1,1),
"sa": "vsa", "sp": "vsp"}
if entryWithAddr:
entry.update({"a": "va", "p": "vp"})
if entryWithDAddr:
entry.update({"da": "vda", "dp": "vdp"})
# run
Action(spec).act(entry)
# check the result
expect.update({"bits": 6, "time": 23668144, "saddr": "vsa"})
assert dnat._mappings == [expect], "Got:\n" + str(dnat._mappings) + "\ninstead of:\n" + str([expect])
def unitTests():
whenNoSaddrThenError()
whenNoAddrNorAddrvalueThenError()
whenNoAddrButAddrvalueThenNoError()
whenNoAddrvalueButAddrThenNoError()
whenNoKeepsecondsThen6bits()
whenKeepsecondsIs150Then8bits()
whenInsufficientEntryThenNoMapping()
whenFieldAndOrValueThenCheckMapping({"addr": "a"}, True, True,
{"sport": None, "addr": "va", "port": None, "daddr": None, "dport": None})
whenFieldAndOrValueThenCheckMapping({"addrValue": "x"}, True, True,
{"sport": None, "addr": "x", "port": None, "daddr": None, "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "addrValue": "x"}, True, True,
{"sport": None, "addr": "va", "port": None, "daddr": None, "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "addrValue": "x"}, False, True,
{"sport": None, "addr": "x", "port": None, "daddr": None, "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "daddr": "da"}, True, True,
{"sport": None, "addr": "va", "port": None, "daddr": "vda", "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "daddrValue": "x"}, True, True,
{"sport": None, "addr": "va", "port": None, "daddr": "x", "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "daddr": "da", "daddrValue": "x"}, True, True,
{"sport": None, "addr": "va", "port": None, "daddr": "vda", "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "daddr": "da", "daddrValue": "x"}, True, False,
{"sport": None, "addr": "va", "port": None, "daddr": "x", "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "port": "p"}, True, True,
{"sport": None, "addr": "va", "port": "vp", "daddr": None, "dport": None})
whenFieldAndOrValueThenCheckMapping({"addr": "a", "dport": "dp"}, True, True,
{"sport": None, "addr": "va", "port": None, "daddr": None, "dport": "vdp"})

View File

@ -1,74 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from datetime import datetime
from pyruse import dnat
from pyruse.actions.action_dnatReplace import Action
def whenNoSaddrintoThenError():
try:
Action(dict(addr=1))
except Exception:
return
assert False, "An exception should be raised when saddrInto is absent"
def whenNoMatchFieldThenError():
try:
Action(dict(saddrInto=1))
except Exception:
return
assert False, "An exception should be raised when no match-field is present"
def whenSaddrintoAndAtLeastOneMatchFieldThenNoError():
a = Action(dict(saddrInto=1, dport=1))
assert a.matchers == [(1, "dport")], "Got:\n" + str(a.matchers) + "\ninstead of:\n" + str([(1, "dport")])
assert a.updaters == [(1, "saddr")], "Got:\n" + str(a.updaters) + "\ninstead of:\n" + str([(1, "saddr")])
def whenNoMatchingEntryThenNoChange():
dnat._mappings = [{
"bits": 7, "time": 1183407200,
"saddr": "bad", "sport": None,
"addr": "prox", "port": 12345,
"daddr": "serv", "dport": None}]
a = Action(dict(saddrInto="sa", port="sp"))
entryIn = dict(sa = "prox", da = "serv")
entryOut = entryIn.copy()
a.act(entryOut)
assert entryIn == entryOut, "Got:\n" + str(entryOut) + "\ninstead of:\n" + str(entryIn)
def whenNoMatchingValueThenNoChange():
dnat._mappings = [{
"bits": 7, "time": 1183407200,
"saddr": "bad", "sport": None,
"addr": "prox", "port": 12345,
"daddr": "serv", "dport": None}]
a = Action(dict(saddrInto="sa", port="sp"))
entryIn = dict(sa = "prox", sp = 1234, da = "serv")
entryOut = entryIn.copy()
a.act(entryOut)
assert entryIn == entryOut, "Got:\n" + str(entryOut) + "\ninstead of:\n" + str(entryIn)
def whenMatchingEntryThenChange():
dnat._mappings = [{
"bits": 7, "time": 1183407200,
"saddr": "bad", "sport": None,
"addr": "prox", "port": 12345,
"daddr": "serv", "dport": None}]
a = Action(dict(saddrInto="sa", port="sp"))
entryIn = dict(sa = "prox", sp = 12345, da = "serv")
expect = entryIn.copy()
expect.update({"sa": "bad"})
entryOut = entryIn.copy()
a.act(entryOut)
assert expect == entryOut, "Got:\n" + str(entryOut) + "\ninstead of:\n" + str(expect)
def unitTests():
whenNoSaddrintoThenError()
whenNoMatchFieldThenError()
whenSaddrintoAndAtLeastOneMatchFieldThenNoError()
whenNoMatchingEntryThenNoChange()
whenNoMatchingValueThenNoChange()
whenMatchingEntryThenChange()

View File

@ -1,77 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import os
import re
from pyruse.actions.action_email import Action
from pyruse import config
mail_filename = "email.dump"
def whenEmailWithSubjectThenCheckContents():
if os.path.exists(mail_filename):
os.remove(mail_filename)
Action({"subject": "Test1", "message": "TestMsg{m}"}).act({"m": "#1"})
assert os.path.exists(mail_filename)
conf = config.Config().asMap().get("email", {})
reSubject = re.compile(r"^Subject: (.*)")
reFrom = re.compile(r"^From: (.*)")
reTo = re.compile(r"^To: (.*)")
subjOK = False
fromOK = False
toOK = False
nbMsg = 0
with open(mail_filename, 'rt') as m:
for line in m:
match = reSubject.match(line)
if match:
subjOK = match.group(1) == "Test1"
match = reFrom.match(line)
if match:
fromOK = match.group(1) == conf.get("from", "pyruse")
match = reTo.match(line)
if match:
toOK = match.group(1).split(", ") == conf.get("to", ["hostmaster"])
if "TestMsg#1" in line:
nbMsg += 1
assert subjOK
assert fromOK
assert toOK
assert nbMsg == 1
os.remove(mail_filename)
def whenEmailWithoutSubjectThenCheckContents():
if os.path.exists(mail_filename):
os.remove(mail_filename)
Action({"message": "TestMsg{m}"}).act({"m": "#2"})
assert os.path.exists(mail_filename)
conf = config.Config().asMap().get("email", {})
reSubject = re.compile(r"^Subject: (.*)")
reFrom = re.compile(r"^From: (.*)")
reTo = re.compile(r"^To: (.*)")
subjOK = False
fromOK = False
toOK = False
nbMsg = 0
with open(mail_filename, 'rt') as m:
for line in m:
match = reSubject.match(line)
if match:
subjOK = match.group(1) == "Pyruse Notification"
match = reFrom.match(line)
if match:
fromOK = match.group(1) == conf.get("from", "pyruse")
match = reTo.match(line)
if match:
toOK = match.group(1).split(", ") == conf.get("to", ["hostmaster"])
if "TestMsg#2" in line:
nbMsg += 1
assert subjOK
assert fromOK
assert toOK
assert nbMsg == 1
os.remove(mail_filename)
def unitTests():
whenEmailWithSubjectThenCheckContents()
whenEmailWithoutSubjectThenCheckContents()

View File

@ -1,147 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import json
import os
import time
from pyruse.actions.action_ipsetBan import Action
ipBanCmd = "ipsetBan.cmd"
ipBanState = "action_ipsetBan.py.json"
def _clean():
if os.path.exists(ipBanCmd):
os.remove(ipBanCmd)
if os.path.exists(ipBanState):
os.remove(ipBanState)
def whenBanIPv4ThenAddToIPv4Set():
_clean()
Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}).act({"ip": "10.0.0.1"})
assert os.path.exists(ipBanCmd)
assert os.path.exists(ipBanState)
nbLines = 0
with open(ipBanCmd, "rt") as c:
for line in c:
assert line == "add I4ban 10.0.0.1\n", line
nbLines += 1
assert nbLines == 1, nbLines
nbBans = 0
with open(ipBanState) as s:
for ban in json.load(s):
assert ban["IP"] == "10.0.0.1" and ban["nfSet"] == "I4ban", str(ban)
nbBans += 1
assert nbBans == 1, nbBans
_clean()
def whenBanIPv6ThenAddToIPv6Set():
_clean()
Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}).act({"ip": "::1"})
assert os.path.exists(ipBanCmd)
assert os.path.exists(ipBanState)
nbLines = 0
with open(ipBanCmd, "rt") as c:
for line in c:
assert line == "add I6ban ::1\n", line
nbLines += 1
assert nbLines == 1, nbLines
nbBans = 0
with open(ipBanState) as s:
for ban in json.load(s):
assert ban["IP"] == "::1" and ban["nfSet"] == "I6ban", str(ban)
nbBans += 1
assert nbBans == 1, nbBans
_clean()
def whenBanTwoIPThenTwoLinesInState():
_clean()
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"})
action.act({"ip": "10.0.0.1"})
action.act({"ip": "::1"})
action.act({"ip": "10.0.0.1"})
assert os.path.exists(ipBanState)
nbBans = 0
with open(ipBanState) as s:
for ban in json.load(s):
if ban["IP"] == "10.0.0.1":
assert ban["nfSet"] == "I4ban", str(ban)
elif ban["IP"] == "::1":
assert ban["nfSet"] == "I6ban", str(ban)
else:
assert false, str(ban)
nbBans += 1
assert nbBans == 2, nbBans
_clean()
def whenBanAnewThenNoDuplicate():
_clean()
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"})
action.act({"ip": "10.0.0.1"})
action.act({"ip": "10.0.0.1"})
assert os.path.exists(ipBanCmd)
assert os.path.exists(ipBanState)
lineCount = 0
with open(ipBanCmd, "rt") as c:
for line in c:
lineCount += 1
if lineCount == 1:
assert line == "add I4ban 10.0.0.1\n", line
elif lineCount == 2:
assert line == "del I4ban 10.0.0.1\n", line
elif lineCount == 3:
assert line == "add I4ban 10.0.0.1\n", line
assert lineCount == 3, lineCount
nbBans = 0
with open(ipBanState) as s:
for ban in json.load(s):
if ban["IP"] == "10.0.0.1":
assert ban["nfSet"] == "I4ban", str(ban)
nbBans += 1
assert nbBans == 1, nbBans
_clean()
def whenFinishedBanThenAsIfNotThere():
_clean()
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban", "banSeconds": 1})
action.act({"ip": "10.0.0.1"})
time.sleep(1)
action.act({"ip": "10.0.0.1"})
assert os.path.exists(ipBanCmd)
lineCount = 0
with open(ipBanCmd, "rt") as c:
for line in c:
lineCount += 1
if lineCount == 1:
assert line == "add I4ban 10.0.0.1 timeout 1\n", line
elif lineCount == 2:
assert line == "add I4ban 10.0.0.1 timeout 1\n", line
assert lineCount == 2, lineCount
_clean()
def whenUnfinishedBanThenTimeoutReset():
_clean()
action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban", "banSeconds": 2})
action.act({"ip": "10.0.0.1"})
time.sleep(1)
action.act({"ip": "10.0.0.1"})
assert os.path.exists(ipBanCmd)
lineCount = 0
with open(ipBanCmd, "rt") as c:
for line in c:
lineCount += 1
if lineCount == 1:
assert line == "add I4ban 10.0.0.1 timeout 2\n", line
elif lineCount == 2:
assert line == "del I4ban 10.0.0.1\n", line
elif lineCount == 3:
assert line == "add I4ban 10.0.0.1 timeout 2\n", line
assert lineCount == 3, lineCount
_clean()
def unitTests():
whenBanIPv4ThenAddToIPv4Set()
whenBanIPv6ThenAddToIPv6Set()
whenBanTwoIPThenTwoLinesInState()
whenBanAnewThenNoDuplicate()
whenFinishedBanThenAsIfNotThere()
whenUnfinishedBanThenTimeoutReset()

View File

@ -1,15 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from unittest.mock import patch
from pyruse import log
from pyruse.actions.action_log import Action
@patch('pyruse.actions.action_log.log.log')
def whenLogThenRightSystemdCall(mockLog):
for level in log.Level:
Action({"level": level.name, "message": "Test: {text}"}).act({"text": "test message"})
mockLog.assert_called_with(level, "Test: test message")
def unitTests():
whenLogThenRightSystemdCall()

View File

@ -1,147 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import json
import os
import time
from pyruse.actions.action_nftBan import Action
nftBanCmd = "nftBan.cmd"
nftBanState = "action_nftBan.py.json"
def _clean():
if os.path.exists(nftBanCmd):
os.remove(nftBanCmd)
if os.path.exists(nftBanState):
os.remove(nftBanState)
def whenBanIPv4ThenAddToIPv4Set():
_clean()
Action({"IP": "ip", "nftSetIPv4": "ip I4 ban", "nftSetIPv6": "ip6 I6 ban"}).act({"ip": "10.0.0.1"})
assert os.path.exists(nftBanCmd)
assert os.path.exists(nftBanState)
nbLines = 0
with open(nftBanCmd, "rt") as c:
for line in c:
assert line == "add element ip I4 ban {10.0.0.1}\n", line
nbLines += 1
assert nbLines == 1, nbLines
nbBans = 0
with open(nftBanState) as s:
for ban in json.load(s):
assert ban["IP"] == "10.0.0.1" and ban["nfSet"] == "ip I4 ban", str(ban)
nbBans += 1
assert nbBans == 1, nbBans
_clean()
def whenBanIPv6ThenAddToIPv6Set():
_clean()
Action({"IP": "ip", "nftSetIPv4": "ip I4 ban", "nftSetIPv6": "ip6 I6 ban"}).act({"ip": "::1"})
assert os.path.exists(nftBanCmd)
assert os.path.exists(nftBanState)
nbLines = 0
with open(nftBanCmd, "rt") as c:
for line in c:
assert line == "add element ip6 I6 ban {::1}\n", line
nbLines += 1
assert nbLines == 1, nbLines
nbBans = 0
with open(nftBanState) as s:
for ban in json.load(s):
assert ban["IP"] == "::1" and ban["nfSet"] == "ip6 I6 ban", str(ban)
nbBans += 1
assert nbBans == 1, nbBans
_clean()
def whenBanTwoIPThenTwoLinesInState():
_clean()
action = Action({"IP": "ip", "nftSetIPv4": "ip I4 ban", "nftSetIPv6": "ip6 I6 ban"})
action.act({"ip": "10.0.0.1"})
action.act({"ip": "::1"})
action.act({"ip": "10.0.0.1"})
assert os.path.exists(nftBanState)
nbBans = 0
with open(nftBanState) as s:
for ban in json.load(s):
if ban["IP"] == "10.0.0.1":
assert ban["nfSet"] == "ip I4 ban", str(ban)
elif ban["IP"] == "::1":
assert ban["nfSet"] == "ip6 I6 ban", str(ban)
else:
assert false, str(ban)
nbBans += 1
assert nbBans == 2, nbBans
_clean()
def whenBanAnewThenNoDuplicate():
_clean()
action = Action({"IP": "ip", "nftSetIPv4": "ip I4 ban", "nftSetIPv6": "ip6 I6 ban"})
action.act({"ip": "10.0.0.1"})
action.act({"ip": "10.0.0.1"})
assert os.path.exists(nftBanCmd)
assert os.path.exists(nftBanState)
lineCount = 0
with open(nftBanCmd, "rt") as c:
for line in c:
lineCount += 1
if lineCount == 1:
assert line == "add element ip I4 ban {10.0.0.1}\n", line
elif lineCount == 2:
assert line == "delete element ip I4 ban {10.0.0.1}\n", line
elif lineCount == 3:
assert line == "add element ip I4 ban {10.0.0.1}\n", line
assert lineCount == 3, lineCount
nbBans = 0
with open(nftBanState) as s:
for ban in json.load(s):
if ban["IP"] == "10.0.0.1":
assert ban["nfSet"] == "ip I4 ban", str(ban)
nbBans += 1
assert nbBans == 1, nbBans
_clean()
def whenFinishedBanThenAsIfNotThere():
_clean()
action = Action({"IP": "ip", "nftSetIPv4": "ip I4 ban", "nftSetIPv6": "ip6 I6 ban", "banSeconds": 1})
action.act({"ip": "10.0.0.1"})
time.sleep(1)
action.act({"ip": "10.0.0.1"})
assert os.path.exists(nftBanCmd)
lineCount = 0
with open(nftBanCmd, "rt") as c:
for line in c:
lineCount += 1
if lineCount == 1:
assert line == "add element ip I4 ban {10.0.0.1 timeout 1s}\n", line
elif lineCount == 2:
assert line == "add element ip I4 ban {10.0.0.1 timeout 1s}\n", line
assert lineCount == 2, lineCount
_clean()
def whenUnfinishedBanThenTimeoutReset():
_clean()
action = Action({"IP": "ip", "nftSetIPv4": "ip I4 ban", "nftSetIPv6": "ip6 I6 ban", "banSeconds": 2})
action.act({"ip": "10.0.0.1"})
time.sleep(1)
action.act({"ip": "10.0.0.1"})
assert os.path.exists(nftBanCmd)
lineCount = 0
with open(nftBanCmd, "rt") as c:
for line in c:
lineCount += 1
if lineCount == 1:
assert line == "add element ip I4 ban {10.0.0.1 timeout 2s}\n", line
elif lineCount == 2:
assert line == "delete element ip I4 ban {10.0.0.1}\n", line
elif lineCount == 3:
assert line == "add element ip I4 ban {10.0.0.1 timeout 2s}\n", line
assert lineCount == 3, lineCount
_clean()
def unitTests():
whenBanIPv4ThenAddToIPv4Set()
whenBanIPv6ThenAddToIPv6Set()
whenBanTwoIPThenTwoLinesInState()
whenBanAnewThenNoDuplicate()
whenFinishedBanThenAsIfNotThere()
whenUnfinishedBanThenTimeoutReset()

View File

@ -1,22 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_equals import Filter
def whenGreaterThenFalse():
assert not Filter({"field": "v", "value": 2}).filter({"v": 3})
def whenEqualSameTypeThenTrue():
assert Filter({"field": "v", "value": 2}).filter({"v": 2})
def whenEqualDiffTypeThenTrue():
assert Filter({"field": "v", "value": 2.0}).filter({"v": 2})
def whenLowerThenFalse():
assert not Filter({"field": "v", "value": 2}).filter({"v": 0})
def unitTests():
whenGreaterThenFalse()
whenEqualSameTypeThenTrue()
whenEqualDiffTypeThenTrue()
whenLowerThenFalse()

View File

@ -1,26 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_greaterOrEquals import Filter
def whenGreaterPosIntThenTrue():
assert Filter({"field": "v", "value": 2}).filter({"v": 3})
def whenGreaterNegFloatThenTrue():
assert Filter({"field": "v", "value": -2.1}).filter({"v": -1.9})
def whenEqualSameTypeThenTrue():
assert Filter({"field": "v", "value": 2}).filter({"v": 2})
def whenEqualDiffTypeThenTrue():
assert Filter({"field": "v", "value": 2.0}).filter({"v": 2})
def whenLowerThenFalse():
assert not Filter({"field": "v", "value": 2}).filter({"v": 0})
def unitTests():
whenGreaterPosIntThenTrue()
whenGreaterNegFloatThenTrue()
whenEqualSameTypeThenTrue()
whenEqualDiffTypeThenTrue()
whenLowerThenFalse()

View File

@ -1,22 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_in import Filter
def whenNotInListThenFalse():
assert not Filter({"field": "v", "values": [0, "test"]}).filter({"v": 3})
def whenInListSameTypeThenTrue():
assert Filter({"field": "v", "values": [2]}).filter({"v": 2})
def whenInListDiffTypeThenTrue():
assert Filter({"field": "v", "values": [2.0]}).filter({"v": 2})
def whenNoFieldThenFalse():
assert not Filter({"field": "v", "values": [0]}).filter({"other": 0})
def unitTests():
whenNotInListThenFalse()
whenInListSameTypeThenTrue()
whenInListDiffTypeThenTrue()
whenNoFieldThenFalse()

View File

@ -1,50 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_inNetworks import Filter
def whenIp4InNet4ThenTrue():
assert Filter({"field": "ip", "nets": ["34.56.78.90/12"]}).filter({"ip": "34.48.0.1"})
def whenIp4NotInNet4ThenFalse():
assert not Filter({"field": "ip", "nets": ["34.56.78.90/12"]}).filter({"ip": "34.47.255.254"})
def whenIp4ItselfThenTrue():
assert Filter({"field": "ip", "nets": ["12.34.56.78"]}).filter({"ip": "12.34.56.78"})
def whenIp6InNet6ThenTrue():
assert Filter({"field": "ip", "nets": ["2001:db8:1:1a0::/59"]}).filter({"ip": "2001:db8:1:1a0::1"})
def whenIp6NotInNet6ThenFalse():
assert not Filter({"field": "ip", "nets": ["2001:db8:1:1a0::/59"]}).filter({"ip": "2001:db8:1:19f:ffff:ffff:ffff:fffe"})
def whenIp6ItselfThenTrue():
assert Filter({"field": "ip", "nets": ["2001:db8:1:1a0::"]}).filter({"ip": "2001:db8:1:1a0::"})
def whenNumericIp6InNet4ThenFalse():
assert not Filter({"field": "ip", "nets": ["34.56.78.90/12"]}).filter({"ip": "::2230:1"})
def whenNumericIp4InNet6ThenFalse():
assert not Filter({"field": "ip", "nets": ["::2230:1/108"]}).filter({"ip": "34.48.0.1"})
def whenIpInOneNetworkThenTrue():
assert Filter({"field": "ip", "nets": ["::2230:1/108", "10.0.0.0/8", "34.56.78.90/12", "2001:db8:1:1a0::/59"]}).filter({"ip": "34.48.0.1"})
def whenNoIpThenFalse():
assert not Filter({"field": "ip", "nets": ["::2230:1/108", "10.0.0.0/8"]}).filter({"no_ip": "Hi!"})
def whenNoNetworkThenFalse():
assert not Filter({"field": "ip", "nets": []}).filter({"ip": "34.48.0.1"})
def unitTests():
whenIp4InNet4ThenTrue()
whenIp4NotInNet4ThenFalse()
whenIp4ItselfThenTrue()
whenIp6InNet6ThenTrue()
whenIp6NotInNet6ThenFalse()
whenIp6ItselfThenTrue()
whenNumericIp6InNet4ThenFalse()
whenNumericIp4InNet6ThenFalse()
whenIpInOneNetworkThenTrue()
whenNoIpThenFalse()
whenNoNetworkThenFalse()

View File

@ -1,26 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_lowerOrEquals import Filter
def whenLowerNegIntThenTrue():
assert Filter({"field": "v", "value": -2}).filter({"v": -3})
def whenLowerPosFloatThenTrue():
assert Filter({"field": "v", "value": 2.1}).filter({"v": 1.9})
def whenEqualSameTypeThenTrue():
assert Filter({"field": "v", "value": 2}).filter({"v": 2})
def whenEqualDiffTypeThenTrue():
assert Filter({"field": "v", "value": 2.0}).filter({"v": 2})
def whenGreaterThenFalse():
assert not Filter({"field": "v", "value": 0}).filter({"v": 2})
def unitTests():
whenLowerNegIntThenTrue()
whenLowerPosFloatThenTrue()
whenEqualSameTypeThenTrue()
whenEqualDiffTypeThenTrue()
whenGreaterThenFalse()

View File

@ -1,26 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_pcre import Filter
def whenMatchesThenTrue():
assert Filter({"field": "v", "re": "ok"}).filter({"v": "joke"})
def whenNoMatchThenFalse():
assert not Filter({"field": "v", "re": "ko"}).filter({"v": "Koala"})
def whenSaveThenGroupsInEntry():
entry = {"v": "yet another test"}
Filter({"field": "v", "re": "^(.).* .*(.)r .*(.).$", "save": [ "y", "e", "s" ]}).filter(entry)
assert entry["y"] + entry["e"] + entry["s"] == "yes"
def whenNamedGroupsThenFoundInEntry():
entry = {"v": "yet another test"}
Filter({"field": "v", "re": "^(?P<y>.).* .*(?P<e>.)r .*(?P<s>.).$"}).filter(entry)
assert entry["y"] + entry["e"] + entry["s"] == "yes"
def unitTests():
whenMatchesThenTrue()
whenNoMatchThenFalse()
whenSaveThenGroupsInEntry()
whenNamedGroupsThenFoundInEntry()

View File

@ -1,20 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_pcreAny import Filter
def whenMatchesThenTrue():
assert Filter({"field": "v", "re": ["cool", "ok"]}).filter({"v": "joke"})
def whenNoMatchThenFalse():
assert not Filter({"field": "v", "re": ["bad", "ko"]}).filter({"v": "Koala"})
def whenNamedGroupsThenFoundInEntry():
entry = {"v": "It works or not"}
Filter({"field": "v", "re": ["^(?P<o>It)(?P<k> works)", "(?P<k>or)(?P<o> not)$"]}).filter(entry)
assert entry["o"] + entry["k"] == "It works"
def unitTests():
whenMatchesThenTrue()
whenNoMatchThenFalse()
whenNamedGroupsThenFoundInEntry()

View File

@ -1,14 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.filters.filter_userExists import Filter
def whenUserExistsThenTrue():
assert Filter({"field": "user"}).filter({"user": "root"})
def whenGarbageThenFalse():
assert not Filter({"field": "user"}).filter({"user": "auietsnr"})
def unitTests():
whenUserExistsThenTrue()
whenGarbageThenFalse()

View File

@ -1,97 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
import os
import subprocess
import sys
from datetime import datetime
sys.path.insert(1, "..")
from pyruse import actions, config, module, workflow
def _clean():
for f in ['acted_on.log', 'action_nftBan.py.json', 'email.dump', 'nftBan.cmd', 'unfiltered.log']:
if os.path.exists(f):
os.remove(f)
def main():
global _microsec
conf = config.Config(os.curdir)
# Unit tests
import filter_equals, filter_greaterOrEquals, filter_in, filter_inNetworks, filter_lowerOrEquals, filter_pcre, filter_pcreAny, filter_userExists
import action_counterRaise, action_counterReset, action_dailyReport, action_dnatCapture, action_dnatReplace, action_email, action_ipsetBan, action_log, action_nftBan
filter_equals.unitTests()
filter_greaterOrEquals.unitTests()
filter_in.unitTests()
filter_inNetworks.unitTests()
filter_lowerOrEquals.unitTests()
filter_pcre.unitTests()
filter_pcreAny.unitTests()
filter_userExists.unitTests()
action_counterRaise.unitTests()
action_counterReset.unitTests()
action_dailyReport.unitTests()
action_dnatCapture.unitTests()
action_dnatReplace.unitTests()
action_email.unitTests()
action_ipsetBan.unitTests()
action_log.unitTests()
action_nftBan.unitTests()
# Integration test
wf = workflow.Workflow(conf.asMap().get("actions", {}))
_microsec = 0
test = [
entry("dmz", "ftp", "an ftp message", 0),
entry("dmz", "login", "Failed password for Unknown User from 1.2.3.4"),
entry("dmz", "login", "Failed password for nobody from 5.6.7.8"),
entry("dmz", "login", "End of session for root on localhost"),
entry("dmz", "login", "Failed password for User Unknown from 1.2.3.4"),
entry("bck", "ftp", "file requested"),
entry("dmz", "login", "Accepted password for root from 1.2.3.4"),
entry("bck", "login", "Failed password for root from 1.2.3.4"),
entry("bck", "login", "Failed password for nobody from 1.2.3.4"),
entry("dmz", "login", "Failed password for foobar from 1.2.3.4"),
entry("dmz", "login", "Failed password for nobody from 5.6.7.8")
]
_clean()
for e in test:
run(wf, e)
actions.action_dailyReport.Action._hour = 25
run(wf, entry("bck", "login", "Failed password for root from ::1", 11))
for f in ['acted_on.log', 'email.dump', 'nftBan.cmd', 'unfiltered.log']:
assert os.path.exists(f), "file should exist: " + f
try:
subprocess.run(
[ "/usr/bin/bash",
"-c",
"diff -U0 \"$0\"{,.test_ref} | grep -vE '^[-+@^]{2,3} |={5,}[0-9]+=='",
f],
check = True)
assert False, "differences found in " + f
except subprocess.CalledProcessError:
pass # OK, no difference found
_clean()
os.remove('action_dailyReport.py.journal')
def entry(host, service, message, microsecond = None):
global _microsec
if microsecond:
_microsec = microsecond
_microsec += 1
return {
"__REALTIME_TIMESTAMP": datetime(2118,1,1,8,1,1,_microsec),
"_HOSTNAME": host,
"service": service,
"MESSAGE": message
}
def run(workflow, logEntry):
step = workflow.firstStep
while step is not None:
step = step.run(logEntry)
if __name__ == '__main__':
main()

View File

@ -1,14 +0,0 @@
# pyruse is intended as a replacement to both fail2ban and epylog
# Copyright © 20172018 Y. Gablin
# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing.
from pyruse.actions import action_dailyReport
class Action(action_dailyReport.Action):
def __init__(self, args):
super().__init__(args)
self.filename = args["outFile"]
def act(self, entry):
super().act(entry)
with open(self.filename, "a") as f:
f.write(str(entry) + "\n")