Merge branch '0.11'

pull/2842/head
sebres 2020-08-24 16:33:30 +02:00
commit 7327fee2c8
16 changed files with 283 additions and 116 deletions

View File

@ -43,13 +43,21 @@ ver. 1.0.1-dev-1 (20??/??/??) - development nightly edition
- `aggressive`: matches 401 and any variant (with and without username) - `aggressive`: matches 401 and any variant (with and without username)
* `filter.d/sshd.conf`: normalizing of user pattern in all RE's, allowing empty user (gh-2749) * `filter.d/sshd.conf`: normalizing of user pattern in all RE's, allowing empty user (gh-2749)
### New Features ### New Features and Enhancements
* new filter and jail for GitLab recognizing failed application logins (gh-2689) * new filter and jail for GitLab recognizing failed application logins (gh-2689)
### Enhancements
* introduced new prefix `{UNB}` for `datepattern` to disable word boundaries in regex; * introduced new prefix `{UNB}` for `datepattern` to disable word boundaries in regex;
* datetemplate: improved anchor detection for capturing groups `(^...)`; * datetemplate: improved anchor detection for capturing groups `(^...)`;
* datepattern: improved handling with wrong recognized timestamps (timezones, no datepattern, etc)
as well as some warnings signaling user about invalid pattern or zone (gh-2814):
- filter gets mode in-operation, which gets activated if filter starts processing of new messages;
in this mode a timestamp read from log-line that appeared recently (not an old line), deviating too much
from now (up too 24h), will be considered as now (assuming a timezone issue), so could avoid unexpected
bypass of failure (previously exceeding `findtime`);
- better interaction with non-matching optional datepattern or invalid timestamps;
- implements special datepattern `{NONE}` - allow to find failures totally without date-time in log messages,
whereas filter will use now as timestamp (gh-2802)
* performance optimization of `datepattern` (better search algorithm in datedetector, especially for single template); * performance optimization of `datepattern` (better search algorithm in datedetector, especially for single template);
* fail2ban-client: extended to unban IP range(s) by subnet (CIDR/mask) or hostname (DNS), gh-2791;
* extended capturing of alternate tags in filter, allowing combine of multiple groups to single tuple token with new tag * extended capturing of alternate tags in filter, allowing combine of multiple groups to single tuple token with new tag
prefix `<F-TUPLE_`, that would combine value of `<F-V>` with all value of `<F-TUPLE_V?_n?>` tags (gh-2755) prefix `<F-TUPLE_`, that would combine value of `<F-V>` with all value of `<F-TUPLE_V?_n?>` tags (gh-2755)
* `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair * `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair

View File

@ -168,19 +168,6 @@ class Fail2banClient(Fail2banCmdLine, Thread):
if not ret: if not ret:
return None return None
# verify that directory for the socket file exists
socket_dir = os.path.dirname(self._conf["socket"])
if not os.path.exists(socket_dir):
logSys.error(
"There is no directory %s to contain the socket file %s."
% (socket_dir, self._conf["socket"]))
return None
if not os.access(socket_dir, os.W_OK | os.X_OK): # pragma: no cover
logSys.error(
"Directory %s exists but not accessible for writing"
% (socket_dir,))
return None
# Check already running # Check already running
if not self._conf["force"] and os.path.exists(self._conf["socket"]): if not self._conf["force"] and os.path.exists(self._conf["socket"]):
logSys.error("Fail2ban seems to be in unexpected state (not running but the socket exists)") logSys.error("Fail2ban seems to be in unexpected state (not running but the socket exists)")

View File

@ -308,6 +308,10 @@ class Fail2banCmdLine():
# since method is also exposed in API via globally bound variable # since method is also exposed in API via globally bound variable
@staticmethod @staticmethod
def _exit(code=0): def _exit(code=0):
# implicit flush without to produce broken pipe error (32):
sys.stderr.close()
sys.stdout.close()
# exit:
if hasattr(os, '_exit') and os._exit: if hasattr(os, '_exit') and os._exit:
os._exit(code) os._exit(code)
else: else:
@ -318,8 +322,6 @@ class Fail2banCmdLine():
logSys.debug("Exit with code %s", code) logSys.debug("Exit with code %s", code)
# because of possible buffered output in python, we should flush it before exit: # because of possible buffered output in python, we should flush it before exit:
logging.shutdown() logging.shutdown()
sys.stdout.flush()
sys.stderr.flush()
# exit # exit
Fail2banCmdLine._exit(code) Fail2banCmdLine._exit(code)

View File

@ -262,7 +262,7 @@ class Actions(JailThread, Mapping):
if ip is None: if ip is None:
return self.__flushBan(db) return self.__flushBan(db)
# Multiple IPs: # Multiple IPs:
if isinstance(ip, list): if isinstance(ip, (list, tuple)):
missed = [] missed = []
cnt = 0 cnt = 0
for i in ip: for i in ip:
@ -284,6 +284,14 @@ class Actions(JailThread, Mapping):
# Unban the IP. # Unban the IP.
self.__unBan(ticket) self.__unBan(ticket)
else: else:
# Multiple IPs by subnet or dns:
if not isinstance(ip, IPAddr):
ipa = IPAddr(ip)
if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname):
ips = filter(ipa.contains, self.__banManager.getBanList())
if ips:
return self.removeBannedIP(ips, db, ifexists)
# not found:
msg = "%s is not banned" % ip msg = "%s is not banned" % ip
logSys.log(logging.MSG, msg) logSys.log(logging.MSG, msg)
if ifexists: if ifexists:

View File

@ -282,6 +282,8 @@ class DateDetector(object):
elif "{DATE}" in key: elif "{DATE}" in key:
self.addDefaultTemplate(preMatch=pattern, allDefaults=False) self.addDefaultTemplate(preMatch=pattern, allDefaults=False)
return return
elif key == "{NONE}":
template = _getPatternTemplate('{UNB}^', key)
else: else:
template = _getPatternTemplate(pattern, key) template = _getPatternTemplate(pattern, key)

View File

@ -113,6 +113,8 @@ class Filter(JailThread):
self.onIgnoreRegex = None self.onIgnoreRegex = None
## if true ignores obsolete failures (failure time < now - findTime): ## if true ignores obsolete failures (failure time < now - findTime):
self.checkFindTime = True self.checkFindTime = True
## shows that filter is in operation mode (processing new messages):
self.inOperation = True
## if true prevents against retarded banning in case of RC by too many failures (disabled only for test purposes): ## if true prevents against retarded banning in case of RC by too many failures (disabled only for test purposes):
self.banASAP = True self.banASAP = True
## Ticks counter ## Ticks counter
@ -587,16 +589,26 @@ class Filter(JailThread):
if self.__ignoreCache: c.set(key, False) if self.__ignoreCache: c.set(key, False)
return False return False
def _logWarnOnce(self, nextLTM, *args):
"""Log some issue as warning once per day, otherwise level 7"""
if MyTime.time() < getattr(self, nextLTM, 0):
if logSys.getEffectiveLevel() <= 7: logSys.log(7, *(args[0]))
else:
setattr(self, nextLTM, MyTime.time() + 24*60*60)
for args in args:
logSys.warning('[%s] ' + args[0], self.jailName, *args[1:])
def processLine(self, line, date=None): def processLine(self, line, date=None):
"""Split the time portion from log msg and return findFailures on them """Split the time portion from log msg and return findFailures on them
""" """
logSys.log(7, "Working on line %r", line)
noDate = False
if date: if date:
tupleLine = line tupleLine = line
self.__lastTimeText = tupleLine[1] self.__lastTimeText = tupleLine[1]
self.__lastDate = date self.__lastDate = date
else: else:
logSys.log(7, "Working on line %r", line)
# try to parse date: # try to parse date:
timeMatch = self.dateDetector.matchTime(line) timeMatch = self.dateDetector.matchTime(line)
m = timeMatch[0] m = timeMatch[0]
@ -607,22 +619,59 @@ class Filter(JailThread):
tupleLine = (line[:s], m, line[e:]) tupleLine = (line[:s], m, line[e:])
if m: # found and not empty - retrive date: if m: # found and not empty - retrive date:
date = self.dateDetector.getTime(m, timeMatch) date = self.dateDetector.getTime(m, timeMatch)
if date is not None:
if date is None:
if m: logSys.error("findFailure failed to parse timeText: %s", m)
date = self.__lastDate
else:
# Lets get the time part # Lets get the time part
date = date[0] date = date[0]
self.__lastTimeText = m self.__lastTimeText = m
self.__lastDate = date self.__lastDate = date
else: else:
tupleLine = (line, self.__lastTimeText, "") logSys.error("findFailure failed to parse timeText: %s", m)
# matched empty value - date is optional or not available - set it to last known or now:
elif self.__lastDate and self.__lastDate > MyTime.time() - 60:
# set it to last known:
tupleLine = ("", self.__lastTimeText, line)
date = self.__lastDate date = self.__lastDate
else:
# set it to now:
date = MyTime.time()
else:
tupleLine = ("", "", line)
# still no date - try to use last known:
if date is None:
noDate = True
if self.__lastDate and self.__lastDate > MyTime.time() - 60:
tupleLine = ("", self.__lastTimeText, line)
date = self.__lastDate
if self.checkFindTime:
# if in operation (modifications have been really found):
if self.inOperation:
# if weird date - we'd simulate now for timeing issue (too large deviation from now):
if (date is None or date < MyTime.time() - 60 or date > MyTime.time() + 60):
# log time zone issue as warning once per day:
self._logWarnOnce("_next_simByTimeWarn",
("Simulate NOW in operation since found time has too large deviation %s ~ %s +/- %s",
date, MyTime.time(), 60),
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s",
line))
# simulate now as date:
date = MyTime.time()
self.__lastDate = date
else:
# in initialization (restore) phase, if too old - ignore:
if date is not None and date < MyTime.time() - self.getFindTime():
# log time zone issue as warning once per day:
self._logWarnOnce("_next_ignByTimeWarn",
("Ignore line since time %s < %s - %s",
date, MyTime.time(), self.getFindTime()),
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s",
line))
# ignore - too old (obsolete) entry:
return []
# save last line (lazy convert of process line tuple to string on demand): # save last line (lazy convert of process line tuple to string on demand):
self.processedLine = lambda: "".join(tupleLine[::2]) self.processedLine = lambda: "".join(tupleLine[::2])
return self.findFailure(tupleLine, date) return self.findFailure(tupleLine, date, noDate=noDate)
def processLineAndAdd(self, line, date=None): def processLineAndAdd(self, line, date=None):
"""Processes the line for failures and populates failManager """Processes the line for failures and populates failManager
@ -634,6 +683,9 @@ class Filter(JailThread):
fail = element[3] fail = element[3]
logSys.debug("Processing line with time:%s and ip:%s", logSys.debug("Processing line with time:%s and ip:%s",
unixTime, ip) unixTime, ip)
# ensure the time is not in the future, e. g. by some estimated (assumed) time:
if self.checkFindTime and unixTime > MyTime.time():
unixTime = MyTime.time()
tick = FailTicket(ip, unixTime, data=fail) tick = FailTicket(ip, unixTime, data=fail)
if self._inIgnoreIPList(ip, tick): if self._inIgnoreIPList(ip, tick):
continue continue
@ -756,7 +808,7 @@ 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, tupleLine, date): def findFailure(self, tupleLine, date, noDate=False):
failList = list() failList = list()
ll = logSys.getEffectiveLevel() ll = logSys.getEffectiveLevel()
@ -766,11 +818,6 @@ class Filter(JailThread):
returnRawHost = True returnRawHost = True
cidr = IPAddr.CIDR_RAW cidr = IPAddr.CIDR_RAW
if self.checkFindTime and date is not None and date < MyTime.time() - self.getFindTime():
if ll <= 5: logSys.log(5, "Ignore line since time %s < %s - %s",
date, MyTime.time(), self.getFindTime())
return failList
if self.__lineBufferSize > 1: if self.__lineBufferSize > 1:
self.__lineBuffer.append(tupleLine) self.__lineBuffer.append(tupleLine)
orgBuffer = self.__lineBuffer = self.__lineBuffer[-self.__lineBufferSize:] orgBuffer = self.__lineBuffer = self.__lineBuffer[-self.__lineBufferSize:]
@ -821,17 +868,13 @@ class Filter(JailThread):
if not self.checkAllRegex: if not self.checkAllRegex:
break break
continue continue
if date is None: if noDate:
logSys.warning( self._logWarnOnce("_next_noTimeWarn",
"Found a match for %r but no valid date/time " ("Found a match but no valid date/time found for %r.", tupleLine[1]),
"found for %r. Please try setting a custom " ("Match without a timestamp: %s", "\n".join(failRegex.getMatchedLines())),
"date pattern (see man page jail.conf(5)). " ("Please try setting a custom date pattern (see man page jail.conf(5)).",)
"If format is complex, please " )
"file a detailed issue on" if date is None and self.checkFindTime: continue
" https://github.com/fail2ban/fail2ban/issues "
"in order to get support for this format.",
"\n".join(failRegex.getMatchedLines()), tupleLine[1])
continue
# we should check all regex (bypass on multi-line, otherwise too complex): # we should check all regex (bypass on multi-line, otherwise too complex):
if not self.checkAllRegex or self.__lineBufferSize > 1: if not self.checkAllRegex or self.__lineBufferSize > 1:
self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None
@ -940,7 +983,7 @@ class FileFilter(Filter):
log.setPos(lastpos) log.setPos(lastpos)
self.__logs[path] = log self.__logs[path] = log
logSys.info("Added logfile: %r (pos = %s, hash = %s)" , path, log.getPos(), log.getHash()) logSys.info("Added logfile: %r (pos = %s, hash = %s)" , path, log.getPos(), log.getHash())
if autoSeek: if autoSeek and not tail:
self.__autoSeek[path] = autoSeek self.__autoSeek[path] = autoSeek
self._addLogPath(path) # backend specific self._addLogPath(path) # backend specific
@ -1024,7 +1067,7 @@ class FileFilter(Filter):
# MyTime.time()-self.findTime. When a failure is detected, a FailTicket # MyTime.time()-self.findTime. When a failure is detected, a FailTicket
# is created and is added to the FailManager. # is created and is added to the FailManager.
def getFailures(self, filename): def getFailures(self, filename, inOperation=None):
log = self.getLog(filename) log = self.getLog(filename)
if log is None: if log is None:
logSys.error("Unable to get failures in %s", filename) logSys.error("Unable to get failures in %s", filename)
@ -1069,9 +1112,14 @@ class FileFilter(Filter):
if has_content: if has_content:
while not self.idle: while not self.idle:
line = log.readline() line = log.readline()
if not line or not self.active: if not self.active: break; # jail has been stopped
# The jail reached the bottom or has been stopped if not line:
# The jail reached the bottom, simply set in operation for this log
# (since we are first time at end of file, growing is only possible after modifications):
log.inOperation = True
break break
# acquire in operation from log and process:
self.inOperation = inOperation if inOperation is not None else log.inOperation
self.processLineAndAdd(line.rstrip('\r\n')) self.processLineAndAdd(line.rstrip('\r\n'))
finally: finally:
log.close() log.close()
@ -1231,6 +1279,8 @@ class FileContainer:
self.__pos = 0 self.__pos = 0
finally: finally:
handler.close() handler.close()
## shows that log is in operation mode (expecting new messages only from here):
self.inOperation = tail
def getFileName(self): def getFileName(self):
return self.__filename return self.__filename
@ -1304,16 +1354,17 @@ class FileContainer:
return line.decode(enc, 'strict') return line.decode(enc, 'strict')
except (UnicodeDecodeError, UnicodeEncodeError) as e: except (UnicodeDecodeError, UnicodeEncodeError) as e:
global _decode_line_warn global _decode_line_warn
lev = logging.DEBUG lev = 7
if _decode_line_warn.get(filename, 0) <= MyTime.time(): if not _decode_line_warn.get(filename, 0):
lev = logging.WARNING lev = logging.WARNING
_decode_line_warn[filename] = MyTime.time() + 24*60*60 _decode_line_warn.set(filename, 1)
logSys.log(lev, logSys.log(lev,
"Error decoding line from '%s' with '%s'." "Error decoding line from '%s' with '%s'.", filename, enc)
" Consider setting logencoding=utf-8 (or another appropriate" if logSys.getEffectiveLevel() <= lev:
logSys.log(lev, "Consider setting logencoding=utf-8 (or another appropriate"
" encoding) for this jail. Continuing" " encoding) for this jail. Continuing"
" to process line ignoring invalid characters: %r", " to process line ignoring invalid characters: %r",
filename, enc, line) line)
# decode with replacing error chars: # decode with replacing error chars:
line = line.decode(enc, 'replace') line = line.decode(enc, 'replace')
return line return line
@ -1334,7 +1385,7 @@ class FileContainer:
## print "D: Closed %s with pos %d" % (handler, self.__pos) ## print "D: Closed %s with pos %d" % (handler, self.__pos)
## sys.stdout.flush() ## sys.stdout.flush()
_decode_line_warn = {} _decode_line_warn = Utils.Cache(maxCount=1000, maxTime=24*60*60);
## ##

View File

@ -111,6 +111,8 @@ class FilterPoll(FileFilter):
modlst = [] modlst = []
Utils.wait_for(lambda: not self.active or self.getModified(modlst), Utils.wait_for(lambda: not self.active or self.getModified(modlst),
self.sleeptime) self.sleeptime)
if not self.active: # pragma: no cover - timing
break
for filename in modlst: for filename in modlst:
self.getFailures(filename) self.getFailures(filename)
self.__modified = True self.__modified = True
@ -140,7 +142,7 @@ class FilterPoll(FileFilter):
try: try:
logStats = os.stat(filename) logStats = os.stat(filename)
stats = logStats.st_mtime, logStats.st_ino, logStats.st_size stats = logStats.st_mtime, logStats.st_ino, logStats.st_size
pstats = self.__prevStats.get(filename, (0)) pstats = self.__prevStats.get(filename, (0,))
if logSys.getEffectiveLevel() <= 4: if logSys.getEffectiveLevel() <= 4:
# we do not want to waste time on strftime etc if not necessary # we do not want to waste time on strftime etc if not necessary
dt = logStats.st_mtime - pstats[0] dt = logStats.st_mtime - pstats[0]

View File

@ -517,6 +517,11 @@ class IPAddr(object):
return (self.addr & mask) == net.addr return (self.addr & mask) == net.addr
def contains(self, ip):
"""Return whether the object (as network) contains given IP
"""
return isinstance(ip, IPAddr) and (ip == self or ip.isInNet(self))
# Pre-calculated map: addr to maskplen # Pre-calculated map: addr to maskplen
def __getMaskMap(): def __getMaskMap():
m6 = (1 << 128)-1 m6 = (1 << 128)-1

View File

@ -291,9 +291,8 @@ def reGroupDictStrptime(found_dict, msec=False, default_tz=None):
date_result -= datetime.timedelta(days=1) date_result -= datetime.timedelta(days=1)
if assume_year: if assume_year:
if not now: now = MyTime.now() if not now: now = MyTime.now()
if date_result > now: if date_result > now + datetime.timedelta(days=1): # ignore by timezone issues (+24h)
# Could be last year? # assume last year - also reset month and day as it's not yesterday...
# also reset month and day as it's not yesterday...
date_result = date_result.replace( date_result = date_result.replace(
year=year-1, month=month, day=day) year=year-1, month=month, day=day)

View File

@ -125,6 +125,10 @@ class Utils():
with self.__lock: with self.__lock:
self._cache.pop(k, None) self._cache.pop(k, None)
def clear(self):
with self.__lock:
self._cache.clear()
@staticmethod @staticmethod
def setFBlockMode(fhandle, value): def setFBlockMode(fhandle, value):

View File

@ -655,12 +655,6 @@ class Fail2banClientTest(Fail2banClientServerBase):
self.assertLogged("Base configuration directory " + pjoin(tmp, "miss") + " does not exist") self.assertLogged("Base configuration directory " + pjoin(tmp, "miss") + " does not exist")
self.pruneLog() self.pruneLog()
## wrong socket
self.execCmd(FAILED, (),
"--async", "-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "miss/f2b.sock"), "start")
self.assertLogged("There is no directory " + pjoin(tmp, "miss") + " to contain the socket file")
self.pruneLog()
## not running ## not running
self.execCmd(FAILED, (), self.execCmd(FAILED, (),
"-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "f2b.sock"), "reload") "-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "f2b.sock"), "reload")
@ -756,12 +750,6 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertLogged("Base configuration directory " + pjoin(tmp, "miss") + " does not exist") self.assertLogged("Base configuration directory " + pjoin(tmp, "miss") + " does not exist")
self.pruneLog() self.pruneLog()
## wrong socket
self.execCmd(FAILED, (),
"-c", pjoin(tmp, "config"), "-x", "-s", pjoin(tmp, "miss/f2b.sock"))
self.assertLogged("There is no directory " + pjoin(tmp, "miss") + " to contain the socket file")
self.pruneLog()
## already exists: ## already exists:
open(pjoin(tmp, "f2b.sock"), 'a').close() open(pjoin(tmp, "f2b.sock"), 'a').close()
self.execCmd(FAILED, (), self.execCmd(FAILED, (),
@ -1215,13 +1203,41 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertNotLogged("[test-jail1] Found 192.0.2.5") self.assertNotLogged("[test-jail1] Found 192.0.2.5")
# unban single ips: # unban single ips:
self.pruneLog("[test-phase 6]") self.pruneLog("[test-phase 6a]")
self.execCmd(SUCCESS, startparams, self.execCmd(SUCCESS, startparams,
"--async", "unban", "192.0.2.5", "192.0.2.6") "--async", "unban", "192.0.2.5", "192.0.2.6")
self.assertLogged( self.assertLogged(
"192.0.2.5 is not banned", "192.0.2.5 is not banned",
"[test-jail1] Unban 192.0.2.6", all=True, wait=MID_WAITTIME "[test-jail1] Unban 192.0.2.6", all=True, wait=MID_WAITTIME
) )
# unban ips by subnet (cidr/mask):
self.pruneLog("[test-phase 6b]")
self.execCmd(SUCCESS, startparams,
"--async", "unban", "192.0.2.2/31")
self.assertLogged(
"[test-jail1] Unban 192.0.2.2",
"[test-jail1] Unban 192.0.2.3", all=True, wait=MID_WAITTIME
)
self.execCmd(SUCCESS, startparams,
"--async", "unban", "192.0.2.8/31", "192.0.2.100/31")
self.assertLogged(
"[test-jail1] Unban 192.0.2.8",
"192.0.2.100/31 is not banned", all=True, wait=MID_WAITTIME)
# ban/unban subnet(s):
self.pruneLog("[test-phase 6c]")
self.execCmd(SUCCESS, startparams,
"--async", "set", "test-jail1", "banip", "192.0.2.96/28", "192.0.2.112/28")
self.assertLogged(
"[test-jail1] Ban 192.0.2.96/28",
"[test-jail1] Ban 192.0.2.112/28", all=True, wait=MID_WAITTIME
)
self.execCmd(SUCCESS, startparams,
"--async", "set", "test-jail1", "unbanip", "192.0.2.64/26"); # contains both subnets .96/28 and .112/28
self.assertLogged(
"[test-jail1] Unban 192.0.2.96/28",
"[test-jail1] Unban 192.0.2.112/28", all=True, wait=MID_WAITTIME
)
# reload all (one jail) with unban all: # reload all (one jail) with unban all:
self.pruneLog("[test-phase 7]") self.pruneLog("[test-phase 7]")
@ -1232,8 +1248,6 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertLogged( self.assertLogged(
"Jail 'test-jail1' reloaded", "Jail 'test-jail1' reloaded",
"[test-jail1] Unban 192.0.2.1", "[test-jail1] Unban 192.0.2.1",
"[test-jail1] Unban 192.0.2.2",
"[test-jail1] Unban 192.0.2.3",
"[test-jail1] Unban 192.0.2.4", all=True "[test-jail1] Unban 192.0.2.4", all=True
) )
# no restart occurred, no more ban (unbanned all using option "--unban"): # no restart occurred, no more ban (unbanned all using option "--unban"):
@ -1241,8 +1255,6 @@ class Fail2banServerTest(Fail2banClientServerBase):
"Jail 'test-jail1' stopped", "Jail 'test-jail1' stopped",
"Jail 'test-jail1' started", "Jail 'test-jail1' started",
"[test-jail1] Ban 192.0.2.1", "[test-jail1] Ban 192.0.2.1",
"[test-jail1] Ban 192.0.2.2",
"[test-jail1] Ban 192.0.2.3",
"[test-jail1] Ban 192.0.2.4", all=True "[test-jail1] Ban 192.0.2.4", all=True
) )

View File

@ -81,6 +81,7 @@ def _test_exec_command_line(*args):
return _exit_code return _exit_code
STR_00 = "Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0" STR_00 = "Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
STR_00_NODT = "[sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
RE_00 = r"(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>" RE_00 = r"(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>"
RE_00_ID = r"Authentication failure for <F-ID>.*?</F-ID> from <ADDR>$" RE_00_ID = r"Authentication failure for <F-ID>.*?</F-ID> from <ADDR>$"
@ -172,7 +173,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
"--print-all-matched", "--print-all-matched",
FILENAME_01, RE_00 FILENAME_01, RE_00
)) ))
self.assertLogged('Lines: 19 lines, 0 ignored, 13 matched, 6 missed') self.assertLogged('Lines: 19 lines, 0 ignored, 16 matched, 3 missed')
self.assertLogged('Error decoding line'); self.assertLogged('Error decoding line');
self.assertLogged('Continuing to process line ignoring invalid characters') self.assertLogged('Continuing to process line ignoring invalid characters')
@ -186,7 +187,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
"--print-all-matched", "--raw", "--print-all-matched", "--raw",
FILENAME_01, RE_00 FILENAME_01, RE_00
)) ))
self.assertLogged('Lines: 19 lines, 0 ignored, 16 matched, 3 missed') self.assertLogged('Lines: 19 lines, 0 ignored, 19 matched, 0 missed')
def testDirectRE_1raw_noDns(self): def testDirectRE_1raw_noDns(self):
self.assertTrue(_test_exec( self.assertTrue(_test_exec(
@ -194,7 +195,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
"--print-all-matched", "--raw", "--usedns=no", "--print-all-matched", "--raw", "--usedns=no",
FILENAME_01, RE_00 FILENAME_01, RE_00
)) ))
self.assertLogged('Lines: 19 lines, 0 ignored, 13 matched, 6 missed') self.assertLogged('Lines: 19 lines, 0 ignored, 16 matched, 3 missed')
# usage of <F-ID>\S+</F-ID> causes raw handling automatically: # usage of <F-ID>\S+</F-ID> causes raw handling automatically:
self.pruneLog() self.pruneLog()
self.assertTrue(_test_exec( self.assertTrue(_test_exec(
@ -378,6 +379,24 @@ class Fail2banRegexTest(LogCaptureTestCase):
self.assertLogged('192.0.2.0, kevin, inet4') self.assertLogged('192.0.2.0, kevin, inet4')
self.pruneLog() self.pruneLog()
def testNoDateTime(self):
# datepattern doesn't match:
self.assertTrue(_test_exec('-d', '{^LN-BEG}EPOCH', '-o', 'Found-ID:<F-ID>', STR_00_NODT, RE_00_ID))
self.assertLogged(
"Found a match but no valid date/time found",
"Match without a timestamp:",
"Found-ID:kevin", all=True)
self.pruneLog()
# explicitly no datepattern:
self.assertTrue(_test_exec('-d', '{NONE}', '-o', 'Found-ID:<F-ID>', STR_00_NODT, RE_00_ID))
self.assertLogged(
"Found-ID:kevin", all=True)
self.assertNotLogged(
"Found a match but no valid date/time found",
"Match without a timestamp:", all=True)
self.pruneLog()
def testFrmtOutputWrapML(self): def testFrmtOutputWrapML(self):
unittest.F2B.SkipIfCfgMissing(stock=True) unittest.F2B.SkipIfCfgMissing(stock=True)
# complex substitution using tags and message (ip, user, msg): # complex substitution using tags and message (ip, user, msg):

View File

@ -8,9 +8,9 @@ Jul 4 18:39:39 mail courieresmtpd: error,relay=::ffff:1.2.3.4,from=<picaro@astr
Jul 6 03:42:28 whistler courieresmtpd: error,relay=::ffff:1.2.3.4,from=<>,to=<admin at memcpy>: 550 User unknown. Jul 6 03:42:28 whistler courieresmtpd: error,relay=::ffff:1.2.3.4,from=<>,to=<admin at memcpy>: 550 User unknown.
# failJSON: { "time": "2004-11-21T23:16:17", "match": true , "host": "1.2.3.4" } # failJSON: { "time": "2004-11-21T23:16:17", "match": true , "host": "1.2.3.4" }
Nov 21 23:16:17 server courieresmtpd: error,relay=::ffff:1.2.3.4,from=<>,to=<>: 550 User unknown. Nov 21 23:16:17 server courieresmtpd: error,relay=::ffff:1.2.3.4,from=<>,to=<>: 550 User unknown.
# failJSON: { "time": "2004-08-14T12:51:04", "match": true , "host": "1.2.3.4" } # failJSON: { "time": "2005-08-14T12:51:04", "match": true , "host": "1.2.3.4" }
Aug 14 12:51:04 HOSTNAME courieresmtpd: error,relay=::ffff:1.2.3.4,from=<firozquarl@aclunc.org>,to=<BOGUSUSER@HOSTEDDOMAIN.org>: 550 User unknown. Aug 14 12:51:04 HOSTNAME courieresmtpd: error,relay=::ffff:1.2.3.4,from=<firozquarl@aclunc.org>,to=<BOGUSUSER@HOSTEDDOMAIN.org>: 550 User unknown.
# failJSON: { "time": "2004-08-14T12:51:04", "match": true , "host": "1.2.3.4" } # failJSON: { "time": "2005-08-14T12:51:04", "match": true , "host": "1.2.3.4" }
Aug 14 12:51:04 mail.server courieresmtpd[26762]: error,relay=::ffff:1.2.3.4,msg="535 Authentication failed.",cmd: AUTH PLAIN AAAAABBBBCCCCWxlZA== admin Aug 14 12:51:04 mail.server courieresmtpd[26762]: error,relay=::ffff:1.2.3.4,msg="535 Authentication failed.",cmd: AUTH PLAIN AAAAABBBBCCCCWxlZA== admin
# failJSON: { "time": "2004-08-14T12:51:05", "match": true , "host": "192.0.2.3" } # failJSON: { "time": "2005-08-14T12:51:05", "match": true , "host": "192.0.2.3" }
Aug 14 12:51:05 mail.server courieresmtpd[425070]: error,relay=::ffff:192.0.2.3,port=43632,msg="535 Authentication failed.",cmd: AUTH LOGIN PlcmSpIp@example.com Aug 14 12:51:05 mail.server courieresmtpd[425070]: error,relay=::ffff:192.0.2.3,port=43632,msg="535 Authentication failed.",cmd: AUTH LOGIN PlcmSpIp@example.com

View File

@ -30,8 +30,8 @@ Jun 21 16:55:02 <auth.info> machine kernel: [ 970.699396] @vserver_demo test-
# failJSON: { "time": "2005-06-21T16:55:03", "match": true , "host": "192.0.2.3" } # failJSON: { "time": "2005-06-21T16:55:03", "match": true , "host": "192.0.2.3" }
[Jun 21 16:55:03] <auth.info> machine kernel: [ 970.699396] @vserver_demo test-demo(pam_unix)[13709] [ID 255 test] F2B: failure from 192.0.2.3 [Jun 21 16:55:03] <auth.info> machine kernel: [ 970.699396] @vserver_demo test-demo(pam_unix)[13709] [ID 255 test] F2B: failure from 192.0.2.3
# -- wrong time direct in journal-line (used last known date): # -- wrong time direct in journal-line (used last known date or now, but null because no checkFindTime in samples test factory):
# failJSON: { "time": "2005-06-21T16:55:03", "match": true , "host": "192.0.2.1" } # failJSON: { "time": null, "match": true , "host": "192.0.2.1" }
0000-12-30 00:00:00 server test-demo[47831]: F2B: failure from 192.0.2.1 0000-12-30 00:00:00 server test-demo[47831]: F2B: failure from 192.0.2.1
# -- wrong time after newline in message (plist without escaped newlines): # -- wrong time after newline in message (plist without escaped newlines):
# failJSON: { "match": false } # failJSON: { "match": false }
@ -42,8 +42,8 @@ Jun 22 20:37:04 server test-demo[402]: writeToStorage plist={
applicationDate = "0000-12-30 00:00:00 +0000"; applicationDate = "0000-12-30 00:00:00 +0000";
# failJSON: { "match": false } # failJSON: { "match": false }
} }
# -- wrong time direct in journal-line (used last known date): # -- wrong time direct in journal-line (used last known date, but null because no checkFindTime in samples test factory):
# failJSON: { "time": "2005-06-22T20:37:04", "match": true , "host": "192.0.2.2" } # failJSON: { "time": null, "match": true , "host": "192.0.2.2" }
0000-12-30 00:00:00 server test-demo[47831]: F2B: failure from 192.0.2.2 0000-12-30 00:00:00 server test-demo[47831]: F2B: failure from 192.0.2.2
# -- test no zone and UTC/GMT named zone "2005-06-21T14:55:10 UTC" == "2005-06-21T16:55:10 CEST" (diff +2h in CEST): # -- test no zone and UTC/GMT named zone "2005-06-21T14:55:10 UTC" == "2005-06-21T16:55:10 CEST" (diff +2h in CEST):

View File

@ -215,7 +215,7 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line
# Write: all at once and flush # Write: all at once and flush
if isinstance(fout, str): if isinstance(fout, str):
fout = open(fout, mode) fout = open(fout, mode)
fout.write('\n'.join(lines)) fout.write('\n'.join(lines)+'\n')
fout.flush() fout.flush()
if isinstance(in_, str): # pragma: no branch - only used with str in test cases if isinstance(in_, str): # pragma: no branch - only used with str in test cases
# Opened earlier, therefore must close it # Opened earlier, therefore must close it
@ -394,12 +394,13 @@ class IgnoreIP(LogCaptureTestCase):
finally: finally:
tearDownMyTime() tearDownMyTime()
def testTimeJump(self): def _testTimeJump(self, inOperation=False):
try: try:
self.filter.addFailRegex('^<HOST>') self.filter.addFailRegex('^<HOST>')
self.filter.setDatePattern(r'{^LN-BEG}%Y-%m-%d %H:%M:%S(?:\s*%Z)?\s') self.filter.setDatePattern(r'{^LN-BEG}%Y-%m-%d %H:%M:%S(?:\s*%Z)?\s')
self.filter.setFindTime(10); # max 10 seconds back self.filter.setFindTime(10); # max 10 seconds back
self.filter.setMaxRetry(5); # don't ban here self.filter.setMaxRetry(5); # don't ban here
self.filter.inOperation = inOperation
# #
self.pruneLog('[phase 1] DST time jump') self.pruneLog('[phase 1] DST time jump')
# check local time jump (DST hole): # check local time jump (DST hole):
@ -430,6 +431,47 @@ class IgnoreIP(LogCaptureTestCase):
self.assertNotLogged('Ignore line') self.assertNotLogged('Ignore line')
finally: finally:
tearDownMyTime() tearDownMyTime()
def testTimeJump(self):
self._testTimeJump(inOperation=False)
def testTimeJump_InOperation(self):
self._testTimeJump(inOperation=True)
def testWrongTimeZone(self):
try:
self.filter.addFailRegex('fail from <ADDR>$')
self.filter.setDatePattern(r'{^LN-BEG}%Y-%m-%d %H:%M:%S(?:\s*%Z)?\s')
self.filter.setMaxRetry(5); # don't ban here
self.filter.inOperation = True; # real processing (all messages are new)
# current time is 1h later than log-entries:
MyTime.setTime(1572138000+3600)
#
self.pruneLog("[phase 1] simulate wrong TZ")
for i in (1,2,3):
self.filter.processLineAndAdd('2019-10-27 02:00:00 fail from 192.0.2.15'); # +3 = 3
self.assertLogged(
"Simulate NOW in operation since found time has too large deviation",
"Please check jail has possibly a timezone issue.",
"192.0.2.15:1", "192.0.2.15:2", "192.0.2.15:3",
"Total # of detected failures: 3.", wait=True)
#
self.pruneLog("[phase 2] wrong TZ given in log")
for i in (1,2,3):
self.filter.processLineAndAdd('2019-10-27 04:00:00 GMT fail from 192.0.2.16'); # +3 = 6
self.assertLogged(
"192.0.2.16:1", "192.0.2.16:2", "192.0.2.16:3",
"Total # of detected failures: 6.", all=True, wait=True)
self.assertNotLogged("Found a match but no valid date/time found")
#
self.pruneLog("[phase 3] other timestamp (don't match datepattern), regex matches")
for i in range(3):
self.filter.processLineAndAdd('27.10.2019 04:00:00 fail from 192.0.2.17'); # +3 = 9
self.assertLogged(
"Found a match but no valid date/time found",
"Match without a timestamp:",
"192.0.2.17:1", "192.0.2.17:2", "192.0.2.17:3",
"Total # of detected failures: 9.", all=True, wait=True)
finally:
tearDownMyTime()
def testAddAttempt(self): def testAddAttempt(self):
self.filter.setMaxRetry(3) self.filter.setMaxRetry(3)
@ -878,7 +920,7 @@ class LogFileMonitor(LogCaptureTestCase):
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
# and it should have not been enough # and it should have not been enough
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=5) _copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=12, n=3)
self.filter.getFailures(self.name) self.filter.getFailures(self.name)
_assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01) _assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01)
@ -897,7 +939,7 @@ class LogFileMonitor(LogCaptureTestCase):
# filter "marked" as the known beginning, otherwise it # filter "marked" as the known beginning, otherwise it
# would not detect "rotation" # would not detect "rotation"
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name, self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
skip=3, mode='w') skip=12, n=3, mode='w')
self.filter.getFailures(self.name) self.filter.getFailures(self.name)
#self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) #self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
_assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01) _assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01)
@ -916,7 +958,7 @@ class LogFileMonitor(LogCaptureTestCase):
# move aside, but leaving the handle still open... # move aside, but leaving the handle still open...
os.rename(self.name, self.name + '.bak') os.rename(self.name, self.name + '.bak')
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14).close() _copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14, n=1).close()
self.filter.getFailures(self.name) self.filter.getFailures(self.name)
_assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01) _assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 3) self.assertEqual(self.filter.failManager.getFailTotal(), 3)
@ -1027,13 +1069,13 @@ def get_monitor_failures_testcase(Filter_):
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
# Now let's feed it with entries from the file # Now let's feed it with entries from the file
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=5) _copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=12)
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
# and our dummy jail is empty as well # and our dummy jail is empty as well
self.assertFalse(len(self.jail)) self.assertFalse(len(self.jail))
# since it should have not been enough # since it should have not been enough
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=5) _copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=12, n=3)
if idle: if idle:
self.waitForTicks(1) self.waitForTicks(1)
self.assertTrue(self.isEmpty(1)) self.assertTrue(self.isEmpty(1))
@ -1052,7 +1094,7 @@ def get_monitor_failures_testcase(Filter_):
#return #return
# just for fun let's copy all of them again and see if that results # just for fun let's copy all of them again and see if that results
# in a new ban # in a new ban
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100) _copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=12, n=3)
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
def test_rewrite_file(self): def test_rewrite_file(self):
@ -1066,7 +1108,7 @@ def get_monitor_failures_testcase(Filter_):
# filter "marked" as the known beginning, otherwise it # filter "marked" as the known beginning, otherwise it
# would not detect "rotation" # would not detect "rotation"
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name, self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
skip=3, mode='w') skip=12, n=3, mode='w')
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
def _wait4failures(self, count=2): def _wait4failures(self, count=2):
@ -1087,13 +1129,13 @@ def get_monitor_failures_testcase(Filter_):
# move aside, but leaving the handle still open... # move aside, but leaving the handle still open...
os.rename(self.name, self.name + '.bak') os.rename(self.name, self.name + '.bak')
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14).close() _copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14, n=1).close()
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 3) self.assertEqual(self.filter.failManager.getFailTotal(), 3)
# now remove the moved file # now remove the moved file
_killfile(None, self.name + '.bak') _killfile(None, self.name + '.bak')
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100).close() _copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=12, n=3).close()
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 6) self.assertEqual(self.filter.failManager.getFailTotal(), 6)
@ -1169,8 +1211,7 @@ def get_monitor_failures_testcase(Filter_):
def _test_move_into_file(self, interim_kill=False): def _test_move_into_file(self, interim_kill=False):
# if we move a new file into the location of an old (monitored) file # if we move a new file into the location of an old (monitored) file
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, _copy_lines_between_files(GetFailures.FILENAME_01, self.name).close()
n=100).close()
# make sure that it is monitored first # make sure that it is monitored first
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 3) self.assertEqual(self.filter.failManager.getFailTotal(), 3)
@ -1181,14 +1222,14 @@ def get_monitor_failures_testcase(Filter_):
# now create a new one to override old one # now create a new one to override old one
_copy_lines_between_files(GetFailures.FILENAME_01, self.name + '.new', _copy_lines_between_files(GetFailures.FILENAME_01, self.name + '.new',
n=100).close() skip=12, n=3).close()
os.rename(self.name + '.new', self.name) os.rename(self.name + '.new', self.name)
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 6) self.assertEqual(self.filter.failManager.getFailTotal(), 6)
# and to make sure that it now monitored for changes # and to make sure that it now monitored for changes
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
n=100).close() skip=12, n=3).close()
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 9) self.assertEqual(self.filter.failManager.getFailTotal(), 9)
@ -1207,7 +1248,7 @@ def get_monitor_failures_testcase(Filter_):
# create a bogus file in the same directory and see if that doesn't affect # create a bogus file in the same directory and see if that doesn't affect
open(self.name + '.bak2', 'w').close() open(self.name + '.bak2', 'w').close()
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100).close() _copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=12, n=3).close()
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
self.assertEqual(self.filter.failManager.getFailTotal(), 6) self.assertEqual(self.filter.failManager.getFailTotal(), 6)
_killfile(None, self.name + '.bak2') _killfile(None, self.name + '.bak2')
@ -1239,7 +1280,7 @@ def get_monitor_failures_testcase(Filter_):
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=6) # was needed if we write twice above self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=6) # was needed if we write twice above
# now copy and get even more # now copy and get even more
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100) _copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=12, n=3)
# check for 3 failures (not 9), because 6 already get above... # check for 3 failures (not 9), because 6 already get above...
self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assert_correct_last_attempt(GetFailures.FAILURES_01)
# total count in this test: # total count in this test:
@ -1606,16 +1647,24 @@ class GetFailures(LogCaptureTestCase):
_assert_correct_last_attempt(self, self.filter, output) _assert_correct_last_attempt(self, self.filter, output)
def testGetFailures03(self): def testGetFailures03(self):
output = ('203.162.223.135', 7, 1124013544.0) output = ('203.162.223.135', 6, 1124013600.0)
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=0) self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=0)
self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown") self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown")
self.filter.getFailures(GetFailures.FILENAME_03) self.filter.getFailures(GetFailures.FILENAME_03)
_assert_correct_last_attempt(self, self.filter, output) _assert_correct_last_attempt(self, self.filter, output)
def testGetFailures03_InOperation(self):
output = ('203.162.223.135', 9, 1124013600.0)
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=0)
self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown")
self.filter.getFailures(GetFailures.FILENAME_03, inOperation=True)
_assert_correct_last_attempt(self, self.filter, output)
def testGetFailures03_Seek1(self): def testGetFailures03_Seek1(self):
# same test as above but with seek to 'Aug 14 11:55:04' - so other output ... # same test as above but with seek to 'Aug 14 11:55:04' - so other output ...
output = ('203.162.223.135', 5, 1124013544.0) output = ('203.162.223.135', 3, 1124013600.0)
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2] - 4*60) self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2] - 4*60)
self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown") self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown")
@ -1624,7 +1673,7 @@ class GetFailures(LogCaptureTestCase):
def testGetFailures03_Seek2(self): def testGetFailures03_Seek2(self):
# same test as above but with seek to 'Aug 14 11:59:04' - so other output ... # same test as above but with seek to 'Aug 14 11:59:04' - so other output ...
output = ('203.162.223.135', 1, 1124013544.0) output = ('203.162.223.135', 2, 1124013600.0)
self.filter.setMaxRetry(1) self.filter.setMaxRetry(1)
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2]) self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2])
@ -1652,6 +1701,7 @@ class GetFailures(LogCaptureTestCase):
_assert_correct_last_attempt(self, self.filter, output) _assert_correct_last_attempt(self, self.filter, output)
def testGetFailuresWrongChar(self): def testGetFailuresWrongChar(self):
self.filter.checkFindTime = False
# write wrong utf-8 char: # write wrong utf-8 char:
fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='crlf') fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='crlf')
fout = fopen(fname, 'wb') fout = fopen(fname, 'wb')
@ -1672,6 +1722,8 @@ class GetFailures(LogCaptureTestCase):
for enc in (None, 'utf-8', 'ascii'): for enc in (None, 'utf-8', 'ascii'):
if enc is not None: if enc is not None:
self.tearDown();self.setUp(); self.tearDown();self.setUp();
if DefLogSys.getEffectiveLevel() > 7: DefLogSys.setLevel(7); # ensure decode_line logs always
self.filter.checkFindTime = False;
self.filter.setLogEncoding(enc); self.filter.setLogEncoding(enc);
# speedup search using exact date pattern: # speedup search using exact date pattern:
self.filter.setDatePattern(r'^%ExY-%Exm-%Exd %ExH:%ExM:%ExS') self.filter.setDatePattern(r'^%ExY-%Exm-%Exd %ExH:%ExM:%ExS')

View File

@ -460,11 +460,27 @@ Similar to actions, filters have an [Init] section which can be overridden in \f
specifies the maximum number of lines to buffer to match multi-line regexs. For some log formats this will not required to be changed. Other logs may require to increase this value if a particular log file is frequently written to. specifies the maximum number of lines to buffer to match multi-line regexs. For some log formats this will not required to be changed. Other logs may require to increase this value if a particular log file is frequently written to.
.TP .TP
\fBdatepattern\fR \fBdatepattern\fR
specifies a custom date pattern/regex as an alternative to the default date detectors e.g. %Y-%m-%d %H:%M(?::%S)?. For a list of valid format directives, see Python library documentation for strptime behaviour. specifies a custom date pattern/regex as an alternative to the default date detectors e.g. %%Y-%%m-%%d %%H:%%M(?::%%S)?.
.br For a list of valid format directives, see Python library documentation for strptime behaviour.
Also, special values of \fIEpoch\fR (UNIX Timestamp), \fITAI64N\fR and \fIISO8601\fR can be used.
.br .br
\fBNOTE:\fR due to config file string substitution, that %'s must be escaped by an % in config files. \fBNOTE:\fR due to config file string substitution, that %'s must be escaped by an % in config files.
.br
Also, special values of \fIEpoch\fR (UNIX Timestamp), \fITAI64N\fR and \fIISO8601\fR can be used as datepattern.
.br
Normally the regexp generated for datepattern additionally gets word-start and word-end boundaries to avoid accidental match inside of some word in a message.
There are several prefixes and words with special meaning that could be specified with custom datepattern to control resulting regex:
.RS
.IP
\fI{DEFAULT}\fR - can be used to add default date patterns of fail2ban.
.IP
\fI{DATE}\fR - can be used as part of regex that will be replaced with default date patterns.
.IP
\fI{^LN-BEG}\fR - prefix (similar to \fI^\fR) changing word-start boundary to line-start boundary (ignoring up to 2 characters). If used as value (not as a prefix), it will also set all default date patterns (similar to \fI{DEFAULT}\fR), but anchored at begin of message line.
.IP
\fI{UNB}\fR - prefix to disable automatic word boundaries in regex.
.IP
\fI{NONE}\fR - value would allow to find failures totally without date-time in log message. Filter will use now as a timestamp (or last known timestamp from previous line with timestamp).
.RE
.TP .TP
\fBjournalmatch\fR \fBjournalmatch\fR
specifies the systemd journal match used to filter the journal entries. See \fBjournalctl(1)\fR and \fBsystemd.journal-fields(7)\fR for matches syntax and more details on special journal fields. This option is only valid for the \fIsystemd\fR backend. specifies the systemd journal match used to filter the journal entries. See \fBjournalctl(1)\fR and \fBsystemd.journal-fields(7)\fR for matches syntax and more details on special journal fields. This option is only valid for the \fIsystemd\fR backend.