mirror of
https://github.com/fail2ban/fail2ban.git
synced 2026-03-11 08:55:31 +00:00
This allows direct setting of action properties and calling of methods from the fail2ban-client if so required.
281 lines
9.4 KiB
Python
281 lines
9.4 KiB
Python
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
|
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
|
|
|
# This file is part of Fail2Ban.
|
|
#
|
|
# Fail2Ban is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Fail2Ban is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Fail2Ban; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
|
__author__ = "Yaroslav Halchenko"
|
|
__copyright__ = "Copyright (c) 2013 Yaroslav Halchenko"
|
|
__license__ = "GPL"
|
|
|
|
import logging, os, re, traceback, time, unittest, sys
|
|
from os.path import basename, dirname
|
|
from StringIO import StringIO
|
|
|
|
if sys.version_info >= (2, 6):
|
|
import json
|
|
else:
|
|
try:
|
|
import simplejson as json
|
|
except ImportError:
|
|
json = None
|
|
|
|
from fail2ban.server.mytime import MyTime
|
|
|
|
logSys = logging.getLogger(__name__)
|
|
|
|
#
|
|
# Following "traceback" functions are adopted from PyMVPA distributed
|
|
# under MIT/Expat and copyright by PyMVPA developers (i.e. me and
|
|
# Michael). Hereby I re-license derivative work on these pieces under GPL
|
|
# to stay in line with the main Fail2Ban license
|
|
#
|
|
def mbasename(s):
|
|
"""Custom function to include directory name if filename is too common
|
|
|
|
Also strip .py at the end
|
|
"""
|
|
base = basename(s)
|
|
if base.endswith('.py'):
|
|
base = base[:-3]
|
|
if base in set(['base', '__init__']):
|
|
base = basename(dirname(s)) + '.' + base
|
|
return base
|
|
|
|
class TraceBack(object):
|
|
"""Customized traceback to be included in debug messages
|
|
"""
|
|
|
|
def __init__(self, compress=False):
|
|
"""Initialize TrackBack metric
|
|
|
|
Parameters
|
|
----------
|
|
compress : bool
|
|
if True then prefix common with previous invocation gets
|
|
replaced with ...
|
|
"""
|
|
self.__prev = ""
|
|
self.__compress = compress
|
|
|
|
def __call__(self):
|
|
ftb = traceback.extract_stack(limit=100)[:-2]
|
|
entries = [[mbasename(x[0]), dirname(x[0]), str(x[1])] for x in ftb]
|
|
entries = [ [e[0], e[2]] for e in entries
|
|
if not (e[0] in ['unittest', 'logging.__init__']
|
|
or e[1].endswith('/unittest'))]
|
|
|
|
# lets make it more concise
|
|
entries_out = [entries[0]]
|
|
for entry in entries[1:]:
|
|
if entry[0] == entries_out[-1][0]:
|
|
entries_out[-1][1] += ',%s' % entry[1]
|
|
else:
|
|
entries_out.append(entry)
|
|
sftb = '>'.join(['%s:%s' % (mbasename(x[0]),
|
|
x[1]) for x in entries_out])
|
|
if self.__compress:
|
|
# lets remove part which is common with previous invocation
|
|
prev_next = sftb
|
|
common_prefix = os.path.commonprefix((self.__prev, sftb))
|
|
common_prefix2 = re.sub('>[^>]*$', '', common_prefix)
|
|
|
|
if common_prefix2 != "":
|
|
sftb = '...' + sftb[len(common_prefix2):]
|
|
self.__prev = prev_next
|
|
|
|
return sftb
|
|
|
|
class FormatterWithTraceBack(logging.Formatter):
|
|
"""Custom formatter which expands %(tb) and %(tbc) with tracebacks
|
|
|
|
TODO: might need locking in case of compressed tracebacks
|
|
"""
|
|
def __init__(self, fmt, *args, **kwargs):
|
|
logging.Formatter.__init__(self, fmt=fmt, *args, **kwargs)
|
|
compress = '%(tbc)s' in fmt
|
|
self._tb = TraceBack(compress=compress)
|
|
|
|
def format(self, record):
|
|
record.tbc = record.tb = self._tb()
|
|
return logging.Formatter.format(self, record)
|
|
|
|
def mtimesleep():
|
|
# no sleep now should be necessary since polling tracks now not only
|
|
# mtime but also ino and size
|
|
pass
|
|
|
|
old_TZ = os.environ.get('TZ', None)
|
|
def setUpMyTime():
|
|
# Set the time to a fixed, known value
|
|
# Sun Aug 14 12:00:00 CEST 2005
|
|
# yoh: we need to adjust TZ to match the one used by Cyril so all the timestamps match
|
|
os.environ['TZ'] = 'Europe/Zurich'
|
|
time.tzset()
|
|
MyTime.setTime(1124013600)
|
|
|
|
def tearDownMyTime():
|
|
os.environ.pop('TZ')
|
|
if old_TZ:
|
|
os.environ['TZ'] = old_TZ
|
|
time.tzset()
|
|
MyTime.myTime = None
|
|
|
|
def gatherTests(regexps=None, no_network=False):
|
|
# Import all the test cases here instead of a module level to
|
|
# avoid circular imports
|
|
from fail2ban.tests import banmanagertestcase
|
|
from fail2ban.tests import clientreadertestcase
|
|
from fail2ban.tests import failmanagertestcase
|
|
from fail2ban.tests import filtertestcase
|
|
from fail2ban.tests import servertestcase
|
|
from fail2ban.tests import datedetectortestcase
|
|
from fail2ban.tests import actiontestcase
|
|
from fail2ban.tests import actionstestcase
|
|
from fail2ban.tests import sockettestcase
|
|
from fail2ban.tests import misctestcase
|
|
from fail2ban.tests import databasetestcase
|
|
if json:
|
|
from fail2ban.tests import samplestestcase
|
|
|
|
if not regexps: # pragma: no cover
|
|
tests = unittest.TestSuite()
|
|
else: # pragma: no cover
|
|
import re
|
|
class FilteredTestSuite(unittest.TestSuite):
|
|
_regexps = [re.compile(r) for r in regexps]
|
|
def addTest(self, suite):
|
|
suite_str = str(suite)
|
|
for r in self._regexps:
|
|
if r.search(suite_str):
|
|
super(FilteredTestSuite, self).addTest(suite)
|
|
return
|
|
|
|
tests = FilteredTestSuite()
|
|
|
|
# Server
|
|
#tests.addTest(unittest.makeSuite(servertestcase.StartStop))
|
|
tests.addTest(unittest.makeSuite(servertestcase.Transmitter))
|
|
tests.addTest(unittest.makeSuite(servertestcase.JailTests))
|
|
tests.addTest(unittest.makeSuite(servertestcase.RegexTests))
|
|
tests.addTest(unittest.makeSuite(actiontestcase.CommandActionTest))
|
|
tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions))
|
|
# FailManager
|
|
tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure))
|
|
# BanManager
|
|
tests.addTest(unittest.makeSuite(banmanagertestcase.AddFailure))
|
|
# ClientReaders
|
|
tests.addTest(unittest.makeSuite(clientreadertestcase.ConfigReaderTest))
|
|
tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest))
|
|
tests.addTest(unittest.makeSuite(clientreadertestcase.FilterReaderTest))
|
|
tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTest))
|
|
# CSocket and AsyncServer
|
|
tests.addTest(unittest.makeSuite(sockettestcase.Socket))
|
|
# Misc helpers
|
|
tests.addTest(unittest.makeSuite(misctestcase.HelpersTest))
|
|
tests.addTest(unittest.makeSuite(misctestcase.SetupTest))
|
|
tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest))
|
|
tests.addTest(unittest.makeSuite(misctestcase.CustomDateFormatsTest))
|
|
# Database
|
|
tests.addTest(unittest.makeSuite(databasetestcase.DatabaseTest))
|
|
|
|
# Filter
|
|
tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIP))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.BasicFilter))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.LogFile))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.LogFileFilterPoll))
|
|
if not no_network:
|
|
tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIPDNS))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.GetFailures))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests))
|
|
tests.addTest(unittest.makeSuite(filtertestcase.JailTests))
|
|
|
|
# DateDetector
|
|
tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest))
|
|
if json:
|
|
# Filter Regex tests with sample logs
|
|
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex))
|
|
else:
|
|
logSys.warning("I: Skipping filter samples testing. No simplejson/json module")
|
|
|
|
#
|
|
# Extensive use-tests of different available filters backends
|
|
#
|
|
|
|
from fail2ban.server.filterpoll import FilterPoll
|
|
filters = [FilterPoll] # always available
|
|
|
|
# Additional filters available only if external modules are available
|
|
# yoh: Since I do not know better way for parametric tests
|
|
# with good old unittest
|
|
try:
|
|
from fail2ban.server.filtergamin import FilterGamin
|
|
filters.append(FilterGamin)
|
|
except Exception, e: # pragma: no cover
|
|
logSys.warning("Skipping gamin backend testing. Got exception '%s'" % e)
|
|
|
|
try:
|
|
from fail2ban.server.filterpyinotify import FilterPyinotify
|
|
filters.append(FilterPyinotify)
|
|
except Exception, e: # pragma: no cover
|
|
logSys.warning("I: Skipping pyinotify backend testing. Got exception '%s'" % e)
|
|
|
|
for Filter_ in filters:
|
|
tests.addTest(unittest.makeSuite(
|
|
filtertestcase.get_monitor_failures_testcase(Filter_)))
|
|
try: # pragma: systemd no cover
|
|
from fail2ban.server.filtersystemd import FilterSystemd
|
|
tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd)))
|
|
except Exception, e: # pragma: no cover
|
|
logSys.warning("I: Skipping systemd backend testing. Got exception '%s'" % e)
|
|
|
|
|
|
# Server test for logging elements which break logging used to support
|
|
# testcases analysis
|
|
tests.addTest(unittest.makeSuite(servertestcase.TransmitterLogging))
|
|
|
|
return tests
|
|
|
|
class LogCaptureTestCase(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
|
|
# For extended testing of what gets output into logging
|
|
# system, we will redirect it to a string
|
|
logSys = logging.getLogger("fail2ban")
|
|
|
|
# Keep old settings
|
|
self._old_level = logSys.level
|
|
self._old_handlers = logSys.handlers
|
|
# Let's log everything into a string
|
|
self._log = StringIO()
|
|
logSys.handlers = [logging.StreamHandler(self._log)]
|
|
logSys.setLevel(getattr(logging, 'DEBUG'))
|
|
|
|
def tearDown(self):
|
|
"""Call after every test case."""
|
|
# print "O: >>%s<<" % self._log.getvalue()
|
|
logSys = logging.getLogger("fail2ban")
|
|
logSys.handlers = self._old_handlers
|
|
logSys.level = self._old_level
|
|
|
|
def _is_logged(self, s):
|
|
return s in self._log.getvalue()
|
|
|
|
def printLog(self):
|
|
print(self._log.getvalue())
|