Merge branch '0.10' into fix-sshd-filter-suff

# Conflicts resolved:
#	fail2ban/server/filter.py
pull/2090/head
sebres 2018-04-03 13:30:57 +02:00
commit 4ee07adde6
10 changed files with 88 additions and 60 deletions

View File

@ -52,6 +52,7 @@ ver. 0.10.3-dev-1 (20??/??/??) - development edition
### New Features ### New Features
### Enhancements ### Enhancements
* `filter.d/apache-noscript.conf`: extend failregex to match "Primary script unknown", e. g. from php-fpm (gh-2073);
* date-detector extended with long epoch (`LEPOCH`) to parse milliseconds/microseconds posix-dates (gh-2029); * date-detector extended with long epoch (`LEPOCH`) to parse milliseconds/microseconds posix-dates (gh-2029);
* possibility to specify own regex-pattern to match epoch date-time, e. g. `^\[{EPOCH}\]` or `^\[{LEPOCH}\]` (gh-2038); * possibility to specify own regex-pattern to match epoch date-time, e. g. `^\[{EPOCH}\]` or `^\[{LEPOCH}\]` (gh-2038);
the epoch-pattern similar to `{DATE}` patterns does the capture and cuts out the match of whole pattern from the log-line, the epoch-pattern similar to `{DATE}` patterns does the capture and cuts out the match of whole pattern from the log-line,

View File

@ -6,50 +6,51 @@
## Fail2Ban: ban hosts that cause multiple authentication errors ## Fail2Ban: ban hosts that cause multiple authentication errors
Fail2Ban scans log files like `/var/log/auth.log` and bans IP addresses having Fail2Ban scans log files like `/var/log/auth.log` and bans IP addresses conducting
too many failed login attempts. It does this by updating system firewall rules too many failed login attempts. It does this by updating system firewall rules
to reject new connections from those IP addresses, for a configurable amount to reject new connections from those IP addresses, for a configurable amount
of time. Fail2Ban comes out-of-the-box ready to read many standard log files, of time. Fail2Ban comes out-of-the-box ready to read many standard log files,
such as those for sshd and Apache, and is easy to configure to read any log such as those for sshd and Apache, and is easily configured to read any log
file you choose, for any error you choose. file of your choosing, for any error you wish.
Though Fail2Ban is able to reduce the rate of incorrect authentications Though Fail2Ban is able to reduce the rate of incorrect authentication
attempts, it cannot eliminate the risk that weak authentication presents. attempts, it cannot eliminate the risk presented by weak authentication.
Configure services to use only two factor or public/private authentication Set up services to use only two factor, or public/private authentication
mechanisms if you really want to protect services. mechanisms if you really want to protect services.
<img src="http://www.worldipv6launch.org/wp-content/themes/ipv6/downloads/World_IPv6_launch_logo.svg" height="52pt"/> | Since v0.10 fail2ban supports the matching of the IPv6 addresses. <img src="http://www.worldipv6launch.org/wp-content/themes/ipv6/downloads/World_IPv6_launch_logo.svg" height="52pt"/> | Since v0.10 fail2ban supports the matching of the IPv6 addresses.
------|------ ------|------
This README is a quick introduction to Fail2ban. More documentation, FAQ, HOWTOs This README is a quick introduction to Fail2Ban. More documentation, FAQ, and HOWTOs
are available in fail2ban(1) manpage, [Wiki](https://github.com/fail2ban/fail2ban/wiki) to be found on fail2ban(1) manpage, [Wiki](https://github.com/fail2ban/fail2ban/wiki)
and on the website http://www.fail2ban.org and the website: https://www.fail2ban.org
Installation: Installation:
------------- -------------
**It is possible that Fail2ban is already packaged for your distribution. In **It is possible that Fail2Ban is already packaged for your distribution. In
this case, you should use it instead.** this case, you should use that instead.**
Required: Required:
- [Python2 >= 2.6 or Python >= 3.2](http://www.python.org) or [PyPy](http://pypy.org) - [Python2 >= 2.6 or Python >= 3.2](https://www.python.org) or [PyPy](https://pypy.org)
Optional: Optional:
- [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify) - [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify), may require:
- Linux >= 2.6.13 * Linux >= 2.6.13
- [gamin >= 0.0.21](http://www.gnome.org/~veillard/gamin) - [gamin >= 0.0.21](http://www.gnome.org/~veillard/gamin)
- [systemd >= 204](http://www.freedesktop.org/wiki/Software/systemd) and python bindings: - [systemd >= 204](http://www.freedesktop.org/wiki/Software/systemd) and python bindings:
- [python-systemd package](https://www.freedesktop.org/software/systemd/python-systemd/index.html) * [python-systemd package](https://www.freedesktop.org/software/systemd/python-systemd/index.html)
- [dnspython](http://www.dnspython.org/) - [dnspython](http://www.dnspython.org/)
To install, just do:
To install:
tar xvfj fail2ban-0.10.3.tar.bz2 tar xvfj fail2ban-0.10.3.tar.bz2
cd fail2ban-0.10.3 cd fail2ban-0.10.3
python setup.py install python setup.py install
This will install Fail2Ban into the python library directory. The executable This will install Fail2Ban into the python library directory. The executable
scripts are placed into `/usr/bin`, and configuration under `/etc/fail2ban`. scripts are placed into `/usr/bin`, and configuration in `/etc/fail2ban`.
Fail2Ban should be correctly installed now. Just type: Fail2Ban should be correctly installed now. Just type:
@ -91,7 +92,7 @@ Contact:
See [CONTRIBUTING.md](https://github.com/fail2ban/fail2ban/blob/master/CONTRIBUTING.md) See [CONTRIBUTING.md](https://github.com/fail2ban/fail2ban/blob/master/CONTRIBUTING.md)
### You just appreciate this program: ### You just appreciate this program:
send kudos to the original author ([Cyril Jaquier](mailto:cyril.jaquier@fail2ban.org)) Send kudos to the original author ([Cyril Jaquier](mailto:cyril.jaquier@fail2ban.org))
or *better* to the [mailing list](https://lists.sourceforge.net/lists/listinfo/fail2ban-users) or *better* to the [mailing list](https://lists.sourceforge.net/lists/listinfo/fail2ban-users)
since Fail2Ban is "community-driven" for years now. since Fail2Ban is "community-driven" for years now.

View File

@ -17,8 +17,13 @@ before = apache-common.conf
[Definition] [Definition]
failregex = ^%(_apache_error_client)s ((AH001(28|30): )?File does not exist|(AH01264: )?script not found or unable to stat): /\S*(php([45]|[.-]cgi)?|\.asp|\.exe|\.pl)(, referer: \S+)?\s*$ script = /\S*(?:php(?:[45]|[.-]cgi)?|\.asp|\.exe|\.pl)
^%(_apache_error_client)s script '/\S*(php([45]|[.-]cgi)?|\.asp|\.exe|\.pl)\S*' not found or unable to stat(, referer: \S+)?\s*$
prefregex = ^%(_apache_error_client)s (?:AH0(?:01(?:28|30)|1(?:264|071)): )?(?:(?:[Ff]ile|script|[Gg]ot) )<F-CONTENT>.+</F-CONTENT>$
failregex = ^(?:does not exist|not found or unable to stat): <script>\b
^'<script>\S*' not found or unable to stat
^error '[Pp]rimary script unknown\\n'
ignoreregex = ignoreregex =

View File

@ -207,12 +207,12 @@ class DateEpoch(DateTemplate):
def __init__(self, lineBeginOnly=False, pattern=None, longFrm=False): def __init__(self, lineBeginOnly=False, pattern=None, longFrm=False):
DateTemplate.__init__(self) DateTemplate.__init__(self)
self.name = "Epoch" self.name = "Epoch" if not pattern else pattern
self._longFrm = longFrm; self._longFrm = longFrm;
self._grpIdx = 1 self._grpIdx = 1
epochRE = r"\d{10,11}\b(?:\.\d{3,6})?" epochRE = r"\d{10,11}\b(?:\.\d{3,6})?"
if longFrm: if longFrm:
self.name = "LongEpoch"; self.name = "LongEpoch" if not pattern else pattern
epochRE = r"\d{10,11}(?:\d{3}(?:\.\d{1,6}|\d{3})?)?" epochRE = r"\d{10,11}(?:\d{3}(?:\.\d{1,6}|\d{3})?)?"
if pattern: if pattern:
# pattern should capture/cut out the whole match: # pattern should capture/cut out the whole match:

View File

@ -198,6 +198,13 @@ class Regex:
def getRegex(self): def getRegex(self):
return self._regex return self._regex
##
# Returns string buffer using join of the tupleLines.
#
@staticmethod
def _tupleLinesBuf(tupleLines):
return "\n".join(map(lambda v: "".join(v[::2]), tupleLines)) + "\n"
## ##
# Searches the regular expression. # Searches the regular expression.
# #
@ -207,8 +214,10 @@ class Regex:
# @param a list of tupples. The tupples are ( prematch, datematch, postdatematch ) # @param a list of tupples. The tupples are ( prematch, datematch, postdatematch )
def search(self, tupleLines, orgLines=None): def search(self, tupleLines, orgLines=None):
self._matchCache = self._regexObj.search( buf = tupleLines
"\n".join("".join(value[::2]) for value in tupleLines) + "\n") if not isinstance(tupleLines, basestring):
buf = Regex._tupleLinesBuf(tupleLines)
self._matchCache = self._regexObj.search(buf)
if self._matchCache: if self._matchCache:
if orgLines is None: orgLines = tupleLines if orgLines is None: orgLines = tupleLines
# if single-line: # if single-line:

View File

@ -582,8 +582,9 @@ class Filter(JailThread):
# @return: a boolean # @return: a boolean
def ignoreLine(self, tupleLines): def ignoreLine(self, tupleLines):
buf = Regex._tupleLinesBuf(tupleLines)
for ignoreRegexIndex, ignoreRegex in enumerate(self.__ignoreRegex): for ignoreRegexIndex, ignoreRegex in enumerate(self.__ignoreRegex):
ignoreRegex.search(tupleLines) ignoreRegex.search(buf, tupleLines)
if ignoreRegex.hasMatched(): if ignoreRegex.hasMatched():
return ignoreRegexIndex return ignoreRegexIndex
return None return None
@ -681,6 +682,7 @@ class Filter(JailThread):
def findFailure(self, tupleLine, date=None): def findFailure(self, tupleLine, date=None):
failList = list() failList = list()
ll = logSys.getEffectiveLevel()
returnRawHost = self.returnRawHost returnRawHost = self.returnRawHost
cidr = IPAddr.CIDR_UNSPEC cidr = IPAddr.CIDR_UNSPEC
if self.__useDns == "raw": if self.__useDns == "raw":
@ -690,7 +692,7 @@ class Filter(JailThread):
# Checks if we mut ignore this line. # Checks if we mut ignore this line.
if self.ignoreLine([tupleLine[::2]]) is not None: if self.ignoreLine([tupleLine[::2]]) is not None:
# The ignoreregex matched. Return. # The ignoreregex matched. Return.
logSys.log(7, "Matched ignoreregex and was \"%s\" ignored", if ll <= 7: logSys.log(7, "Matched ignoreregex and was \"%s\" ignored",
"".join(tupleLine[::2])) "".join(tupleLine[::2]))
return failList return failList
@ -717,7 +719,7 @@ class Filter(JailThread):
date = self.__lastDate date = self.__lastDate
if self.checkFindTime and date is not None and date < MyTime.time() - self.getFindTime(): if self.checkFindTime and date is not None and date < MyTime.time() - self.getFindTime():
logSys.log(5, "Ignore line since time %s < %s - %s", if ll <= 5: logSys.log(5, "Ignore line since time %s < %s - %s",
date, MyTime.time(), self.getFindTime()) date, MyTime.time(), self.getFindTime())
return failList return failList
@ -726,44 +728,45 @@ class Filter(JailThread):
self.__lineBuffer + [tupleLine[:3]])[-self.__lineBufferSize:] self.__lineBuffer + [tupleLine[:3]])[-self.__lineBufferSize:]
else: else:
orgBuffer = self.__lineBuffer = [tupleLine[:3]] orgBuffer = self.__lineBuffer = [tupleLine[:3]]
logSys.log(5, "Looking for match of %r", self.__lineBuffer) if ll <= 5: logSys.log(5, "Looking for match of %r", self.__lineBuffer)
buf = Regex._tupleLinesBuf(self.__lineBuffer)
# Pre-filter fail regex (if available): # Pre-filter fail regex (if available):
preGroups = {} preGroups = {}
if self.__prefRegex: if self.__prefRegex:
if logSys.getEffectiveLevel() <= logging.HEAVYDEBUG: # pragma: no cover if ll <= 5: logSys.log(5, " Looking for prefregex %r", self.__prefRegex.getRegex())
logSys.log(5, " Looking for prefregex %r", self.__prefRegex.getRegex()) self.__prefRegex.search(buf, self.__lineBuffer)
self.__prefRegex.search(self.__lineBuffer)
if not self.__prefRegex.hasMatched(): if not self.__prefRegex.hasMatched():
logSys.log(5, " Prefregex not matched") if ll <= 5: logSys.log(5, " Prefregex not matched")
return failList return failList
preGroups = self.__prefRegex.getGroups() preGroups = self.__prefRegex.getGroups()
logSys.log(7, " Pre-filter matched %s", preGroups) if ll <= 7: logSys.log(7, " Pre-filter matched %s", preGroups)
repl = preGroups.get('content') repl = preGroups.get('content')
# Content replacement: # Content replacement:
if repl: if repl:
del preGroups['content'] del preGroups['content']
self.__lineBuffer = [('', '', repl)] self.__lineBuffer, buf = [('', '', repl)], None
# Iterates over all the regular expressions. # Iterates over all the regular expressions.
for failRegexIndex, failRegex in enumerate(self.__failRegex): for failRegexIndex, failRegex in enumerate(self.__failRegex):
# retrieve failure-id, host, etc from failure match:
try: try:
if logSys.getEffectiveLevel() <= logging.HEAVYDEBUG: # pragma: no cover # buffer from tuples if changed:
logSys.log(5, " Looking for failregex %d - %r", failRegexIndex, failRegex.getRegex()) if buf is None:
failRegex.search(self.__lineBuffer, orgBuffer) buf = Regex._tupleLinesBuf(self.__lineBuffer)
if ll <= 5: logSys.log(5, " Looking for failregex %d - %r", failRegexIndex, failRegex.getRegex())
failRegex.search(buf, orgBuffer)
if not failRegex.hasMatched(): if not failRegex.hasMatched():
continue continue
# current failure data (matched group dict): # current failure data (matched group dict):
fail = failRegex.getGroups() fail = failRegex.getGroups()
# The failregex matched. # The failregex matched.
logSys.log(7, " Matched failregex %d: %s", failRegexIndex, fail) if ll <= 7: logSys.log(7, " Matched failregex %d: %s", failRegexIndex, fail)
# Checks if we must ignore this match. # Checks if we must ignore this match.
if self.ignoreLine(failRegex.getMatchedTupleLines()) \ if self.ignoreLine(failRegex.getMatchedTupleLines()) \
is not None: is not None:
# The ignoreregex matched. Remove ignored match. # The ignoreregex matched. Remove ignored match.
self.__lineBuffer = failRegex.getUnmatchedTupleLines() self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None
logSys.log(7, " Matched ignoreregex and was ignored") if ll <= 7: logSys.log(7, " Matched ignoreregex and was ignored")
if not self.checkAllRegex: if not self.checkAllRegex:
break break
else: else:
@ -781,7 +784,7 @@ class Filter(JailThread):
continue 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.getMaxLines() > 1: if not self.checkAllRegex or self.getMaxLines() > 1:
self.__lineBuffer = failRegex.getUnmatchedTupleLines() self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None
# merge data if multi-line failure: # merge data if multi-line failure:
raw = returnRawHost raw = returnRawHost
if preGroups: if preGroups:
@ -793,8 +796,7 @@ class Filter(JailThread):
fail = self._mergeFailure(mlfid, fail, failRegex) fail = self._mergeFailure(mlfid, fail, failRegex)
# bypass if no-failure case: # bypass if no-failure case:
if fail.get('nofail'): if fail.get('nofail'):
# if not users or len(users) <= 1: if ll <= 7: logSys.log(7, "Nofail by mlfid %r in regex %s: %s",
logSys.log(7, "Nofail by mlfid %r in regex %s: %s",
mlfid, failRegexIndex, fail.get('mlfforget', "waiting for failure")) mlfid, failRegexIndex, fail.get('mlfforget', "waiting for failure"))
if not self.checkAllRegex: return failList if not self.checkAllRegex: return failList
else: else:
@ -823,7 +825,7 @@ class Filter(JailThread):
cidr = IPAddr.CIDR_RAW cidr = IPAddr.CIDR_RAW
# if mlfid case (not failure): # if mlfid case (not failure):
if host is None: if host is None:
logSys.log(7, "No failure-id by mlfid %r in regex %s: %s", if ll <= 7: logSys.log(7, "No failure-id by mlfid %r in regex %s: %s",
mlfid, failRegexIndex, fail.get('mlfforget', "waiting for identifier")) mlfid, failRegexIndex, fail.get('mlfforget', "waiting for identifier"))
if not self.checkAllRegex: return failList if not self.checkAllRegex: return failList
ips = [None] ips = [None]

View File

@ -58,6 +58,7 @@ if sys.version_info >= (2,7): # pragma: no cover - may be unavailable
self.jail.actions.add("badips", pythonModuleName, initOpts={ self.jail.actions.add("badips", pythonModuleName, initOpts={
'category': "ssh", 'category': "ssh",
'banaction': "test", 'banaction': "test",
'age': "2w",
'score': 5, 'score': 5,
'key': "fail2ban-test-suite", 'key': "fail2ban-test-suite",
#'bankey': "fail2ban-test-suite", #'bankey': "fail2ban-test-suite",

View File

@ -16,3 +16,5 @@
# apache 2.4 # apache 2.4
# failJSON: { "time": "2013-12-23T07:49:01", "match": true , "host": "204.232.202.107" } # failJSON: { "time": "2013-12-23T07:49:01", "match": true , "host": "204.232.202.107" }
[Mon Dec 23 07:49:01.981912 2013] [:error] [pid 3790] [client 204.232.202.107:46301] script '/var/www/timthumb.php' not found or unable to stat [Mon Dec 23 07:49:01.981912 2013] [:error] [pid 3790] [client 204.232.202.107:46301] script '/var/www/timthumb.php' not found or unable to stat
# failJSON: { "time": "2018-03-11T08:56:20", "match": true , "host": "192.0.2.106", "desc": "php-fpm error" }
[Sun Mar 11 08:56:20.913548 2018] [proxy_fcgi:error] [pid 742:tid 140142593419008] [client 192.0.2.106:50900] AH01071: Got error 'Primary script unknown\n'

View File

@ -1145,6 +1145,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
super(MonitorJournalFailures, self).setUp() super(MonitorJournalFailures, self).setUp()
self._runtimeJournal = None
self.test_file = os.path.join(TEST_FILES_DIR, "testcase-journal.log") self.test_file = os.path.join(TEST_FILES_DIR, "testcase-journal.log")
self.jail = DummyJail() self.jail = DummyJail()
self.filter = None self.filter = None
@ -1156,6 +1157,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid} 'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid}
def _initFilter(self, **kwargs): def _initFilter(self, **kwargs):
self._getRuntimeJournal() # check journal available
self.filter = Filter_(self.jail, **kwargs) self.filter = Filter_(self.jail, **kwargs)
self.filter.addJournalMatch([ self.filter.addJournalMatch([
"SYSLOG_IDENTIFIER=fail2ban-testcases", "SYSLOG_IDENTIFIER=fail2ban-testcases",
@ -1176,21 +1178,26 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
def _getRuntimeJournal(self): def _getRuntimeJournal(self):
"""Retrieve current system journal path """Retrieve current system journal path
If none found, None will be returned If not found, SkipTest exception will be raised.
""" """
# Depending on the system, it could be found under /run or /var/log (e.g. Debian) # we can cache it:
# which are pointed by different systemd-path variables. We will if self._runtimeJournal is None:
# check one at at time until the first hit # Depending on the system, it could be found under /run or /var/log (e.g. Debian)
for systemd_var in 'system-runtime-logs', 'system-state-logs': # which are pointed by different systemd-path variables. We will
tmp = Utils.executeCmd( # check one at at time until the first hit
'find "$(systemd-path %s)" -name system.journal' % systemd_var, for systemd_var in 'system-runtime-logs', 'system-state-logs':
timeout=10, shell=True, output=True tmp = Utils.executeCmd(
) 'find "$(systemd-path %s)" -name system.journal' % systemd_var,
self.assertTrue(tmp) timeout=10, shell=True, output=True
out = str(tmp[1].decode('utf-8')).split('\n')[0] )
if out: self.assertTrue(tmp)
return out out = str(tmp[1].decode('utf-8')).split('\n')[0]
if out: break
self._runtimeJournal = out
if self._runtimeJournal:
return self._runtimeJournal
raise unittest.SkipTest('systemd journal seems to be not available (e. g. no rights to read)')
def testJournalFilesArg(self): def testJournalFilesArg(self):
# retrieve current system journal path # retrieve current system journal path
jrnlfile = self._getRuntimeJournal() jrnlfile = self._getRuntimeJournal()

View File

@ -680,7 +680,7 @@ class LogCaptureTestCase(unittest.TestCase):
return self._val return self._val
# try to lock, if not possible - return cached/empty (max 5 times): # try to lock, if not possible - return cached/empty (max 5 times):
lck = self._lock.acquire(False) lck = self._lock.acquire(False)
if not lck: # pargma: no cover (may be too sporadic on slow systems) if not lck: # pragma: no cover (may be too sporadic on slow systems)
self._nolckCntr += 1 self._nolckCntr += 1
if self._nolckCntr <= 5: if self._nolckCntr <= 5:
return self._val if self._val is not None else '' return self._val if self._val is not None else ''