From f7d328195fb56ca8a44bdf334e79f643ad01a96f Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 00:15:07 +0100 Subject: [PATCH 01/16] NF: Add systemd journal backend --- bin/fail2ban-regex | 58 +++++- config/filter.d/dovecot.conf | 8 + config/filter.d/postfix.conf | 8 + config/filter.d/recidive.conf | 8 + config/filter.d/sshd-ddos.conf | 8 + config/filter.d/sshd.conf | 8 + config/jail.conf | 5 +- fail2ban/client/beautifier.py | 6 + fail2ban/client/filterreader.py | 4 + fail2ban/protocol.py | 3 + fail2ban/server/filter.py | 15 ++ fail2ban/server/filtersystemd.py | 237 ++++++++++++++++++++++ fail2ban/server/jail.py | 9 +- fail2ban/server/server.py | 36 +++- fail2ban/server/transmitter.py | 10 + fail2ban/tests/files/testcase-journal.log | 19 ++ fail2ban/tests/filtertestcase.py | 156 ++++++++++++++ fail2ban/tests/utils.py | 6 + 18 files changed, 596 insertions(+), 8 deletions(-) create mode 100644 fail2ban/server/filtersystemd.py create mode 100644 fail2ban/tests/files/testcase-journal.log diff --git a/bin/fail2ban-regex b/bin/fail2ban-regex index e3d75f20..078a936f 100755 --- a/bin/fail2ban-regex +++ b/bin/fail2ban-regex @@ -25,6 +25,12 @@ __license__ = "GPL" import getopt, sys, time, logging, os, locale from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError +try: + from fail2ban.server.filtersystemd import FilterSystemd + from systemd import journal +except: + journal = None + from fail2ban.version import version from fail2ban.client.configparserinc import SafeConfigParserWithIncludes from fail2ban.server.filter import Filter @@ -69,6 +75,7 @@ class Fail2banRegex: self.__filter = Filter(None) self.__ignoreregex = list() self.__failregex = list() + self.__journalmatch = "" self.__verbose = False self.__maxlines_set = False # so we allow to override maxlines in cmdline self.encoding = locale.getpreferredencoding() @@ -111,10 +118,14 @@ class Fail2banRegex: print " -V, --version print the version" print " -v, --verbose verbose output" print " -l INT, --maxlines=INT set maxlines for multi-line regex default: 1" + print " -m MATCHES, --matches=MATCHES" + print " journalctl style matches, overriding filter file." + print " Special value \"ALL\" searches entire journal" print print "Log:" print " string a string representing a log line" print " filename path to a log file (/var/log/auth.log)" + print " \"systemd-journal\" search systemd journal (systemd python required)" print print "Regex:" print " string a string representing a 'failregex'" @@ -223,6 +234,10 @@ class Fail2banRegex: print "ERROR: Invalid value for maxlines (%(maxlines)r) " \ "read from %(value)s" % locals() return False + try: + self.__journalmatch = reader.get("Init", "journalmatch") + except (NoSectionError, NoOptionError): + pass else: if len(value) > 53: stripReg = value[0:50] + "..." @@ -342,13 +357,16 @@ class Fail2banRegex: print "information." return True + def getJournalMatch(self): + return self.__journalmatch if __name__ == "__main__": fail2banRegex = Fail2banRegex() # Reads the command line options. try: - cmdOpts = 'hVcvl:e:' - cmdLongOpts = ['help', 'version', 'verbose', 'maxlines=', 'encoding='] + cmdOpts = 'hVcvl:e:m:' + cmdLongOpts = ['help', 'version', 'verbose', 'maxlines=', 'encoding=', + 'matches='] optList, args = getopt.getopt(sys.argv[1:], cmdOpts, cmdLongOpts) except getopt.GetoptError: fail2banRegex.dispUsage() @@ -391,6 +409,42 @@ if __name__ == "__main__": print e print sys.exit(-1) + elif cmd_log == "systemd-journal": + if journal is None: + print "Error: systemd library not found. Exiting..." + sys.exit(-1) + myjournal = journal.Reader() + journalmatch = "" + # Parse journal matches from command line + for opt in optList: + if opt[0] in ["-m", "--matches"]: + journalmatch = opt[1] + # If no command line option, take journal match from filter + if not journalmatch: + journalmatch = fail2banRegex.getJournalMatch() + try: + if journalmatch != "ALL": + for element in journalmatch.split(): + if element == "+": + myjournal.add_disjunction() + else: + myjournal.add_match(element) + except ValueError: + print "Error: Invalid journal match: %s" % journalmatch + print "Exiting..." + sys.exit(-1) + print "Use systemd journal match: %s" % (journalmatch or "ALL") + while True: + try: + entry = myjournal.get_next() + except OSError: + continue + else: + if not entry: + break + line = FilterSystemd.formatJournalEntry(entry) + fail2banRegex.testIgnoreRegex(line) + fail2banRegex.testRegex(line) else: if len(sys.argv[1]) > 53: stripLog = cmd_log[0:50] + "..." diff --git a/config/filter.d/dovecot.conf b/config/filter.d/dovecot.conf index d7fb6e6d..8e0857db 100644 --- a/config/filter.d/dovecot.conf +++ b/config/filter.d/dovecot.conf @@ -21,3 +21,11 @@ failregex = .*(?:pop3-login|imap-login):.*(?:Authentication failure|Aborted logi # Values: TEXT # ignoreregex = + +[Init] + +# Option: journalmatch +# Notes.: systemd journalctl style match filter for journal based backends +# Values: TEXT +# +journalmatch = _SYSTEMD_UNIT=dovecot.service diff --git a/config/filter.d/postfix.conf b/config/filter.d/postfix.conf index f92c3619..7bed801f 100644 --- a/config/filter.d/postfix.conf +++ b/config/filter.d/postfix.conf @@ -21,3 +21,11 @@ failregex = reject: RCPT from (.*)\[\]: 554 # Values: TEXT # ignoreregex = + +[Init] + +# Option: journalmatch +# Notes.: systemd journalctl style match filter for journal based backends +# Values: TEXT +# +journalmatch = _SYSTEMD_UNIT=postfix.service diff --git a/config/filter.d/recidive.conf b/config/filter.d/recidive.conf index bbb48008..a8778b6d 100644 --- a/config/filter.d/recidive.conf +++ b/config/filter.d/recidive.conf @@ -36,3 +36,11 @@ failregex = fail2ban.actions:\s+WARNING\s+\[(?:.*)\]\s+Ban\s+ # # Ignore our own bans, to keep our counts exact. ignoreregex = fail2ban.actions:\s+WARNING\s+\[%(_jailname)s\]\s+Ban\s+ + +[Init] + +# Option: journalmatch +# Notes.: systemd journalctl style match filter for journal based backends +# Values: TEXT +# +journalmatch = _SYSTEMD_UNIT=fail2ban.service diff --git a/config/filter.d/sshd-ddos.conf b/config/filter.d/sshd-ddos.conf index 58698ced..ce2a290d 100644 --- a/config/filter.d/sshd-ddos.conf +++ b/config/filter.d/sshd-ddos.conf @@ -34,3 +34,11 @@ failregex = ^%(__prefix_line)sDid not receive identification string from \ # Values: TEXT # ignoreregex = + +[Init] + +# Option: journalmatch +# Notes.: systemd journalctl style match filter for journal based backend +# Values: TEXT +# +journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd diff --git a/config/filter.d/sshd.conf b/config/filter.d/sshd.conf index 18ac6668..51bd5b9c 100644 --- a/config/filter.d/sshd.conf +++ b/config/filter.d/sshd.conf @@ -39,3 +39,11 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|erro # Values: TEXT # ignoreregex = + +[Init] + +# Option: journalmatch +# Notes.: systemd journalctl style match filter for journal based backend +# Values: TEXT +# +journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd diff --git a/config/jail.conf b/config/jail.conf index af21167a..0d8acce0 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -42,7 +42,7 @@ findtime = 600 maxretry = 5 # "backend" specifies the backend used to get files modification. -# Available options are "pyinotify", "gamin", "polling" and "auto". +# Available options are "pyinotify", "gamin", "polling", "systemd" and "auto". # This option can be overridden in each jail as well. # # pyinotify: requires pyinotify (a file alteration monitor) to be installed. @@ -50,6 +50,9 @@ maxretry = 5 # gamin: requires Gamin (a file alteration monitor) to be installed. # If Gamin is not installed, Fail2ban will use auto. # polling: uses a polling algorithm which does not require external libraries. +# systemd: uses systemd python library to access the systemd journal. +# Specifying "logpath" is not valid for this backend. +# See "journalmatch" in the jails associated filter config # auto: will try to use the following backends, in order: # pyinotify, gamin, polling. backend = auto diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 542611d4..ea03b7b8 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -113,6 +113,12 @@ class Beautifier: elif inC[2] == "logencoding": msg = "Current log encoding is set to:\n" msg = msg + response + elif inC[2] in ("journalmatch", "addjournalmatch", "deljournalmatch"): + if len(response) == 0: + msg = "No journal match filter set" + else: + msg = "Current match filter:\n" + msg += ' + '.join(response) elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"): if len(response) == 0: msg = "No IP address/network is ignored" diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index b9146f44..5c58046b 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -56,5 +56,9 @@ class FilterReader(DefinitionInitConfigReader): if self._initOpts: if 'maxlines' in self._initOpts: stream.append(["set", self._jailName, "maxlines", self._initOpts["maxlines"]]) + # Do not send a command if the match is empty. + if self._initOpts.get("journalmatch", '') != '': + for match in self._initOpts["journalmatch"].split("\n"): + stream.append(["set", self._jailName, "addjournalmatch", match]) return stream diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 48609f0b..e0fb018d 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -55,6 +55,8 @@ protocol = [ ["set addlogpath ", "adds to the monitoring list of "], ["set dellogpath ", "removes from the monitoring list of "], ["set logencoding ", "sets the of the log files for "], +["set addjournalmatch ", "adds to the journal filter of "], +["set deljournalmatch ", "removes from the journal filter of "], ["set addfailregex ", "adds the regular expression which must match failures for "], ["set delfailregex ", "removes the regular expression at for failregex"], ["set addignoreregex ", "adds the regular expression which should match pattern to exclude for "], @@ -79,6 +81,7 @@ protocol = [ ['', "JAIL INFORMATION", ""], ["get logpath", "gets the list of the monitored files for "], ["get logencoding ", "gets the of the log files for "], +["get journalmatch", "gets the journal filter match for "], ["get ignoreip", "gets the list of ignored IP addresses for "], ["get failregex", "gets the list of regular expressions which matches the failures for "], ["get ignoreregex", "gets the list of regular expressions which matches patterns to ignore for "], diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index c4c4bc22..4e892acb 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -643,6 +643,21 @@ class FileContainer: self.__handler = None +## +# JournalFilter class. +# +# Base interface class for systemd journal filters + +class JournalFilter(Filter): + + def addJournalMatch(self, match): + pass + + def delJournalMatch(self, match): + pass + + def getJournalMatch(self, match): + return [] ## # Utils class for DNS and IP handling. diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py new file mode 100644 index 00000000..ddb27cc2 --- /dev/null +++ b/fail2ban/server/filtersystemd.py @@ -0,0 +1,237 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# Original author: Cyril Jaquier + +__author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko, Steven Hiscocks" +__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko, 2013 Steven Hiscocks" +__license__ = "GPL" + +import logging, datetime +from distutils.version import LooseVersion + +from systemd import journal +if LooseVersion(getattr(journal, '__version__', "0")) < '204': + raise ImportError("Fail2Ban requires systemd >= 204") + +from failmanager import FailManagerEmpty +from filter import JournalFilter +from mytime import MyTime + + +# Gets the instance of the logger. +logSys = logging.getLogger("fail2ban.filter") + +## +# Journal reader class. +# +# This class reads from systemd journal and detects login failures or anything +# else that matches a given regular expression. This class is instantiated by +# a Jail object. + +class FilterSystemd(JournalFilter): + ## + # Constructor. + # + # Initialize the filter object with default values. + # @param jail the jail object + + def __init__(self, jail, **kwargs): + JournalFilter.__init__(self, jail, **kwargs) + self.__modified = False + # Initialise systemd-journal connection + self.__journal = journal.Reader(converters={'__CURSOR': lambda x: x}) + self.__matches = [] + logSys.debug("Created FilterSystemd") + + ## + # Add a journal match filter + # + # @param match journalctl syntax matches + + def addJournalMatch(self, match): + if self.__matches: + self.__journal.add_disjunction() # Add OR + try: + for match_element in match.split(): + if match_element == "+": + self.__journal.add_disjunction() + else: + self.__journal.add_match(match_element) + except: + logSys.error("Error adding journal match for: %s", match) + self.resetJournalMatches() + else: + for match_element in match.split('+'): + self.__matches.append(match_element.strip()) + logSys.debug("Adding journal match for: %s", match) + ## + # Reset a journal match filter called on removal or failure + # + # @return None + + def resetJournalMatches(self): + self.__journal.flush_matches() + logSys.debug("Flushed all journal matches") + match_copy = self.__matches[:] + self.__matches = [] + for match in match_copy: + self.addJournalMatch(match) + + ## + # Delete a journal match filter + # + # @param match journalctl syntax matches + + def delJournalMatch(self, match): + if match in self.__matches: + del self.__matches[self.__matches.index(match)] + self.resetJournalMatches() + + ## + # Get current journal match filter + # + # @return journalctl syntax matches + + def getJournalMatch(self): + return self.__matches + + ## + # Join group of log elements which may be a mix of bytes and strings + # + # @param elements list of strings and bytes + # @return elements joined as string + + @staticmethod + def _joinStrAndBytes(elements): + strElements = [] + for element in elements: + if isinstance(element, str): + strElements.append(element) + else: + strElements.append(str(element, errors='ignore')) + return " ".join(strElements) + + ## + # Format journal log entry into syslog style + # + # @param entry systemd journal entry dict + # @return format log line + + @staticmethod + def formatJournalEntry(logentry): + logelements = [logentry.get('_SOURCE_REALTIME_TIMESTAMP', + logentry.get('__REALTIME_TIMESTAMP')).strftime("%b %d %H:%M:%S %Y")] + if logentry.get('_HOSTNAME'): + logelements.append(logentry['_HOSTNAME']) + if logentry.get('SYSLOG_IDENTIFIER'): + logelements.append(logentry['SYSLOG_IDENTIFIER']) + if logentry.get('_PID'): + logelements[-1] += ("[%i]" % logentry['_PID']) + logelements[-1] += ":" + elif logentry.get('_COMM'): + logelements.append(logentry['_COMM']) + if logentry.get('_PID'): + logelements[-1] += ("[%i]" % logentry['_PID']) + logelements[-1] += ":" + if logelements[-1] == "kernel:": + if '_SOURCE_MONOTONIC_TIMESTAMP' in logentry: + monotonic = logentry.get('_SOURCE_MONOTONIC_TIMESTAMP') + else: + monotonic = logentry.get('__MONOTONIC_TIMESTAMP')[0] + logelements.append("[%12.6f]" % monotonic.total_seconds()) + if isinstance(logentry.get('MESSAGE',''), list): + logelements.append(" ".join(logentry['MESSAGE'])) + else: + logelements.append(logentry.get('MESSAGE', '')) + + try: + logline = u" ".join(logelements) + u"\n" + except UnicodeDecodeError: + # Python 2, so treat as string + logline = " ".join([str(logline) for logline in logelements]) + "\n" + except TypeError: + # Python 3, one or more elements bytes + logSys.warning("Error decoding log elements from journal: %s" % + repr(logelements)) + logline = self._joinStrAndBytes(logelements) + "\n" + + logSys.debug("Read systemd journal entry: %s" % repr(logline)) + return logline + + ## + # Main loop. + # + # Peridocily check for new journal entries matching the filter and + # handover to FailManager + + def run(self): + self.setActive(True) + + # Seek to now - findtime in journal + start_time = datetime.datetime.now() - \ + datetime.timedelta(seconds=int(self.getFindTime())) + self.__journal.seek_realtime(start_time) + # Move back one entry to ensure do not end up in dead space + # if start time beyond end of journal + try: + self.__journal.get_previous() + except OSError: + pass # Reading failure, so safe to ignore + + while self._isActive(): + if not self.getIdle(): + while self._isActive(): + try: + logentry = self.__journal.get_next() + except OSError: + logSys.warning( + "Error reading line from systemd journal") + continue + if logentry: + self.processLineAndAdd( + self.formatJournalEntry(logentry)) + self.__modified = True + else: + break + if self.__modified: + try: + while True: + ticket = self.failManager.toBan() + self.jail.putFailTicket(ticket) + except FailManagerEmpty: + self.failManager.cleanup(MyTime.time()) + self.dateDetector.sortTemplate() + self.__modified = False + self.__journal.wait(self.getSleepTime()) + logSys.debug((self.jail is not None and self.jail.getName() + or "jailless") +" filter terminated") + return True + + ## + # Get the status of the filter. + # + # Get some informations about the filter state such as the total + # number of failures. + # @return a list with tuple + + def status(self): + ret = JournalFilter.status(self) + ret.append(("Journal matches", [" + ".join(self.__matches)])) + return ret diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index fa2a8fa5..a53f1c0d 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -35,7 +35,7 @@ class Jail: #Known backends. Each backend should have corresponding __initBackend method # yoh: stored in a list instead of a tuple since only # list had .index until 2.6 - _BACKENDS = ['pyinotify', 'gamin', 'polling'] + _BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd'] def __init__(self, name, backend = "auto"): self.__name = name @@ -101,6 +101,13 @@ class Jail: from filterpyinotify import FilterPyinotify self.__filter = FilterPyinotify(self) + def _initSystemd(self): + # Try to import systemd + import systemd + logSys.info("Jail '%s' uses systemd" % self.__name) + from filtersystemd import FilterSystemd + self.__filter = FilterSystemd(self) + def setName(self, name): self.__name = name diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 0ed6292d..b496b6ad 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -26,6 +26,7 @@ __license__ = "GPL" from threading import Lock, RLock from jails import Jails +from filter import FileFilter, JournalFilter from transmitter import Transmitter from asyncserver import AsyncServer from asyncserver import AsyncServerException @@ -169,14 +170,41 @@ class Server: return self.__jails.getFilter(name).getIgnoreIP() def addLogPath(self, name, fileName): - self.__jails.getFilter(name).addLogPath(fileName) + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, FileFilter): + filter_.addLogPath(fileName) def delLogPath(self, name, fileName): - self.__jails.getFilter(name).delLogPath(fileName) + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, FileFilter): + self.__jails.getFilter(name).delLogPath(fileName) def getLogPath(self, name): - return [m.getFileName() - for m in self.__jails.getFilter(name).getLogPath()] + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, FileFilter): + return [m.getFileName() + for m in filter_.getLogPath()] + else: + logSys.info("Jail %s is not a FileFilter instance" % name) + return [] + + def addJournalMatch(self, name, match): + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, JournalFilter): + filter_.addJournalMatch(match) + + def delJournalMatch(self, name, match): + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, JournalFilter): + filter_.delJournalMatch(match) + + def getJournalMatch(self, name): + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, JournalFilter): + return filter_.getJournalMatch() + else: + logSys.info("Jail %s is not a JournalFilter instance" % name) + return [] def setLogEncoding(self, name, encoding): return self.__jails.getFilter(name).setLogEncoding(encoding) diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 22681bf7..c012ceac 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -144,6 +144,14 @@ class Transmitter: value = command[2] self.__server.setLogEncoding(name, value) return self.__server.getLogEncoding(name) + elif command[1] == "addjournalmatch": + value = ' '.join(command[2:]) + self.__server.addJournalMatch(name, value) + return self.__server.getJournalMatch(name) + elif command[1] == "deljournalmatch": + value = ' '.join(command[2:]) + self.__server.delJournalMatch(name, value) + return self.__server.getJournalMatch(name) elif command[1] == "addfailregex": value = command[2] self.__server.addFailRegex(name, value) @@ -250,6 +258,8 @@ class Transmitter: return self.__server.getLogPath(name) elif command[1] == "logencoding": return self.__server.getLogEncoding(name) + elif command[1] == "journalmatch": + return self.__server.getJournalMatch(name) elif command[1] == "ignoreip": return self.__server.getIgnoreIP(name) elif command[1] == "failregex": diff --git a/fail2ban/tests/files/testcase-journal.log b/fail2ban/tests/files/testcase-journal.log new file mode 100644 index 00000000..720a3130 --- /dev/null +++ b/fail2ban/tests/files/testcase-journal.log @@ -0,0 +1,19 @@ +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from failed.dns.ch +error: PAM: Authentication failure for kevin from failed.dns.ch +error: PAM: Authentication failure for kevin from failed.dns.ch +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 193.168.0.128 +error: PAM: Authentication failure for kevin from 87.142.124.10 +error: PAM: Authentication failure for kevin from 87.142.124.10 +error: PAM: Authentication failure for kevin from 87.142.124.10 +error: PAM: Authentication failure for kevin from 87.142.124.10 diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 70e386d6..0fe2367a 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -29,6 +29,11 @@ import sys import time import tempfile +try: + from systemd import journal +except ImportError: + journal = None + from fail2ban.server.jail import Jail from fail2ban.server.filterpoll import FilterPoll from fail2ban.server.filter import FileFilter, DNSUtils @@ -160,6 +165,34 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line time.sleep(0.1) return fout +def _copy_lines_to_journal(in_, fields={},n=None, skip=0, terminal_line=""): + """Copy lines from one file to systemd journal + + Returns None + """ + if isinstance(in_, str): # pragma: no branch - only used with str in test cases + fin = open(in_, 'r') + else: + fin = in_ + # Required for filtering + fields.update({"SYSLOG_IDENTIFIER": "fail2ban-testcases", + "PRIORITY": "7", + }) + # Skip + for i in xrange(skip): + _ = fin.readline() + # Read/Write + i = 0 + while n is None or i < n: + l = fin.readline() + if terminal_line is not None and l == terminal_line: + break + journal.send(MESSAGE=l.strip(), **fields) + i += 1 + if isinstance(in_, str): # pragma: no branch - only used with str in test cases + # Opened earlier, therefore must close it + fin.close() + # # Actual tests # @@ -574,6 +607,129 @@ def get_monitor_failures_testcase(Filter_): % (Filter_.__name__, testclass_name) # 'tempfile') return MonitorFailures +def get_monitor_failures_journal_testcase(Filter_): + """Generator of TestCase's for journal based filters/backends + """ + + class MonitorJournalFailures(unittest.TestCase): + def setUp(self): + """Call before every test case.""" + self.test_file = os.path.join(TEST_FILES_DIR, "testcase-journal.log") + self.jail = DummyJail() + self.filter = Filter_(self.jail) + # UUID used to ensure that only meeages generated + # as part of this test are picked up by the filter + import uuid + self.test_uuid = str(uuid.uuid4()) + self.name = "monitorjournalfailures-%s" % self.test_uuid + self.filter.addJournalMatch( + "SYSLOG_IDENTIFIER=fail2ban-testcases " + "TEST_FIELD=1 " + "TEST_UUID=%s" % str(self.test_uuid)) + self.filter.addJournalMatch( + "SYSLOG_IDENTIFIER=fail2ban-testcases " + "TEST_FIELD=2 " + "TEST_UUID=%s" % self.test_uuid) + self.journal_fields = { + 'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid} + self.filter.setActive(True) + self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) ") + self.filter.start() + + def tearDown(self): + self.filter.stop() + self.filter.join() # wait for the thread to terminate + pass + + def __str__(self): + return "MonitorJournalFailures%s(%s)" \ + % (Filter_, hasattr(self, 'name') and self.name or 'tempfile') + + def isFilled(self, delay=2.): + """Wait up to `delay` sec to assure that it was modified or not + """ + time0 = time.time() + while time.time() < time0 + delay: + if len(self.jail): + return True + time.sleep(0.1) + return False + + def isEmpty(self, delay=0.4): + # shorter wait time for not modified status + return not self.isFilled(delay) + + def assert_correct_ban(self, test_ip, test_attempts): + self.assertTrue(self.isFilled(10)) # give Filter a chance to react + ticket = self.jail.getFailTicket() + + attempts = ticket.getAttempt() + ip = ticket.getIP() + matches = ticket.getMatches() + + self.assertEqual(ip, test_ip) + self.assertEqual(attempts, test_attempts) + + def test_grow_file(self): + self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) + + # Now let's feed it with entries from the file + _copy_lines_to_journal( + self.test_file, self.journal_fields, n=2) + self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) + # and our dummy jail is empty as well + self.assertFalse(len(self.jail)) + # since it should have not been enough + + _copy_lines_to_journal( + self.test_file, self.journal_fields, skip=2, n=3) + self.assertTrue(self.isFilled(6)) + # so we sleep for up to 6 sec for it not to become empty, + # and meanwhile pass to other thread(s) and filter should + # have gathered new failures and passed them into the + # DummyJail + self.assertEqual(len(self.jail), 1) + # and there should be no "stuck" ticket in failManager + self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) + self.assert_correct_ban("193.168.0.128", 3) + self.assertEqual(len(self.jail), 0) + + # Lets read some more to check it bans again + _copy_lines_to_journal( + self.test_file, self.journal_fields, skip=5, n=4) + self.assert_correct_ban("193.168.0.128", 3) + + def test_delJournalMatch(self): + # Smoke test for removing of match + + # basic full test + _copy_lines_to_journal( + self.test_file, self.journal_fields, n=5) + self.assert_correct_ban("193.168.0.128", 3) + + # and now remove the JournalMatch + self.filter.delJournalMatch( + "SYSLOG_IDENTIFIER=fail2ban-testcases " + "TEST_FIELD=1 " + "TEST_UUID=%s" % str(self.test_uuid)) + + _copy_lines_to_journal( + self.test_file, self.journal_fields, n=5, skip=5) + # so we should get no more failures detected + self.assertTrue(self.isEmpty(2)) + + # but then if we add it back again + self.filter.addJournalMatch( + "SYSLOG_IDENTIFIER=fail2ban-testcases " + "TEST_FIELD=1 " + "TEST_UUID=%s" % str(self.test_uuid)) + self.assert_correct_ban("193.168.0.128", 4) + _copy_lines_to_journal( + self.test_file, self.journal_fields, n=6, skip=10) + # we should detect the failures + self.assertTrue(self.isFilled(6)) + + return MonitorJournalFailures class GetFailures(unittest.TestCase): diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 37301e07..2e089445 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -201,6 +201,12 @@ def gatherTests(regexps=None, no_network=False): for Filter_ in filters: tests.addTest(unittest.makeSuite( filtertestcase.get_monitor_failures_testcase(Filter_))) + try: + from fail2ban.server.filtersystemd import FilterSystemd + tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd))) + except Exception, e: # pragma: no cover + logSys.warning("I: Skipping systemd backend testing. Got exception '%s'" % e) + # Server test for logging elements which break logging used to support # testcases analysis From 7751f4ad5a8a4f2ba015c2ed5e58aa3da58aca95 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 00:15:33 +0100 Subject: [PATCH 02/16] TST: Coverage ignore for systemd backend on TravisCI --- .travis_coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis_coveragerc b/.travis_coveragerc index ac4a15d5..49fc3134 100644 --- a/.travis_coveragerc +++ b/.travis_coveragerc @@ -4,3 +4,4 @@ branch = True omit = /usr/* /home/travis/virtualenv/* + fail2ban/server/filtersystemd.py From e584ab66ac06c4679261e518ea0e6a94cb45f979 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 10:27:26 +0100 Subject: [PATCH 03/16] BF: Avoid setting of log encoding for systemd backend --- fail2ban/server/server.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index b496b6ad..c411820c 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -177,7 +177,7 @@ class Server: def delLogPath(self, name, fileName): filter_ = self.__jails.getFilter(name) if isinstance(filter_, FileFilter): - self.__jails.getFilter(name).delLogPath(fileName) + filter_.delLogPath(fileName) def getLogPath(self, name): filter_ = self.__jails.getFilter(name) @@ -207,10 +207,14 @@ class Server: return [] def setLogEncoding(self, name, encoding): - return self.__jails.getFilter(name).setLogEncoding(encoding) + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, FileFilter): + filter_.setLogEncoding(encoding) def getLogEncoding(self, name): - return self.__jails.getFilter(name).getLogEncoding() + filter_ = self.__jails.getFilter(name) + if isinstance(filter_, FileFilter): + return filter_.getLogEncoding() def setFindTime(self, name, value): self.__jails.getFilter(name).setFindTime(value) From 4b5d6b69401855a59a22a8e7ff9f740ad1753d23 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 10:28:06 +0100 Subject: [PATCH 04/16] ENH: systemd backend produce more reliable isoformat dates --- fail2ban/server/filtersystemd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index ddb27cc2..3664a5bf 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -137,7 +137,7 @@ class FilterSystemd(JournalFilter): @staticmethod def formatJournalEntry(logentry): logelements = [logentry.get('_SOURCE_REALTIME_TIMESTAMP', - logentry.get('__REALTIME_TIMESTAMP')).strftime("%b %d %H:%M:%S %Y")] + logentry.get('__REALTIME_TIMESTAMP')).isoformat()] if logentry.get('_HOSTNAME'): logelements.append(logentry['_HOSTNAME']) if logentry.get('SYSLOG_IDENTIFIER'): From 809873f3592840659df6386020b3a9b45696036c Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 10:48:27 +0100 Subject: [PATCH 05/16] DOC: Update MANIFEST for systemd backend --- MANIFEST | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST b/MANIFEST index 9ead6241..e2979e12 100644 --- a/MANIFEST +++ b/MANIFEST @@ -27,6 +27,7 @@ fail2ban/server/filter.py fail2ban/server/filterpyinotify.py fail2ban/server/filtergamin.py fail2ban/server/filterpoll.py +fail2ban/server/filtersystemd.py fail2ban/server/iso8601.py fail2ban/server/server.py fail2ban/server/actions.py From b9630c29793d8ec0a296460f4cd5a890f63a7dd3 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 17:06:53 +0100 Subject: [PATCH 06/16] DOC: Add version requirement for systemd backend to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d8e81c9e..92e0a9ab 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Optional: - [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify) - Linux >= 2.6.13 - [gamin >= 0.0.21](http://www.gnome.org/~veillard/gamin) +- [systemd >= 204](http://www.freedesktop.org/wiki/Software/systemd) To install, just do: From 970291867bf3499f772d88d23c1fa628e6c6363e Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 17:14:13 +0100 Subject: [PATCH 07/16] TST: Improve tests for JailReader extract options --- fail2ban/tests/clientreadertestcase.py | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 61101aa5..79bd4664 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -145,11 +145,35 @@ class JailReaderTest(unittest.TestCase): self.assertEqual(jail.getName(), 'sshd') def testSplitOption(self): - action = "mail-whois[name=SSH]" + # Simple example + option = "mail-whois[name=SSH]" expected = ['mail-whois', {'name': 'SSH'}] - result = JailReader.extractOptions(action) - self.assertEquals(expected, result) + result = JailReader.extractOptions(option) + self.assertEqual(expected, result) + # Empty option + option = "abc[]" + expected = ['abc', {}] + result = JailReader.extractOptions(option) + self.assertEqual(expected, result) + + # More complex examples + option = 'option[opt01=abc,opt02="123",opt03="with=okay?",opt04="andwith,okay...",opt05="how about spaces",opt06="single\'in\'double",opt07=\'double"in"single\', opt08= leave some space, opt09=one for luck, opt10=, opt11=]' + expected = ['option', { + 'opt01': "abc", + 'opt02': "123", + 'opt03': "with=okay?", + 'opt04': "andwith,okay...", + 'opt05': "how about spaces", + 'opt06': "single'in'double", + 'opt07': "double\"in\"single", + 'opt08': "leave some space", + 'opt09': "one for luck", + 'opt10': "", + 'opt11': "", + }] + result = JailReader.extractOptions(option) + self.assertEqual(expected, result) class FilterReaderTest(unittest.TestCase): From 33a7763cfc5ab68cc2e6926632fe2f626f24babc Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 10 May 2013 17:24:23 +0100 Subject: [PATCH 08/16] RF+BF+ENH: Rewrite extract options, and now allow "=" char in options --- fail2ban/client/jailreader.py | 53 ++++++-------------------- fail2ban/tests/clientreadertestcase.py | 8 ++-- 2 files changed, 15 insertions(+), 46 deletions(-) diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 26882964..def4fcf1 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -36,6 +36,8 @@ logSys = logging.getLogger(__name__) class JailReader(ConfigReader): optionCRE = re.compile("^((?:\w|-|_|\.)+)(?:\[(.*)\])?$") + optionExtractRE = re.compile( + r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,]*))(?:,|$)') def __init__(self, name, force_enable=False, **kwargs): ConfigReader.__init__(self, **kwargs) @@ -155,46 +157,13 @@ class JailReader(ConfigReader): #@staticmethod def extractOptions(option): - m = JailReader.optionCRE.match(option) - d = dict() - mgroups = m.groups() - if len(mgroups) == 2: - option_name, option_opts = mgroups - elif len(mgroups) == 1: - option_name, option_opts = mgroups[0], None - else: - raise ValueError("While reading option %s we should have got up to " - "2 groups. Got: %r" % (option, mgroups)) - if not option_opts is None: - # Huge bad hack :( This method really sucks. TODO Reimplement it. - options = "" - escapeChar = None - allowComma = False - for c in option_opts: - if c in ('"', "'") and not allowComma: - # Start - escapeChar = c - allowComma = True - elif c == escapeChar: - # End - escapeChar = None - allowComma = False - else: - if c == ',' and allowComma: - options += "" - else: - options += c - - # Split using , - optionsSplit = options.split(',') - # Replace the tag with , - optionsSplit = [n.replace("", ',') for n in optionsSplit] - - for param in optionsSplit: - p = param.split('=') - try: - d[p[0].strip()] = p[1].strip() - except IndexError: - logSys.error("Invalid argument %s in '%s'" % (p, option_opts)) - return [option_name, d] + option_name, optstr = JailReader.optionCRE.match(option).groups() + option_opts = dict() + if optstr: + for optmatch in JailReader.optionExtractRE.finditer(optstr): + opt = optmatch.group(1) + value = [ + val for val in optmatch.group(2,3,4) if val is not None][0] + option_opts[opt.strip()] = value.strip() + return option_name, option_opts extractOptions = staticmethod(extractOptions) diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 79bd4664..5e35eabc 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -147,19 +147,19 @@ class JailReaderTest(unittest.TestCase): def testSplitOption(self): # Simple example option = "mail-whois[name=SSH]" - expected = ['mail-whois', {'name': 'SSH'}] + expected = ('mail-whois', {'name': 'SSH'}) result = JailReader.extractOptions(option) self.assertEqual(expected, result) # Empty option option = "abc[]" - expected = ['abc', {}] + expected = ('abc', {}) result = JailReader.extractOptions(option) self.assertEqual(expected, result) # More complex examples option = 'option[opt01=abc,opt02="123",opt03="with=okay?",opt04="andwith,okay...",opt05="how about spaces",opt06="single\'in\'double",opt07=\'double"in"single\', opt08= leave some space, opt09=one for luck, opt10=, opt11=]' - expected = ['option', { + expected = ('option', { 'opt01': "abc", 'opt02': "123", 'opt03': "with=okay?", @@ -171,7 +171,7 @@ class JailReaderTest(unittest.TestCase): 'opt09': "one for luck", 'opt10': "", 'opt11': "", - }] + }) result = JailReader.extractOptions(option) self.assertEqual(expected, result) From c08bd67f5061f90afede2fcffe4961d66dcff458 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sun, 12 May 2013 13:05:21 +0100 Subject: [PATCH 09/16] BF: fail2ban-regex systemd-journal field fix for __CURSOR --- bin/fail2ban-regex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fail2ban-regex b/bin/fail2ban-regex index 078a936f..59f4f45e 100755 --- a/bin/fail2ban-regex +++ b/bin/fail2ban-regex @@ -413,7 +413,7 @@ if __name__ == "__main__": if journal is None: print "Error: systemd library not found. Exiting..." sys.exit(-1) - myjournal = journal.Reader() + myjournal = journal.Reader(converters={'__CURSOR': lambda x: x}) journalmatch = "" # Parse journal matches from command line for opt in optList: From 90de5aa568c3c205565ebe98712b948c8d4b1332 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sun, 12 May 2013 13:40:25 +0100 Subject: [PATCH 10/16] TST: Update travis coverage config to exempt systemd related code --- .travis.yml | 2 ++ .travis_coveragerc | 8 ++++++++ fail2ban/server/filter.py | 2 +- fail2ban/server/filtersystemd.py | 2 +- fail2ban/server/jail.py | 2 +- fail2ban/server/server.py | 8 ++++---- fail2ban/server/transmitter.py | 6 +++--- fail2ban/tests/filtertestcase.py | 4 ++-- fail2ban/tests/utils.py | 2 +- 9 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8cfeeff1..4d312575 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,4 +17,6 @@ script: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then export PYTHONPATH="$PYTHONPATH:/usr/share/pyshared:/usr/lib/pyshared/python2.7"; fi - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coverage run --rcfile=.travis_coveragerc setup.py test; else python setup.py test; fi after_success: +# Coverage config file must be .coveragerc for coveralls + - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cp -v .travis_coveragerc .coveragerc; fi - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coveralls; fi diff --git a/.travis_coveragerc b/.travis_coveragerc index 49fc3134..70cdc3e3 100644 --- a/.travis_coveragerc +++ b/.travis_coveragerc @@ -5,3 +5,11 @@ omit = /usr/* /home/travis/virtualenv/* fail2ban/server/filtersystemd.py + +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # systemd backend related + pragma: systemd no cover diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 4e892acb..51d9fd94 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -648,7 +648,7 @@ class FileContainer: # # Base interface class for systemd journal filters -class JournalFilter(Filter): +class JournalFilter(Filter): # pragma: systemd no cover def addJournalMatch(self, match): pass diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index 3664a5bf..2c3ca0ae 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -45,7 +45,7 @@ logSys = logging.getLogger("fail2ban.filter") # else that matches a given regular expression. This class is instantiated by # a Jail object. -class FilterSystemd(JournalFilter): +class FilterSystemd(JournalFilter): # pragma: systemd no cover ## # Constructor. # diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index a53f1c0d..86b901af 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -101,7 +101,7 @@ class Jail: from filterpyinotify import FilterPyinotify self.__filter = FilterPyinotify(self) - def _initSystemd(self): + def _initSystemd(self): # pragma: systemd no cover # Try to import systemd import systemd logSys.info("Jail '%s' uses systemd" % self.__name) diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index c411820c..436aa40e 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -184,21 +184,21 @@ class Server: if isinstance(filter_, FileFilter): return [m.getFileName() for m in filter_.getLogPath()] - else: + else: # pragma: systemd no cover logSys.info("Jail %s is not a FileFilter instance" % name) return [] - def addJournalMatch(self, name, match): + def addJournalMatch(self, name, match): # pragma: systemd no cover filter_ = self.__jails.getFilter(name) if isinstance(filter_, JournalFilter): filter_.addJournalMatch(match) - def delJournalMatch(self, name, match): + def delJournalMatch(self, name, match): # pragma: systemd no cover filter_ = self.__jails.getFilter(name) if isinstance(filter_, JournalFilter): filter_.delJournalMatch(match) - def getJournalMatch(self, name): + def getJournalMatch(self, name): # pragma: systemd no cover filter_ = self.__jails.getFilter(name) if isinstance(filter_, JournalFilter): return filter_.getJournalMatch() diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index c012ceac..8bacdd0a 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -144,11 +144,11 @@ class Transmitter: value = command[2] self.__server.setLogEncoding(name, value) return self.__server.getLogEncoding(name) - elif command[1] == "addjournalmatch": + elif command[1] == "addjournalmatch": # pragma: systemd no cover value = ' '.join(command[2:]) self.__server.addJournalMatch(name, value) return self.__server.getJournalMatch(name) - elif command[1] == "deljournalmatch": + elif command[1] == "deljournalmatch": # pragma: systemd no cover value = ' '.join(command[2:]) self.__server.delJournalMatch(name, value) return self.__server.getJournalMatch(name) @@ -258,7 +258,7 @@ class Transmitter: return self.__server.getLogPath(name) elif command[1] == "logencoding": return self.__server.getLogEncoding(name) - elif command[1] == "journalmatch": + elif command[1] == "journalmatch": # pragma: systemd no cover return self.__server.getJournalMatch(name) elif command[1] == "ignoreip": return self.__server.getIgnoreIP(name) diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 0fe2367a..c2f43fb4 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -165,7 +165,7 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line time.sleep(0.1) return fout -def _copy_lines_to_journal(in_, fields={},n=None, skip=0, terminal_line=""): +def _copy_lines_to_journal(in_, fields={},n=None, skip=0, terminal_line=""): # pragma: systemd no cover """Copy lines from one file to systemd journal Returns None @@ -607,7 +607,7 @@ def get_monitor_failures_testcase(Filter_): % (Filter_.__name__, testclass_name) # 'tempfile') return MonitorFailures -def get_monitor_failures_journal_testcase(Filter_): +def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover """Generator of TestCase's for journal based filters/backends """ diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 2e089445..160dc802 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -201,7 +201,7 @@ def gatherTests(regexps=None, no_network=False): for Filter_ in filters: tests.addTest(unittest.makeSuite( filtertestcase.get_monitor_failures_testcase(Filter_))) - try: + try: # pragma: systemd no cover from fail2ban.server.filtersystemd import FilterSystemd tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd))) except Exception, e: # pragma: no cover From fe3ed176df131dbc4ec3a29dd79e9dfeeb9db9c5 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 13 May 2013 21:12:58 +0100 Subject: [PATCH 11/16] DOC: Update jail.conf man page with systemd elements --- man/jail.conf.5 | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/man/jail.conf.5 b/man/jail.conf.5 index 14dc5663..5a79b331 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -60,6 +60,26 @@ The following options are applicable to all jails. Their meaning is described in .TP \fBusedns\fR .PP +.SS Backends +\fBbackend\fR specifies the backend used to get files modification. This option can be overridden in each jail as well. +Available options are listed below. +.TP +\fIpyinotify\fR +requires pyinotify (a file alteration monitor) to be installed. If pyinotify is not installed, Fail2ban will use auto. +.TP +\fIgamin\fR +requires Gamin (a file alteration monitor) to be installed. If Gamin is not installed, Fail2ban will use auto. +.TP +\fIpolling\fR +uses a polling algorithm which does not require external libraries. +.TP +\fIsystemd\fR +uses systemd python library to access the systemd journal. Specifying \fBlogpath\fR is not valid for this backend and instead utilises \fBjournalmatch\fR from the jails associated filter config. +.TP +\fIauto\fR +will try to use the following backends, in order: pyinotify, gamin, polling +.PP +.SS Actions Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename. In the case where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplicatione.g.: .PP .nf @@ -153,6 +173,9 @@ Similar to actions, filters have an [Init] section which can be overridden in \f .TP \fBmaxlines\fR specifies the maximum number of lines to buffer to match multi-line regexs. For some log formats this will not required to be changed. Other logs may require to increase this value if a particular log file is frequently written to. +.TP +\fBjournalmatch\fR +specifies the systemd journal match used to filter the journal entries. See \fBjournalctl(1)\fR and \fBsystemd.journal-fields(7)\fR for matches syntax and more details on special journal fields. This option is only valid for the \fIsystemd\fR backend. .PP Filters can also have a section called [INCLUDES]. This is used to read other configuration files. From c1226afe92854d545d5bf0fa817f0e7e324098e4 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 13 May 2013 23:42:09 +0100 Subject: [PATCH 12/16] TST: Add tests for transmitter journalmatch --- fail2ban/server/filter.py | 6 +-- fail2ban/tests/servertestcase.py | 84 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 51d9fd94..38c67f5f 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -650,13 +650,13 @@ class FileContainer: class JournalFilter(Filter): # pragma: systemd no cover - def addJournalMatch(self, match): + def addJournalMatch(self, match): # pragma: no cover - Base class, not used pass - def delJournalMatch(self, match): + def delJournalMatch(self, match): # pragma: no cover - Base class, not used pass - def getJournalMatch(self, match): + def getJournalMatch(self, match): # pragma: no cover - Base class, not used return [] ## diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 6e07eaaf..953cf318 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -28,6 +28,10 @@ import unittest, socket, time, tempfile, os, locale from fail2ban.server.server import Server from fail2ban.exceptions import UnknownJailException +try: + from fail2ban.server import filtersystemd +except ImportError: + filtersystemd = None TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") @@ -498,6 +502,86 @@ class Transmitter(TransmitterBase): self.assertEqual( self.transm.proceed(["status", "INVALID", "COMMAND"])[0],1) + if filtersystemd: # pragma: systemd no cover + def testJournalMatch(self): + jailName = "TestJail2" + self.server.addJail(jailName, "systemd") + self.jailAddDelTest( + "journalmatch", + [ + "_SYSTEMD_UNIT=sshd.service", + "TEST_FIELD1=ABC TEST_FIELD2=123", + "_HOSTNAME=example.com", + ], + jailName + ) + values = [ + '"FIELD=Test + Value+ \\\"Test+Value=\'Test"', + 'FIELD="Test + Value+ \\\"Test+Value=\'Test"', + ] + + # Test shell like escaping for spaces + for value in values: + self.assertEqual( + self.transm.proceed( + ["set", jailName, "addjournalmatch", value]), + (0, ["FIELD=Test + Value+ \"Test+Value='Test"])) + self.assertEqual( + self.transm.proceed( + ["set", jailName, "deljournalmatch", value]), + (0, [])) + + # Try duplicates + value = "_COMM=sshd" + self.assertEqual( + self.transm.proceed( + ["set", jailName, "addjournalmatch", value]), + (0, [value])) + # Duplicates are accepted, as automatically OR'd, and journalctl + # also accepts them without issue. + self.assertEqual( + self.transm.proceed( + ["set", jailName, "addjournalmatch", value]), + (0, [value, value])) + # Remove first instance + self.assertEqual( + self.transm.proceed( + ["set", jailName, "deljournalmatch", value]), + (0, [value])) + # Remove second instance + self.assertEqual( + self.transm.proceed( + ["set", jailName, "deljournalmatch", value]), + (0, [])) + + # Test splitting of OR'd values + value1, value2 = "_COMM=sshd", "_SYSTEMD_UNIT=sshd.service" + self.assertEqual( + self.transm.proceed( + ["set", jailName, "addjournalmatch", + " + ".join([value1, value2])]), + (0, [value1, value2])) + self.assertEqual( + self.transm.proceed( + ["set", jailName, "deljournalmatch", value1]), + (0, [value2])) + self.assertEqual( + self.transm.proceed( + ["set", jailName, "deljournalmatch", value2]), + (0, [])) + + # Invalid match + value = "This isn't valid!" + result = self.transm.proceed( + ["set", jailName, "addjournalmatch", value]) + self.assertTrue(isinstance(result[1], ValueError)) + + # Delete invalid match + value = "FIELD=NotPresent" + result = self.transm.proceed( + ["set", jailName, "deljournalmatch", value]) + self.assertTrue(isinstance(result[1], ValueError)) + class TransmitterLogging(TransmitterBase): def setUp(self): From 09199095b4457f3374056c8f596db27e1de28a0b Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 13 May 2013 23:42:33 +0100 Subject: [PATCH 13/16] BF: Allow journal matches with spaces and "+" in --- fail2ban/server/filtersystemd.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index 2c3ca0ae..b4f7e7db 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -23,7 +23,7 @@ __author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko, Steven Hiscocks" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko, 2013 Steven Hiscocks" __license__ = "GPL" -import logging, datetime +import logging, datetime, shlex from distutils.version import LooseVersion from systemd import journal @@ -68,18 +68,22 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover def addJournalMatch(self, match): if self.__matches: self.__journal.add_disjunction() # Add OR + newMatches = [[]] try: - for match_element in match.split(): + for match_element in shlex.split(match): if match_element == "+": self.__journal.add_disjunction() + newMatches.append([]) else: self.__journal.add_match(match_element) - except: + newMatches[-1].append(match_element) + except ValueError: logSys.error("Error adding journal match for: %s", match) self.resetJournalMatches() + raise else: - for match_element in match.split('+'): - self.__matches.append(match_element.strip()) + self.__matches.extend( + " ".join(newMatch) for newMatch in newMatches) logSys.debug("Adding journal match for: %s", match) ## # Reset a journal match filter called on removal or failure @@ -100,9 +104,12 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover # @param match journalctl syntax matches def delJournalMatch(self, match): + match = " ".join(shlex.split(match)) if match in self.__matches: del self.__matches[self.__matches.index(match)] self.resetJournalMatches() + else: + raise ValueError("Match not found") ## # Get current journal match filter From 82211e3891937733d2c37909ae8501d6b6b0c275 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 13 May 2013 23:44:01 +0100 Subject: [PATCH 14/16] TST: TravisCI coverage now includes untested systemd backend elements --- .travis_coveragerc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis_coveragerc b/.travis_coveragerc index 70cdc3e3..49fc3134 100644 --- a/.travis_coveragerc +++ b/.travis_coveragerc @@ -5,11 +5,3 @@ omit = /usr/* /home/travis/virtualenv/* fail2ban/server/filtersystemd.py - -[report] -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # systemd backend related - pragma: systemd no cover From 00e289e11b7bc5b3219df953249ab19ec034ed4b Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Wed, 15 May 2013 00:19:22 +0100 Subject: [PATCH 15/16] BF+TST: Fix handling of spaces and + char for journalmatch Previous fix attempted shlex split which whilst worked for reading from config file, failed when using fail2ban-client, as the input is already effectively shelx split by the executing shell. FilterSystemd journal match methods now handle list structures which should be shlex split when reading from config file, and simply pass all the relevant arguments from the shell when using fail2ban-client --- fail2ban/client/beautifier.py | 2 +- fail2ban/client/filterreader.py | 6 +- fail2ban/server/filtersystemd.py | 55 +++++++++++++------ fail2ban/server/transmitter.py | 4 +- fail2ban/tests/clientreadertestcase.py | 7 ++- fail2ban/tests/files/filter.d/testcase01.conf | 7 +++ fail2ban/tests/filtertestcase.py | 32 +++++------ fail2ban/tests/servertestcase.py | 44 ++++++--------- 8 files changed, 90 insertions(+), 67 deletions(-) diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index ea03b7b8..0c5e31fb 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -118,7 +118,7 @@ class Beautifier: msg = "No journal match filter set" else: msg = "Current match filter:\n" - msg += ' + '.join(response) + msg += ' + '.join(" ".join(res) for res in response) elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"): if len(response) == 0: msg = "No IP address/network is ignored" diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index 5c58046b..e5fe8f4f 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -24,7 +24,7 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -import logging, os +import logging, os, shlex from configreader import ConfigReader, DefinitionInitConfigReader # Gets the instance of the logger. @@ -59,6 +59,8 @@ class FilterReader(DefinitionInitConfigReader): # Do not send a command if the match is empty. if self._initOpts.get("journalmatch", '') != '': for match in self._initOpts["journalmatch"].split("\n"): - stream.append(["set", self._jailName, "addjournalmatch", match]) + stream.append( + ["set", self._jailName, "addjournalmatch"] + + shlex.split(match)) return stream diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index b4f7e7db..838805a5 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -23,7 +23,7 @@ __author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko, Steven Hiscocks" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko, 2013 Steven Hiscocks" __license__ = "GPL" -import logging, datetime, shlex +import logging, datetime from distutils.version import LooseVersion from systemd import journal @@ -60,31 +60,45 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover self.__matches = [] logSys.debug("Created FilterSystemd") + + ## + # Add a journal match filters from list structure + # + # @param matches list structure with journal matches + + def _addJournalMatches(self, matches): + if self.__matches: + self.__journal.add_disjunction() # Add OR + newMatches = [] + for match in matches: + newMatches.append([]) + for match_element in match: + self.__journal.add_match(match_element) + newMatches[-1].append(match_element) + self.__journal.add_disjunction() + self.__matches.extend(newMatches) + ## # Add a journal match filter # - # @param match journalctl syntax matches + # @param match journalctl syntax matches in list structure def addJournalMatch(self, match): - if self.__matches: - self.__journal.add_disjunction() # Add OR newMatches = [[]] + for match_element in match: + if match_element == "+": + newMatches.append([]) + else: + newMatches[-1].append(match_element) try: - for match_element in shlex.split(match): - if match_element == "+": - self.__journal.add_disjunction() - newMatches.append([]) - else: - self.__journal.add_match(match_element) - newMatches[-1].append(match_element) + self._addJournalMatches(newMatches) except ValueError: - logSys.error("Error adding journal match for: %s", match) + logSys.error( + "Error adding journal match for: %r", " ".join(match)) self.resetJournalMatches() raise else: - self.__matches.extend( - " ".join(newMatch) for newMatch in newMatches) - logSys.debug("Adding journal match for: %s", match) + logSys.info("Added journal match for: %r", " ".join(match)) ## # Reset a journal match filter called on removal or failure # @@ -95,8 +109,13 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover logSys.debug("Flushed all journal matches") match_copy = self.__matches[:] self.__matches = [] - for match in match_copy: - self.addJournalMatch(match) + try: + self._addJournalMatches(match_copy) + except ValueError: + logSys.error("Error restoring journal matches") + raise + else: + logSys.debug("Journal matches restored") ## # Delete a journal match filter @@ -104,12 +123,12 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover # @param match journalctl syntax matches def delJournalMatch(self, match): - match = " ".join(shlex.split(match)) if match in self.__matches: del self.__matches[self.__matches.index(match)] self.resetJournalMatches() else: raise ValueError("Match not found") + logSys.info("Removed journal match for: %r" % " ".join(match)) ## # Get current journal match filter diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 8bacdd0a..39c74452 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -145,11 +145,11 @@ class Transmitter: self.__server.setLogEncoding(name, value) return self.__server.getLogEncoding(name) elif command[1] == "addjournalmatch": # pragma: systemd no cover - value = ' '.join(command[2:]) + value = command[2:] self.__server.addJournalMatch(name, value) return self.__server.getJournalMatch(name) elif command[1] == "deljournalmatch": # pragma: systemd no cover - value = ' '.join(command[2:]) + value = command[2:] self.__server.delJournalMatch(name, value) return self.__server.getJournalMatch(name) elif command[1] == "addfailregex": diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 5e35eabc..6fd9f451 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -197,7 +197,12 @@ class FilterReaderTest(unittest.TestCase): "+$^.+ module for .* from \\s*$"], ['set', 'testcase01', 'addignoreregex', "^.+ john from host 192.168.1.1\\s*$"], - ['set', 'testcase01', 'maxlines', "1"]] + ['set', 'testcase01', 'addjournalmatch', + "_COMM=sshd", "+", "_SYSTEMD_UNIT=sshd.service", "_UID=0"], + ['set', 'testcase01', 'addjournalmatch', + "FIELD= with spaces ", "+", "AFIELD= with + char and spaces"], + ['set', 'testcase01', 'maxlines', "1"], # Last for overide test + ] filterReader = FilterReader("testcase01", "testcase01", {}) filterReader.setBaseDir(TEST_FILES_DIR) filterReader.read() diff --git a/fail2ban/tests/files/filter.d/testcase01.conf b/fail2ban/tests/files/filter.d/testcase01.conf index c549572d..8bc4261d 100644 --- a/fail2ban/tests/files/filter.d/testcase01.conf +++ b/fail2ban/tests/files/filter.d/testcase01.conf @@ -36,3 +36,10 @@ ignoreregex = ^.+ john from host 192.168.1.1\s*$ [Init] # "maxlines" is number of log lines to buffer for multi-line regex searches maxlines = 1 + +# Option: journalmatch +# Notes.: systemd journalctl style match filter for journal based backends +# Values: TEXT +# +journalmatch = _COMM=sshd + _SYSTEMD_UNIT=sshd.service _UID=0 + "FIELD= with spaces " + AFIELD=" with + char and spaces" diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index c2f43fb4..7b68e16c 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -622,14 +622,14 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover import uuid self.test_uuid = str(uuid.uuid4()) self.name = "monitorjournalfailures-%s" % self.test_uuid - self.filter.addJournalMatch( - "SYSLOG_IDENTIFIER=fail2ban-testcases " - "TEST_FIELD=1 " - "TEST_UUID=%s" % str(self.test_uuid)) - self.filter.addJournalMatch( - "SYSLOG_IDENTIFIER=fail2ban-testcases " - "TEST_FIELD=2 " - "TEST_UUID=%s" % self.test_uuid) + self.filter.addJournalMatch([ + "SYSLOG_IDENTIFIER=fail2ban-testcases", + "TEST_FIELD=1", + "TEST_UUID=%s" % self.test_uuid]) + self.filter.addJournalMatch([ + "SYSLOG_IDENTIFIER=fail2ban-testcases", + "TEST_FIELD=2", + "TEST_UUID=%s" % self.test_uuid]) self.journal_fields = { 'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid} self.filter.setActive(True) @@ -708,10 +708,10 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover self.assert_correct_ban("193.168.0.128", 3) # and now remove the JournalMatch - self.filter.delJournalMatch( - "SYSLOG_IDENTIFIER=fail2ban-testcases " - "TEST_FIELD=1 " - "TEST_UUID=%s" % str(self.test_uuid)) + self.filter.delJournalMatch([ + "SYSLOG_IDENTIFIER=fail2ban-testcases", + "TEST_FIELD=1", + "TEST_UUID=%s" % self.test_uuid]) _copy_lines_to_journal( self.test_file, self.journal_fields, n=5, skip=5) @@ -719,10 +719,10 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover self.assertTrue(self.isEmpty(2)) # but then if we add it back again - self.filter.addJournalMatch( - "SYSLOG_IDENTIFIER=fail2ban-testcases " - "TEST_FIELD=1 " - "TEST_UUID=%s" % str(self.test_uuid)) + self.filter.addJournalMatch([ + "SYSLOG_IDENTIFIER=fail2ban-testcases", + "TEST_FIELD=1", + "TEST_UUID=%s" % self.test_uuid]) self.assert_correct_ban("193.168.0.128", 4) _copy_lines_to_journal( self.test_file, self.journal_fields, n=6, skip=10) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 953cf318..19a070c4 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -506,68 +506,58 @@ class Transmitter(TransmitterBase): def testJournalMatch(self): jailName = "TestJail2" self.server.addJail(jailName, "systemd") - self.jailAddDelTest( - "journalmatch", - [ - "_SYSTEMD_UNIT=sshd.service", - "TEST_FIELD1=ABC TEST_FIELD2=123", - "_HOSTNAME=example.com", - ], - jailName - ) values = [ - '"FIELD=Test + Value+ \\\"Test+Value=\'Test"', - 'FIELD="Test + Value+ \\\"Test+Value=\'Test"', + "_SYSTEMD_UNIT=sshd.service", + "TEST_FIELD1=ABC", + "_HOSTNAME=example.com", ] - - # Test shell like escaping for spaces - for value in values: + for n, value in enumerate(values): self.assertEqual( self.transm.proceed( ["set", jailName, "addjournalmatch", value]), - (0, ["FIELD=Test + Value+ \"Test+Value='Test"])) + (0, [[val] for val in values[:n+1]])) + for n, value in enumerate(values): self.assertEqual( self.transm.proceed( ["set", jailName, "deljournalmatch", value]), - (0, [])) + (0, [[val] for val in values[n+1:]])) # Try duplicates value = "_COMM=sshd" self.assertEqual( self.transm.proceed( ["set", jailName, "addjournalmatch", value]), - (0, [value])) + (0, [[value]])) # Duplicates are accepted, as automatically OR'd, and journalctl # also accepts them without issue. self.assertEqual( self.transm.proceed( ["set", jailName, "addjournalmatch", value]), - (0, [value, value])) + (0, [[value], [value]])) # Remove first instance self.assertEqual( self.transm.proceed( ["set", jailName, "deljournalmatch", value]), - (0, [value])) + (0, [[value]])) # Remove second instance self.assertEqual( self.transm.proceed( ["set", jailName, "deljournalmatch", value]), (0, [])) - # Test splitting of OR'd values - value1, value2 = "_COMM=sshd", "_SYSTEMD_UNIT=sshd.service" + value = [ + "_COMM=sshd", "+", "_SYSTEMD_UNIT=sshd.service", "_UID=0"] self.assertEqual( self.transm.proceed( - ["set", jailName, "addjournalmatch", - " + ".join([value1, value2])]), - (0, [value1, value2])) + ["set", jailName, "addjournalmatch"] + value), + (0, [["_COMM=sshd"], ["_SYSTEMD_UNIT=sshd.service", "_UID=0"]])) self.assertEqual( self.transm.proceed( - ["set", jailName, "deljournalmatch", value1]), - (0, [value2])) + ["set", jailName, "deljournalmatch"] + value[:1]), + (0, [["_SYSTEMD_UNIT=sshd.service", "_UID=0"]])) self.assertEqual( self.transm.proceed( - ["set", jailName, "deljournalmatch", value2]), + ["set", jailName, "deljournalmatch"] + value[2:]), (0, [])) # Invalid match From 01109e3a048a617ec79831f4228ba6c30dfce3b9 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sun, 26 May 2013 14:05:11 +0100 Subject: [PATCH 16/16] BF: Fix status of systemd filter backend --- fail2ban/server/filtersystemd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index 838805a5..140d8020 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -259,5 +259,6 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover def status(self): ret = JournalFilter.status(self) - ret.append(("Journal matches", [" + ".join(self.__matches)])) + ret.append(("Journal matches", + [" + ".join(" ".join(match) for match in self.__matches)])) return ret