Exposes filter group captures in actions (non-recursive interpolation of tags `<F-...>`);

Closes gh-1110
pull/1698/head
sebres 2017-02-18 00:08:35 +01:00
parent 6d878f3a43
commit 61c8cd11b8
5 changed files with 59 additions and 23 deletions

View File

@ -23,6 +23,7 @@ __license__ = "GPL"
import logging import logging
import os import os
import re
import signal import signal
import subprocess import subprocess
import tempfile import tempfile
@ -31,6 +32,7 @@ import time
from abc import ABCMeta from abc import ABCMeta
from collections import MutableMapping from collections import MutableMapping
from .failregex import mapTag2Opt
from .ipdns import asip from .ipdns import asip
from .mytime import MyTime from .mytime import MyTime
from .utils import Utils 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'`: # Todo: make it configurable resp. automatically set, ex.: `[ -f /proc/net/if_inet6 ] && echo 'yes' || echo 'no'`:
allowed_ipv6 = True allowed_ipv6 = True
# capture groups from filter for map to ticket data:
FCUSTAG_CRE = re.compile(r'<F-([A-Z0-9_\-]+)>'); # currently uppercase only
class CallingMap(MutableMapping): class CallingMap(MutableMapping):
"""A Mapping type which returns the result of callable values. """A Mapping type which returns the result of callable values.
@ -72,9 +78,9 @@ class CallingMap(MutableMapping):
def __getitem__(self, key): def __getitem__(self, key):
value = self.data[key] value = self.data[key]
if callable(value): if callable(value):
return value() value = value()
else: self.data[key] = value
return value return value
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.data[key] = value self.data[key] = value
@ -546,6 +552,18 @@ class CommandAction(ActionBase):
# Replace dynamical tags (don't use cache here) # Replace dynamical tags (don't use cache here)
if aInfo is not None: if aInfo is not None:
realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional) 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: else:
realCmd = cmd realCmd = cmd

View File

@ -30,7 +30,7 @@ from .ipdns import IPAddr
FTAG_CRE = re.compile(r'</?[\w\-]+/?>') FTAG_CRE = re.compile(r'</?[\w\-]+/?>')
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 = [ R_HOST = [
# separated ipv4: # separated ipv4:
@ -83,6 +83,12 @@ R_MAP = {
"PORT": "fport", "PORT": "fport",
} }
def mapTag2Opt(tag):
try: # if should be mapped:
return R_MAP[tag]
except KeyError:
return tag.lower()
## ##
# Regular expression class. # Regular expression class.
# #
@ -144,7 +150,7 @@ class Regex:
# (begin / end tag) for customizable expressions, additionally used as # (begin / end tag) for customizable expressions, additionally used as
# user custom tags (match will be stored in ticket data, can be used in actions): # 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-... if m: # match F-...
m = m.groups() m = m.groups()
tn = m[1] tn = m[1]
@ -156,10 +162,8 @@ class Regex:
return tag; # tag not opened, use original return tag; # tag not opened, use original
# open tag: # open tag:
openTags[tn] = 1 openTags[tn] = 1
try: # if should be mapped: # if should be mapped:
tn = R_MAP[tn] tn = mapTag2Opt(tn)
except KeyError:
tn = tn.lower()
return "(?P<%s>" % (tn,) return "(?P<%s>" % (tn,)
# original, no replacement: # original, no replacement:

View File

@ -56,7 +56,9 @@ class Ticket(object):
self._time = time if time is not None else MyTime.time() self._time = time if time is not None else MyTime.time()
self._data = {'matches': matches or [], 'failures': 0} self._data = {'matches': matches or [], 'failures': 0}
if data is not None: 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: if ticket:
# ticket available - copy whole information from ticket: # ticket available - copy whole information from ticket:
self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__) self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)

View File

@ -329,13 +329,24 @@ class CommandActionTest(LogCaptureTestCase):
self.assertEqual(self.__action.ROST,"192.0.2.0") self.assertEqual(self.__action.ROST,"192.0.2.0")
def testExecuteActionUnbanAinfo(self): def testExecuteActionUnbanAinfo(self):
aInfo = { aInfo = CallingMap({
'ABC': "123", 'ABC': "123",
} 'ip': '192.0.2.1',
self.__action.actionban = "touch /tmp/fail2ban.test.123" 'F-*': lambda: {
self.__action.actionunban = "rm /tmp/fail2ban.test.<ABC>" 'fid': 111,
'fport': 222,
'user': "tester"
}
})
self.__action.actionban = "touch /tmp/fail2ban.test.123; echo 'failure <F-ID> of <F-USER> -<F-TEST>- from <ip>:<F-PORT>'"
self.__action.actionunban = "rm /tmp/fail2ban.test.<ABC>; echo 'user <F-USER> unbanned'"
self.__action.ban(aInfo) self.__action.ban(aInfo)
self.__action.unban(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): def testExecuteActionStartEmpty(self):
self.__action.actionstart = "" self.__action.actionstart = ""

View File

@ -761,9 +761,10 @@ class Fail2banServerTest(Fail2banClientServerBase):
"[Definition]", "[Definition]",
"norestored = %(_exec_once)s", "norestored = %(_exec_once)s",
"restore = ", "restore = ",
"info = ",
"actionstart = echo '[%(name)s] %(actname)s: ** start'", start, "actionstart = echo '[%(name)s] %(actname)s: ** start'", start,
"actionreload = echo '[%(name)s] %(actname)s: .. reload'", reload, "actionreload = echo '[%(name)s] %(actname)s: .. reload'", reload,
"actionban = echo '[%(name)s] %(actname)s: ++ ban <ip> %(restore)s'", ban, "actionban = echo '[%(name)s] %(actname)s: ++ ban <ip> %(restore)s%(info)s'", ban,
"actionunban = echo '[%(name)s] %(actname)s: -- unban <ip>'", unban, "actionunban = echo '[%(name)s] %(actname)s: -- unban <ip>'", unban,
"actionstop = echo '[%(name)s] %(actname)s: __ stop'", stop, "actionstop = echo '[%(name)s] %(actname)s: __ stop'", stop,
) )
@ -777,28 +778,28 @@ class Fail2banServerTest(Fail2banClientServerBase):
"usedns = no", "usedns = no",
"maxretry = 3", "maxretry = 3",
"findtime = 10m", "findtime = 10m",
"failregex = ^\s*failure (401|403) from <HOST>", "failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
"datepattern = {^LN-BEG}EPOCH", "datepattern = {^LN-BEG}EPOCH",
"", "",
"[test-jail1]", "backend = " + backend, "filter =", "[test-jail1]", "backend = " + backend, "filter =",
"action = ", "action = ",
" test-action1[name='%(__name__)s']" \ " test-action1[name='%(__name__)s']" \
if 1 in actions else "", if 1 in actions else "",
" test-action2[name='%(__name__)s', restore='restored: <restored>']" \ " test-action2[name='%(__name__)s', restore='restored: <restored>', info=', err-code: <F-ERRCODE>']" \
if 2 in actions else "", if 2 in actions else "",
" test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <restored>']" \ " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <restored>']" \
if 3 in actions else "", 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 <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
" ^\s*error (401|403) from <HOST>" \ " ^\s*error <F-ERRCODE>401|403</F-ERRCODE> from <HOST>" \
if 2 in enabled else "", 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>']" \ " test-action2[name='%(__name__)s', restore='restored: <restored>', info=', err-code: <F-ERRCODE>']" \
if 2 in actions else "", if 2 in actions else "",
" test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <restored>']" \ " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <restored>']" \
if 3 in actions else "", if 3 in actions else "",
@ -837,7 +838,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
"stdout: '[test-jail1] test-action2: ** start'", all=True) "stdout: '[test-jail1] test-action2: ** start'", all=True)
# test restored is 0 (both actions available): # test restored is 0 (both actions available):
self.assertLogged( 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'", "stdout: '[test-jail1] test-action3: ++ ban 192.0.2.1 restored: 0'",
all=True, wait=MID_WAITTIME) all=True, wait=MID_WAITTIME)
@ -958,8 +959,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
) )
# test restored is 1 (only test-action2): # test restored is 1 (only test-action2):
self.assertLogged( self.assertLogged(
"stdout: '[test-jail2] test-action2: ++ ban 192.0.2.4 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'", "stdout: '[test-jail2] test-action2: ++ ban 192.0.2.8 restored: 1, err-code: 401'",
all=True, wait=MID_WAITTIME) all=True, wait=MID_WAITTIME)
# test test-action3 not executed at all (norestored check): # test test-action3 not executed at all (norestored check):
self.assertNotLogged( self.assertNotLogged(