mirror of https://github.com/fail2ban/fail2ban
Exposes filter group captures in actions (non-recursive interpolation of tags `<F-...>`);
Closes gh-1110pull/1698/head
parent
6d878f3a43
commit
61c8cd11b8
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue