modules and filters POC
parent
8aaa04389f
commit
c99c3e111c
|
@ -0,0 +1,8 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = true
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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 systemd’s 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'],
|
||||
)
|
|
@ -1,22 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,22 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,177 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
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()
|
|
@ -1,18 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,18 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,22 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,37 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,22 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,39 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,11 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,85 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,56 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,34 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,106 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
||||
)
|
|
@ -1,97 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,37 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
||||
)
|
|
@ -1,13 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,13 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,13 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,50 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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))
|
|
@ -1,13 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,21 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,23 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,17 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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
|
|
@ -1,28 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -1,53 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,41 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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]
|
|
@ -1,73 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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)
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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>;
|
|
@ -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>
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,9 @@
|
|||
mod actions;
|
||||
mod common;
|
||||
mod config;
|
||||
mod filters;
|
||||
mod modules;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,57 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,159 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,93 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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"})
|
|
@ -1,74 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,77 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,147 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,15 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,147 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,22 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,26 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,22 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,50 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,26 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,26 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,20 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,14 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,97 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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()
|
|
@ -1,14 +0,0 @@
|
|||
# pyruse is intended as a replacement to both fail2ban and epylog
|
||||
# Copyright © 2017–2018 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")
|
Loading…
Reference in New Issue