From c99c3e111cdbbda5b0016a7fa99ecfba786ad87d Mon Sep 17 00:00:00 2001 From: Y Date: Sat, 23 Nov 2019 23:10:40 +0100 Subject: [PATCH] modules and filters POC --- .editorconfig | 8 + .gitignore | 11 +- Cargo.toml | 13 ++ extra/setup/setup.py | 33 ----- pyruse/actions/action_counterRaise.py | 22 --- pyruse/actions/action_counterReset.py | 22 --- pyruse/actions/action_dailyReport.py | 177 ----------------------- pyruse/actions/action_dnatCapture.py | 18 --- pyruse/actions/action_dnatReplace.py | 18 --- pyruse/actions/action_email.py | 22 --- pyruse/actions/action_ipsetBan.py | 37 ----- pyruse/actions/action_log.py | 22 --- pyruse/actions/action_nftBan.py | 39 ----- pyruse/actions/action_noop.py | 11 -- pyruse/ban.py | 85 ----------- pyruse/base.py | 56 ------- pyruse/config.py | 34 ----- pyruse/counter.py | 106 -------------- pyruse/dnat.py | 97 ------------- pyruse/email.py | 37 ----- pyruse/filters/filter_equals.py | 13 -- pyruse/filters/filter_greaterOrEquals.py | 13 -- pyruse/filters/filter_in.py | 13 -- pyruse/filters/filter_inNetworks.py | 50 ------- pyruse/filters/filter_lowerOrEquals.py | 13 -- pyruse/filters/filter_pcre.py | 21 --- pyruse/filters/filter_pcreAny.py | 23 --- pyruse/filters/filter_userExists.py | 17 --- pyruse/log.py | 28 ---- pyruse/main.py | 53 ------- pyruse/module.py | 41 ------ pyruse/workflow.py | 73 ---------- src/actions/mod.rs | 17 +++ src/actions/noop.rs | 38 +++++ src/common.rs | 12 ++ src/config/mod.rs | 18 +++ src/filters/equals.rs | 99 +++++++++++++ src/filters/mod.rs | 14 ++ src/main.rs | 9 ++ src/modules.rs | 23 +++ tests/action_counterRaise.py | 58 -------- tests/action_counterReset.py | 57 -------- tests/action_dailyReport.py | 159 -------------------- tests/action_dnatCapture.py | 93 ------------ tests/action_dnatReplace.py | 74 ---------- tests/action_email.py | 77 ---------- tests/action_ipsetBan.py | 147 ------------------- tests/action_log.py | 15 -- tests/action_nftBan.py | 147 ------------------- tests/filter_equals.py | 22 --- tests/filter_greaterOrEquals.py | 26 ---- tests/filter_in.py | 22 --- tests/filter_inNetworks.py | 50 ------- tests/filter_lowerOrEquals.py | 26 ---- tests/filter_pcre.py | 26 ---- tests/filter_pcreAny.py | 20 --- tests/filter_userExists.py | 14 -- tests/main.py | 97 ------------- tests/pyruse/actions/action_testLog.py | 14 -- 59 files changed, 261 insertions(+), 2339 deletions(-) create mode 100644 .editorconfig create mode 100644 Cargo.toml delete mode 100644 extra/setup/setup.py delete mode 100644 pyruse/actions/action_counterRaise.py delete mode 100644 pyruse/actions/action_counterReset.py delete mode 100644 pyruse/actions/action_dailyReport.py delete mode 100644 pyruse/actions/action_dnatCapture.py delete mode 100644 pyruse/actions/action_dnatReplace.py delete mode 100644 pyruse/actions/action_email.py delete mode 100644 pyruse/actions/action_ipsetBan.py delete mode 100644 pyruse/actions/action_log.py delete mode 100644 pyruse/actions/action_nftBan.py delete mode 100644 pyruse/actions/action_noop.py delete mode 100644 pyruse/ban.py delete mode 100644 pyruse/base.py delete mode 100644 pyruse/config.py delete mode 100644 pyruse/counter.py delete mode 100644 pyruse/dnat.py delete mode 100644 pyruse/email.py delete mode 100644 pyruse/filters/filter_equals.py delete mode 100644 pyruse/filters/filter_greaterOrEquals.py delete mode 100644 pyruse/filters/filter_in.py delete mode 100644 pyruse/filters/filter_inNetworks.py delete mode 100644 pyruse/filters/filter_lowerOrEquals.py delete mode 100644 pyruse/filters/filter_pcre.py delete mode 100644 pyruse/filters/filter_pcreAny.py delete mode 100644 pyruse/filters/filter_userExists.py delete mode 100644 pyruse/log.py delete mode 100644 pyruse/main.py delete mode 100644 pyruse/module.py delete mode 100644 pyruse/workflow.py create mode 100644 src/actions/mod.rs create mode 100644 src/actions/noop.rs create mode 100644 src/common.rs create mode 100644 src/config/mod.rs create mode 100644 src/filters/equals.rs create mode 100644 src/filters/mod.rs create mode 100644 src/main.rs create mode 100644 src/modules.rs delete mode 100644 tests/action_counterRaise.py delete mode 100644 tests/action_counterReset.py delete mode 100644 tests/action_dailyReport.py delete mode 100644 tests/action_dnatCapture.py delete mode 100644 tests/action_dnatReplace.py delete mode 100644 tests/action_email.py delete mode 100644 tests/action_ipsetBan.py delete mode 100644 tests/action_log.py delete mode 100644 tests/action_nftBan.py delete mode 100644 tests/filter_equals.py delete mode 100644 tests/filter_greaterOrEquals.py delete mode 100644 tests/filter_in.py delete mode 100644 tests/filter_inNetworks.py delete mode 100644 tests/filter_lowerOrEquals.py delete mode 100644 tests/filter_pcre.py delete mode 100644 tests/filter_pcreAny.py delete mode 100644 tests/filter_userExists.py delete mode 100644 tests/main.py delete mode 100644 tests/pyruse/actions/action_testLog.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..aab1dfa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61f2dc9..088ba6b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bf3f585 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pyruse" +version = "2.1.0" +authors = ["Y. "] +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" diff --git a/extra/setup/setup.py b/extra/setup/setup.py deleted file mode 100644 index ec5b5fd..0000000 --- a/extra/setup/setup.py +++ /dev/null @@ -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'], -) diff --git a/pyruse/actions/action_counterRaise.py b/pyruse/actions/action_counterRaise.py deleted file mode 100644 index 075e36d..0000000 --- a/pyruse/actions/action_counterRaise.py +++ /dev/null @@ -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 diff --git a/pyruse/actions/action_counterReset.py b/pyruse/actions/action_counterReset.py deleted file mode 100644 index bc4ffb5..0000000 --- a/pyruse/actions/action_counterReset.py +++ /dev/null @@ -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 diff --git a/pyruse/actions/action_dailyReport.py b/pyruse/actions/action_dailyReport.py deleted file mode 100644 index 2b4e20e..0000000 --- a/pyruse/actions/action_dailyReport.py +++ /dev/null @@ -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 "
".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 = '\n\n\n

Pyruse Report

\n' - _htmDocStop = '' - _htmHeadWarn = '

WARNING Messages

\n' - _htmHeadInfo = '

Information Messages

\n' - _htmHeadOther = '

Other log events

\n' - _htmTableStart = '\n\n' - _htmTableStop = '
CountMessageDate+time for each occurrence
\n' - _htmPreStart = '
'
-    _htmPreStop = '
\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 "{count}{text}{times}\n".format_map({ - "count": sum(len(t) for (_void, t) in times.items()), - "text": self._encode(msg), - "times": "

".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() diff --git a/pyruse/actions/action_dnatCapture.py b/pyruse/actions/action_dnatCapture.py deleted file mode 100644 index 06ba6b4..0000000 --- a/pyruse/actions/action_dnatCapture.py +++ /dev/null @@ -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) diff --git a/pyruse/actions/action_dnatReplace.py b/pyruse/actions/action_dnatReplace.py deleted file mode 100644 index b171595..0000000 --- a/pyruse/actions/action_dnatReplace.py +++ /dev/null @@ -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) diff --git a/pyruse/actions/action_email.py b/pyruse/actions/action_email.py deleted file mode 100644 index 524f5a9..0000000 --- a/pyruse/actions/action_email.py +++ /dev/null @@ -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() diff --git a/pyruse/actions/action_ipsetBan.py b/pyruse/actions/action_ipsetBan.py deleted file mode 100644 index b035d20..0000000 --- a/pyruse/actions/action_ipsetBan.py +++ /dev/null @@ -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) diff --git a/pyruse/actions/action_log.py b/pyruse/actions/action_log.py deleted file mode 100644 index 245c269..0000000 --- a/pyruse/actions/action_log.py +++ /dev/null @@ -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) diff --git a/pyruse/actions/action_nftBan.py b/pyruse/actions/action_nftBan.py deleted file mode 100644 index fe9a13b..0000000 --- a/pyruse/actions/action_nftBan.py +++ /dev/null @@ -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) diff --git a/pyruse/actions/action_noop.py b/pyruse/actions/action_noop.py deleted file mode 100644 index 64f60bb..0000000 --- a/pyruse/actions/action_noop.py +++ /dev/null @@ -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 diff --git a/pyruse/ban.py b/pyruse/ban.py deleted file mode 100644 index dd120a4..0000000 --- a/pyruse/ban.py +++ /dev/null @@ -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) diff --git a/pyruse/base.py b/pyruse/base.py deleted file mode 100644 index d2dae52..0000000 --- a/pyruse/base.py +++ /dev/null @@ -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 diff --git a/pyruse/config.py b/pyruse/config.py deleted file mode 100644 index 6a4af75..0000000 --- a/pyruse/config.py +++ /dev/null @@ -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 diff --git a/pyruse/counter.py b/pyruse/counter.py deleted file mode 100644 index 5ba3df3..0000000 --- a/pyruse/counter.py +++ /dev/null @@ -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 - ) diff --git a/pyruse/dnat.py b/pyruse/dnat.py deleted file mode 100644 index 06fc6fe..0000000 --- a/pyruse/dnat.py +++ /dev/null @@ -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 diff --git a/pyruse/email.py b/pyruse/email.py deleted file mode 100644 index 8e1ab91..0000000 --- a/pyruse/email.py +++ /dev/null @@ -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() - ) diff --git a/pyruse/filters/filter_equals.py b/pyruse/filters/filter_equals.py deleted file mode 100644 index 844b066..0000000 --- a/pyruse/filters/filter_equals.py +++ /dev/null @@ -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 diff --git a/pyruse/filters/filter_greaterOrEquals.py b/pyruse/filters/filter_greaterOrEquals.py deleted file mode 100644 index 30ff85c..0000000 --- a/pyruse/filters/filter_greaterOrEquals.py +++ /dev/null @@ -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 diff --git a/pyruse/filters/filter_in.py b/pyruse/filters/filter_in.py deleted file mode 100644 index 904e1ca..0000000 --- a/pyruse/filters/filter_in.py +++ /dev/null @@ -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 diff --git a/pyruse/filters/filter_inNetworks.py b/pyruse/filters/filter_inNetworks.py deleted file mode 100644 index 63c9b63..0000000 --- a/pyruse/filters/filter_inNetworks.py +++ /dev/null @@ -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< 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) diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..ee1395c --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,17 @@ +mod noop; +pub use self::noop::*; + +/* +pub trait Action { + fn act(&self, record: &mut Record) -> Result<(), ()>; +} + +impl Module for T { + fn run(&self, record: &mut Record) -> Result { + match self.act(record) { + Ok(()) => Ok(true), + Err(()) => Err(()) + } + } +} +*/ diff --git a/src/actions/noop.rs b/src/actions/noop.rs new file mode 100644 index 0000000..c26fe3a --- /dev/null +++ b/src/actions/noop.rs @@ -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 { + 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()); + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..793b9b1 --- /dev/null +++ b/src/common.rs @@ -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) +} + +pub type Record<'a> = HashMap<&'a str, Value>; diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..c98853b --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,18 @@ +use crate::common::Record; +use crate::modules::{ModuleArgs,ModuleType}; + +pub struct Config<'a> { + actions: Vec>, + options: Record<'a> +} + +pub struct Chain<'a> { + name: String, + steps: Vec> +} + +pub struct Step<'a> { + module_name: String, + module_type: ModuleType, + args: ModuleArgs<'a> +} diff --git a/src/filters/equals.rs b/src/filters/equals.rs new file mode 100644 index 0000000..d974d63 --- /dev/null +++ b/src/filters/equals.rs @@ -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 { + 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()); + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs new file mode 100644 index 0000000..d05eaff --- /dev/null +++ b/src/filters/mod.rs @@ -0,0 +1,14 @@ +mod equals; +pub use self::equals::*; + +/* +pub trait Filter { + fn filter(&self, record: &mut Record) -> bool; +} + +impl Module for T { + fn run(&self, record: &mut Record) -> Result { + Ok(self.filter(record)) + } +} +*/ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dc86bba --- /dev/null +++ b/src/main.rs @@ -0,0 +1,9 @@ +mod actions; +mod common; +mod config; +mod filters; +mod modules; + +fn main() { + println!("Hello, world!"); +} diff --git a/src/modules.rs b/src/modules.rs new file mode 100644 index 0000000..7729708 --- /dev/null +++ b/src/modules.rs @@ -0,0 +1,23 @@ +use crate::common::Record; +use crate::{actions,filters}; + +struct Available { + name: &'static str, + cons: fn(ModuleArgs) -> Box +} + +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; +} + +pub type ModuleArgs<'a> = Record<'a>; + +pub enum ModuleType { + Filter, + Action +} diff --git a/tests/action_counterRaise.py b/tests/action_counterRaise.py deleted file mode 100644 index 0f06c4b..0000000 --- a/tests/action_counterRaise.py +++ /dev/null @@ -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() diff --git a/tests/action_counterReset.py b/tests/action_counterReset.py deleted file mode 100644 index 30a20ef..0000000 --- a/tests/action_counterReset.py +++ /dev/null @@ -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() diff --git a/tests/action_dailyReport.py b/tests/action_dailyReport.py deleted file mode 100644 index 01ee63a..0000000 --- a/tests/action_dailyReport.py +++ /dev/null @@ -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 "" 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() diff --git a/tests/action_dnatCapture.py b/tests/action_dnatCapture.py deleted file mode 100644 index c26c52b..0000000 --- a/tests/action_dnatCapture.py +++ /dev/null @@ -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"}) diff --git a/tests/action_dnatReplace.py b/tests/action_dnatReplace.py deleted file mode 100644 index eb21956..0000000 --- a/tests/action_dnatReplace.py +++ /dev/null @@ -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() diff --git a/tests/action_email.py b/tests/action_email.py deleted file mode 100644 index e3e431f..0000000 --- a/tests/action_email.py +++ /dev/null @@ -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() diff --git a/tests/action_ipsetBan.py b/tests/action_ipsetBan.py deleted file mode 100644 index 726daad..0000000 --- a/tests/action_ipsetBan.py +++ /dev/null @@ -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() diff --git a/tests/action_log.py b/tests/action_log.py deleted file mode 100644 index 2dbeda5..0000000 --- a/tests/action_log.py +++ /dev/null @@ -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() diff --git a/tests/action_nftBan.py b/tests/action_nftBan.py deleted file mode 100644 index df035ab..0000000 --- a/tests/action_nftBan.py +++ /dev/null @@ -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() diff --git a/tests/filter_equals.py b/tests/filter_equals.py deleted file mode 100644 index 9d0f8e6..0000000 --- a/tests/filter_equals.py +++ /dev/null @@ -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() diff --git a/tests/filter_greaterOrEquals.py b/tests/filter_greaterOrEquals.py deleted file mode 100644 index bf150cc..0000000 --- a/tests/filter_greaterOrEquals.py +++ /dev/null @@ -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() diff --git a/tests/filter_in.py b/tests/filter_in.py deleted file mode 100644 index 8e86739..0000000 --- a/tests/filter_in.py +++ /dev/null @@ -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() diff --git a/tests/filter_inNetworks.py b/tests/filter_inNetworks.py deleted file mode 100644 index 1e1d1b3..0000000 --- a/tests/filter_inNetworks.py +++ /dev/null @@ -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() diff --git a/tests/filter_lowerOrEquals.py b/tests/filter_lowerOrEquals.py deleted file mode 100644 index 77b3a09..0000000 --- a/tests/filter_lowerOrEquals.py +++ /dev/null @@ -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() diff --git a/tests/filter_pcre.py b/tests/filter_pcre.py deleted file mode 100644 index 0190ddf..0000000 --- a/tests/filter_pcre.py +++ /dev/null @@ -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.).* .*(?P.)r .*(?P.).$"}).filter(entry) - assert entry["y"] + entry["e"] + entry["s"] == "yes" - -def unitTests(): - whenMatchesThenTrue() - whenNoMatchThenFalse() - whenSaveThenGroupsInEntry() - whenNamedGroupsThenFoundInEntry() diff --git a/tests/filter_pcreAny.py b/tests/filter_pcreAny.py deleted file mode 100644 index 3f80e75..0000000 --- a/tests/filter_pcreAny.py +++ /dev/null @@ -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": ["^(?PIt)(?P works)", "(?Por)(?P not)$"]}).filter(entry) - assert entry["o"] + entry["k"] == "It works" - -def unitTests(): - whenMatchesThenTrue() - whenNoMatchThenFalse() - whenNamedGroupsThenFoundInEntry() diff --git a/tests/filter_userExists.py b/tests/filter_userExists.py deleted file mode 100644 index 2abfc11..0000000 --- a/tests/filter_userExists.py +++ /dev/null @@ -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() diff --git a/tests/main.py b/tests/main.py deleted file mode 100644 index 3e3d403..0000000 --- a/tests/main.py +++ /dev/null @@ -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() diff --git a/tests/pyruse/actions/action_testLog.py b/tests/pyruse/actions/action_testLog.py deleted file mode 100644 index 66aa783..0000000 --- a/tests/pyruse/actions/action_testLog.py +++ /dev/null @@ -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")