ENH+BF+TST: Filter now returns reference to failregex and ignoreregex

This avoids duplication of code across fail2ban-regex and samples test
cases. This also now more neatly resolves the issue of double counting
date templates matches in fail2ban-regex.
In addition, the samples test cases now also print a warning message
that not all regexs have samples for them, with future plan to change
this to an assertion.
pull/298/merge^2
Steven Hiscocks 2013-07-15 22:16:40 +01:00
parent 5bd186b854
commit 1a2b6442a0
5 changed files with 79 additions and 75 deletions

View File

@ -46,7 +46,6 @@ from client.configparserinc import SafeConfigParserWithIncludes
from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError
from server.filter import Filter from server.filter import Filter
from server.failregex import RegexException from server.failregex import RegexException
from server.datedetector import DateDetector
from testcases.utils import FormatterWithTraceBack from testcases.utils import FormatterWithTraceBack
# Gets the instance of the logger. # Gets the instance of the logger.
@ -130,7 +129,7 @@ class RegexStat(object):
return self._failregex return self._failregex
def appendIP(self, value): def appendIP(self, value):
self._ipList.extend(value) self._ipList.append(value)
def getIPList(self): def getIPList(self):
return self._ipList return self._ipList
@ -173,8 +172,6 @@ class Fail2banRegex(object):
self._ignoreregex = list() self._ignoreregex = list()
self._failregex = list() self._failregex = list()
self._line_stats = LineStats() self._line_stats = LineStats()
self._dateDetector = DateDetector()
self._dateDetector.addDefaultTemplate()
def readRegex(self, value, regextype): def readRegex(self, value, regextype):
@ -204,53 +201,39 @@ class Fail2banRegex(object):
regex_values = [RegexStat(value)] regex_values = [RegexStat(value)]
setattr(self, "_" + regex, regex_values) setattr(self, "_" + regex, regex_values)
for regex in regex_values:
getattr(
self._filter,
'add%sRegex' % regextype.title())(regex.getFailRegex())
return True return True
def testIgnoreRegex(self, line): def testIgnoreRegex(self, line):
found = False found = False
for regex in self._ignoreregex:
try:
self._filter.addIgnoreRegex(regex.getFailRegex())
try: try:
ret = self._filter.ignoreLine(line) ret = self._filter.ignoreLine(line)
if ret: if ret is not None:
found = True found = True
regex.inc() regex = self._ignoreregex[ret].inc()
except RegexException, e: except RegexException, e:
print e print e
return False return False
finally:
self._filter.delIgnoreRegex(0)
return found return found
def testRegex(self, line): def testRegex(self, line):
found = False
for regex in self._ignoreregex:
self._filter.addIgnoreRegex(regex.getFailRegex())
for regex in self._failregex:
try: try:
self._filter.addFailRegex(regex.getFailRegex()) ret = self._filter.processLine(line, checkAllRegex=True)
try: for match in ret:
ret = self._filter.processLine(line) match.append(len(ret)>1)
if len(ret): regex = self._failregex[match[0]]
if found == True:
ret[0].append(True)
else:
found = True
ret[0].append(False)
regex.inc() regex.inc()
regex.appendIP(ret) regex.appendIP(match)
except RegexException, e: except RegexException, e:
print e print e
return False return False
except IndexError: except IndexError:
print "Sorry, but no <host> found in regex" print "Sorry, but no <host> found in regex"
return False return False
finally: return len(ret) > 0
self._filter.delFailRegex(0)
for regex in self._ignoreregex:
self._filter.delIgnoreRegex(0)
return found
def process(self, test_lines): def process(self, test_lines):
@ -259,9 +242,6 @@ class Fail2banRegex(object):
if line.startswith('#') or not line.strip(): if line.startswith('#') or not line.strip():
# skip comment and empty lines # skip comment and empty lines
continue continue
self._dateDetector.matchTime(line)
is_ignored = fail2banRegex.testIgnoreRegex(line) is_ignored = fail2banRegex.testIgnoreRegex(line)
if is_ignored: if is_ignored:
self._line_stats.ignored_lines.append(line) self._line_stats.ignored_lines.append(line)
@ -302,10 +282,13 @@ class Fail2banRegex(object):
if self._verbose and len(failregex.getIPList()): if self._verbose and len(failregex.getIPList()):
for ip in failregex.getIPList(): for ip in failregex.getIPList():
timeTuple = time.localtime(ip[1]) timeTuple = time.localtime(ip[2])
timeString = time.strftime("%a %b %d %H:%M:%S %Y", timeTuple) timeString = time.strftime("%a %b %d %H:%M:%S %Y", timeTuple)
out.append(" %s %s%s" % ( out.append(
ip[0], timeString, ip[2] and " (already matched)" or "")) " %s %s%s" % (
ip[1],
timeString,
ip[3] and " (multiple regex matched)" or ""))
print "\n%s: %d total" % (title, total) print "\n%s: %d total" % (title, total)
pprint_list(out, " #) [# of hits] regular expression") pprint_list(out, " #) [# of hits] regular expression")
@ -318,7 +301,7 @@ class Fail2banRegex(object):
print "\nDate template hits:" print "\nDate template hits:"
out = [] out = []
for template in self._dateDetector.getTemplates(): for template in self._filter.dateDetector.getTemplates():
if self._verbose or template.getHits(): if self._verbose or template.getHits():
out.append("[%d] %s" % (template.getHits(), template.getName())) out.append("[%d] %s" % (template.getHits(), template.getName()))
pprint_list(out, "[# of hits] date format") pprint_list(out, "[# of hits] date format")

View File

@ -174,6 +174,7 @@ class DateDetector:
match = template.matchDate(line) match = template.matchDate(line)
if not match is None: if not match is None:
logSys.debug("Matched time template %s" % template.getName()) logSys.debug("Matched time template %s" % template.getName())
template.incHits()
return match return match
return None return None
finally: finally:

View File

@ -60,10 +60,11 @@ class DateTemplate:
def getHits(self): def getHits(self):
return self.__hits return self.__hits
def incHits(self):
self.__hits += 1
def matchDate(self, line): def matchDate(self, line):
dateMatch = self.__cRegex.search(line) dateMatch = self.__cRegex.search(line)
if not dateMatch is None:
self.__hits += 1
return dateMatch return dateMatch
def getDate(self, line): def getDate(self, line):

View File

@ -284,7 +284,7 @@ class Filter(JailThread):
return False return False
def processLine(self, line, returnRawHost=False): def processLine(self, line, returnRawHost=False, checkAllRegex=False):
"""Split the time portion from log msg and return findFailures on them """Split the time portion from log msg and return findFailures on them
""" """
try: try:
@ -306,14 +306,15 @@ class Filter(JailThread):
else: else:
timeLine = l timeLine = l
logLine = l logLine = l
return self.findFailure(timeLine, logLine, returnRawHost) return self.findFailure(timeLine, logLine, returnRawHost, checkAllRegex)
def processLineAndAdd(self, line): def processLineAndAdd(self, line):
"""Processes the line for failures and populates failManager """Processes the line for failures and populates failManager
""" """
for element in self.processLine(line): for element in self.processLine(line):
ip = element[0] failregex = element[0]
unixTime = element[1] ip = element[1]
unixTime = element[2]
logSys.debug("Processing line with time:%s and ip:%s" logSys.debug("Processing line with time:%s and ip:%s"
% (unixTime, ip)) % (unixTime, ip))
if unixTime < MyTime.time() - self.getFindTime(): if unixTime < MyTime.time() - self.getFindTime():
@ -335,11 +336,11 @@ class Filter(JailThread):
# @return: a boolean # @return: a boolean
def ignoreLine(self, line): def ignoreLine(self, line):
for ignoreRegex in self.__ignoreRegex: for ignoreRegexIndex, ignoreRegex in enumerate(self.__ignoreRegex):
ignoreRegex.search(line) ignoreRegex.search(line)
if ignoreRegex.hasMatched(): if ignoreRegex.hasMatched():
return True return ignoreRegexIndex
return False return None
## ##
# Finds the failure in a line given split into time and log parts. # Finds the failure in a line given split into time and log parts.
@ -348,18 +349,19 @@ class Filter(JailThread):
# to find the logging time. # to find the logging time.
# @return a dict with IP and timestamp. # @return a dict with IP and timestamp.
def findFailure(self, timeLine, logLine, returnRawHost=False): def findFailure(self, timeLine, logLine,
returnRawHost=False, checkAllRegex=False):
failList = list() failList = list()
# Checks if we must ignore this line. # Checks if we must ignore this line.
if self.ignoreLine(logLine): if self.ignoreLine(logLine) is not None:
# The ignoreregex matched. Return. # The ignoreregex matched. Return.
return failList return failList
date = self.dateDetector.getUnixTime(timeLine)
# Iterates over all the regular expressions. # Iterates over all the regular expressions.
for failRegex in self.__failRegex: for failRegexIndex, failRegex in enumerate(self.__failRegex):
failRegex.search(logLine) failRegex.search(logLine)
if failRegex.hasMatched(): if failRegex.hasMatched():
# The failregex matched. # The failregex matched.
date = self.dateDetector.getUnixTime(timeLine)
logSys.log(7, "Date: %r, message: %r", logSys.log(7, "Date: %r, message: %r",
timeLine, logLine) timeLine, logLine)
if date is None: if date is None:
@ -372,13 +374,15 @@ class Filter(JailThread):
try: try:
host = failRegex.getHost() host = failRegex.getHost()
if returnRawHost: if returnRawHost:
failList.append([host, date]) failList.append([failRegexIndex, host, date])
if not checkAllRegex:
break break
else:
ipMatch = DNSUtils.textToIp(host, self.__useDns) ipMatch = DNSUtils.textToIp(host, self.__useDns)
if ipMatch: if ipMatch:
for ip in ipMatch: for ip in ipMatch:
failList.append([ip, date]) failList.append([failRegexIndex, ip, date])
# We matched a regex, it is enough to stop. if not checkAllRegex:
break break
except RegexException, e: # pragma: no cover - unsure if reachable except RegexException, e: # pragma: no cover - unsure if reachable
logSys.error(e) logSys.error(e)

View File

@ -82,6 +82,7 @@ def testSampleRegexsFactory(name):
logFile = fileinput.FileInput( logFile = fileinput.FileInput(
os.path.join(TEST_FILES_DIR, "logs", name)) os.path.join(TEST_FILES_DIR, "logs", name))
regexsUsed = set()
for line in logFile: for line in logFile:
jsonREMatch = re.match("^# ?failJSON:(.+)$", line) jsonREMatch = re.match("^# ?failJSON:(.+)$", line)
if jsonREMatch: if jsonREMatch:
@ -96,7 +97,8 @@ def testSampleRegexsFactory(name):
else: else:
faildata = {} faildata = {}
ret = self.filter.processLine(line, returnRawHost=True) ret = self.filter.processLine(
line, returnRawHost=True, checkAllRegex=True)
if not ret: if not ret:
# Check line is flagged as none match # Check line is flagged as none match
self.assertFalse(faildata.get('match', True), self.assertFalse(faildata.get('match', True),
@ -107,15 +109,28 @@ def testSampleRegexsFactory(name):
self.assertTrue(faildata.get('match', False), self.assertTrue(faildata.get('match', False),
"Line matched when shouldn't have: %s:%i %r" % "Line matched when shouldn't have: %s:%i %r" %
(logFile.filename(), logFile.filelineno(), line)) (logFile.filename(), logFile.filelineno(), line))
self.assertEqual(len(ret), 1) self.assertEqual(len(ret), 1, "Multiple regexs matched")
# Verify timestamp and host as expected # Verify timestamp and host as expected
host, time = ret[0] failregex, host, time = ret[0]
self.assertEqual(host, faildata.get("host", None)) self.assertEqual(host, faildata.get("host", None))
self.assertEqual( self.assertEqual(
datetime.datetime.fromtimestamp(time), datetime.datetime.fromtimestamp(time),
datetime.datetime.strptime( datetime.datetime.strptime(
faildata.get("time", None), "%Y-%m-%dT%H:%M:%S")) faildata.get("time", None), "%Y-%m-%dT%H:%M:%S"))
regexsUsed.add(failregex)
# TODO: Remove exception handling once all regexs have samples
for failRegexIndex, failRegex in enumerate(self.filter.getFailRegex()):
try:
self.assertTrue(
failRegexIndex in regexsUsed,
"Regex for filter '%s' has no samples: %i: %r" %
(name, failRegexIndex, failRegex))
except AssertionError:
print "I: Regex for filter '%s' has no samples: %i: %r" % (
name, failRegexIndex, failRegex)
return testFilter return testFilter
for filter_ in os.listdir(os.path.join(CONFIG_DIR, "filter.d")): for filter_ in os.listdir(os.path.join(CONFIG_DIR, "filter.d")):