Merge pull request #1669 from sebres/0.10-recognize-restored-tickets

Recognize state of restored tickets
pull/1597/merge
Serg G. Brester 8 years ago committed by GitHub
commit 5bfdd521f0

@ -165,6 +165,31 @@ fail2ban-client set loglevel INFO
- faster match and fewer searching of appropriate templates - faster match and fewer searching of appropriate templates
(DateDetector.matchTime calls rarer DateTemplate.matchDate now); (DateDetector.matchTime calls rarer DateTemplate.matchDate now);
- several standard filters extended with exact prefixed or anchored date templates; - several standard filters extended with exact prefixed or anchored date templates;
* Added possibility to recognize restored state of the tickets (see gh-1669).
New option `norestored` introduced, to ignore restored tickets (after restart).
To avoid execution of ban/unban for the restored tickets, `norestored = true`
could be added in definition section of action.
For conditional usage in the shell-based actions an interpolation `<restored>`
could be used also. E. g. it is enough to add following script-piece at begin
of `actionban` (or `actionunban`) to prevent execution:
`if [ '<restored>' = '1' ]; then exit 0; fi;`
Several actions extended now using `norestored` option:
- complain.conf
- dshield.conf
- mail-buffered.conf
- mail-whois-lines.conf
- mail-whois.conf
- mail.conf
- sendmail-buffered.conf
- sendmail-geoip-lines.conf
- sendmail-whois-ipjailmatches.conf
- sendmail-whois-ipmatches.conf
- sendmail-whois-lines.conf
- sendmail-whois-matches.conf
- sendmail-whois.conf
- sendmail.conf
- smtp.py
- xarf-login-attack.conf
* fail2ban-testcases: * fail2ban-testcases:
- `assertLogged` extended with parameter wait (to wait up to specified timeout, - `assertLogged` extended with parameter wait (to wait up to specified timeout,
before we throw assert exception) + test cases rewritten using that before we throw assert exception) + test cases rewritten using that

@ -34,6 +34,9 @@ before = helpers-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD

@ -28,6 +28,9 @@
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD

@ -7,6 +7,9 @@
_grep_logs = logpath="<logpath>"; grep <grepopts> -E %(_grep_logs_args)s $logpath | <greplimit> _grep_logs = logpath="<logpath>"; grep <grepopts> -E %(_grep_logs_args)s $logpath | <greplimit>
_grep_logs_args = '(^|[^0-9])<ip>([^0-9]|$)' _grep_logs_args = '(^|[^0-9])<ip>([^0-9]|$)'
# Used for actions, that should not by executed if ticket was restored:
_bypass_if_restored = if [ '<restored>' = '1' ]; then exit 0; fi;
[Init] [Init]
greplimit = tail -n <grepmax> greplimit = tail -n <grepmax>
grepmax = 1000 grepmax = 1000

@ -6,6 +6,9 @@
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD

@ -11,6 +11,9 @@ before = mail-whois-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
@ -52,6 +55,7 @@ _ban_mail_content = ( printf %%b "Hi,\n
printf %%b "\n printf %%b "\n
Regards,\n Regards,\n
Fail2Ban" ) Fail2Ban" )
actionban = %(_ban_mail_content)s | <mailcmd> "[Fail2Ban] <name>: banned <ip> from `uname -n`" <dest> actionban = %(_ban_mail_content)s | <mailcmd> "[Fail2Ban] <name>: banned <ip> from `uname -n`" <dest>
# Option: actionunban # Option: actionunban

@ -10,6 +10,9 @@ before = mail-whois-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD

@ -6,6 +6,9 @@
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD

@ -10,6 +10,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionstart # Option: actionstart
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD

@ -11,6 +11,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: Command executed when banning an IP. Take care that the # Notes.: Command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -10,6 +10,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -10,6 +10,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -11,6 +11,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -10,6 +10,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -10,6 +10,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -10,6 +10,9 @@ before = sendmail-common.conf
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.

@ -126,6 +126,9 @@ class SMTPAction(ActionBase):
bantime = self._jail.actions.getBanTime, bantime = self._jail.actions.getBanTime,
) )
# bypass ban/unban for restored tickets
self.norestored = 1
def _sendMessage(self, subject, text): def _sendMessage(self, subject, text):
"""Sends message based on arguments and instance's properties. """Sends message based on arguments and instance's properties.
@ -211,6 +214,8 @@ class SMTPAction(ActionBase):
Dictionary which includes information in relation to Dictionary which includes information in relation to
the ban. the ban.
""" """
if aInfo.get('restored'):
return
aInfo.update(self.message_values) aInfo.update(self.message_values)
message = "".join([ message = "".join([
messages['ban']['head'], messages['ban']['head'],

@ -32,6 +32,9 @@
[Definition] [Definition]
# bypass ban/unban for restored tickets
norestored = 1
actionstart = actionstart =
actionstop = actionstop =

@ -43,10 +43,15 @@ class ActionReader(DefinitionInitConfigReader):
"actionrepair": ["string", None], "actionrepair": ["string", None],
"actionban": ["string", None], "actionban": ["string", None],
"actionunban": ["string", None], "actionunban": ["string", None],
"norestored": ["string", None],
} }
def __init__(self, file_, jailName, initOpts, **kwargs): def __init__(self, file_, jailName, initOpts, **kwargs):
self._name = initOpts.get("actname", file_) actname = initOpts.get("actname")
if actname is None:
actname = file_
initOpts["actname"] = actname
self._name = actname
DefinitionInitConfigReader.__init__( DefinitionInitConfigReader.__init__(
self, file_, jailName, initOpts, **kwargs) self, file_, jailName, initOpts, **kwargs)
@ -64,16 +69,22 @@ class ActionReader(DefinitionInitConfigReader):
return self._name return self._name
def convert(self): def convert(self):
opts = self.getCombined(ignore=('timeout', 'bantime'))
# type-convert only after combined (otherwise boolean converting prevents substitution):
if opts.get('norestored'):
opts['norestored'] = self._convert_to_boolean(opts['norestored'])
# stream-convert:
head = ["set", self._jailName] head = ["set", self._jailName]
stream = list() stream = list()
stream.append(head + ["addaction", self._name]) stream.append(head + ["addaction", self._name])
multi = [] multi = []
for opt, optval in self._opts.iteritems(): for opt, optval in opts.iteritems():
if opt in self._configOpts: if opt in self._configOpts:
multi.append([opt, optval]) multi.append([opt, optval])
if self._initOpts: if self._initOpts:
for opt, optval in self._initOpts.iteritems(): for opt, optval in self._initOpts.iteritems():
multi.append([opt, optval]) if opt not in self._configOpts:
multi.append([opt, optval])
if len(multi) > 1: if len(multi) > 1:
stream.append(["multi-set", self._jailName, "action", self._name, multi]) stream.append(["multi-set", self._jailName, "action", self._name, multi])
elif len(multi): elif len(multi):

@ -30,6 +30,7 @@ from ConfigParser import NoOptionError, NoSectionError
from .configparserinc import sys, SafeConfigParserWithIncludes, logLevel from .configparserinc import sys, SafeConfigParserWithIncludes, logLevel
from ..helpers import getLogger from ..helpers import getLogger
from ..server.action import CommandAction
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -319,6 +320,28 @@ class DefinitionInitConfigReader(ConfigReader):
self._initOpts['known/'+opt] = v self._initOpts['known/'+opt] = v
if not opt in self._initOpts: if not opt in self._initOpts:
self._initOpts[opt] = v self._initOpts[opt] = v
def _convert_to_boolean(self, value):
return value.lower() in ("1", "yes", "true", "on")
def getCombined(self, ignore=()):
combinedopts = self._opts
ignore = set(ignore).copy()
if self._initOpts:
combinedopts = _merge_dicts(self._opts, self._initOpts)
if not len(combinedopts):
return {}
# ignore conditional options:
for n in combinedopts:
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n)
if cond:
n, cond = cond.groups()
ignore.add(n)
# substiture options already specified direct:
opts = CommandAction.substituteRecursiveTags(combinedopts, ignore=ignore)
if not opts:
raise ValueError('recursive tag definitions unable to be resolved')
return opts
def convert(self): def convert(self):
raise NotImplementedError raise NotImplementedError

@ -27,8 +27,7 @@ __license__ = "GPL"
import os import os
import shlex import shlex
from .configreader import DefinitionInitConfigReader, _merge_dicts from .configreader import DefinitionInitConfigReader
from ..server.action import CommandAction
from ..helpers import getLogger from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
@ -52,17 +51,6 @@ class FilterReader(DefinitionInitConfigReader):
def getFile(self): def getFile(self):
return self.__file return self.__file
def getCombined(self):
combinedopts = self._opts
if self._initOpts:
combinedopts = _merge_dicts(self._opts, self._initOpts)
if not len(combinedopts):
return {}
opts = CommandAction.substituteRecursiveTags(combinedopts)
if not opts:
raise ValueError('recursive tag definitions unable to be resolved')
return opts
def convert(self): def convert(self):
stream = list() stream = list()
opts = self.getCombined() opts = self.getCombined()
@ -70,6 +58,7 @@ class FilterReader(DefinitionInitConfigReader):
return stream return stream
for opt, value in opts.iteritems(): for opt, value in opts.iteritems():
if opt in ("failregex", "ignoreregex"): if opt in ("failregex", "ignoreregex"):
if value is None: continue
multi = [] multi = []
for regex in value.split('\n'): for regex in value.split('\n'):
# Do not send a command if the rule is empty. # Do not send a command if the rule is empty.
@ -87,6 +76,7 @@ class FilterReader(DefinitionInitConfigReader):
stream.append(["set", self._jailName, "datepattern", value]) stream.append(["set", self._jailName, "datepattern", value])
# Do not send a command if the match is empty. # Do not send a command if the match is empty.
elif opt == 'journalmatch': elif opt == 'journalmatch':
if value is None: continue
for match in value.split("\n"): for match in value.split("\n"):
if match == '': continue if match == '': continue
stream.append( stream.append(

@ -136,7 +136,8 @@ class JailReader(ConfigReader):
if not filterName: if not filterName:
raise JailDefError("Invalid filter definition %r" % flt) raise JailDefError("Invalid filter definition %r" % flt)
self.__filter = FilterReader( self.__filter = FilterReader(
filterName, self.__name, filterOpt, share_config=self.share_config, basedir=self.getBaseDir()) filterName, self.__name, filterOpt,
share_config=self.share_config, basedir=self.getBaseDir())
ret = self.__filter.read() ret = self.__filter.read()
# merge options from filter as 'known/...': # merge options from filter as 'known/...':
self.__filter.getOptions(self.__opts) self.__filter.getOptions(self.__opts)

@ -219,9 +219,9 @@ class CommandAction(ActionBase):
self.timeout = 60 self.timeout = 60
## Command executed in order to initialize the system. ## Command executed in order to initialize the system.
self.actionstart = '' self.actionstart = ''
## Command executed when an IP address gets banned. ## Command executed when ticket gets banned.
self.actionban = '' self.actionban = ''
## Command executed when an IP address gets removed. ## Command executed when ticket gets removed.
self.actionunban = '' self.actionunban = ''
## Command executed in order to check requirements. ## Command executed in order to check requirements.
self.actioncheck = '' self.actioncheck = ''
@ -365,7 +365,7 @@ class CommandAction(ActionBase):
return self._executeOperation('<actionreload>', 'reloading') return self._executeOperation('<actionreload>', 'reloading')
@classmethod @classmethod
def substituteRecursiveTags(cls, inptags, conditional=''): def substituteRecursiveTags(cls, inptags, conditional='', ignore=()):
"""Sort out tag definitions within other tags. """Sort out tag definitions within other tags.
Since v.0.9.2 supports embedded interpolation (see test cases for examples). Since v.0.9.2 supports embedded interpolation (see test cases for examples).
@ -387,21 +387,26 @@ class CommandAction(ActionBase):
# copy return tags dict to prevent modifying of inptags: # copy return tags dict to prevent modifying of inptags:
tags = inptags.copy() tags = inptags.copy()
t = TAG_CRE t = TAG_CRE
ignore = set(ignore)
done = cls._escapedTags.copy() | ignore
# repeat substitution while embedded-recursive (repFlag is True) # repeat substitution while embedded-recursive (repFlag is True)
done = cls._escapedTags.copy()
while True: while True:
repFlag = False repFlag = False
# substitute each value: # substitute each value:
for tag in tags.iterkeys(): for tag in tags.iterkeys():
# ignore escaped or already done: # ignore escaped or already done (or in ignore list):
if tag in done: continue if tag in done: continue
value = str(tags[tag]) value = orgval = str(tags[tag])
# search and replace all tags within value, that can be interpolated using other tags: # search and replace all tags within value, that can be interpolated using other tags:
m = t.search(value) m = t.search(value)
refCounts = {} refCounts = {}
#logSys.log(5, 'TAG: %s, value: %s' % (tag, value)) #logSys.log(5, 'TAG: %s, value: %s' % (tag, value))
while m: while m:
found_tag = m.group(1) found_tag = m.group(1)
# don't replace tags that should be currently ignored (pre-replacement):
if found_tag in ignore:
m = t.search(value, m.end())
continue
#logSys.log(5, 'found: %s' % found_tag) #logSys.log(5, 'found: %s' % found_tag)
if found_tag == tag or refCounts.get(found_tag, 1) > MAX_TAG_REPLACE_COUNT: if found_tag == tag or refCounts.get(found_tag, 1) > MAX_TAG_REPLACE_COUNT:
# recursive definitions are bad # recursive definitions are bad
@ -429,7 +434,7 @@ class CommandAction(ActionBase):
m = t.search(value, m.start()) m = t.search(value, m.start())
#logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value)) #logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value))
# was substituted? # was substituted?
if tags[tag] != value: if orgval != value:
# check still contains any tag - should be repeated (possible embedded-recursive substitution): # check still contains any tag - should be repeated (possible embedded-recursive substitution):
if t.search(value): if t.search(value):
repFlag = True repFlag = True

@ -348,6 +348,8 @@ class Actions(JailThread, Mapping):
aInfo["failures"] = bTicket.getAttempt() aInfo["failures"] = bTicket.getAttempt()
aInfo["time"] = bTicket.getTime() aInfo["time"] = bTicket.getTime()
aInfo["matches"] = "\n".join(bTicket.getMatches()) aInfo["matches"] = "\n".join(bTicket.getMatches())
# to bypass actions, that should not be executed for restored tickets
aInfo["restored"] = 1 if ticket.restored else 0
if self._jail.database is not None: if self._jail.database is not None:
mi4ip = lambda overalljails=False, self=self, \ mi4ip = lambda overalljails=False, self=self, \
mi={'ip':ip, 'ticket':bTicket}: self.__getBansMerged(mi, overalljails) mi={'ip':ip, 'ticket':bTicket}: self.__getBansMerged(mi, overalljails)
@ -361,6 +363,8 @@ class Actions(JailThread, Mapping):
logSys.notice("[%s] %sBan %s", self._jail.name, ('' if not bTicket.restored else 'Restore '), ip) logSys.notice("[%s] %sBan %s", self._jail.name, ('' if not bTicket.restored else 'Restore '), ip)
for name, action in self._actions.iteritems(): for name, action in self._actions.iteritems():
try: try:
if ticket.restored and getattr(action, 'norestored', False):
continue
action.ban(aInfo.copy()) action.ban(aInfo.copy())
except Exception as e: except Exception as e:
logSys.error( logSys.error(
@ -449,10 +453,14 @@ class Actions(JailThread, Mapping):
aInfo["failures"] = ticket.getAttempt() aInfo["failures"] = ticket.getAttempt()
aInfo["time"] = ticket.getTime() aInfo["time"] = ticket.getTime()
aInfo["matches"] = "".join(ticket.getMatches()) aInfo["matches"] = "".join(ticket.getMatches())
# to bypass actions, that should not be executed for restored tickets
aInfo["restored"] = 1 if ticket.restored else 0
if actions is None: if actions is None:
logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"]) logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"])
for name, action in unbactions.iteritems(): for name, action in unbactions.iteritems():
try: try:
if ticket.restored and getattr(action, 'norestored', False):
continue
logSys.debug("[%s] action %r: unban %s", self._jail.name, name, aInfo["ip"]) logSys.debug("[%s] action %r: unban %s", self._jail.name, name, aInfo["ip"])
action.unban(aInfo.copy()) action.unban(aInfo.copy())
except Exception as e: except Exception as e:

@ -94,16 +94,21 @@ class SMTPActionTest(unittest.TestCase):
"Subject: [Fail2Ban] %s: stopped" % "Subject: [Fail2Ban] %s: stopped" %
self.jail.name in self.smtpd.data) self.jail.name in self.smtpd.data)
def testBan(self): def _testBan(self, restored=False):
aInfo = { aInfo = {
'ip': "127.0.0.2", 'ip': "127.0.0.2",
'failures': 3, 'failures': 3,
'matches': "Test fail 1\n", 'matches': "Test fail 1\n",
'ipjailmatches': "Test fail 1\nTest Fail2\n", 'ipjailmatches': "Test fail 1\nTest Fail2\n",
'ipmatches': "Test fail 1\nTest Fail2\nTest Fail3\n", 'ipmatches': "Test fail 1\nTest Fail2\nTest Fail3\n",
} }
if restored:
aInfo['restored'] = 1
self.action.ban(aInfo) self.action.ban(aInfo)
if restored: # no mail, should raises attribute error:
self.assertRaises(AttributeError, lambda: self.smtpd.mailfrom)
return
self.assertEqual(self.smtpd.mailfrom, "fail2ban") self.assertEqual(self.smtpd.mailfrom, "fail2ban")
self.assertEqual(self.smtpd.rcpttos, ["root"]) self.assertEqual(self.smtpd.rcpttos, ["root"])
subject = "Subject: [Fail2Ban] %s: banned %s" % ( subject = "Subject: [Fail2Ban] %s: banned %s" % (
@ -123,6 +128,12 @@ class SMTPActionTest(unittest.TestCase):
self.action.matches = "ipmatches" self.action.matches = "ipmatches"
self.action.ban(aInfo) self.action.ban(aInfo)
self.assertIn(aInfo['ipmatches'], self.smtpd.data) self.assertIn(aInfo['ipmatches'], self.smtpd.data)
def testBan(self):
self._testBan()
def testNOPByRestored(self):
self._testBan(restored=True)
def testOptions(self): def testOptions(self):
self.action.start() self.action.start()

@ -347,7 +347,7 @@ class FilterReaderTest(unittest.TestCase):
['set', 'testcase01', 'addjournalmatch', ['set', 'testcase01', 'addjournalmatch',
"FIELD= with spaces ", "+", "AFIELD= with + char and spaces"], "FIELD= with spaces ", "+", "AFIELD= with + char and spaces"],
['set', 'testcase01', 'datepattern', "%Y %m %d %H:%M:%S"], ['set', 'testcase01', 'datepattern', "%Y %m %d %H:%M:%S"],
['set', 'testcase01', 'maxlines', "1"], # Last for overide test ['set', 'testcase01', 'maxlines', 1], # Last for overide test
] ]
filterReader = FilterReader("testcase01", "testcase01", {}) filterReader = FilterReader("testcase01", "testcase01", {})
filterReader.setBaseDir(TEST_FILES_DIR) filterReader.setBaseDir(TEST_FILES_DIR)
@ -517,12 +517,10 @@ class JailsReaderTest(LogCaptureTestCase):
['add', 'brokenaction', 'auto'], ['add', 'brokenaction', 'auto'],
['set', 'brokenaction', 'addfailregex', '<IP>'], ['set', 'brokenaction', 'addfailregex', '<IP>'],
['set', 'brokenaction', 'addaction', 'brokenaction'], ['set', 'brokenaction', 'addaction', 'brokenaction'],
['set', ['multi-set', 'brokenaction', 'action', 'brokenaction', [
'brokenaction', ['actionban', 'hit with big stick <ip>'],
'action', ['actname', 'brokenaction']
'brokenaction', ]],
'actionban',
'hit with big stick <ip>'],
['add', 'parse_to_end_of_jail.conf', 'auto'], ['add', 'parse_to_end_of_jail.conf', 'auto'],
['set', 'parse_to_end_of_jail.conf', 'addfailregex', '<IP>'], ['set', 'parse_to_end_of_jail.conf', 'addfailregex', '<IP>'],
['start', 'emptyaction'], ['start', 'emptyaction'],
@ -548,7 +546,10 @@ class JailsReaderTest(LogCaptureTestCase):
actionName = os.path.basename(actionConfig).replace('.conf', '') actionName = os.path.basename(actionConfig).replace('.conf', '')
actionReader = ActionReader(actionName, "TEST", {}, basedir=CONFIG_DIR) actionReader = ActionReader(actionName, "TEST", {}, basedir=CONFIG_DIR)
self.assertTrue(actionReader.read()) self.assertTrue(actionReader.read())
actionReader.getOptions({}) # populate _opts try:
actionReader.getOptions({}) # populate _opts
except Exception as e: # pragma: no cover
self.fail("action %r\n%s: %s" % (actionName, type(e).__name__, e))
if not actionName.endswith('-common'): if not actionName.endswith('-common'):
self.assertIn('Definition', actionReader.sections(), self.assertIn('Definition', actionReader.sections(),
msg="Action file %r is lacking [Definition] section" % actionConfig) msg="Action file %r is lacking [Definition] section" % actionConfig)

@ -755,12 +755,17 @@ class Fail2banServerTest(Fail2banClientServerBase):
os.remove(fn) os.remove(fn)
return return
_write_file(fn, "w", _write_file(fn, "w",
"[DEFAULT]",
"_exec_once = 0",
"",
"[Definition]", "[Definition]",
"actionstart = echo '[<name>] %s: ** start'" % actname, start, "norestored = %(_exec_once)s",
"actionreload = echo '[<name>] %s: .. reload'" % actname, reload, "restore = ",
"actionban = echo '[<name>] %s: ++ ban <ip>'" % actname, ban, "actionstart = echo '[%(name)s] %(actname)s: ** start'", start,
"actionunban = echo '[<name>] %s: -- unban <ip>'" % actname, unban, "actionreload = echo '[%(name)s] %(actname)s: .. reload'", reload,
"actionstop = echo '[<name>] %s: __ stop'" % actname, stop, "actionban = echo '[%(name)s] %(actname)s: ++ ban <ip> %(restore)s'", ban,
"actionunban = echo '[%(name)s] %(actname)s: -- unban <ip>'", unban,
"actionstop = echo '[%(name)s] %(actname)s: __ stop'", stop,
) )
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
_out_file(fn) _out_file(fn)
@ -777,17 +782,26 @@ class Fail2banServerTest(Fail2banClientServerBase):
"", "",
"[test-jail1]", "backend = " + backend, "filter =", "[test-jail1]", "backend = " + backend, "filter =",
"action = ", "action = ",
" test-action1[name='%(__name__)s']" if 1 in actions else "", " test-action1[name='%(__name__)s']" \
" test-action2[name='%(__name__)s']" if 2 in actions else "", if 1 in actions else "",
" test-action2[name='%(__name__)s', restore='restored: <restored>']" \
if 2 in actions else "",
" test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <restored>']" \
if 3 in actions else "",
"logpath = " + test1log, "logpath = " + test1log,
" " + test2log if 2 in enabled else "", " " + test2log if 2 in enabled else "",
" " + test3log if 2 in enabled else "", " " + test3log if 2 in enabled else "",
"failregex = ^\s*failure (401|403) from <HOST>", "failregex = ^\s*failure (401|403) from <HOST>",
" ^\s*error (401|403) from <HOST>" if 2 in enabled else "", " ^\s*error (401|403) from <HOST>" \
if 2 in enabled else "",
"enabled = true" if 1 in enabled else "", "enabled = true" if 1 in enabled else "",
"", "",
"[test-jail2]", "backend = " + backend, "filter =", "[test-jail2]", "backend = " + backend, "filter =",
"action =", "action = ",
" test-action2[name='%(__name__)s', restore='restored: <restored>']" \
if 2 in actions else "",
" test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <restored>']" \
if 3 in actions else "",
"logpath = " + test2log, "logpath = " + test2log,
"enabled = true" if 2 in enabled else "", "enabled = true" if 2 in enabled else "",
) )
@ -798,7 +812,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
_write_action_cfg(actname="test-action1") _write_action_cfg(actname="test-action1")
_write_action_cfg(actname="test-action2") _write_action_cfg(actname="test-action2")
_write_jail_cfg(enabled=[1], actions=[1,2]) _write_jail_cfg(enabled=[1], actions=[1,2,3])
# append one wrong configured jail: # append one wrong configured jail:
_write_file(pjoin(cfg, "jail.conf"), "a", "", "[broken-jail]", _write_file(pjoin(cfg, "jail.conf"), "a", "", "[broken-jail]",
"", "filter = broken-jail-filter", "enabled = true") "", "filter = broken-jail-filter", "enabled = true")
@ -821,6 +835,11 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertLogged( self.assertLogged(
"stdout: '[test-jail1] test-action1: ** start'", "stdout: '[test-jail1] test-action1: ** start'",
"stdout: '[test-jail1] test-action2: ** start'", all=True) "stdout: '[test-jail1] test-action2: ** start'", all=True)
# test restored is 0 (both actions available):
self.assertLogged(
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.1 restored: 0'",
"stdout: '[test-jail1] test-action3: ++ ban 192.0.2.1 restored: 0'",
all=True, wait=MID_WAITTIME)
# broken jail was logged (in client and server log): # broken jail was logged (in client and server log):
self.assertLogged( self.assertLogged(
@ -882,10 +901,10 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertNotLogged( self.assertNotLogged(
"stdout: '[test-jail1] test-action1: -- unban 192.0.2.1'") "stdout: '[test-jail1] test-action1: -- unban 192.0.2.1'")
# don't need both actions anymore: # don't need action1 anymore:
_write_action_cfg(actname="test-action1", allow=False) _write_action_cfg(actname="test-action1", allow=False)
_write_action_cfg(actname="test-action2", allow=False) # leave action2 just to test restored interpolation:
_write_jail_cfg(actions=[]) _write_jail_cfg(actions=[2,3])
# write new failures: # write new failures:
self.pruneLog("[test-phase 2b]") self.pruneLog("[test-phase 2b]")
@ -913,7 +932,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
"[test-jail2] Found 192.0.2.2", "[test-jail2] Found 192.0.2.2",
"[test-jail2] Ban 192.0.2.2", "[test-jail2] Ban 192.0.2.2",
"[test-jail2] Found 192.0.2.3", "[test-jail2] Found 192.0.2.3",
"[test-jail2] Ban 192.0.2.3", all=True) "[test-jail2] Ban 192.0.2.3",
all=True)
# rotate logs: # rotate logs:
_write_file(test1log, "w+") _write_file(test1log, "w+")
@ -936,6 +956,20 @@ class Fail2banServerTest(Fail2banClientServerBase):
"[test-jail2] Restore Ban 192.0.2.4", "[test-jail2] Restore Ban 192.0.2.4",
"[test-jail2] Restore Ban 192.0.2.8", all=True "[test-jail2] Restore Ban 192.0.2.8", all=True
) )
# test restored is 1 (only test-action2):
self.assertLogged(
"stdout: '[test-jail2] test-action2: ++ ban 192.0.2.4 restored: 1'",
"stdout: '[test-jail2] test-action2: ++ ban 192.0.2.8 restored: 1'",
all=True, wait=MID_WAITTIME)
# test test-action3 not executed at all (norestored check):
self.assertNotLogged(
"stdout: '[test-jail2] test-action3: ++ ban 192.0.2.4 restored: 1'",
"stdout: '[test-jail2] test-action3: ++ ban 192.0.2.8 restored: 1'",
all=True)
# don't need actions anymore:
_write_action_cfg(actname="test-action2", allow=False)
_write_jail_cfg(actions=[])
# restart jail with unban all: # restart jail with unban all:
self.pruneLog("[test-phase 2d]") self.pruneLog("[test-phase 2d]")

Loading…
Cancel
Save