diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index 42df308e..13d70699 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -26,7 +26,8 @@ import time from threading import Lock -from .datetemplate import re, DateTemplate, DatePatternRegex, DateTai64n, DateEpoch +from .datetemplate import re, DateTemplate, DatePatternRegex, DateTai64n, DateEpoch, \ + RE_EPOCH_PATTERN from .strptime import validateTimeZone from .utils import Utils from ..helpers import getLogger @@ -36,7 +37,7 @@ logSys = getLogger(__name__) logLevel = 6 -RE_DATE_PREMATCH = re.compile("\{DATE\}", re.IGNORECASE) +RE_DATE_PREMATCH = re.compile(r"(?(?<=^\[))|(?P(?<=\baudit\()))\d{10,11}\b(?:\.\d{3,6})?)(?:(?(selinux)(?=:\d+\)))|(?(square)(?=\])))" + self._longFrm = longFrm; + self._grpIdx = 1 + epochRE = r"\d{10,11}\b(?:\.\d{3,6})?" + if longFrm: + self.name = "LongEpoch"; + epochRE = r"\d{10,11}(?:\d{3}(?:\.\d{1,6}|\d{3})?)?" + if pattern: + # pattern should capture/cut out the whole match: + regex = "(" + RE_EPOCH_PATTERN.sub(lambda v: "(%s)" % epochRE, pattern) + ")" + self._grpIdx = 2 + self.setRegex(regex) + elif not lineBeginOnly: + regex = r"((?:^|(?P(?<=^\[))|(?P(?<=\baudit\()))%s)(?:(?(selinux)(?=:\d+\)))|(?(square)(?=\])))" % epochRE self.setRegex(regex, wordBegin=False) ;# already line begin resp. word begin anchored else: - regex = r"((?P(?<=^\[))?\d{10,11}\b(?:\.\d{3,6})?)(?(square)(?=\]))" + regex = r"((?P(?<=^\[))?%s)(?(square)(?=\]))" % epochRE self.setRegex(regex, wordBegin='start', wordEnd=True) def getDate(self, line, dateMatch=None, default_tz=None): @@ -220,8 +234,14 @@ class DateEpoch(DateTemplate): if not dateMatch: dateMatch = self.matchDate(line) if dateMatch: + v = dateMatch.group(self._grpIdx) # extract part of format which represents seconds since epoch - return (float(dateMatch.group(1)), dateMatch) + if self._longFrm and len(v) >= 13: + if len(v) >= 16 and '.' not in v: + v = float(v) / 1000000 + else: + v = float(v) / 1000 + return (float(v), dateMatch) class DatePatternRegex(DateTemplate): diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index 02facf30..36471489 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -77,6 +77,48 @@ class DateDetectorTest(LogCaptureTestCase): log = date + " [sshd] error: PAM: Authentication failure" datelog = self.datedetector.getTime(log) self.assertFalse(datelog) + + def testGetEpochMsTime(self): + self.__datedetector = DateDetector() + self.__datedetector.appendTemplate('LEPOCH') + # correct short/long epoch time, using all variants: + for fact in (1, 1000, 1000000): + for dateUnix in (1138049999, 32535244799): + for date in ("%s", "[%s]", "[%s]", "audit(%s:101)"): + dateLong = dateUnix * fact + date = date % dateLong + log = date + " [sshd] error: PAM: Authentication failure" + datelog = self.datedetector.getTime(log) + self.assertTrue(datelog, "Parse epoch time for %s failed" % (date,)) + ( datelog, matchlog ) = datelog + self.assertEqual(int(datelog), dateUnix) + self.assertEqual(matchlog.group(1), str(dateLong)) + # wrong, no epoch time (< 10 digits, more as 17 digits, begin/end of word) : + for dateUnix in ('123456789', '999999999999999999', '1138049999A', 'A1138049999'): + for date in ("%s", "[%s]", "[%s.555]", "audit(%s.555:101)"): + date = date % dateUnix + log = date + " [sshd] error: PAM: Authentication failure" + datelog = self.datedetector.getTime(log) + self.assertFalse(datelog) + + def testGetEpochPattern(self): + self.__datedetector = DateDetector() + self.__datedetector.appendTemplate('(?<=\|\s){LEPOCH}(?=\s\|)') + # correct short/long epoch time, using all variants: + for fact in (1, 1000, 1000000): + for dateUnix in (1138049999, 32535244799): + dateLong = dateUnix * fact + log = "auth-error | %s | invalid password" % dateLong + datelog = self.datedetector.getTime(log) + self.assertTrue(datelog, "Parse epoch time failed: %r" % (log,)) + ( datelog, matchlog ) = datelog + self.assertEqual(int(datelog), dateUnix) + self.assertEqual(matchlog.group(1), str(dateLong)) + # wrong epoch time format (does not match pattern): + for log in ("test%s123", "test-right | %stest", "test%s | test-left"): + log = log % dateLong + datelog = self.datedetector.getTime(log) + self.assertFalse(datelog) def testGetTime(self): log = "Jan 23 21:59:59 [sshd] error: PAM: Authentication failure" diff --git a/fail2ban/tests/fail2banregextestcase.py b/fail2ban/tests/fail2banregextestcase.py index f3a51773..148d774c 100644 --- a/fail2ban/tests/fail2banregextestcase.py +++ b/fail2ban/tests/fail2banregextestcase.py @@ -290,6 +290,17 @@ class Fail2banRegexTest(LogCaptureTestCase): self.assertTrue(fail2banRegex.start(args)) self.assertLogged('Lines: 1 lines, 0 ignored, 1 matched, 0 missed') + def testRegexEpochPatterns(self): + (opts, args, fail2banRegex) = _Fail2banRegex( + "-r", "-d", "^\[{LEPOCH}\]\s+", "--maxlines", "5", + "[1516469849] 192.0.2.1 FAIL: failure\n" + "[1516469849551] 192.0.2.2 FAIL: failure\n" + "[1516469849551000] 192.0.2.3 FAIL: failure\n" + "[1516469849551.000] 192.0.2.4 FAIL: failure", + r"^ FAIL\b" + ) + self.assertTrue(fail2banRegex.start(args)) + self.assertLogged('Lines: 4 lines, 0 ignored, 4 matched, 0 missed') def testWrongFilterFile(self): # use test log as filter file to cover eror cases...