Merge remote-tracking branch 'remotes/kwirk/sebres-strptime-bug' into ban-time-incr

pull/716/head
sebres 2014-05-15 17:12:11 +02:00
commit 8fd083a1ea
27 changed files with 249 additions and 142 deletions

View File

@ -6,6 +6,7 @@ python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "pypy"
before_install:
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo apt-get update -qq; fi

View File

@ -22,13 +22,21 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
* Nginx filter to support missing server_name. Closes gh-676
* fail2ban-regex assertion error caused by miscount missed lines with
multiline regex
* Fix actions failing to execute for Python 3.4.0. Workaround for
http://bugs.python.org/issue21207
* Database now returns persistent bans on restart (bantime < 0)
* Recursive action tags now fully processed. Fixes issue with bsd-ipfw
action
* Correct times for non-timezone date times formats - Thanks sebres
- New features:
- Added monit filter thanks Jason H Martin.
- Enhancements
* Fail2ban-regex - add print-all-matched option. Closes gh-652
* Suppress fail2ban-client warnings for non-critical config options
* Match non "Bye Bye" disconnect messages for sshd locked account regex
ver. 0.9.0 (2014/03/14) - beta
----------

2
THANKS
View File

@ -48,6 +48,7 @@ Ivo Truxa
John Thoe
Jacques Lav!gnotte
Ioan Indreias
Jason H Martin
Jonathan Kamens
Jonathan Lanning
Jonathan Underwood
@ -85,6 +86,7 @@ Rolf Fokkens
Roman Gelfand
Russell Odom
Sebastian Arcus
sebres
Sireyessire
silviogarbes
Stefan Tatschner

View File

@ -51,6 +51,7 @@ class Fail2banClient:
self.__conf["conf"] = "/etc/fail2ban"
self.__conf["dump"] = False
self.__conf["force"] = False
self.__conf["background"] = True
self.__conf["verbose"] = 1
self.__conf["interactive"] = False
self.__conf["socket"] = None
@ -83,6 +84,8 @@ class Fail2banClient:
print " -v increase verbosity"
print " -q decrease verbosity"
print " -x force execution of the server (remove socket file)"
print " -b start server in background (default)"
print " -f start server in foreground (note that the client forks once itself)"
print " -h, --help display this help message"
print " -V, --version print the version"
print
@ -125,6 +128,10 @@ class Fail2banClient:
self.__conf["force"] = True
elif opt[0] == "-i":
self.__conf["interactive"] = True
elif opt[0] == "-b":
self.__conf["background"] = True
elif opt[0] == "-f":
self.__conf["background"] = False
elif opt[0] in ["-h", "--help"]:
self.dispUsage()
sys.exit(0)
@ -194,7 +201,8 @@ class Fail2banClient:
# Start the server
self.__startServerAsync(self.__conf["socket"],
self.__conf["pidfile"],
self.__conf["force"])
self.__conf["force"],
self.__conf["background"])
try:
# Wait for the server to start
self.__waitOnServer()
@ -242,14 +250,12 @@ class Fail2banClient:
#
# Start the Fail2ban server in daemon mode.
def __startServerAsync(self, socket, pidfile, force = False):
def __startServerAsync(self, socket, pidfile, force = False, background = True):
# Forks the current process.
pid = os.fork()
if pid == 0:
args = list()
args.append(self.SERVER)
# Start in background mode.
args.append("-b")
# Set the socket path.
args.append("-s")
args.append(socket)
@ -259,6 +265,12 @@ class Fail2banClient:
# Force the execution if needed.
if force:
args.append("-x")
# Start in foreground mode if requested.
if background:
args.append("-b")
else:
args.append("-f")
try:
# Use the current directory.
exe = os.path.abspath(os.path.join(sys.path[0], self.SERVER))
@ -312,7 +324,7 @@ class Fail2banClient:
# Reads the command line options.
try:
cmdOpts = 'hc:s:p:xdviqV'
cmdOpts = 'hc:s:p:xfbdviqV'
cmdLongOpts = ['help', 'version']
optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts)
except getopt.GetoptError:

View File

@ -45,7 +45,7 @@ from fail2ban.client.filterreader import FilterReader
from fail2ban.server.filter import Filter
from fail2ban.server.failregex import RegexException
from fail2ban.tests.utils import FormatterWithTraceBack
from fail2ban.helpers import FormatterWithTraceBack
# Gets the instance of the logger.
logSys = logging.getLogger("fail2ban")

View File

@ -34,7 +34,8 @@ if os.path.exists("fail2ban/__init__.py"):
sys.path.insert(0, ".")
from fail2ban.version import version
from fail2ban.tests.utils import FormatterWithTraceBack, gatherTests
from fail2ban.tests.utils import gatherTests
from fail2ban.helpers import FormatterWithTraceBack
from fail2ban.server.mytime import MyTime
from optparse import OptionParser, Option

View File

@ -0,0 +1,9 @@
# Fail2Ban filter for monit.conf, looks for failed access attempts
#
#
[Definition]
failregex = ^\[[A-Z]+\s+\]\s*error\s*:\s*Warning:\s+Client '<HOST>' supplied unknown user '\w+' accessing monit httpd$
^\[[A-Z]+\s+\]\s*error\s*:\s*Warning:\s+Client '<HOST>' supplied wrong password for user '\w+' accessing monit httpd$

View File

@ -30,7 +30,7 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|erro
^%(__prefix_line)sReceived disconnect from <HOST>: 3: \S+: Auth fail$
^%(__prefix_line)sUser .+ from <HOST> not allowed because a group is listed in DenyGroups\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*$
^(?P<__prefix>%(__prefix_line)s)User .+ not allowed because account is locked<SKIPLINES>(?P=__prefix)(?:error: )?Received disconnect from <HOST>: 11: Bye Bye \[preauth\]$
^(?P<__prefix>%(__prefix_line)s)User .+ not allowed because account is locked<SKIPLINES>(?P=__prefix)(?:error: )?Received disconnect from <HOST>: 11: .+ \[preauth\]$
^(?P<__prefix>%(__prefix_line)s)Disconnecting: Too many authentication failures for .+? \[preauth\]<SKIPLINES>(?P=__prefix)(?:error: )?Connection closed by <HOST> \[preauth\]$
^(?P<__prefix>%(__prefix_line)s)Connection from <HOST> port \d+<SKIPLINES>(?P=__prefix)Disconnecting: Too many authentication failures for .+? \[preauth\]$

View File

@ -409,6 +409,12 @@ maxretry = 5
port = http,https
logpath = /var/log/tomcat*/catalina.out
[monit]
#Ban clients brute-forcing the monit gui login
filter = monit
port = 2812
logpath = /var/log/monit
[webmin-auth]

View File

@ -20,9 +20,90 @@
__author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko"
__license__ = "GPL"
import sys
import os
import traceback
import re
import logging
def formatExceptionInfo():
""" Consistently format exception information """
import sys
cla, exc = sys.exc_info()[:2]
return (cla.__name__, str(exc))
#
# Following "traceback" functions are adopted from PyMVPA distributed
# under MIT/Expat and copyright by PyMVPA developers (i.e. me and
# Michael). Hereby I re-license derivative work on these pieces under GPL
# to stay in line with the main Fail2Ban license
#
def mbasename(s):
"""Custom function to include directory name if filename is too common
Also strip .py at the end
"""
base = os.path.basename(s)
if base.endswith('.py'):
base = base[:-3]
if base in set(['base', '__init__']):
base = os.path.basename(os.path.dirname(s)) + '.' + base
return base
class TraceBack(object):
"""Customized traceback to be included in debug messages
"""
def __init__(self, compress=False):
"""Initialize TrackBack metric
Parameters
----------
compress : bool
if True then prefix common with previous invocation gets
replaced with ...
"""
self.__prev = ""
self.__compress = compress
def __call__(self):
ftb = traceback.extract_stack(limit=100)[:-2]
entries = [
[mbasename(x[0]), os.path.dirname(x[0]), str(x[1])] for x in ftb]
entries = [ [e[0], e[2]] for e in entries
if not (e[0] in ['unittest', 'logging.__init__']
or e[1].endswith('/unittest'))]
# lets make it more concise
entries_out = [entries[0]]
for entry in entries[1:]:
if entry[0] == entries_out[-1][0]:
entries_out[-1][1] += ',%s' % entry[1]
else:
entries_out.append(entry)
sftb = '>'.join(['%s:%s' % (mbasename(x[0]),
x[1]) for x in entries_out])
if self.__compress:
# lets remove part which is common with previous invocation
prev_next = sftb
common_prefix = os.path.commonprefix((self.__prev, sftb))
common_prefix2 = re.sub('>[^>]*$', '', common_prefix)
if common_prefix2 != "":
sftb = '...' + sftb[len(common_prefix2):]
self.__prev = prev_next
return sftb
class FormatterWithTraceBack(logging.Formatter):
"""Custom formatter which expands %(tb) and %(tbc) with tracebacks
TODO: might need locking in case of compressed tracebacks
"""
def __init__(self, fmt, *args, **kwargs):
logging.Formatter.__init__(self, fmt=fmt, *args, **kwargs)
compress = '%(tbc)s' in fmt
self._tb = TraceBack(compress=compress)
def format(self, record):
record.tbc = record.tb = self._tb()
return logging.Formatter.format(self, record)

View File

@ -194,6 +194,8 @@ class CommandAction(ActionBase):
timeout
"""
_escapedTags = set(('matches', 'ipmatches', 'ipjailmatches'))
def __init__(self, jail, name):
super(CommandAction, self).__init__(jail, name)
self.timeout = 60
@ -351,8 +353,8 @@ class CommandAction(ActionBase):
if not self.executeCmd(stopCmd, self.timeout):
raise RuntimeError("Error stopping action")
@staticmethod
def substituteRecursiveTags(tags):
@classmethod
def substituteRecursiveTags(cls, tags):
"""Sort out tag definitions within other tags.
so: becomes:
@ -371,8 +373,11 @@ class CommandAction(ActionBase):
within the values recursively replaced.
"""
t = re.compile(r'<([^ >]+)>')
for tag, value in tags.iteritems():
value = str(value)
for tag in tags.iterkeys():
if tag in cls._escapedTags:
# Escaped so won't match
continue
value = str(tags[tag])
m = t.search(value)
done = []
#logSys.log(5, 'TAG: %s, value: %s' % (tag, value))
@ -383,6 +388,9 @@ class CommandAction(ActionBase):
# recursive definitions are bad
#logSys.log(5, 'recursion fail tag: %s value: %s' % (tag, value) )
return False
elif found_tag in cls._escapedTags:
# Escaped so won't match
continue
else:
if tags.has_key(found_tag):
value = value.replace('<%s>' % found_tag , tags[found_tag])
@ -441,10 +449,11 @@ class CommandAction(ActionBase):
`query` string with tags replaced.
"""
string = query
aInfo = cls.substituteRecursiveTags(aInfo)
for tag in aInfo:
if "<%s>" % tag in query:
value = str(aInfo[tag]) # assure string
if tag.endswith('matches'):
if tag in cls._escapedTags:
# That one needs to be escaped since its content is
# out of our control
value = cls.escapeTag(value)

View File

@ -417,7 +417,7 @@ class Fail2BanDb(object):
if jail is not None:
query += " AND jail=?"
queryArgs.append(jail.name)
if bantime is not None:
if bantime is not None and bantime >= 0:
query += " AND timeofban > ?"
queryArgs.append(MyTime.time() - bantime)
if ip is not None:
@ -436,7 +436,8 @@ class Fail2BanDb(object):
Jail that the ban belongs to. Default `None`; all jails.
bantime : int
Ban time in seconds, such that bans returned would still be
valid now. Default `None`; no limit.
valid now. Negative values are equivalent to `None`.
Default `None`; no limit.
ip : str
IP Address to filter bans by. Default `None`; all IPs.
@ -464,7 +465,8 @@ class Fail2BanDb(object):
Jail that the ban belongs to. Default `None`; all jails.
bantime : int
Ban time in seconds, such that bans returned would still be
valid now. Default `None`; no limit.
valid now. Negative values are equivalent to `None`.
Default `None`; no limit.
ip : str
IP Address to filter bans by. Default `None`; all IPs.
@ -475,7 +477,8 @@ class Fail2BanDb(object):
in a list. When `ip` argument passed, a single `Ticket` is
returned.
"""
if bantime is None:
cacheKey = None
if bantime is None or bantime < 0:
cacheKey = (ip, jail)
if cacheKey in self._bansMergedCache:
return self._bansMergedCache[cacheKey]
@ -505,7 +508,7 @@ class Fail2BanDb(object):
ticket.setAttempt(failures)
tickets.append(ticket)
if bantime is None:
if cacheKey:
self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
return tickets if ip is None else ticket

View File

@ -529,11 +529,19 @@ class Server:
except (AttributeError, ValueError):
maxfd = 256 # default maximum
# urandom should not be closed in Python 3.4.0. Fixed in 3.4.1
# http://bugs.python.org/issue21207
if sys.version_info[0:3] == (3, 4, 0): # pragma: no cover
urandom_fd = os.open("/dev/urandom", os.O_RDONLY)
for fd in range(0, maxfd):
try:
if not os.path.sameopenfile(urandom_fd, fd):
os.close(fd)
except OSError: # ERROR (ignore)
pass
os.close(urandom_fd)
else:
os.closerange(0, maxfd)
# Redirect the standard file descriptors to /dev/null.
os.open("/dev/null", os.O_RDONLY) # standard input (0)

View File

@ -190,5 +190,5 @@ def reGroupDictStrptime(found_dict):
if gmtoff is not None:
return calendar.timegm(date_result.utctimetuple())
else:
return time.mktime(date_result.utctimetuple())
return time.mktime(date_result.timetuple())

View File

@ -100,17 +100,24 @@ class CommandActionTest(LogCaptureTestCase):
{'ipjailmatches': "some >char< should \< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
# Recursive
aInfo["ABC"] = "<xyz>"
self.assertEqual(
self.__action.replaceTag("Text <xyz> text <ABC> ABC", aInfo),
"Text 890 text 890 ABC")
# Callable
self.assertEqual(
self.__action.replaceTag("09 <callme> 11",
CallingMap(callme=lambda: str(10))),
self.__action.replaceTag("09 <matches> 11",
CallingMap(matches=lambda: str(10))),
"09 10 11")
# As tag not present, therefore callable should not be called
# Will raise ValueError if it is
self.assertEqual(
self.__action.replaceTag("abc",
CallingMap(callme=lambda: int("a"))), "abc")
CallingMap(matches=lambda: int("a"))), "abc")
def testExecuteActionBan(self):
self.__action.actionstart = "touch /tmp/fail2ban.test"

View File

@ -177,10 +177,15 @@ class DatabaseTest(unittest.TestCase):
if Fail2BanDb is None: # pragma: no cover
return
self.testAddJail()
ticket = FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"])
self.db.addBan(self.jail, ticket)
self.db.addBan(
self.jail, FailTicket("127.0.0.1", MyTime.time() - 60, ["abc\n"]))
self.db.addBan(
self.jail, FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"]))
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=50)), 1)
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=20)), 0)
# Negative values are for persistent bans, and such all bans should
# be returned
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2)
def testGetBansMerged(self):
if Fail2BanDb is None: # pragma: no cover
@ -251,6 +256,10 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual(len(tickets), 1)
tickets = self.db.getBansMerged(bantime=5)
self.assertEqual(len(tickets), 0)
# Negative values are for persistent bans, and such all bans should
# be returned
tickets = self.db.getBansMerged(bantime=-1)
self.assertEqual(len(tickets), 2)
def testPurge(self):
if Fail2BanDb is None: # pragma: no cover

View File

@ -131,7 +131,7 @@ class DateDetectorTest(unittest.TestCase):
# see https://github.com/fail2ban/fail2ban/pull/130
# yoh: unfortunately this test is not really effective to reproduce the
# situation but left in place to assure consistent behavior
mu = time.mktime(datetime.datetime(2012, 10, 11, 2, 37, 17).utctimetuple())
mu = time.mktime(datetime.datetime(2012, 10, 11, 2, 37, 17).timetuple())
logdate = self.__datedetector.getTime('2012/10/11 02:37:17 [error] 18434#0')
self.assertNotEqual(logdate, None)
( logTime, logMatch ) = logdate

View File

@ -1,12 +1,12 @@
# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "80.187.101.33" }
# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "80.187.101.33" }
@400000004c91b044077a9e94 imap-login: Info: Aborted login (auth failed, 1 attempts): user=<martin@waschbuesch.de>, method=CRAM-MD5, rip=80.187.101.33, lip=80.254.129.240, TLS
# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "176.61.140.224" }
# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "176.61.140.224" }
@400000004c91b044077a9e94 dovecot-auth: pam_unix(dovecot:auth): authentication failure; logname= uid=0 euid=0 tty=dovecot ruser=web rhost=176.61.140.224
# Above example with injected rhost into ruser -- should not match for 1.2.3.4
# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "192.0.43.10" }
# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "192.0.43.10" }
@400000004c91b044077a9e94 dovecot-auth: pam_unix(dovecot:auth): authentication failure; logname= uid=0 euid=0 tty=dovecot ruser=rhost=1.2.3.4 rhost=192.0.43.10
# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "176.61.140.225" }
# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "176.61.140.225" }
@400000004c91b044077a9e94 dovecot-auth: pam_unix(dovecot:auth): authentication failure; logname= uid=0 euid=0 tty=dovecot ruser=root rhost=176.61.140.225 user=root
# failJSON: { "time": "2004-12-12T11:19:11", "match": true , "host": "190.210.136.21" }

View File

@ -0,0 +1,6 @@
# failJSON: { "time": "2005-04-16T21:05:29", "match": true , "host": "69.93.127.111" }
[PDT Apr 16 21:05:29] error : Warning: Client '69.93.127.111' supplied unknown user 'foo' accessing monit httpd
# failJSON: { "time": "2005-04-16T20:59:33", "match": true , "host": "97.113.189.111" }
[PDT Apr 16 20:59:33] error : Warning: Client '97.113.189.111' supplied wrong password for user 'admin' accessing monit httpd

View File

@ -1,25 +1,25 @@
# failJSON: { "time": "2013-07-09T01:45:16", "match": false , "host": "173.242.116.187" }
# failJSON: { "time": "2013-07-09T02:45:16", "match": false , "host": "173.242.116.187" }
type=USER_LOGIN msg=audit(1373330716.415:4063): user pid=11998 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct="root" exe="/usr/sbin/sshd" hostname=? addr=173.242.116.187 terminal=ssh res=failed'
# failJSON: { "time": "2013-07-09T01:45:17", "match": false , "host": "173.242.116.187" }
# failJSON: { "time": "2013-07-09T02:45:17", "match": false , "host": "173.242.116.187" }
type=USER_LOGIN msg=audit(1373330717.000:4068): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct=28756E6B6E6F776E207573657229 exe="/usr/sbin/sshd" hostname=? addr=173.242.116.187 terminal=ssh res=failed'
# failJSON: { "time": "2013-07-09T01:45:17", "match": true , "host": "173.242.116.187" }
# failJSON: { "time": "2013-07-09T02:45:17", "match": true , "host": "173.242.116.187" }
type=USER_ERR msg=audit(1373330717.000:4070): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:bad_ident acct="?" exe="/usr/sbin/sshd" hostname=173.242.116.187 addr=173.242.116.187 terminal=ssh res=failed'
# failJSON: { "time": "2013-07-09T01:45:17", "match": false , "host": "173.242.116.187" }
# failJSON: { "time": "2013-07-09T02:45:17", "match": false , "host": "173.242.116.187" }
type=USER_LOGIN msg=audit(1373330717.000:4073): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct=28696E76616C6964207573657229 exe="/usr/sbin/sshd" hostname=? addr=173.242.116.187 terminal=ssh res=failed'
# failJSON: { "time": "2013-06-30T01:02:08", "match": false , "host": "113.240.248.18" }
# failJSON: { "time": "2013-06-30T02:02:08", "match": false , "host": "113.240.248.18" }
type=USER_LOGIN msg=audit(1372546928.000:52008): user pid=21569 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct="sshd" exe="/usr/sbin/sshd" hostname=? addr=113.240.248.18 terminal=ssh res=failed'
# failJSON: { "time": "2013-06-30T02:58:20", "match": true , "host": "113.240.248.18" }
# failJSON: { "time": "2013-06-30T03:58:20", "match": true , "host": "113.240.248.18" }
type=USER_ERR msg=audit(1372557500.000:61747): user pid=23684 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:bad_ident acct="?" exe="/usr/sbin/sshd" hostname=113.240.248.18 addr=113.240.248.18 terminal=ssh res=failed'
# failJSON: { "time": "2013-06-30T03:58:20", "match": false , "host": "113.240.248.18" }
# failJSON: { "time": "2013-06-30T04:58:20", "match": false , "host": "113.240.248.18" }
type=USER_LOGIN msg=audit(1372557500.000:61750): user pid=23684 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct=28696E76616C6964207573657229 exe="/usr/sbin/sshd" hostname=? addr=113.240.248.18 terminal=ssh res=failed'
# failJSON: { "time": "2013-07-06T17:48:00", "match": true , "host": "194.228.20.113" }
# failJSON: { "time": "2013-07-06T18:48:00", "match": true , "host": "194.228.20.113" }
type=USER_AUTH msg=audit(1373129280.000:9): user pid=1277 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=pubkey acct="root" exe="/usr/sbin/sshd" hostname=? addr=194.228.20.113 terminal=ssh res=failed'
# failJSON: { "time": "2013-10-30T07:57:43", "match": true , "host": "192.168.3.100" }

View File

@ -136,3 +136,10 @@ Jul 13 18:44:28 mdop sshd[4931]: Received disconnect from 89.24.13.192: 3: com.j
Feb 12 04:09:18 localhost sshd[26713]: Connection from 115.249.163.77 port 51353
# failJSON: { "time": "2005-02-12T04:09:21", "match": true , "host": "115.249.163.77", "desc": "from gh-457" }
Feb 12 04:09:21 localhost sshd[26713]: Disconnecting: Too many authentication failures for root [preauth]
# failJSON: { "match": false }
Apr 27 13:02:04 host sshd[29116]: User root not allowed because account is locked
# failJSON: { "match": false }
Apr 27 13:02:04 host sshd[29116]: input_userauth_request: invalid user root [preauth]
# failJSON: { "time": "2005-04-27T13:02:04", "match": true , "host": "1.2.3.4", "desc": "No Bye-Bye" }
Apr 27 13:02:04 host sshd[29116]: Received disconnect from 1.2.3.4: 11: Normal Shutdown, Thank you for playing [preauth]

View File

@ -794,7 +794,7 @@ class GetFailures(unittest.TestCase):
FILENAME_MULTILINE = os.path.join(TEST_FILES_DIR, "testcase-multiline.log")
# so that they could be reused by other tests
FAILURES_01 = ('193.168.0.128', 3, 1124017199.0,
FAILURES_01 = ('193.168.0.128', 3, 1124013599.0,
[u'Aug 14 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 193.168.0.128']*3)
def setUp(self):
@ -844,7 +844,7 @@ class GetFailures(unittest.TestCase):
def testGetFailures02(self):
output = ('141.3.81.106', 4, 1124017139.0,
output = ('141.3.81.106', 4, 1124013539.0,
[u'Aug 14 11:%d:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:141.3.81.106 port 51332 ssh2'
% m for m in 53, 54, 57, 58])
@ -854,7 +854,7 @@ class GetFailures(unittest.TestCase):
_assert_correct_last_attempt(self, self.filter, output)
def testGetFailures03(self):
output = ('203.162.223.135', 7, 1124017144.0)
output = ('203.162.223.135', 7, 1124013544.0)
self.filter.addLogPath(GetFailures.FILENAME_03)
self.filter.addFailRegex("error,relay=<HOST>,.*550 User unknown")
@ -862,7 +862,7 @@ class GetFailures(unittest.TestCase):
_assert_correct_last_attempt(self, self.filter, output)
def testGetFailures04(self):
output = [('212.41.96.186', 4, 1124017200.0),
output = [('212.41.96.186', 4, 1124013600.0),
('212.41.96.185', 4, 1124017198.0)]
self.filter.addLogPath(GetFailures.FILENAME_04)
@ -877,11 +877,11 @@ class GetFailures(unittest.TestCase):
def testGetFailuresUseDNS(self):
# We should still catch failures with usedns = no ;-)
output_yes = ('93.184.216.119', 2, 1124017139.0,
output_yes = ('93.184.216.119', 2, 1124013539.0,
[u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2',
u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2'])
output_no = ('93.184.216.119', 1, 1124017139.0,
output_no = ('93.184.216.119', 1, 1124013539.0,
[u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2'])
# Actually no exception would be raised -- it will be just set to 'no'
@ -904,7 +904,7 @@ class GetFailures(unittest.TestCase):
def testGetFailuresMultiRegex(self):
output = ('141.3.81.106', 8, 1124017141.0)
output = ('141.3.81.106', 8, 1124013541.0)
self.filter.addLogPath(GetFailures.FILENAME_02)
self.filter.addFailRegex("Failed .* from <HOST>")
@ -923,8 +923,8 @@ class GetFailures(unittest.TestCase):
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
def testGetFailuresMultiLine(self):
output = [("192.0.43.10", 2, 1124017199.0),
("192.0.43.11", 1, 1124017198.0)]
output = [("192.0.43.10", 2, 1124013599.0),
("192.0.43.11", 1, 1124013598.0)]
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE)
self.filter.addFailRegex("^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
self.filter.setMaxLines(100)
@ -942,7 +942,7 @@ class GetFailures(unittest.TestCase):
self.assertEqual(sorted(foundList), sorted(output))
def testGetFailuresMultiLineIgnoreRegex(self):
output = [("192.0.43.10", 2, 1124017199.0)]
output = [("192.0.43.10", 2, 1124013599.0)]
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE)
self.filter.addFailRegex("^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
self.filter.addIgnoreRegex("rsync error: Received SIGINT")
@ -956,9 +956,9 @@ class GetFailures(unittest.TestCase):
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
def testGetFailuresMultiLineMultiRegex(self):
output = [("192.0.43.10", 2, 1124017199.0),
("192.0.43.11", 1, 1124017198.0),
("192.0.43.15", 1, 1124017198.0)]
output = [("192.0.43.10", 2, 1124013599.0),
("192.0.43.11", 1, 1124013598.0),
("192.0.43.15", 1, 1124013598.0)]
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE)
self.filter.addFailRegex("^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
self.filter.addFailRegex("^.* sendmail\[.*, msgid=<(?P<msgid>[^>]+).*relay=\[<HOST>\].*$<SKIPLINES>^.+ spamd: result: Y \d+ .*,mid=<(?P=msgid)>(,bayes=[.\d]+)?(,autolearn=\S+)?\s*$")

View File

@ -32,8 +32,7 @@ import datetime
from glob import glob
from StringIO import StringIO
from .utils import mbasename, TraceBack, FormatterWithTraceBack
from ..helpers import formatExceptionInfo
from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack
from ..server.datetemplate import DatePatternRegex

View File

@ -129,7 +129,7 @@ def testSampleRegexsFactory(name):
jsonTimeLocal = datetime.datetime.strptime(t, "%Y-%m-%dT%H:%M:%S.%f")
jsonTime = time.mktime(jsonTimeLocal.utctimetuple())
jsonTime = time.mktime(jsonTimeLocal.timetuple())
jsonTime += jsonTimeLocal.microsecond / 1000000

View File

@ -22,90 +22,17 @@ __author__ = "Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2013 Yaroslav Halchenko"
__license__ = "GPL"
import logging, os, re, traceback, time, unittest
from os.path import basename, dirname
import logging
import os
import re
import time
import unittest
from StringIO import StringIO
from ..server.mytime import MyTime
logSys = logging.getLogger(__name__)
#
# Following "traceback" functions are adopted from PyMVPA distributed
# under MIT/Expat and copyright by PyMVPA developers (i.e. me and
# Michael). Hereby I re-license derivative work on these pieces under GPL
# to stay in line with the main Fail2Ban license
#
def mbasename(s):
"""Custom function to include directory name if filename is too common
Also strip .py at the end
"""
base = basename(s)
if base.endswith('.py'):
base = base[:-3]
if base in set(['base', '__init__']):
base = basename(dirname(s)) + '.' + base
return base
class TraceBack(object):
"""Customized traceback to be included in debug messages
"""
def __init__(self, compress=False):
"""Initialize TrackBack metric
Parameters
----------
compress : bool
if True then prefix common with previous invocation gets
replaced with ...
"""
self.__prev = ""
self.__compress = compress
def __call__(self):
ftb = traceback.extract_stack(limit=100)[:-2]
entries = [[mbasename(x[0]), dirname(x[0]), str(x[1])] for x in ftb]
entries = [ [e[0], e[2]] for e in entries
if not (e[0] in ['unittest', 'logging.__init__']
or e[1].endswith('/unittest'))]
# lets make it more concise
entries_out = [entries[0]]
for entry in entries[1:]:
if entry[0] == entries_out[-1][0]:
entries_out[-1][1] += ',%s' % entry[1]
else:
entries_out.append(entry)
sftb = '>'.join(['%s:%s' % (mbasename(x[0]),
x[1]) for x in entries_out])
if self.__compress:
# lets remove part which is common with previous invocation
prev_next = sftb
common_prefix = os.path.commonprefix((self.__prev, sftb))
common_prefix2 = re.sub('>[^>]*$', '', common_prefix)
if common_prefix2 != "":
sftb = '...' + sftb[len(common_prefix2):]
self.__prev = prev_next
return sftb
class FormatterWithTraceBack(logging.Formatter):
"""Custom formatter which expands %(tb) and %(tbc) with tracebacks
TODO: might need locking in case of compressed tracebacks
"""
def __init__(self, fmt, *args, **kwargs):
logging.Formatter.__init__(self, fmt=fmt, *args, **kwargs)
compress = '%(tbc)s' in fmt
self._tb = TraceBack(compress=compress)
def format(self, record):
record.tbc = record.tb = self._tb()
return logging.Formatter.format(self, record)
def mtimesleep():
# no sleep now should be necessary since polling tracks now not only
# mtime but also ino and size
@ -146,7 +73,6 @@ def gatherTests(regexps=None, no_network=False):
if not regexps: # pragma: no cover
tests = unittest.TestSuite()
else: # pragma: no cover
import re
class FilteredTestSuite(unittest.TestSuite):
_regexps = [re.compile(r) for r in regexps]
def addTest(self, suite):

View File

@ -1,13 +1,20 @@
description "fail2ban - ban hosts that cause multiple authentication errors"
start on filesystem and started networking
stop on deconfiguring-networking
start on filesystem and static-network-up
stop on runlevel [016]
expect fork
respawn
exec /usr/bin/fail2ban-client -x -b start
env RUNDIR=/var/run/fail2ban
pre-start script
test -d $RUNDIR || mkdir -p $RUNDIR
test ! -e $RUNDIR/fail2ban.sock || rm -f $RUNDIR/fail2ban.sock
end script
exec /usr/bin/fail2ban-client -f -x start
pre-stop exec /usr/bin/fail2ban-client stop
post-stop exec rm -f /var/run/fail2ban/fail2ban.pid
post-stop exec rm -f $RUNDIR/fail2ban.pid

View File

@ -34,6 +34,12 @@ decrease verbosity
\fB\-x\fR
force execution of the server (remove socket file)
.TP
\fB\-b\fR
start the server in background mode (default)
.TP
\fB\-f\fR
start the server in foreground mode (note that the client forks once itself)
.TP
\fB\-h\fR, \fB\-\-help\fR
display this help message
.TP