Last active
December 17, 2015 21:19
-
-
Save kwirk/5674184 to your computer and use it in GitHub Desktop.
fail2ban/fail2ban issue #67
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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