diff --git a/ChangeLog b/ChangeLog index 238bf56c..b22d740c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -41,6 +41,10 @@ ver. 0.10.3-dev-1 (20??/??/??) - development edition * `filter.d/sshd.conf`: - failregex got an optional space in order to match new log-format (see gh-2061); - fixed ddos-mode regex to match refactored message (some versions can contain port now, see gh-2062); + - fixed root login refused regex (optional port before preauth, gh-2080); + - avoid banning of legitimate users when pam_unix used in combination with other password method, so + bypass pam_unix failures if accepted available for this user gh-2070; + - amend to gh-1263 with better handling of multiple attempts (failures for different user-names recognized immediatelly); * `action.d/badips.py`: implicit convert IPAddr to str, solves an issue "expected string, IPAddr found" (gh-2059); * `action.d/hostsdeny.conf`: fixed IPv6 syntax (enclosed in square brackets, gh-2066); * (Free)BSD ipfw actionban fixed to allow same rule added several times (gh-2054); diff --git a/config/filter.d/apache-auth.conf b/config/filter.d/apache-auth.conf index d9a6fa5e..f2d5f793 100644 --- a/config/filter.d/apache-auth.conf +++ b/config/filter.d/apache-auth.conf @@ -15,10 +15,10 @@ prefregex = ^%(_apache_error_client)s (?:AH\d+: )?.+$ auth_type = ([A-Z]\w+: )? failregex = ^client (?:denied by server configuration|used wrong authentication scheme)\b - ^user (?:\S*|.*?) (?:auth(?:oriz|entic)ation failure|not found|denied by provider)\b + ^user (?!`)(?:\S*|.*?) (?:auth(?:oriz|entic)ation failure|not found|denied by provider)\b ^Authorization of user (?:\S*|.*?) to access .*? failed\b ^%(auth_type)suser (?:\S*|.*?): password mismatch\b - ^%(auth_type)suser `(?:[^']*|.*?)' in realm `.+' (not found|denied by provider)\b + ^%(auth_type)suser `(?:[^']*|.*?)' in realm `.+' (auth(?:oriz|entic)ation failure|not found|denied by provider)\b ^%(auth_type)sinvalid nonce .* received - length is not\b ^%(auth_type)srealm mismatch - got `(?:[^']*|.*?)' but expected\b ^%(auth_type)sunknown algorithm `(?:[^']*|.*?)' received\b diff --git a/config/filter.d/pam-generic.conf b/config/filter.d/pam-generic.conf index ff4ea802..0cadbeee 100644 --- a/config/filter.d/pam-generic.conf +++ b/config/filter.d/pam-generic.conf @@ -16,15 +16,14 @@ _ttys_re=\S* __pam_re=\(?%(__pam_auth)s(?:\(\S+\))?\)?:? _daemon = \S+ -prefregex = ^%(__prefix_line)s%(__pam_re)s\s+authentication failure; logname=\S* uid=\S* euid=\S* tty=%(_ttys_re)s .+$ +prefregex = ^%(__prefix_line)s%(__pam_re)s\s+authentication failure;(?:\s+(?:(?:logname|e?uid)=\S*)){0,3} tty=%(_ttys_re)s .+$ -failregex = ^ruser=\S* rhost=\s*$ - ^ruser= rhost=\s+user=\S*\s*$ - ^ruser= rhost=\s+user=.*?\s*$ - ^ruser=.*? rhost=\s*$ +failregex = ^ruser=(?:\S*|.*?) rhost=(?:\s+user=(?:\S*|.*?))?\s*$ ignoreregex = +datepattern = {^LN-BEG} + # DEV Notes: # # for linux-pam before 0.99.2.0 (late 2005) (removed before 0.8.11 release) diff --git a/config/filter.d/sshd.conf b/config/filter.d/sshd.conf index d8bb5edf..d6f39bc5 100644 --- a/config/filter.d/sshd.conf +++ b/config/filter.d/sshd.conf @@ -21,48 +21,52 @@ _daemon = sshd # optional prefix (logged from several ssh versions) like "error: ", "error: PAM: " or "fatal: " __pref = (?:(?:error|fatal): (?:PAM: )?)? # optional suffix (logged from several ssh versions) like " [preauth]" -__suff = (?: \[preauth\])?\s* -__on_port_opt = (?: port \d+)?(?: on \S+(?: port \d+)?)? +#__suff = (?: port \d+)?(?: \[preauth\])?\s* +__suff = (?: (?:port \d+|on \S+|\[preauth\])){0,3}\s* +__on_port_opt = (?: (?:port \d+|on \S+)){0,2} # for all possible (also future) forms of "no matching (cipher|mac|MAC|compression method|key exchange method|host key type) found", # see ssherr.c for all possible SSH_ERR_..._ALG_MATCH errors. __alg_match = (?:(?:\w+ (?!found\b)){0,2}\w+) +# PAM authentication mechanism, can be overridden, e. g. `filter = sshd[__pam_auth='pam_ldap']`: +__pam_auth = pam_[a-z]+ + [Definition] prefregex = ^%(__prefix_line)s%(__pref)s.+$ -cmnfailre = ^[aA]uthentication (?:failure|error|failed) for .* from ( via \S+)?\s*%(__suff)s$ - ^User not known to the underlying authentication module for .* from \s*%(__suff)s$ - ^Failed \S+ for invalid user (?P\S+)|(?:(?! from ).)*? from %(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$) +cmnfailre = ^[aA]uthentication (?:failure|error|failed) for .* from ( via \S+)?%(__suff)s$ + ^User not known to the underlying authentication module for .* from %(__suff)s$ + ^Failed publickey for invalid user (?P\S+)|(?:(?! from ).)*? from %(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$) ^Failed \b(?!publickey)\S+ for (?Pinvalid user )?(?P\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+) from %(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$) - ^ROOT LOGIN REFUSED.* FROM \s*%(__suff)s$ - ^[iI](?:llegal|nvalid) user .*? from %(__on_port_opt)s\s*$ - ^User .+ from not allowed because not listed in AllowUsers\s*%(__suff)s$ - ^User .+ from not allowed because listed in DenyUsers\s*%(__suff)s$ - ^User .+ from not allowed because not in any group\s*%(__suff)s$ - ^refused connect from \S+ \(\)\s*%(__suff)s$ + ^ROOT LOGIN REFUSED FROM + ^[iI](?:llegal|nvalid) user .*? from %(__suff)s$ + ^User .+ from not allowed because not listed in AllowUsers%(__suff)s$ + ^User .+ from not allowed because listed in DenyUsers%(__suff)s$ + ^User .+ from not allowed because not in any group%(__suff)s$ + ^refused connect from \S+ \(\) ^Received disconnect from %(__on_port_opt)s:\s*3: .*: Auth fail%(__suff)s$ - ^User .+ from not allowed because a group is listed in DenyGroups\s*%(__suff)s$ - ^User .+ from not allowed because none of user's groups are listed in AllowGroups\s*%(__suff)s$ - ^pam_unix\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=\S*\s*rhost=\s.*%(__suff)s$ + ^User .+ from not allowed because a group is listed in DenyGroups%(__suff)s$ + ^User .+ from not allowed because none of user's groups are listed in AllowGroups%(__suff)s$ + ^%(__pam_auth)s\(sshd:auth\):\s+authentication failure;(?:\s+(?:(?:logname|e?uid|tty)=\S*)){0,4}\s+ruser=\S*\s+rhost=(?:\s+user=\S*)?%(__suff)s$ ^(error: )?maximum authentication attempts exceeded for .* from %(__on_port_opt)s(?: ssh\d*)?%(__suff)s$ ^User .+ not allowed because account is locked%(__suff)s - ^Disconnecting: Too many authentication failures(?: for .+?)?%(__suff)s + ^Disconnecting: Too many authentication failures(?: for .+?)?%(__suff)s$ ^Received disconnect from %(__on_port_opt)s:\s*11: - ^Connection closed by %(__suff)s$ - ^Accepted publickey for \S+ from (?:\s|$) + ^Connection closed by + ^Accepted \w+ for \S+ from (?:\s|$) mdre-normal = -mdre-ddos = ^Did not receive identification string from %(__on_port_opt)s%(__suff)s - ^Connection reset by %(__on_port_opt)s%(__suff)s +mdre-ddos = ^Did not receive identification string from + ^Connection reset by ^SSH: Server;Ltype: (?:Authname|Version|Kex);Remote: -\d+;[A-Z]\w+: - ^Read from socket failed: Connection reset by peer%(__suff)s + ^Read from socket failed: Connection reset by peer -mdre-extra = ^Received disconnect from %(__on_port_opt)s:\s*14: No supported authentication methods available%(__suff)s$ +mdre-extra = ^Received disconnect from %(__on_port_opt)s:\s*14: No supported authentication methods available ^Unable to negotiate with %(__on_port_opt)s: no matching <__alg_match> found. - ^Unable to negotiate a <__alg_match>%(__suff)s$ + ^Unable to negotiate a <__alg_match> ^no matching <__alg_match> found: mdre-aggressive = %(mdre-ddos)s diff --git a/fail2ban/server/failregex.py b/fail2ban/server/failregex.py index 40d106db..672bc32a 100644 --- a/fail2ban/server/failregex.py +++ b/fail2ban/server/failregex.py @@ -89,6 +89,11 @@ def mapTag2Opt(tag): except KeyError: return tag.lower() + +# alternate names to be merged, e. g. alt_user_1 -> user ... +ALTNAME_PRE = 'alt_' +ALTNAME_CRE = re.compile(r'^' + ALTNAME_PRE + r'(.*)(?:_\d+)?$') + ## # Regular expression class. # @@ -114,6 +119,14 @@ class Regex: try: self._regexObj = re.compile(regex, re.MULTILINE if multiline else 0) self._regex = regex + self._altValues = {} + for k in filter( + lambda k: len(k) > len(ALTNAME_PRE) and k.startswith(ALTNAME_PRE), + self._regexObj.groupindex + ): + n = ALTNAME_CRE.match(k).group(1) + self._altValues[k] = n + self._altValues = list(self._altValues.items()) if len(self._altValues) else None except sre_constants.error: raise RegexException("Unable to compile regular expression '%s'" % regex) @@ -257,7 +270,16 @@ class Regex: # def getGroups(self): - return self._matchCache.groupdict() + if not self._altValues: + return self._matchCache.groupdict() + # merge alternate values (e. g. 'alt_user_1' -> 'user' or 'alt_host' -> 'host'): + fail = self._matchCache.groupdict() + #fail = fail.copy() + for k,n in self._altValues: + v = fail.get(k) + if v and not fail.get(n): + fail[n] = v + return fail ## # Returns skipped lines. diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 0ba54be0..f86d9613 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -589,31 +589,86 @@ class Filter(JailThread): return ignoreRegexIndex return None + def _updateUsers(self, fail, user=()): + users = fail.get('users') + # only for regex contains user: + if user: + if not users: + fail['users'] = users = set() + users.add(user) + return users + return None + + # # ATM incremental (non-empty only) merge deactivated ... + # @staticmethod + # def _updateFailure(self, mlfidGroups, fail): + # # reset old failure-ids when new types of id available in this failure: + # fids = set() + # for k in ('fid', 'ip4', 'ip6', 'dns'): + # if fail.get(k): + # fids.add(k) + # if fids: + # for k in ('fid', 'ip4', 'ip6', 'dns'): + # if k not in fids: + # try: + # del mlfidGroups[k] + # except: + # pass + # # update not empty values: + # mlfidGroups.update(((k,v) for k,v in fail.iteritems() if v)) + def _mergeFailure(self, mlfid, fail, failRegex): mlfidFail = self.mlfidCache.get(mlfid) if self.__mlfidCache else None + users = None + nfflgs = 0 + if fail.get('nofail'): nfflgs |= 1 + if fail.get('mlfforget'): nfflgs |= 2 # if multi-line failure id (connection id) known: if mlfidFail: mlfidGroups = mlfidFail[1] - # update - if not forget (disconnect/reset): - if not fail.get('mlfforget'): - mlfidGroups.update(fail) - else: - self.mlfidCache.unset(mlfid) # remove cached entry - # merge with previous info: - fail2 = mlfidGroups.copy() - fail2.update(fail) - if not fail.get('nofail'): # be sure we've correct current state - try: - del fail2['nofail'] - except KeyError: - pass - fail2["matches"] = fail.get("matches", []) + failRegex.getMatchedTupleLines() - fail = fail2 - elif not fail.get('mlfforget'): + # update users set (hold all users of connect): + users = self._updateUsers(mlfidGroups, fail.get('user')) + # be sure we've correct current state ('nofail' only from last failure) + try: + del mlfidGroups['nofail'] + except KeyError: + pass + # # ATM incremental (non-empty only) merge deactivated (for future version only), + # # it can be simulated using alternate value tags, like ..., + # # so previous value 'val' will be overwritten only if 'alt_val' is not empty... + # _updateFailure(mlfidGroups, fail) + # + # overwrite multi-line failure with all values, available in fail: + mlfidGroups.update(fail) + # new merged failure data: + fail = mlfidGroups + # if forget (disconnect/reset) - remove cached entry: + if nfflgs & 2: + self.mlfidCache.unset(mlfid) + elif not (nfflgs & 2): # not mlfforget + users = self._updateUsers(fail, fail.get('user')) mlfidFail = [self.__lastDate, fail] self.mlfidCache.set(mlfid, mlfidFail) - if fail.get('nofail'): - fail["matches"] = failRegex.getMatchedTupleLines() + # check users in order to avoid reset failure by multiple logon-attempts: + if users and len(users) > 1: + # we've new user, reset 'nofail' because of multiple users attempts: + try: + del fail['nofail'] + except KeyError: + pass + # merge matches: + if not fail.get('nofail'): # current state (corresponding users) + try: + m = fail.pop("nofail-matches") + m += fail.get("matches", []) + except KeyError: + m = fail.get("matches", []) + if not (nfflgs & 2): # not mlfforget: + m += failRegex.getMatchedTupleLines() + fail["matches"] = m + elif not (nfflgs & 2) and (nfflgs & 1): # not mlfforget and nofail: + fail["nofail-matches"] = fail.get("nofail-matches", []) + failRegex.getMatchedTupleLines() + # return merged: return fail @@ -702,8 +757,10 @@ class Filter(JailThread): failRegex.search(buf, orgBuffer) if not failRegex.hasMatched(): continue + # current failure data (matched group dict): + fail = failRegex.getGroups() # The failregex matched. - if ll <= 7: logSys.log(7, " Matched failregex %d: %s", failRegexIndex, failRegex.getGroups()) + if ll <= 7: logSys.log(7, " Matched failregex %d: %s", failRegexIndex, fail) # Checks if we must ignore this match. if self.ignoreLine(failRegex.getMatchedTupleLines()) \ is not None: @@ -725,14 +782,14 @@ class Filter(JailThread): "in order to get support for this format.", "\n".join(failRegex.getMatchedLines()), timeText) continue - self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None - # retrieve failure-id, host, etc from failure match: + # we should check all regex (bypass on multi-line, otherwise too complex): + if not self.checkAllRegex or self.getMaxLines() > 1: + self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None + # merge data if multi-line failure: raw = returnRawHost if preGroups: - fail = preGroups.copy() - fail.update(failRegex.getGroups()) - else: - fail = failRegex.getGroups() + currFail, fail = fail, preGroups.copy() + fail.update(currFail) # first try to check we have mlfid case (caching of connection id by multi-line): mlfid = fail.get('mlfid') if mlfid is not None: diff --git a/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf b/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf index 5560716d..5717c316 100644 --- a/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf +++ b/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf @@ -14,8 +14,8 @@ _daemon = sshd # optional prefix (logged from several ssh versions) like "error: ", "error: PAM: " or "fatal: " __pref = (?:(?:error|fatal): (?:PAM: )?)? # optional suffix (logged from several ssh versions) like " [preauth]" -__suff = (?: \[preauth\])?\s* -__on_port_opt = (?: port \d+)?(?: on \S+(?: port \d+)?)? +__suff = (?: (?:port \d+|on \S+|\[preauth\])){0,3}\s* +__on_port_opt = (?: (?:port \d+|on \S+)){0,2} # single line prefix: __prefix_line_sl = %(__prefix_line)s%(__pref)s @@ -27,22 +27,25 @@ __prefix_line_ml2 = %(__suff)s$^(?P=__prefix)%(__pref)s # see ssherr.c for all possible SSH_ERR_..._ALG_MATCH errors. __alg_match = (?:(?:\w+ (?!found\b)){0,2}\w+) +# PAM authentication mechanism, can be overridden, e. g. `filter = sshd[__pam_auth='pam_ldap']`: +__pam_auth = pam_[a-z]+ + [Definition] cmnfailre = ^%(__prefix_line_sl)s[aA]uthentication (?:failure|error|failed) for .* from ( via \S+)?\s*%(__suff)s$ ^%(__prefix_line_sl)sUser not known to the underlying authentication module for .* from \s*%(__suff)s$ ^%(__prefix_line_sl)sFailed \S+ for invalid user (?P\S+)|(?:(?! from ).)*? from %(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$) ^%(__prefix_line_sl)sFailed \b(?!publickey)\S+ for (?Pinvalid user )?(?P\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+) from %(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$) - ^%(__prefix_line_sl)sROOT LOGIN REFUSED.* FROM \s*%(__suff)s$ - ^%(__prefix_line_sl)s[iI](?:llegal|nvalid) user .*? from %(__on_port_opt)s\s*$ + ^%(__prefix_line_sl)sROOT LOGIN REFUSED FROM + ^%(__prefix_line_sl)s[iI](?:llegal|nvalid) user .*? from %(__suff)s$ ^%(__prefix_line_sl)sUser .+ from not allowed because not listed in AllowUsers\s*%(__suff)s$ ^%(__prefix_line_sl)sUser .+ from not allowed because listed in DenyUsers\s*%(__suff)s$ ^%(__prefix_line_sl)sUser .+ from not allowed because not in any group\s*%(__suff)s$ - ^%(__prefix_line_sl)srefused connect from \S+ \(\)\s*%(__suff)s$ + ^%(__prefix_line_sl)srefused connect from \S+ \(\) ^%(__prefix_line_sl)sReceived disconnect from %(__on_port_opt)s:\s*3: .*: Auth fail%(__suff)s$ ^%(__prefix_line_sl)sUser .+ from not allowed because a group is listed in DenyGroups\s*%(__suff)s$ ^%(__prefix_line_sl)sUser .+ from not allowed because none of user's groups are listed in AllowGroups\s*%(__suff)s$ - ^%(__prefix_line_sl)spam_unix\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=\S*\s*rhost=\s.*%(__suff)s$ + ^%(__prefix_line_ml1)s%(__pam_auth)s\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=\S*\s*rhost=\s.*%(__suff)s$%(__prefix_line_ml2)sConnection closed ^%(__prefix_line_sl)s(error: )?maximum authentication attempts exceeded for .* from %(__on_port_opt)s(?: ssh\d*)? \[preauth\]$ ^%(__prefix_line_ml1)sUser .+ not allowed because account is locked%(__prefix_line_ml2)sReceived disconnect from %(__on_port_opt)s:\s*11: .+%(__suff)s$ ^%(__prefix_line_ml1)sDisconnecting: Too many authentication failures(?: for .+?)?%(__suff)s%(__prefix_line_ml2)sConnection closed by %(__suff)s$ @@ -50,13 +53,13 @@ cmnfailre = ^%(__prefix_line_sl)s[aA]uthentication (?:failure|error|failed) for mdre-normal = -mdre-ddos = ^%(__prefix_line_sl)sDid not receive identification string from %(__on_port_opt)s%(__suff)s - ^%(__prefix_line_sl)sConnection reset by %(__on_port_opt)s%(__suff)s +mdre-ddos = ^%(__prefix_line_sl)sDid not receive identification string from + ^%(__prefix_line_sl)sConnection reset by ^%(__prefix_line_ml1)sSSH: Server;Ltype: (?:Authname|Version|Kex);Remote: -\d+;[A-Z]\w+:.*%(__prefix_line_ml2)sRead from socket failed: Connection reset by peer%(__suff)s$ -mdre-extra = ^%(__prefix_line_sl)sReceived disconnect from %(__on_port_opt)s:\s*14: No supported authentication methods available%(__suff)s$ +mdre-extra = ^%(__prefix_line_sl)sReceived disconnect from %(__on_port_opt)s:\s*14: No supported authentication methods available ^%(__prefix_line_sl)sUnable to negotiate with %(__on_port_opt)s: no matching <__alg_match> found. - ^%(__prefix_line_ml1)sConnection from %(__on_port_opt)s%(__prefix_line_ml2)sUnable to negotiate a <__alg_match>%(__suff)s$ + ^%(__prefix_line_ml1)sConnection from %(__on_port_opt)s%(__prefix_line_ml2)sUnable to negotiate a <__alg_match> ^%(__prefix_line_ml1)sConnection from %(__on_port_opt)s%(__prefix_line_ml2)sno matching <__alg_match> found: mdre-aggressive = %(mdre-ddos)s diff --git a/fail2ban/tests/files/logs/sshd b/fail2ban/tests/files/logs/sshd index e80eb30c..28b2f065 100644 --- a/fail2ban/tests/files/logs/sshd +++ b/fail2ban/tests/files/logs/sshd @@ -24,6 +24,8 @@ Feb 25 14:34:11 belka sshd[31603]: Failed password for invalid user ROOT from aa # failJSON: { "time": "2005-01-05T01:31:41", "match": true , "host": "1.2.3.4" } Jan 5 01:31:41 www sshd[1643]: ROOT LOGIN REFUSED FROM 1.2.3.4 # failJSON: { "time": "2005-01-05T01:31:41", "match": true , "host": "1.2.3.4" } +Jan 5 01:31:41 www sshd[1643]: ROOT LOGIN REFUSED FROM 1.2.3.4 port 12345 [preauth] +# failJSON: { "time": "2005-01-05T01:31:41", "match": true , "host": "1.2.3.4" } Jan 5 01:31:41 www sshd[1643]: ROOT LOGIN REFUSED FROM ::ffff:1.2.3.4 #4 @@ -210,9 +212,46 @@ Apr 27 13:02:04 host sshd[29116]: input_userauth_request: invalid user root [pre # failJSON: { "time": "2005-04-27T13:02:04", "match": true , "host": "1.2.3.4", "desc": "No Bye-Bye" } Apr 27 13:02:04 host sshd[29116]: Received disconnect from 1.2.3.4: 11: Normal Shutdown, Thank you for playing [preauth] -# Match sshd auth errors on OpenSUSE systems -# failJSON: { "time": "2015-04-16T20:02:50", "match": true , "host": "222.186.21.217", "desc": "Authentication for user failed" } -2015-04-16T18:02:50.321974+00:00 host sshd[2716]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.21.217 user=root +# Match sshd auth errors on OpenSUSE systems (gh-1024) +# failJSON: { "match": false, "desc": "No failure until closed or another fail (e. g. F-MLFFORGET by success/accepted password can avoid failure, see gh-2070)" } +2015-04-16T18:02:50.321974+00:00 host sshd[2716]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.0.2.112 user=root +# failJSON: { "time": "2015-04-16T20:02:50", "match": true , "host": "192.0.2.112", "desc": "Should catch failure - no success/no accepted password" } +2015-04-16T18:02:50.568798+00:00 host sshd[2716]: Connection closed by 192.0.2.112 + +# disable this test-cases block for obsolete multi-line filter (zzz-sshd-obsolete...): +# filterOptions: [{"test.condition":"name=='sshd'"}] + +# 2 methods auth: pam_unix and pam_ldap are used in combination (gh-2070), succeeded after "failure" in first method: +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:20 bar sshd[1556]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.0.2.113 user=rda +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:20 bar sshd[1556]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=rda rhost=192.0.2.113 [preauth] +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:20 bar sshd[1556]: Accepted password for rda from 192.0.2.113 port 52100 ssh2 +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:20 bar sshd[1556]: pam_unix(sshd:session): session opened for user rda by (uid=0) +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:20 bar sshd[1556]: Connection closed by 192.0.2.113 + +# several attempts, intruder tries to "forget" failed attempts by success login (all 3 attempts with different users): +# failJSON: { "match": false , "desc": "Still no failure (first try)" } +Mar 7 18:53:22 bar sshd[1558]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=root rhost=192.0.2.114 +# failJSON: { "time": "2005-03-07T18:53:23", "match": true , "attempts": 2, "users": ["root", "sudoer"], "host": "192.0.2.114", "desc": "Failure: attempt 2nd user" } +Mar 7 18:53:23 bar sshd[1558]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=sudoer rhost=192.0.2.114 +# failJSON: { "time": "2005-03-07T18:53:24", "match": true , "attempts": 2, "users": ["root", "sudoer", "known"], "host": "192.0.2.114", "desc": "Failure: attempt 3rd user" } +Mar 7 18:53:24 bar sshd[1558]: Accepted password for known from 192.0.2.114 port 52100 ssh2 +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:24 bar sshd[1558]: pam_unix(sshd:session): session opened for user known by (uid=0) + +# several attempts, intruder tries to "forget" failed attempts by success login (accepted for other user as in first failed attempt): +# failJSON: { "match": false , "desc": "Still no failure (first try)" } +Mar 7 18:53:32 bar sshd[1559]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=root rhost=192.0.2.116 +# failJSON: { "match": false , "desc": "Still no failure (second try, same user)" } +Mar 7 18:53:32 bar sshd[1559]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=root rhost=192.0.2.116 +# failJSON: { "time": "2005-03-07T18:53:34", "match": true , "attempts": 2, "users": ["root", "known"], "host": "192.0.2.116", "desc": "Failure: attempt 2nd user" } +Mar 7 18:53:34 bar sshd[1559]: Accepted password for known from 192.0.2.116 port 52100 ssh2 +# failJSON: { "match": false , "desc": "No failure" } +Mar 7 18:53:38 bar sshd[1559]: Connection closed by 192.0.2.116 # filterOptions: [{"mode": "ddos"}, {"mode": "aggressive"}] diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 5f0a447a..1d326c31 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -144,12 +144,14 @@ def testSampleRegexsFactory(name, basedir): regexsUsedRe = set() # process each test-file (note: array filenames can grow during processing): + faildata = {} i = 0 while i < len(filenames): filename = filenames[i]; i += 1; logFile = fileinput.FileInput(os.path.join(TEST_FILES_DIR, "logs", filename)) + ignoreBlock = False for line in logFile: jsonREMatch = re.match("^#+ ?(failJSON|filterOptions|addFILE):(.+)$", line) if jsonREMatch: @@ -159,9 +161,13 @@ def testSampleRegexsFactory(name, basedir): if jsonREMatch.group(1) == 'filterOptions': # following lines with another filter options: self._filterTests = [] + ignoreBlock = False for opts in (faildata if isinstance(faildata, list) else [faildata]): # unique filter name (using options combination): self.assertTrue(isinstance(opts, dict)) + if opts.get('test.condition'): + ignoreBlock = not eval(opts.get('test.condition')) + del opts['test.condition'] fltName = opts.get('filterName') if not fltName: fltName = str(opts) if opts else '' fltName = name + fltName @@ -178,10 +184,11 @@ def testSampleRegexsFactory(name, basedir): raise ValueError("%s: %s:%i" % (e, logFile.filename(), logFile.filelineno())) line = next(logFile) - elif line.startswith("#") or not line.strip(): + elif ignoreBlock or line.startswith("#") or not line.strip(): continue else: # pragma: no cover - normally unreachable faildata = {} + if ignoreBlock: continue # if filter options was not yet specified: if not self._filterTests: @@ -195,6 +202,7 @@ def testSampleRegexsFactory(name, basedir): regexList = flt.getFailRegex() try: + fail = {} ret = flt.processLine(line) if not ret: # Bypass if filter constraint specified: @@ -222,9 +230,17 @@ def testSampleRegexsFactory(name, basedir): for k, v in faildata.iteritems(): if k not in ("time", "match", "desc", "filter"): fv = fail.get(k, None) - # Fallback for backwards compatibility (previously no fid, was host only): - if k == "host" and fv is None: - fv = fid + if fv is None: + # Fallback for backwards compatibility (previously no fid, was host only): + if k == "host": + fv = fid + # special case for attempts counter: + if k == "attempts": + fv = len(fail.get('matches', {})) + # compare sorted (if set) + if isinstance(fv, (set, list, dict)): + self.assertSortedEqual(fv, v) + continue self.assertEqual(fv, v) t = faildata.get("time", None) @@ -246,8 +262,12 @@ def testSampleRegexsFactory(name, basedir): regexsUsedIdx.add(failregex) regexsUsedRe.add(regexList[failregex]) except AssertionError as e: # pragma: no cover - raise AssertionError("%s: %s on: %s:%i, line:\n%s" % ( - fltName, e, logFile.filename(), logFile.filelineno(), line)) + import pprint + raise AssertionError("%s: %s on: %s:%i, line:\n%s\n" + "faildata: %s\nfail: %s" % ( + fltName, e, logFile.filename(), logFile.filelineno(), line, + '\n'.join(pprint.pformat(faildata).splitlines()), + '\n'.join(pprint.pformat(fail).splitlines()))) # check missing samples for regex using each filter-options combination: for fltName, flt in self._filters.iteritems():