From 61c8cd11b8f4b553ab014b5581bcb11ddb451e57 Mon Sep 17 00:00:00 2001 From: sebres Date: Sat, 18 Feb 2017 00:08:35 +0100 Subject: [PATCH] Exposes filter group captures in actions (non-recursive interpolation of tags ``); Closes gh-1110 --- fail2ban/server/action.py | 24 +++++++++++++++++++++--- fail2ban/server/failregex.py | 16 ++++++++++------ fail2ban/server/ticket.py | 4 +++- fail2ban/tests/actiontestcase.py | 19 +++++++++++++++---- fail2ban/tests/fail2banclienttestcase.py | 19 ++++++++++--------- 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index b4616a15..bf291690 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -23,6 +23,7 @@ __license__ = "GPL" import logging import os +import re import signal import subprocess import tempfile @@ -31,6 +32,7 @@ import time from abc import ABCMeta from collections import MutableMapping +from .failregex import mapTag2Opt from .ipdns import asip from .mytime import MyTime from .utils import Utils @@ -45,6 +47,10 @@ _cmd_lock = threading.Lock() # Todo: make it configurable resp. automatically set, ex.: `[ -f /proc/net/if_inet6 ] && echo 'yes' || echo 'no'`: allowed_ipv6 = True +# capture groups from filter for map to ticket data: +FCUSTAG_CRE = re.compile(r''); # currently uppercase only + + class CallingMap(MutableMapping): """A Mapping type which returns the result of callable values. @@ -72,9 +78,9 @@ class CallingMap(MutableMapping): def __getitem__(self, key): value = self.data[key] if callable(value): - return value() - else: - return value + value = value() + self.data[key] = value + return value def __setitem__(self, key, value): self.data[key] = value @@ -546,6 +552,18 @@ class CommandAction(ActionBase): # Replace dynamical tags (don't use cache here) if aInfo is not None: realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional) + # Replace ticket options (filter capture groups) non-recursive: + if '<' in realCmd: + tickData = aInfo.get("F-*") + if not tickData: tickData = {} + def substTag(m): + tn = mapTag2Opt(m.groups()[0]) + try: + return str(tickData[tn]) + except KeyError: + return "" + + realCmd = FCUSTAG_CRE.sub(substTag, realCmd) else: realCmd = cmd diff --git a/fail2ban/server/failregex.py b/fail2ban/server/failregex.py index 096cb57a..cbd0cef0 100644 --- a/fail2ban/server/failregex.py +++ b/fail2ban/server/failregex.py @@ -30,7 +30,7 @@ from .ipdns import IPAddr FTAG_CRE = re.compile(r'') -FCUSTAG_CRE = re.compile(r'^(/?)F-([A-Z0-9_\-]+)$'); # currently uppercase only +FCUSTNAME_CRE = re.compile(r'^(/?)F-([A-Z0-9_\-]+)$'); # currently uppercase only R_HOST = [ # separated ipv4: @@ -83,6 +83,12 @@ R_MAP = { "PORT": "fport", } +def mapTag2Opt(tag): + try: # if should be mapped: + return R_MAP[tag] + except KeyError: + return tag.lower() + ## # Regular expression class. # @@ -144,7 +150,7 @@ class Regex: # (begin / end tag) for customizable expressions, additionally used as # user custom tags (match will be stored in ticket data, can be used in actions): - m = FCUSTAG_CRE.match(tn) + m = FCUSTNAME_CRE.match(tn) if m: # match F-... m = m.groups() tn = m[1] @@ -156,10 +162,8 @@ class Regex: return tag; # tag not opened, use original # open tag: openTags[tn] = 1 - try: # if should be mapped: - tn = R_MAP[tn] - except KeyError: - tn = tn.lower() + # if should be mapped: + tn = mapTag2Opt(tn) return "(?P<%s>" % (tn,) # original, no replacement: diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index c7bb4d47..a66b64ac 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -56,7 +56,9 @@ class Ticket(object): self._time = time if time is not None else MyTime.time() self._data = {'matches': matches or [], 'failures': 0} if data is not None: - self._data.update(data) + for k,v in data.iteritems(): + if v is not None: + self._data[k] = v if ticket: # ticket available - copy whole information from ticket: self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__) diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 435dc1da..ca908b65 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -329,13 +329,24 @@ class CommandActionTest(LogCaptureTestCase): self.assertEqual(self.__action.ROST,"192.0.2.0") def testExecuteActionUnbanAinfo(self): - aInfo = { + aInfo = CallingMap({ 'ABC': "123", - } - self.__action.actionban = "touch /tmp/fail2ban.test.123" - self.__action.actionunban = "rm /tmp/fail2ban.test." + 'ip': '192.0.2.1', + 'F-*': lambda: { + 'fid': 111, + 'fport': 222, + 'user': "tester" + } + }) + self.__action.actionban = "touch /tmp/fail2ban.test.123; echo 'failure of -- from :'" + self.__action.actionunban = "rm /tmp/fail2ban.test.; echo 'user unbanned'" self.__action.ban(aInfo) self.__action.unban(aInfo) + self.assertLogged( + " -- stdout: 'failure 111 of tester -- from 192.0.2.1:222'", + " -- stdout: 'user tester unbanned'", + all=True + ) def testExecuteActionStartEmpty(self): self.__action.actionstart = "" diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index b8417be5..adaf4719 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -761,9 +761,10 @@ class Fail2banServerTest(Fail2banClientServerBase): "[Definition]", "norestored = %(_exec_once)s", "restore = ", + "info = ", "actionstart = echo '[%(name)s] %(actname)s: ** start'", start, "actionreload = echo '[%(name)s] %(actname)s: .. reload'", reload, - "actionban = echo '[%(name)s] %(actname)s: ++ ban %(restore)s'", ban, + "actionban = echo '[%(name)s] %(actname)s: ++ ban %(restore)s%(info)s'", ban, "actionunban = echo '[%(name)s] %(actname)s: -- unban '", unban, "actionstop = echo '[%(name)s] %(actname)s: __ stop'", stop, ) @@ -777,28 +778,28 @@ class Fail2banServerTest(Fail2banClientServerBase): "usedns = no", "maxretry = 3", "findtime = 10m", - "failregex = ^\s*failure (401|403) from ", + "failregex = ^\s*failure 401|403 from ", "datepattern = {^LN-BEG}EPOCH", "", "[test-jail1]", "backend = " + backend, "filter =", "action = ", " test-action1[name='%(__name__)s']" \ if 1 in actions else "", - " test-action2[name='%(__name__)s', restore='restored: ']" \ + " test-action2[name='%(__name__)s', restore='restored: ', info=', err-code: ']" \ if 2 in actions else "", " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" \ if 3 in actions else "", "logpath = " + test1log, " " + test2log if 2 in enabled else "", " " + test3log if 2 in enabled else "", - "failregex = ^\s*failure (401|403) from ", - " ^\s*error (401|403) from " \ + "failregex = ^\s*failure 401|403 from ", + " ^\s*error 401|403 from " \ if 2 in enabled else "", "enabled = true" if 1 in enabled else "", "", "[test-jail2]", "backend = " + backend, "filter =", "action = ", - " test-action2[name='%(__name__)s', restore='restored: ']" \ + " test-action2[name='%(__name__)s', restore='restored: ', info=', err-code: ']" \ if 2 in actions else "", " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" \ if 3 in actions else "", @@ -837,7 +838,7 @@ class Fail2banServerTest(Fail2banClientServerBase): "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-action2: ++ ban 192.0.2.1 restored: 0, err-code: 401'", "stdout: '[test-jail1] test-action3: ++ ban 192.0.2.1 restored: 0'", all=True, wait=MID_WAITTIME) @@ -958,8 +959,8 @@ class Fail2banServerTest(Fail2banClientServerBase): ) # 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'", + "stdout: '[test-jail2] test-action2: ++ ban 192.0.2.4 restored: 1, err-code: 401'", + "stdout: '[test-jail2] test-action2: ++ ban 192.0.2.8 restored: 1, err-code: 401'", all=True, wait=MID_WAITTIME) # test test-action3 not executed at all (norestored check): self.assertNotLogged(