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 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'<F-([A-Z0-9_\-]+)>'); # 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

View File

@ -30,7 +30,7 @@ from .ipdns import IPAddr
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 = [
# 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:

View File

@ -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__)

View File

@ -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.<ABC>"
'ip': '192.0.2.1',
'F-*': lambda: {
'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.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 = ""

View File

@ -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 <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,
"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 <HOST>",
"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
"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: <restored>']" \
" test-action2[name='%(__name__)s', restore='restored: <restored>', info=', err-code: <F-ERRCODE>']" \
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,
" " + test2log if 2 in enabled else "",
" " + test3log if 2 in enabled else "",
"failregex = ^\s*failure (401|403) from <HOST>",
" ^\s*error (401|403) from <HOST>" \
"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
" ^\s*error <F-ERRCODE>401|403</F-ERRCODE> from <HOST>" \
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: <restored>']" \
" test-action2[name='%(__name__)s', restore='restored: <restored>', info=', err-code: <F-ERRCODE>']" \
if 2 in actions else "",
" test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: <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(