Skip to content

Instantly share code, notes, and snippets.

@kwirk
Last active December 17, 2015 21:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kwirk/5674184 to your computer and use it in GitHub Desktop.
Save kwirk/5674184 to your computer and use it in GitHub Desktop.
fail2ban/fail2ban issue #67
diff --git a/MANIFEST b/MANIFEST
index 8ad73b5..254c9b2 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -30,7 +30,6 @@ fail2ban/server/filterpoll.py
fail2ban/server/iso8601.py
fail2ban/server/server.py
fail2ban/server/actions.py
-fail2ban/server/faildata.py
fail2ban/server/failmanager.py
fail2ban/server/datedetector.py
fail2ban/server/jailthread.py
diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py
index cd5c09e..8bc1f85 100644
--- a/fail2ban/server/action.py
+++ b/fail2ban/server/action.py
@@ -318,6 +318,7 @@ class Action:
if tag == 'matches':
# That one needs to be escaped since its content is
# out of our control
+ # NOTE: Should all tags be escaped??
value = Action.escapeTag(value)
string = string.replace('<' + tag + '>', value)
# New line
diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py
index 520886a..a71213d 100644
--- a/fail2ban/server/actions.py
+++ b/fail2ban/server/actions.py
@@ -171,27 +171,27 @@ class Actions(JailThread):
##
# Check for IP address to ban.
#
- # Look in the Jail queue for FailTicket. If a ticket is available,
+ # Look in the Jail queue for FailuresTicket. If a ticket is available,
# it executes the "ban" command and add a ticket to the BanManager.
# @return True if an IP address get banned
def __checkBan(self):
- ticket = self.jail.getFailTicket()
- if ticket != False:
- aInfo = dict()
+ ticket = self.jail.getFailuresTicket()
+ if ticket:
bTicket = BanManager.createBanTicket(ticket)
- aInfo["ip"] = bTicket.getIP()
- aInfo["failures"] = bTicket.getAttempt()
+ aInfo = bTicket.getGroups(True)
+ aInfo["failures"] = bTicket.getAttempts()
aInfo["time"] = bTicket.getTime()
aInfo["matches"] = "".join(bTicket.getMatches())
if self.__banManager.addBanTicket(bTicket):
- logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"]))
+ logSys.warning("[%s] Ban %s:%r" %
+ (self.jail.getName(), aInfo["ip"], bTicket.getGroups()))
for action in self.__actions:
action.execActionBan(aInfo)
return True
else:
- logSys.info("[%s] %s already banned" % (self.jail.getName(),
- aInfo["ip"]))
+ logSys.info("[%s] %r already banned" %
+ (self.jail.getName(), ticket.getGroups()))
return False
##
@@ -220,12 +220,12 @@ class Actions(JailThread):
# ticket.
def __unBan(self, ticket):
- aInfo = dict()
- aInfo["ip"] = ticket.getIP()
- aInfo["failures"] = ticket.getAttempt()
+ aInfo = ticket.getGroups(True)
+ aInfo["failures"] = ticket.getAttempts()
aInfo["time"] = ticket.getTime()
aInfo["matches"] = "".join(ticket.getMatches())
- logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"]))
+ logSys.warning("[%s] Unban %r" %
+ (self.jail.getName(), ticket.getGroups()))
for action in self.__actions:
action.execActionUnban(aInfo)
diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py
index ef3bf20..19f9571 100644
--- a/fail2ban/server/banmanager.py
+++ b/fail2ban/server/banmanager.py
@@ -35,7 +35,7 @@ logSys = logging.getLogger(__name__)
##
# Banning Manager.
#
-# Manage the banned IP addresses. Convert FailTicket to BanTicket.
+# Manage the banned IP addresses. Convert FailuresTicket to BanTicket.
# This class is mainly used by the Action class.
class BanManager:
@@ -113,25 +113,21 @@ class BanManager:
def getBanList(self):
try:
self.__lock.acquire()
- return [m.getIP() for m in self.__banList]
+ return [m.getGroups() for m in self.__banList]
finally:
self.__lock.release()
##
# Create a ban ticket.
#
- # Create a BanTicket from a FailTicket. The timestamp of the BanTicket
+ # Create a BanTicket from a FailuresTicket. The timestamp of the BanTicket
# is the current time. This is a static method.
- # @param ticket the FailTicket
+ # @param ticket the FailuresTicket
# @return a BanTicket
#@staticmethod
def createBanTicket(ticket):
- ip = ticket.getIP()
- #lastTime = ticket.getTime()
- lastTime = MyTime.time()
- banTicket = BanTicket(ip, lastTime, ticket.getMatches())
- banTicket.setAttempt(ticket.getAttempt())
+ banTicket = BanTicket(ticket)
return banTicket
createBanTicket = staticmethod(createBanTicket)
@@ -176,7 +172,7 @@ class BanManager:
def _inBanList(self, ticket):
for i in self.__banList:
- if ticket.getIP() == i.getIP():
+ if ticket.getGroups().get('ip', None) == i.getGroups().get('ip', None):
return True
return False
@@ -231,7 +227,7 @@ class BanManager:
# Find the ticket the IP goes with and return it
for i, ticket in enumerate(self.__banList):
- if ticket.getIP() == ip:
+ if ticket.getGroups().get('ip', None) == ip:
# Return the ticket after removing (popping)
# if from the ban list.
return self.__banList.pop(i)
diff --git a/fail2ban/server/faildata.py b/fail2ban/server/faildata.py
deleted file mode 100644
index 232a492..0000000
--- a/fail2ban/server/faildata.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# 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.
-
-# Author: Cyril Jaquier
-#
-
-__author__ = "Cyril Jaquier"
-__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
-__license__ = "GPL"
-
-import logging
-
-# Gets the instance of the logger.
-logSys = logging.getLogger(__name__)
-
-class FailData:
-
- def __init__(self):
- self.__retry = 0
- self.__lastTime = 0
- self.__lastReset = 0
- self.__matches = []
-
- def setRetry(self, value):
- self.__retry = value
- # keep only the last matches or reset entirely
- # Explicit if/else for compatibility with Python 2.4
- if value:
- self.__matches = self.__matches[-min(len(self.__matches, value)):]
- else:
- self.__matches = []
-
- def getRetry(self):
- return self.__retry
-
- def getMatches(self):
- return self.__matches
-
- def inc(self, matches=None):
- self.__retry += 1
- self.__matches += matches or []
-
- def setLastTime(self, value):
- if value > self.__lastTime:
- self.__lastTime = value
-
- def getLastTime(self):
- return self.__lastTime
-
- def getLastReset(self):
- return self.__lastReset
-
- def setLastReset(self, value):
- self.__lastReset = value
diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py
index f021a46..bdc7a27 100644
--- a/fail2ban/server/failmanager.py
+++ b/fail2ban/server/failmanager.py
@@ -24,8 +24,7 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
-from faildata import FailData
-from ticket import FailTicket
+from ticket import FailuresTicket
from threading import Lock
import logging
@@ -36,7 +35,7 @@ class FailManager:
def __init__(self):
self.__lock = Lock()
- self.__failList = dict()
+ self.__failList = list()
self.__maxRetry = 3
self.__maxTime = 600
self.__failTotal = 0
@@ -86,22 +85,16 @@ class FailManager:
def addFailure(self, ticket):
try:
self.__lock.acquire()
- ip = ticket.getIP()
- unixTime = ticket.getTime()
- matches = ticket.getMatches()
- if self.__failList.has_key(ip):
- fData = self.__failList[ip]
- if fData.getLastReset() < unixTime - self.__maxTime:
- fData.setLastReset(unixTime)
- fData.setRetry(0)
- fData.inc(matches)
- fData.setLastTime(unixTime)
+ try:
+ failures = self.__failList[self.__failList.index(ticket)]
+ except ValueError:
+ failures = FailuresTicket(ticket)
+ self.__failList.append(failures)
else:
- fData = FailData()
- fData.inc(matches)
- fData.setLastReset(unixTime)
- fData.setLastTime(unixTime)
- self.__failList[ip] = fData
+ if failures.getLastReset() < ticket.getTime() - self.__maxTime:
+ failures.setLastReset(ticket.getTime())
+ failures.setAttempts(0)
+ failures.addTicket(ticket)
self.__failTotal += 1
@@ -109,9 +102,9 @@ class FailManager:
# yoh: Since composing this list might be somewhat time consuming
# in case of having many active failures, it should be ran only
# if debug level is "low" enough
- failures_summary = ', '.join(['%s:%d' % (k, v.getRetry())
- for k,v in self.__failList.iteritems()])
- logSys.debug("Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s"
+ failures_summary = ', '.join(['%r:%d' % (t.getGroups(), t.getAttempts())
+ for t in self.__failList])
+ logSys.debug("Total # of detected failures: %d. Current failures from %d (groups:count): %s"
% (self.__failTotal, len(self.__failList), failures_summary))
finally:
self.__lock.release()
@@ -126,28 +119,17 @@ class FailManager:
def cleanup(self, time):
try:
self.__lock.acquire()
- tmp = self.__failList.copy()
- for item in tmp:
- if tmp[item].getLastTime() < time - self.__maxTime:
- self.__delFailure(item)
+ self.__failList = [ticket for ticket in self.__failList
+ if ticket.getTime() >= time - self.__maxTime]
finally:
self.__lock.release()
- def __delFailure(self, ip):
- if self.__failList.has_key(ip):
- del self.__failList[ip]
-
def toBan(self):
try:
self.__lock.acquire()
- for ip in self.__failList:
- data = self.__failList[ip]
- if data.getRetry() >= self.__maxRetry:
- self.__delFailure(ip)
- # Create a FailTicket from BanData
- failTicket = FailTicket(ip, data.getLastTime(), data.getMatches())
- failTicket.setAttempt(data.getRetry())
- return failTicket
+ for ticket in self.__failList:
+ if ticket.getAttempts() >= self.__maxRetry:
+ return self.__failList.pop(self.__failList.index(ticket))
raise FailManagerEmpty
finally:
self.__lock.release()
diff --git a/fail2ban/server/failregex.py b/fail2ban/server/failregex.py
index 98b4f87..ad4a507 100644
--- a/fail2ban/server/failregex.py
+++ b/fail2ban/server/failregex.py
@@ -46,7 +46,7 @@ class Regex:
regexSplit = regex.split("<SKIPLINES>")
regex = regexSplit[0]
for n, regexLine in enumerate(regexSplit[1:]):
- regex += "\n(?P<skiplines%i>(?:(.*\n)*?))" % n + regexLine
+ regex += "\n(?P<__skiplines%i>(?:(.*\n)*?))" % n + regexLine
if regex.lstrip() == '':
raise RegexException("Cannot add empty regex")
try:
@@ -113,7 +113,7 @@ class Regex:
n = 0
while True:
try:
- skippedLines += self._matchCache.group("skiplines%i" % n)
+ skippedLines += self._matchCache.group("__skiplines%i" % n)
n += 1
except IndexError:
break
@@ -163,6 +163,8 @@ class RegexException(Exception):
class FailRegex(Regex):
+ reservedGroups = set(["ip", "time", "br", "failures", "matches"])
+
##
# Constructor.
#
@@ -173,9 +175,15 @@ class FailRegex(Regex):
def __init__(self, regex):
# Initializes the parent.
Regex.__init__(self, regex)
- # Check for group "host"
- if "host" not in self._regexObj.groupindex:
- raise RegexException("No 'host' group in '%s'" % self._regex)
+ # Check there is at least one group to match on
+ if not [group for group in self._regexObj.groupindex
+ if not group.startswith("_")]:
+ raise RegexException("At least one regex group must be defined")
+ # Check for overlap with special groups
+ reservedOverlap = self.reservedGroups & set(self._regexObj.groupindex)
+ if reservedOverlap:
+ raise RegexException("Reserved group(s) %s found in '%s'" %
+ (", ".join(reservedOverlap), self._regex))
##
# Returns the matched host.
@@ -191,3 +199,10 @@ class FailRegex(Regex):
r = self._matchCache.re
raise RegexException("No 'host' found in '%s' using '%s'" % (s, r))
return str(host)
+
+ def getGroups(self):
+ groups = self._matchCache.groupdict()
+ for key in groups.keys():
+ if key.startswith("__"):
+ del groups[key]
+ return groups
diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py
index c4c4bc2..90eb0b0 100644
--- a/fail2ban/server/filter.py
+++ b/fail2ban/server/filter.py
@@ -244,13 +244,13 @@ class Filter(JailThread):
def addBannedIP(self, ip):
unixTime = MyTime.time()
for i in xrange(self.failManager.getMaxRetry()):
- self.failManager.addFailure(FailTicket(ip, unixTime))
+ self.failManager.addFailure(FailTicket(unixTime, {'ip': ip}))
# Perform the banning of the IP now.
try: # pragma: no branch - exception is the only way out
while True:
ticket = self.failManager.toBan()
- self.jail.putFailTicket(ticket)
+ self.jail.putFailuresTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
@@ -331,19 +331,22 @@ class Filter(JailThread):
"""Processes the line for failures and populates failManager
"""
for element in self.processLine(line):
- ip = element[0]
- unixTime = element[1]
- logSys.debug("Processing line with time:%s and ip:%s"
- % (unixTime, ip))
+ unixTime = element[0]
+ groups = element[1]
+ logSys.debug("Processing line with time:%s: groups:%r"
+ % (unixTime, groups))
if unixTime < MyTime.time() - self.getFindTime():
logSys.debug("Ignore line since time %s < %s - %s"
% (unixTime, MyTime.time(), self.getFindTime()))
break
- if self.inIgnoreIPList(ip):
- logSys.debug("Ignore %s" % ip)
+ if groups.get('ip', None) is not None and \
+ self.inIgnoreIPList(groups['ip']):
+ logSys.debug("Ignore %s" % groups['ip'])
continue
- logSys.debug("Found %s" % ip)
- self.failManager.addFailure(FailTicket(ip, unixTime, [line]))
+ #TODO: Handle ignore groups
+ logSys.debug("Found %r" % groups)
+ self.failManager.addFailure(
+ FailTicket(unixTime, groups, [line]))
##
# Returns true if the line should be ignored.
@@ -390,14 +393,24 @@ class Filter(JailThread):
self.__lineBuffer = failRegex.getUnmatchedLines()
try:
host = failRegex.getHost()
+ except RegexException, e: # pragma: no cover - unsure if reachable
+ logSys.error(e)
+ break
+ except IndexError:
+ # No host group in regex
+ failList.append([date, failRegex.getGroups()])
+ else:
+ # Host group defined
ipMatch = DNSUtils.textToIp(host, self.__useDns)
if ipMatch:
+ groups = failRegex.getGroups()
for ip in ipMatch:
- failList.append([ip, date])
- # We matched a regex, it is enough to stop.
- break
- except RegexException, e: # pragma: no cover - unsure if reachable
- logSys.error(e)
+ groupsIP = groups.copy()
+ groupsIP.update({'ip': ip})
+ failList.append([date, groupsIP])
+
+ # We matched a regex, it is enough to stop.
+ break
return failList
diff --git a/fail2ban/server/filtergamin.py b/fail2ban/server/filtergamin.py
index b48bdd3..e75b843 100644
--- a/fail2ban/server/filtergamin.py
+++ b/fail2ban/server/filtergamin.py
@@ -77,7 +77,7 @@ class FilterGamin(FileFilter):
try:
while True:
ticket = self.failManager.toBan()
- self.jail.putFailTicket(ticket)
+ self.jail.putFailuresTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.dateDetector.sortTemplate()
diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py
index 305adf4..a27a322 100644
--- a/fail2ban/server/filterpoll.py
+++ b/fail2ban/server/filterpoll.py
@@ -96,7 +96,7 @@ class FilterPoll(FileFilter):
try:
while True:
ticket = self.failManager.toBan()
- self.jail.putFailTicket(ticket)
+ self.jail.putFailuresTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.dateDetector.sortTemplate()
diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py
index 2296f1c..ee338b8 100644
--- a/fail2ban/server/filterpyinotify.py
+++ b/fail2ban/server/filterpyinotify.py
@@ -104,7 +104,7 @@ class FilterPyinotify(FileFilter):
try:
while True:
ticket = self.failManager.toBan()
- self.jail.putFailTicket(ticket)
+ self.jail.putFailuresTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.dateDetector.sortTemplate()
diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py
index 4df4d42..1c03520 100644
--- a/fail2ban/server/jail.py
+++ b/fail2ban/server/jail.py
@@ -117,10 +117,10 @@ class Jail:
def getAction(self):
return self.__action
- def putFailTicket(self, ticket):
+ def putFailuresTicket(self, ticket):
self.__queue.put(ticket)
- def getFailTicket(self):
+ def getFailuresTicket(self):
try:
return self.__queue.get(False)
except Queue.Empty:
diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py
index 02576d2..edc6f78 100644
--- a/fail2ban/server/ticket.py
+++ b/fail2ban/server/ticket.py
@@ -29,66 +29,117 @@ import logging
# Gets the instance of the logger.
logSys = logging.getLogger(__name__)
-class Ticket:
+class Ticket(object):
- def __init__(self, ip, time, matches=None):
+ def __init__(self, time, groups, matches=None):
"""Ticket constructor
- @param ip the IP address
@param time the ban time
+ @param groups regex matched groups
@param matches (log) lines caused the ticket
"""
- self.setIP(ip)
- self.__time = time
- self.__attempt = 0
- self.__file = None
- self.__matches = matches or []
+ self._time = time
+ if groups:
+ self._groups = groups.copy()
+ else:
+ self._groups = dict()
+ self._matches = matches or []
def __str__(self):
- return "%s: ip=%s time=%s #attempts=%d" % \
- (self.__class__, self.__ip, self.__time, self.__attempt)
+ return "%s: time=%s groups=%r" % \
+ (self.__class__, self._time, self._groups)
+ def __eq__(self, other):
+ if isinstance(other, Ticket):
+ return self.getGroups(False) == other.getGroups(False)
+ else:
+ return False
+
+ def __ne__(self, other):
+ return not self == other
- def setIP(self, value):
- if isinstance(value, basestring):
- # guarantee using regular str instead of unicode for the IP
- value = str(value)
- self.__ip = value
-
- def getIP(self):
- return self.__ip
-
- def setFile(self, value):
- self.__file = value
-
- def getFile(self):
- return self.__file
-
- def setTime(self, value):
- self.__time = value
-
def getTime(self):
- return self.__time
-
- def setAttempt(self, value):
- self.__attempt = value
+ return self._time
- def getAttempt(self):
- return self.__attempt
-
def getMatches(self):
- return self.__matches
-
+ return self._matches
+
+ def getGroups(self, private=False):
+ if private:
+ return self._groups.copy()
+ else:
+ # Only return groups used as part of matching
+ groups = self._groups.copy()
+ for key in groups.keys():
+ # _prefix, or host (as is handled with IP
+ if key.startswith("_") or key == "host":
+ del groups[key]
+ return groups
class FailTicket(Ticket):
pass
+##
+# Failures Ticket.
+#
+# This extends the standard ticket, representing multiple failures with
+# an "attempts" count
+
+class FailuresTicket(Ticket):
+
+ def __init__(self, *args, **kwargs):
+ if not kwargs and len(args) == 1 and isinstance(args[0], Ticket):
+ ticket = args[0]
+ super(FailuresTicket, self).__init__(
+ ticket.getTime(), ticket.getGroups(), ticket.getMatches())
+ self._lastReset = ticket.getTime()
+ if isinstance(ticket, FailuresTicket):
+ self._attempts = ticket.getAttempts()
+ return
+ else:
+ super(FailuresTicket, self).__init__(*args, **kwargs)
+ self._lastReset = args[1]
+ self._attempts = 1
+
+ def __str__(self):
+ return " ".join(
+ [super(FailuresTicket, self), "#attempts=%d" % self._attempts])
+
+ def setAttempts(self, value):
+ self._attempts = value
+ # keep only the last matches or reset entirely
+ # Explicit if/else for compatibility with Python 2.4
+ if value:
+ self._matches = self._matches[-min((len(self._matches), value)):]
+ else:
+ self._matches = []
+
+ def getAttempts(self):
+ return self._attempts
+
+ def addTicket(self, ticket):
+ if not isinstance(ticket, FailTicket) or self != ticket:
+ raise ValueError("Matching FailTicket can only be added")
+ self._attempts += 1
+ self._matches += ticket.getMatches()
+ self._setTime(ticket.getTime())
+ self._groups.update(ticket.getGroups(True))
+
+ def _setTime(self, value):
+ if value > self._time:
+ self._time = value
+
+ def getLastReset(self):
+ return self._lastReset
+
+ def setLastReset(self, value):
+ self._lastReset = value
##
# Ban Ticket.
#
# This class extends the Ticket class. It is mainly used by the BanManager.
-class BanTicket(Ticket):
+class BanTicket(FailuresTicket):
pass
diff --git a/fail2ban/tests/banmanagertestcase.py b/fail2ban/tests/banmanagertestcase.py
index aff1688..bedb353 100644
--- a/fail2ban/tests/banmanagertestcase.py
+++ b/fail2ban/tests/banmanagertestcase.py
@@ -33,7 +33,7 @@ class AddFailure(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
- self.__ticket = BanTicket('193.168.0.128', 1167605999.0)
+ self.__ticket = BanTicket(1167605999.0, {'ip': '193.168.0.128'})
self.__banManager = BanManager()
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
@@ -48,10 +48,10 @@ class AddFailure(unittest.TestCase):
self.assertEqual(self.__banManager.size(), 1)
def testInListOK(self):
- ticket = BanTicket('193.168.0.128', 1167605999.0)
+ ticket = BanTicket(1167605999.0, {'ip': '193.168.0.128'})
self.assertTrue(self.__banManager._inBanList(ticket))
def testInListNOK(self):
- ticket = BanTicket('111.111.1.111', 1167605999.0)
+ ticket = BanTicket(1167605999.0, {'ip': '111.111.1.111'})
self.assertFalse(self.__banManager._inBanList(ticket))
diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py
index c2fea4f..1337e98 100644
--- a/fail2ban/tests/failmanagertestcase.py
+++ b/fail2ban/tests/failmanagertestcase.py
@@ -49,7 +49,7 @@ class AddFailure(unittest.TestCase):
self.__failManager = FailManager()
for i in self.__items:
- self.__failManager.addFailure(FailTicket(i[0], i[1]))
+ self.__failManager.addFailure(FailTicket(i[1], {'ip': i[0]}))
def tearDown(self):
"""Call after every test case."""
@@ -77,8 +77,8 @@ class AddFailure(unittest.TestCase):
self.__failManager.setMaxRetry(5)
#ticket = FailTicket('193.168.0.128', None)
ticket = self.__failManager.toBan()
- self.assertEqual(ticket.getIP(), "193.168.0.128")
- self.assertTrue(isinstance(ticket.getIP(), str))
+ self.assertEqual(ticket.getGroups().get('ip', None), "193.168.0.128")
+ self.assertTrue(isinstance(ticket.getGroups().get('ip', None), str))
def testbanNOK(self):
self.__failManager.setMaxRetry(10)
@@ -86,7 +86,7 @@ class AddFailure(unittest.TestCase):
def testWindow(self):
ticket = self.__failManager.toBan()
- self.assertNotEqual(ticket.getIP(), "100.100.10.10")
+ self.assertNotEqual(ticket.getGroups().get('ip', None), "100.100.10.10")
ticket = self.__failManager.toBan()
- self.assertNotEqual(ticket.getIP(), "100.100.10.10")
+ self.assertNotEqual(ticket.getGroups().get('ip', None), "100.100.10.10")
self.assertRaises(FailManagerEmpty, self.__failManager.toBan)
diff --git a/fail2ban/tests/failregextestcase.py b/fail2ban/tests/failregextestcase.py
index e69de29..e8325ab 100644
--- a/fail2ban/tests/failregextestcase.py
+++ b/fail2ban/tests/failregextestcase.py
@@ -0,0 +1,136 @@
+# 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.
+
+__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
+__license__ = "GPL"
+
+import unittest
+
+from fail2ban.server.failregex import Regex, FailRegex, RegexException
+
+class FailRegexTests(unittest.TestCase):
+
+ def setUp(self):
+ """Call before every test case."""
+
+ def tearDown(self):
+ """Call after every test case."""
+
+ def testFailRegexHost(self):
+ regex = FailRegex("auth failed for \S+ from <HOST>")
+
+ regex.search("auth failed for john from 198.51.100.73")
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "198.51.100.73")
+
+ regex.search("auth failed for john from example.com")
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "example.com")
+
+ def testFailRegexSkippedLines(self):
+ regex = FailRegex(
+ "comm\[(?P<pid>\d+)\]: connection from <HOST><SKIPLINES>"
+ "comm\[(?P=pid)\]: auth failed"
+ )
+ regex.search(
+ "comm[123]: connection from 198.51.100.65\n"
+ "comm[456]: auth failed\n"
+ "comm[123]: auth failed\n"
+ )
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "198.51.100.65")
+ self.assertEqual(regex.getSkippedLines(), ["comm[456]: auth failed"])
+
+ regex = FailRegex(
+ "comm\[(?P<pid>\d+)\]: connection from:<SKIPLINES>"
+ "comm\[(?P=pid)\]: <HOST>:\d+<SKIPLINES>"
+ "comm\[(?P=pid)\]: auth failed")
+ regex.search(
+ "comm[123]: connection from:\n"
+ "comm[456]: connection from:\n"
+ "comm[456]: 198.51.100.72:1234\n"
+ "comm[123]: example.com:6432\n"
+ "comm[123]: auth failed\n"
+ "comm[456]: auth failed\n"
+ )
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "example.com")
+ self.assertEqual(regex.getGroups(), {
+ 'pid': "123",
+ 'host': "example.com",
+ })
+
+ self.assertEqual(regex.getUnmatchedLines(),[
+ "comm[456]: connection from:",
+ "comm[456]: 198.51.100.72:1234",
+ "comm[456]: auth failed",
+ ])
+ regex.search("\n".join(regex.getUnmatchedLines()))
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "198.51.100.72")
+ self.assertEqual(regex.getGroups(), {
+ 'pid': "456",
+ 'host': "198.51.100.72",
+ })
+
+ def testFailRegexGroup(self):
+ regex = FailRegex("auth failed for (?P<user>\S+) from <HOST>")
+ regex.search("auth failed for john from 198.51.100.73")
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "198.51.100.73")
+ self.assertEqual(regex.getGroups(), {
+ 'user': "john",
+ 'host': "198.51.100.73",
+ })
+ regex.search("auth failed for paul from example.com")
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "example.com")
+ self.assertEqual(regex.getGroups(), {
+ 'user': "paul",
+ 'host': "example.com",
+ })
+
+ # No host
+ regex = FailRegex("auth failed from email account (?P<email>\S+)")
+ regex.search("auth failed from email account john@example.com")
+ self.assertTrue(regex.hasMatched())
+ self.assertRaises(IndexError, regex.getHost)
+ self.assertEqual(regex.getGroups(), {
+ 'email': "john@example.com",
+ })
+
+ # Ignored group
+ regex = FailRegex("auth failed for (?P<__user>\S+) from <HOST>")
+ regex.search("auth failed for john from 198.51.100.73")
+ self.assertTrue(regex.hasMatched())
+ self.assertEqual(regex.getHost(), "198.51.100.73")
+ self.assertEqual(regex._matchCache.groupdict()['__user'], "john")
+ self.assertEqual(regex.getGroups(), {
+ 'host': "198.51.100.73",
+ })
+
+ def testReservedNOK(self):
+ self.assertRaises(RegexException, FailRegex, "(?P<ip>.+)")
+ self.assertRaises(RegexException, FailRegex, "(?P<time>.+)")
+ self.assertRaises(RegexException, FailRegex, "(?P<br>.+)")
+ self.assertRaises(RegexException, FailRegex, "(?P<failures>.+)")
+ self.assertRaises(RegexException, FailRegex, "(?P<matches>.+)")
+ # Multiple in one regex
+ self.assertRaises(RegexException, FailRegex,
+ "(?P<br>.+)(?P<ip>.+)(?P<failures>.+)")
diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py
index 92beb0c..8e491a1 100644
--- a/fail2ban/tests/filtertestcase.py
+++ b/fail2ban/tests/filtertestcase.py
@@ -97,18 +97,21 @@ def _assert_equal_entries(utest, found, output, count=None):
time.localtime(found[2]),\
time.localtime(output[2])
utest.assertEqual(found_time, output_time)
- if len(output) > 3 and count is None: # match matches
+ if len(output) > 3 and output[3] is not None:
+ utest.assertEqual(found[3], output[3])
+ if len(output) > 4 and count is None: # match matches
# do not check if custom count (e.g. going through them twice)
- utest.assertEqual(repr(found[3]), repr(output[3]))
+ utest.assertEqual(repr(found[4]), repr(output[4]))
def _ticket_tuple(ticket):
"""Create a tuple for easy comparison from fail ticket
"""
- attempts = ticket.getAttempt()
+ attempts = ticket.getAttempts()
date = ticket.getTime()
- ip = ticket.getIP()
+ ip = ticket.getGroups().get('ip', None)
+ groups = ticket.getGroups()
matches = ticket.getMatches()
- return (ip, attempts, date, matches)
+ return (ip, attempts, date, groups, matches)
def _assert_correct_last_attempt(utest, filter_, output, count=None):
"""Additional helper to wrap most common test case
@@ -116,7 +119,7 @@ def _assert_correct_last_attempt(utest, filter_, output, count=None):
Test filter to contain target ticket
"""
if isinstance(filter_, DummyJail):
- found = _ticket_tuple(filter_.getFailTicket())
+ found = _ticket_tuple(filter_.getFailuresTicket())
else:
# when we are testing without jails
found = _ticket_tuple(filter_.failManager.toBan())
@@ -343,14 +346,14 @@ class DummyJail(object):
finally:
self.lock.release()
- def putFailTicket(self, ticket):
+ def putFailuresTicket(self, ticket):
try:
self.lock.acquire()
self.queue.append(ticket)
finally:
self.lock.release()
- def getFailTicket(self):
+ def getFailuresTicket(self):
try:
self.lock.acquire()
return self.queue.pop()
@@ -585,7 +588,7 @@ class GetFailures(unittest.TestCase):
FILENAME_MULTILINE = os.path.join(TEST_FILES_DIR, "testcase-multiline.log")
# so that they could be reused by other tests
- FAILURES_01 = ('193.168.0.128', 3, 1124013599.0,
+ FAILURES_01 = ('193.168.0.128', 3, 1124013599.0, None,
[u'Aug 14 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 193.168.0.128\n']*3)
def setUp(self):
@@ -624,14 +627,14 @@ class GetFailures(unittest.TestCase):
# now see if we should be getting the "same" failures
self.testGetFailures01(filename=fname,
- failures=GetFailures.FAILURES_01[:3] +
+ failures=GetFailures.FAILURES_01[:4] +
([x.rstrip('\n') + '\r\n' for x in
GetFailures.FAILURES_01[-1]],))
_killfile(fout, fname)
def testGetFailures02(self):
- output = ('141.3.81.106', 4, 1124013539.0,
+ output = ('141.3.81.106', 4, 1124013539.0, None,
[u'Aug 14 11:%d:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:141.3.81.106 port 51332 ssh2\n'
% m for m in 53, 54, 57, 58])
@@ -664,11 +667,11 @@ class GetFailures(unittest.TestCase):
def testGetFailuresUseDNS(self):
# We should still catch failures with usedns = no ;-)
- output_yes = ('192.0.43.10', 2, 1124013539.0,
+ output_yes = ('192.0.43.10', 2, 1124013539.0, None,
[u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2\n',
u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:192.0.43.10 port 51332 ssh2\n'])
- output_no = ('192.0.43.10', 1, 1124013539.0,
+ output_no = ('192.0.43.10', 1, 1124013539.0, None,
[u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:192.0.43.10 port 51332 ssh2\n'])
# Actually no exception would be raised -- it will be just set to 'no'
@@ -698,6 +701,15 @@ class GetFailures(unittest.TestCase):
self.filter.getFailures(GetFailures.FILENAME_02)
_assert_correct_last_attempt(self, self.filter, output)
+ def testGetFailuresNoHostRegex(self):
+ output = (None, 3, 1124013599.0, {'user': 'kevin'},
+ [u'Aug 14 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 193.168.0.128\n']*3)
+
+ self.filter.addLogPath(GetFailures.FILENAME_01)
+ self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) (?P<user>.+)(?: from|FROM) \S+$")
+ self.filter.getFailures(GetFailures.FILENAME_01)
+ _assert_correct_last_attempt(self, self.filter, output)
+
def testGetFailuresIgnoreRegex(self):
output = ('141.3.81.106', 8, 1124013541.0)
@@ -714,7 +726,7 @@ class GetFailures(unittest.TestCase):
output = [("192.0.43.10", 2, 1124013599.0),
("192.0.43.11", 1, 1124013598.0)]
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE)
- self.filter.addFailRegex("^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
+ self.filter.addFailRegex("^.*rsyncd\[(?P<_pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=_pid)\]: rsync error: .*$")
self.filter.setMaxLines(100)
self.filter.setMaxRetry(1)
@@ -732,7 +744,7 @@ class GetFailures(unittest.TestCase):
def testGetFailuresMultiLineIgnoreRegex(self):
output = [("192.0.43.10", 2, 1124013599.0)]
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE)
- self.filter.addFailRegex("^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
+ self.filter.addFailRegex("^.*rsyncd\[(?P<_pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=_pid)\]: rsync error: .*$")
self.filter.addIgnoreRegex("rsync error: Received SIGINT")
self.filter.setMaxLines(100)
self.filter.setMaxRetry(1)
@@ -748,7 +760,7 @@ class GetFailures(unittest.TestCase):
("192.0.43.11", 1, 1124013598.0),
("192.0.43.15", 1, 1124013598.0)]
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE)
- self.filter.addFailRegex("^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
+ self.filter.addFailRegex("^.*rsyncd\[(?P<_pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=_pid)\]: rsync error: .*$")
self.filter.addFailRegex("^.* sendmail\[.*, msgid=<(?P<msgid>[^>]+).*relay=\[<HOST>\].*$<SKIPLINES>^.+ spamd: result: Y \d+ .*,mid=<(?P=msgid)>(,bayes=[.\d]+)?(,autolearn=\S+)?\s*$")
self.filter.setMaxLines(100)
self.filter.setMaxRetry(1)
diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py
index 8870c34..5e940e5 100644
--- a/fail2ban/tests/utils.py
+++ b/fail2ban/tests/utils.py
@@ -128,6 +128,7 @@ def gatherTests(regexps=None, no_network=False):
from fail2ban.tests import clientreadertestcase
from fail2ban.tests import failmanagertestcase
from fail2ban.tests import filtertestcase
+ from fail2ban.tests import failregextestcase
from fail2ban.tests import servertestcase
from fail2ban.tests import datedetectortestcase
from fail2ban.tests import actiontestcase
@@ -156,6 +157,8 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite(actiontestcase.ExecuteAction))
# FailManager
tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure))
+ # FailRegex
+ tests.addTest(unittest.makeSuite(failregextestcase.FailRegexTests))
# BanManager
tests.addTest(unittest.makeSuite(banmanagertestcase.AddFailure))
# ClientReaders
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment