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
pull/224/head
Steven Hiscocks 2013-05-15 00:19:22 +01:00
parent 82211e3891
commit 00e289e11b
8 changed files with 90 additions and 67 deletions

View File

@ -118,7 +118,7 @@ class Beautifier:
msg = "No journal match filter set" msg = "No journal match filter set"
else: else:
msg = "Current match filter:\n" msg = "Current match filter:\n"
msg += ' + '.join(response) msg += ' + '.join(" ".join(res) for res in response)
elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"): elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"):
if len(response) == 0: if len(response) == 0:
msg = "No IP address/network is ignored" msg = "No IP address/network is ignored"

View File

@ -24,7 +24,7 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, os import logging, os, shlex
from configreader import ConfigReader, DefinitionInitConfigReader from configreader import ConfigReader, DefinitionInitConfigReader
# Gets the instance of the logger. # Gets the instance of the logger.
@ -59,6 +59,8 @@ class FilterReader(DefinitionInitConfigReader):
# Do not send a command if the match is empty. # Do not send a command if the match is empty.
if self._initOpts.get("journalmatch", '') != '': if self._initOpts.get("journalmatch", '') != '':
for match in self._initOpts["journalmatch"].split("\n"): 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 return stream

View File

@ -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" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko, 2013 Steven Hiscocks"
__license__ = "GPL" __license__ = "GPL"
import logging, datetime, shlex import logging, datetime
from distutils.version import LooseVersion from distutils.version import LooseVersion
from systemd import journal from systemd import journal
@ -60,31 +60,45 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
self.__matches = [] self.__matches = []
logSys.debug("Created FilterSystemd") 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 # Add a journal match filter
# #
# @param match journalctl syntax matches # @param match journalctl syntax matches in list structure
def addJournalMatch(self, match): def addJournalMatch(self, match):
if self.__matches:
self.__journal.add_disjunction() # Add OR
newMatches = [[]] newMatches = [[]]
try: for match_element in match:
for match_element in shlex.split(match):
if match_element == "+": if match_element == "+":
self.__journal.add_disjunction()
newMatches.append([]) newMatches.append([])
else: else:
self.__journal.add_match(match_element)
newMatches[-1].append(match_element) newMatches[-1].append(match_element)
try:
self._addJournalMatches(newMatches)
except ValueError: except ValueError:
logSys.error("Error adding journal match for: %s", match) logSys.error(
"Error adding journal match for: %r", " ".join(match))
self.resetJournalMatches() self.resetJournalMatches()
raise raise
else: else:
self.__matches.extend( logSys.info("Added journal match for: %r", " ".join(match))
" ".join(newMatch) for newMatch in newMatches)
logSys.debug("Adding journal match for: %s", match)
## ##
# Reset a journal match filter called on removal or failure # 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") logSys.debug("Flushed all journal matches")
match_copy = self.__matches[:] match_copy = self.__matches[:]
self.__matches = [] self.__matches = []
for match in match_copy: try:
self.addJournalMatch(match) self._addJournalMatches(match_copy)
except ValueError:
logSys.error("Error restoring journal matches")
raise
else:
logSys.debug("Journal matches restored")
## ##
# Delete a journal match filter # Delete a journal match filter
@ -104,12 +123,12 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
# @param match journalctl syntax matches # @param match journalctl syntax matches
def delJournalMatch(self, match): def delJournalMatch(self, match):
match = " ".join(shlex.split(match))
if match in self.__matches: if match in self.__matches:
del self.__matches[self.__matches.index(match)] del self.__matches[self.__matches.index(match)]
self.resetJournalMatches() self.resetJournalMatches()
else: else:
raise ValueError("Match not found") raise ValueError("Match not found")
logSys.info("Removed journal match for: %r" % " ".join(match))
## ##
# Get current journal match filter # Get current journal match filter

View File

@ -145,11 +145,11 @@ class Transmitter:
self.__server.setLogEncoding(name, value) self.__server.setLogEncoding(name, value)
return self.__server.getLogEncoding(name) return self.__server.getLogEncoding(name)
elif command[1] == "addjournalmatch": # pragma: systemd no cover elif command[1] == "addjournalmatch": # pragma: systemd no cover
value = ' '.join(command[2:]) value = command[2:]
self.__server.addJournalMatch(name, value) self.__server.addJournalMatch(name, value)
return self.__server.getJournalMatch(name) return self.__server.getJournalMatch(name)
elif command[1] == "deljournalmatch": # pragma: systemd no cover elif command[1] == "deljournalmatch": # pragma: systemd no cover
value = ' '.join(command[2:]) value = command[2:]
self.__server.delJournalMatch(name, value) self.__server.delJournalMatch(name, value)
return self.__server.getJournalMatch(name) return self.__server.getJournalMatch(name)
elif command[1] == "addfailregex": elif command[1] == "addfailregex":

View File

@ -197,7 +197,12 @@ class FilterReaderTest(unittest.TestCase):
"+$<SKIPLINES>^.+ module for .* from <HOST>\\s*$"], "+$<SKIPLINES>^.+ module for .* from <HOST>\\s*$"],
['set', 'testcase01', 'addignoreregex', ['set', 'testcase01', 'addignoreregex',
"^.+ john from host 192.168.1.1\\s*$"], "^.+ 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 = FilterReader("testcase01", "testcase01", {})
filterReader.setBaseDir(TEST_FILES_DIR) filterReader.setBaseDir(TEST_FILES_DIR)
filterReader.read() filterReader.read()

View File

@ -36,3 +36,10 @@ ignoreregex = ^.+ john from host 192.168.1.1\s*$
[Init] [Init]
# "maxlines" is number of log lines to buffer for multi-line regex searches # "maxlines" is number of log lines to buffer for multi-line regex searches
maxlines = 1 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"

View File

@ -622,14 +622,14 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
import uuid import uuid
self.test_uuid = str(uuid.uuid4()) self.test_uuid = str(uuid.uuid4())
self.name = "monitorjournalfailures-%s" % self.test_uuid self.name = "monitorjournalfailures-%s" % self.test_uuid
self.filter.addJournalMatch( self.filter.addJournalMatch([
"SYSLOG_IDENTIFIER=fail2ban-testcases " "SYSLOG_IDENTIFIER=fail2ban-testcases",
"TEST_FIELD=1 " "TEST_FIELD=1",
"TEST_UUID=%s" % str(self.test_uuid)) "TEST_UUID=%s" % self.test_uuid])
self.filter.addJournalMatch( self.filter.addJournalMatch([
"SYSLOG_IDENTIFIER=fail2ban-testcases " "SYSLOG_IDENTIFIER=fail2ban-testcases",
"TEST_FIELD=2 " "TEST_FIELD=2",
"TEST_UUID=%s" % self.test_uuid) "TEST_UUID=%s" % self.test_uuid])
self.journal_fields = { self.journal_fields = {
'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid} 'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid}
self.filter.setActive(True) 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) self.assert_correct_ban("193.168.0.128", 3)
# and now remove the JournalMatch # and now remove the JournalMatch
self.filter.delJournalMatch( self.filter.delJournalMatch([
"SYSLOG_IDENTIFIER=fail2ban-testcases " "SYSLOG_IDENTIFIER=fail2ban-testcases",
"TEST_FIELD=1 " "TEST_FIELD=1",
"TEST_UUID=%s" % str(self.test_uuid)) "TEST_UUID=%s" % self.test_uuid])
_copy_lines_to_journal( _copy_lines_to_journal(
self.test_file, self.journal_fields, n=5, skip=5) 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)) self.assertTrue(self.isEmpty(2))
# but then if we add it back again # but then if we add it back again
self.filter.addJournalMatch( self.filter.addJournalMatch([
"SYSLOG_IDENTIFIER=fail2ban-testcases " "SYSLOG_IDENTIFIER=fail2ban-testcases",
"TEST_FIELD=1 " "TEST_FIELD=1",
"TEST_UUID=%s" % str(self.test_uuid)) "TEST_UUID=%s" % self.test_uuid])
self.assert_correct_ban("193.168.0.128", 4) self.assert_correct_ban("193.168.0.128", 4)
_copy_lines_to_journal( _copy_lines_to_journal(
self.test_file, self.journal_fields, n=6, skip=10) self.test_file, self.journal_fields, n=6, skip=10)

View File

@ -506,68 +506,58 @@ class Transmitter(TransmitterBase):
def testJournalMatch(self): def testJournalMatch(self):
jailName = "TestJail2" jailName = "TestJail2"
self.server.addJail(jailName, "systemd") self.server.addJail(jailName, "systemd")
self.jailAddDelTest(
"journalmatch",
[
"_SYSTEMD_UNIT=sshd.service",
"TEST_FIELD1=ABC TEST_FIELD2=123",
"_HOSTNAME=example.com",
],
jailName
)
values = [ values = [
'"FIELD=Test + Value+ \\\"Test+Value=\'Test"', "_SYSTEMD_UNIT=sshd.service",
'FIELD="Test + Value+ \\\"Test+Value=\'Test"', "TEST_FIELD1=ABC",
"_HOSTNAME=example.com",
] ]
for n, value in enumerate(values):
# Test shell like escaping for spaces
for value in values:
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "addjournalmatch", value]), ["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.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "deljournalmatch", value]), ["set", jailName, "deljournalmatch", value]),
(0, [])) (0, [[val] for val in values[n+1:]]))
# Try duplicates # Try duplicates
value = "_COMM=sshd" value = "_COMM=sshd"
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "addjournalmatch", value]), ["set", jailName, "addjournalmatch", value]),
(0, [value])) (0, [[value]]))
# Duplicates are accepted, as automatically OR'd, and journalctl # Duplicates are accepted, as automatically OR'd, and journalctl
# also accepts them without issue. # also accepts them without issue.
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "addjournalmatch", value]), ["set", jailName, "addjournalmatch", value]),
(0, [value, value])) (0, [[value], [value]]))
# Remove first instance # Remove first instance
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "deljournalmatch", value]), ["set", jailName, "deljournalmatch", value]),
(0, [value])) (0, [[value]]))
# Remove second instance # Remove second instance
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "deljournalmatch", value]), ["set", jailName, "deljournalmatch", value]),
(0, [])) (0, []))
# Test splitting of OR'd values value = [
value1, value2 = "_COMM=sshd", "_SYSTEMD_UNIT=sshd.service" "_COMM=sshd", "+", "_SYSTEMD_UNIT=sshd.service", "_UID=0"]
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "addjournalmatch", ["set", jailName, "addjournalmatch"] + value),
" + ".join([value1, value2])]), (0, [["_COMM=sshd"], ["_SYSTEMD_UNIT=sshd.service", "_UID=0"]]))
(0, [value1, value2]))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "deljournalmatch", value1]), ["set", jailName, "deljournalmatch"] + value[:1]),
(0, [value2])) (0, [["_SYSTEMD_UNIT=sshd.service", "_UID=0"]]))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", jailName, "deljournalmatch", value2]), ["set", jailName, "deljournalmatch"] + value[2:]),
(0, [])) (0, []))
# Invalid match # Invalid match