diff --git a/ChangeLog b/ChangeLog index a5a51830..95a36870 100644 --- a/ChangeLog +++ b/ChangeLog @@ -84,6 +84,8 @@ ver. 1.1.1-dev-1 (20??/??/??) - development nightly edition * `filter.d/proxmox.conf` - add support to Proxmox Web GUI (gh-2966) * `filter.d/openvpn.conf` - new filter and jail for openvpn recognizing failed TLS handshakes (gh-2702) * `filter.d/vaultwarden.conf` - new filter and jail for Vaultwarden (gh-3979) +* `fail2ban-regex` extended with new option `-i` or `--invert` to output not-matched lines by `-o` or `--out` (gh-4001) + ver. 1.1.0 (2024/04/25) - object-found--norad-59479-cospar-2024-069a--altitude-36267km ----------- diff --git a/fail2ban/client/fail2banregex.py b/fail2ban/client/fail2banregex.py index 3b5ba1c8..b7426085 100644 --- a/fail2ban/client/fail2banregex.py +++ b/fail2ban/client/fail2banregex.py @@ -172,6 +172,8 @@ def get_opt_parser(): help="Disable check for all regex's"), Option("-o", "--out", action="store", dest="out", default=None, help="Set token to print failure information only (row, id, ip, msg, host, ip4, ip6, dns, matches, ...)"), + Option("-i", "--invert", action="store_true", dest="invert", + help="Invert the sense of matching, to output non-matching lines."), Option("--print-no-missed", action='store_true', help="Do not print any missed lines"), Option("--print-no-ignored", action='store_true', @@ -529,7 +531,7 @@ class Fail2banRegex(object): except RegexException as e: # pragma: no cover output( 'ERROR: %s' % e ) return None, 0, None - if self._filter.getMaxLines() > 1: + if self._filter.getMaxLines() > 1 and not self._opts.out: for bufLine in orgLineBuffer[int(fullBuffer):]: if bufLine not in self._filter._Filter__lineBuffer: try: @@ -619,8 +621,10 @@ class Fail2banRegex(object): def process(self, test_lines): t0 = time.time() + out = None if self._opts.out: # get out function out = self._prepaireOutput() + outinv = self._opts.invert for line in test_lines: if isinstance(line, tuple): line_datetimestripped, ret, is_ignored = self.testRegex(line[0], line[1]) @@ -632,8 +636,13 @@ class Fail2banRegex(object): continue line_datetimestripped, ret, is_ignored = self.testRegex(line) - if self._opts.out: # (formatted) output: - if len(ret) > 0 and not is_ignored: out(ret) + if out: # (formatted) output: + if len(ret) > 0 and not is_ignored: + if not outinv: out(ret) + elif outinv: # inverted output (currently only time and message as matches): + if not len(ret): # [failRegexIndex, fid, date, fail] + ret = [[-1, "", self._filter._Filter__lastDate, {"fid":"", "matches":[line]}]] + out(ret) continue if is_ignored: diff --git a/fail2ban/tests/fail2banregextestcase.py b/fail2ban/tests/fail2banregextestcase.py index 28ec9e30..ab2f6782 100644 --- a/fail2ban/tests/fail2banregextestcase.py +++ b/fail2ban/tests/fail2banregextestcase.py @@ -432,8 +432,16 @@ class Fail2banRegexTest(LogCaptureTestCase): self.assertLogged('output: %s' % "['192.0.2.0'", "'ip4': '192.0.2.0'", "'user': 'kevin'", all=True) self.pruneLog() # log msg : - self.assertTrue(_test_exec('-o', 'msg', STR_00, RE_00_USER)) + nmline = "Dec 31 12:00:00 [sshd] error: PAM: No failure for user from 192.0.2.123" + lines = STR_00+"\n"+nmline + self.assertTrue(_test_exec('-o', 'msg', lines, RE_00_USER)) self.assertLogged('output: %s' % STR_00) + self.assertNotLogged('output: %s' % nmline) + self.pruneLog() + # log msg (inverted) : + self.assertTrue(_test_exec('-o', 'msg', '-i', lines, RE_00_USER)) + self.assertLogged('output: %s' % nmline) + self.assertNotLogged('output: %s' % STR_00) self.pruneLog() # item of match (user): self.assertTrue(_test_exec('-o', 'user', STR_00, RE_00_USER)) @@ -443,6 +451,17 @@ class Fail2banRegexTest(LogCaptureTestCase): self.assertTrue(_test_exec('-o', ', , ', STR_00, RE_00_USER)) self.assertLogged('output: %s' % '192.0.2.0, kevin, inet4') self.pruneLog() + # log msg : + lines = nmline+"\n"+STR_00; # just reverse lines (to cover possible order dependencies) + self.assertTrue(_test_exec('-o', '