From 0140a934c2de87afe091f9c08720c99411795319 Mon Sep 17 00:00:00 2001 From: Y Date: Mon, 26 Feb 2018 18:55:45 +0100 Subject: [PATCH] action_log: log to systemd, aka. enable recidive detection --- README.md | 2 +- doc/install.md | 2 +- doc/intro_tech.md | 2 +- doc/{action_nftBan.md => logandban.md} | 35 +++++++++++++++++++++++--- pyruse/actions/action_log.py | 22 ++++++++++++++++ pyruse/log.py | 30 +++++++++++----------- tests/action_log.py | 15 +++++++++++ tests/main.py | 3 ++- 8 files changed, 89 insertions(+), 22 deletions(-) rename doc/{action_nftBan.md => logandban.md} (70%) create mode 100644 pyruse/actions/action_log.py create mode 100644 tests/action_log.py diff --git a/README.md b/README.md index 16b5efe..ae299a1 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,4 @@ For more in-depth documentation, please refer to these pages: - [the `action_noop` module](doc/noop.md) - [the `action_email` module](doc/action_email.md) - [the `action_dailyReport` module](doc/action_dailyReport.md) - - [the `action_nftBan` module](doc/action_nftBan.md) + - [the `action_nftBan` and `action_log` modules](doc/logandban.md) diff --git a/doc/install.md b/doc/install.md index 88ce544..63104e8 100644 --- a/doc/install.md +++ b/doc/install.md @@ -3,7 +3,7 @@ The software requirements are: * a modern systemd-based Linux operating system (eg. [Archlinux](https://archlinux.org/)- or [Fedora](https://getfedora.org/)-based distributions); -* python, at least version 3.1 (or [more, depending on the modules](doc/intro_tech.md) being used); +* python, at least version 3.4 (or [more, depending on the modules](intro_tech.md) being used); * [python-systemd](https://www.freedesktop.org/software/systemd/python-systemd/journal.html); * [nftables](http://wiki.nftables.org/) _if_ IP address bans are to be managed; * a sendmail-like program _if_ emails are wanted. diff --git a/doc/intro_tech.md b/doc/intro_tech.md index 4591763..905d606 100644 --- a/doc/intro_tech.md +++ b/doc/intro_tech.md @@ -15,7 +15,7 @@ It should be noted, that modern Python API are used. Thus: * Python version ≥ 3.1 is required for managing modules (`importlib`); * Python version ≥ 3.1 is required for loading the configuration (json’s `object_pairs_hook`); * Python version ≥ 3.2 is required for the daily report and emails (string’s `format_map`); -* Python version ≥ 3.4 is required for the daily report (`enum`); +* Python version ≥ 3.4 is required for the daily report and logging, thus also the log action (`enum`); * Python version ≥ 3.5 is required for IP address bans and emails, thus also the daily report (subprocess’ `run`); * Python version ≥ 3.6 is required for emails, thus also the daily report (`headerregistry`, `EmailMessage`). diff --git a/doc/action_nftBan.md b/doc/logandban.md similarity index 70% rename from doc/action_nftBan.md rename to doc/logandban.md index 85c6147..729b2f6 100644 --- a/doc/action_nftBan.md +++ b/doc/logandban.md @@ -1,4 +1,33 @@ -# Ban IP addresses after they misbehaved +# Log entries creation, and ban of IP addresses + +## Log entries + +The main purpose of creating new log entries, is to detect recidives in bad behaviour: after an IP address misbehaves, it gets banned, and we generate a log line for that; such log lines get counted, and eventually trigger a harsher, recidive, ban of the same IP address. Several levels of bans can thus be stacked, up to an unlimited ban, if such is wanted. + +Action `action_log` takes a mandatory `message` argument, which is a template for the message to be sent. +Optionally, the log level can be changed from the default (which is “INFO”) by setting the `level` parameter; valid values are “EMERG”, “ALERT”, “CRIT”, “ERR”, “WARNING”, “NOTICE”, “INFO”, and “DEBUG” (see [Syslog severity levels](https://en.wikipedia.org/wiki/Syslog#Severity_level) for the definitions). + +The `message` parameter is a Python [string format](https://docs.python.org/3/library/string.html#formatstrings). +This means that any key in the current entry may be referrenced by its name between curly braces. +This also means that literal curly braces must be doubled, lest they are read as the start of a template placeholder. + +Here are some examples: + +```json +{ + "action": "action_log", "args": { "message": "Ban from SSH for {thatIP}." } +} + +{ + "action": "action_log", + "args": { + "level": "NOTICE", + "message": "Recidive ban from SSH for {thatIP}." + } +} +``` + +## Ban IP addresses after they misbehaved Linux provides a number of firewall solutions: [iptables](http://www.netfilter.org/), its successor [nftables](http://wiki.nftables.org/), and many iptables frontends like [Shorewall](http://www.shorewall.net/) or RedHat’s [firewalld](http://www.firewalld.org/). For Pyruse, **nftables** was chosen, because it is modern and light-weight, and provides interesting features. @@ -59,7 +88,7 @@ Here are examples: } ``` -## List the currently banned addresses +### List the currently banned addresses To see what IP addresses are currently banned, here is the `nft` command: @@ -85,7 +114,7 @@ table ip Inet4 { _Note_: The un-rounded timeouts are post-reboot restored bans. -## Un-ban an IP address +### Un-ban an IP address It is bound to happen some day: you will want to un-ban a banned IP address. diff --git a/pyruse/actions/action_log.py b/pyruse/actions/action_log.py new file mode 100644 index 0000000..245c269 --- /dev/null +++ b/pyruse/actions/action_log.py @@ -0,0 +1,22 @@ +# 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/log.py b/pyruse/log.py index 54910a9..cefd1f0 100644 --- a/pyruse/log.py +++ b/pyruse/log.py @@ -1,28 +1,28 @@ # 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 -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 +@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) + journal.send(string, PRIORITY = level.value) def debug(string): - global DEBUG - log(DEBUG, string) + log(Level.DEBUG, string) def notice(string): - global NOTICE - log(NOTICE, string) + log(Level.NOTICE, string) def error(string): - global ERR - log(ERR, string) + log(Level.ERR, string) diff --git a/tests/action_log.py b/tests/action_log.py new file mode 100644 index 0000000..2dbeda5 --- /dev/null +++ b/tests/action_log.py @@ -0,0 +1,15 @@ +# 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/main.py b/tests/main.py index 201e702..cf426ae 100644 --- a/tests/main.py +++ b/tests/main.py @@ -20,7 +20,7 @@ def main(): # 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_email, action_nftBan + import action_counterRaise, action_counterReset, action_dailyReport, action_email, action_log, action_nftBan filter_equals.unitTests() filter_greaterOrEquals.unitTests() @@ -34,6 +34,7 @@ def main(): action_counterReset.unitTests() action_dailyReport.unitTests() action_email.unitTests() + action_log.unitTests() action_nftBan.unitTests() # Integration test