From 359210f2247e8e845442879aaa581096642554dc Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Tue, 8 Oct 2013 20:37:33 +1100 Subject: [PATCH 01/89] ENH: filter.d/squirrelmail added --- config/filter.d/squirrelmail.conf | 4 ++++ config/jail.conf | 5 +++++ fail2ban/tests/files/logs/squirrelmail | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 config/filter.d/squirrelmail.conf create mode 100644 fail2ban/tests/files/logs/squirrelmail diff --git a/config/filter.d/squirrelmail.conf b/config/filter.d/squirrelmail.conf new file mode 100644 index 00000000..124ca2f8 --- /dev/null +++ b/config/filter.d/squirrelmail.conf @@ -0,0 +1,4 @@ + +[Definition] + +failregex = ^ \[LOGIN_ERROR\].*from : Unknown user or password incorrect.$ diff --git a/config/jail.conf b/config/jail.conf index 178a4c5f..3e87fa05 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -448,6 +448,11 @@ logpath = /var/log/mail.log port = imap2,imap3,imaps,pop3,pop3s logpath = /var/log/maillog +[squirrelmail] + +port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s,http,https,socks +logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log + # # DNS servers # diff --git a/fail2ban/tests/files/logs/squirrelmail b/fail2ban/tests/files/logs/squirrelmail new file mode 100644 index 00000000..3d1cf982 --- /dev/null +++ b/fail2ban/tests/files/logs/squirrelmail @@ -0,0 +1,3 @@ + +# failJSON: { "time": "2013-10-06T15:50:41", "match": true , "host": "151.64.44.11" } +10/06/2013 15:50:41 [LOGIN_ERROR] dadas (mydomain.org) from 151.64.44.11: Unknown user or password incorrect. From 6f104638cfa21ddcca3efc480c783a2d79c6295d Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 30 Dec 2013 22:31:06 +0000 Subject: [PATCH 02/89] BF: Ensure all imports for fail2ban modules are not relative --- fail2ban/client/actionreader.py | 3 ++- fail2ban/client/configreader.py | 3 ++- fail2ban/client/configurator.py | 7 ++++--- fail2ban/client/fail2banreader.py | 3 ++- fail2ban/client/filterreader.py | 3 ++- fail2ban/client/jailreader.py | 6 +++--- fail2ban/client/jailsreader.py | 5 +++-- fail2ban/server/actions.py | 9 +++++---- fail2ban/server/banmanager.py | 7 ++++--- fail2ban/server/datedetector.py | 4 ++-- fail2ban/server/datetemplate.py | 7 +++---- fail2ban/server/failmanager.py | 5 +++-- fail2ban/server/filter.py | 20 ++++++++++---------- fail2ban/server/filtergamin.py | 10 ++++++---- fail2ban/server/filterpoll.py | 8 ++++---- fail2ban/server/filterpyinotify.py | 7 +++---- fail2ban/server/filtersystemd.py | 6 +++--- fail2ban/server/jail.py | 2 +- fail2ban/server/jails.py | 6 +++--- fail2ban/server/server.py | 15 ++++++++------- 20 files changed, 73 insertions(+), 63 deletions(-) diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index 77ca1088..4452abe5 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, os -from configreader import ConfigReader, DefinitionInitConfigReader + +from fail2ban.client.configreader import ConfigReader, DefinitionInitConfigReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index ddbc48db..337eb31e 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import glob, logging, os -from configparserinc import SafeConfigParserWithIncludes from ConfigParser import NoOptionError, NoSectionError +from fail2ban.client.configparserinc import SafeConfigParserWithIncludes + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/configurator.py b/fail2ban/client/configurator.py index aa64704a..bd0fbb79 100644 --- a/fail2ban/client/configurator.py +++ b/fail2ban/client/configurator.py @@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging -from configreader import ConfigReader -from fail2banreader import Fail2banReader -from jailsreader import JailsReader + +from fail2ban.client.configreader import ConfigReader +from fail2ban.client.fail2banreader import Fail2banReader +from fail2ban.client.jailsreader import JailsReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/fail2banreader.py b/fail2ban/client/fail2banreader.py index d5d7285a..0df8d352 100644 --- a/fail2ban/client/fail2banreader.py +++ b/fail2ban/client/fail2banreader.py @@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging -from configreader import ConfigReader + +from fail2ban.client.configreader import ConfigReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index d8a6dbe8..27b3c3c4 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, os, shlex -from configreader import ConfigReader, DefinitionInitConfigReader + +from fail2ban.client.configreader import ConfigReader, DefinitionInitConfigReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index c17ea6c5..6e83f05c 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -26,9 +26,9 @@ __license__ = "GPL" import logging, re, glob, os.path -from configreader import ConfigReader -from filterreader import FilterReader -from actionreader import ActionReader +from fail2ban.client.configreader import ConfigReader +from fail2ban.client.filterreader import FilterReader +from fail2ban.client.actionreader import ActionReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/jailsreader.py b/fail2ban/client/jailsreader.py index c52f1b2b..284d18d8 100644 --- a/fail2ban/client/jailsreader.py +++ b/fail2ban/client/jailsreader.py @@ -25,8 +25,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging -from configreader import ConfigReader -from jailreader import JailReader + +from fail2ban.client.configreader import ConfigReader +from fail2ban.client.jailreader import JailReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index af2b4670..2c141895 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -24,12 +24,13 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -from banmanager import BanManager -from jailthread import JailThread -from action import Action -from mytime import MyTime import time, logging +from fail2ban.server.banmanager import BanManager +from fail2ban.server.jailthread import JailThread +from fail2ban.server.action import Action +from fail2ban.server.mytime import MyTime + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index ef3bf204..82027089 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -24,10 +24,11 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -from ticket import BanTicket -from threading import Lock -from mytime import MyTime import logging +from threading import Lock + +from fail2ban.server.ticket import BanTicket +from fail2ban.server.mytime import MyTime # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index 002a1070..91541b92 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -22,10 +22,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import sys, time, logging - -from datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601 from threading import Lock +from fail2ban.server.datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601 + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/datetemplate.py b/fail2ban/server/datetemplate.py index 320d9dc0..0aa20472 100644 --- a/fail2ban/server/datetemplate.py +++ b/fail2ban/server/datetemplate.py @@ -25,14 +25,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import re, time, calendar - +import logging from datetime import datetime from datetime import timedelta -from mytime import MyTime -import iso8601 +from fail2ban.server.mytime import MyTime +from fail2ban.server import iso8601 -import logging logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index f021a46d..6527e1ab 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -24,11 +24,12 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -from faildata import FailData -from ticket import FailTicket from threading import Lock import logging +from fail2ban.server.faildata import FailData +from fail2ban.server.ticket import FailTicket + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index aa534b73..d2ec1356 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -21,18 +21,18 @@ __author__ = "Cyril Jaquier and Fail2Ban Contributors" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __license__ = "GPL" -from failmanager import FailManagerEmpty -from failmanager import FailManager -from ticket import FailTicket -from jailthread import JailThread -from datedetector import DateDetector -from datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n -from mytime import MyTime -from failregex import FailRegex, Regex, RegexException -from action import Action - import logging, re, os, fcntl, time, sys, locale, codecs +from fail2ban.server.failmanager import FailManagerEmpty +from fail2ban.server.failmanager import FailManager +from fail2ban.server.ticket import FailTicket +from fail2ban.server.jailthread import JailThread +from fail2ban.server.datedetector import DateDetector +from fail2ban.server.datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n +from fail2ban.server.mytime import MyTime +from fail2ban.server.failregex import FailRegex, Regex, RegexException +from fail2ban.server.action import Action + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filtergamin.py b/fail2ban/server/filtergamin.py index b48bdd3c..73834522 100644 --- a/fail2ban/server/filtergamin.py +++ b/fail2ban/server/filtergamin.py @@ -23,11 +23,13 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko" __license__ = "GPL" -from failmanager import FailManagerEmpty -from filter import FileFilter -from mytime import MyTime +import time, logging, fcntl -import time, logging, gamin, fcntl +import gamin + +from fail2ban.server.failmanager import FailManagerEmpty +from fail2ban.server.filter import FileFilter +from fail2ban.server.mytime import MyTime # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py index 7fba65e1..54eea52a 100644 --- a/fail2ban/server/filterpoll.py +++ b/fail2ban/server/filterpoll.py @@ -24,12 +24,12 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko" __license__ = "GPL" -from failmanager import FailManagerEmpty -from filter import FileFilter -from mytime import MyTime - import time, logging, os +from fail2ban.server.failmanager import FailManagerEmpty +from fail2ban.server.filter import FileFilter +from fail2ban.server.mytime import MyTime + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py index 8a83e13a..68b0bd5e 100644 --- a/fail2ban/server/filterpyinotify.py +++ b/fail2ban/server/filterpyinotify.py @@ -24,13 +24,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y __license__ = "GPL" import time, logging, pyinotify - from distutils.version import LooseVersion from os.path import dirname, sep as pathsep -from failmanager import FailManagerEmpty -from filter import FileFilter -from mytime import MyTime +from fail2ban.server.failmanager import FailManagerEmpty +from fail2ban.server.filter import FileFilter +from fail2ban.server.mytime import MyTime if not hasattr(pyinotify, '__version__') \ diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index 180cb84b..1843b5ad 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -29,9 +29,9 @@ from systemd import journal if LooseVersion(getattr(journal, '__version__', "0")) < '204': raise ImportError("Fail2Ban requires systemd >= 204") -from failmanager import FailManagerEmpty -from filter import JournalFilter -from mytime import MyTime +from fail2ban.server.failmanager import FailManagerEmpty +from fail2ban.server.filter import JournalFilter +from fail2ban.server.mytime import MyTime # Gets the instance of the logger. diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index c4e091fe..83012075 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -25,7 +25,7 @@ __license__ = "GPL" import Queue, logging -from actions import Actions +from fail2ban.server.actions import Actions # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/jails.py b/fail2ban/server/jails.py index 12569d7f..07580b2e 100644 --- a/fail2ban/server/jails.py +++ b/fail2ban/server/jails.py @@ -21,11 +21,11 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko" __license__ = "GPL" -from fail2ban.exceptions import DuplicateJailException, UnknownJailException - -from jail import Jail from threading import Lock +from fail2ban.exceptions import DuplicateJailException, UnknownJailException +from fail2ban.server.jail import Jail + ## # Handles the jails. # diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 08e41003..805a52e0 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -25,15 +25,16 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" from threading import Lock, RLock -from jails import Jails -from filter import FileFilter, JournalFilter -from transmitter import Transmitter -from asyncserver import AsyncServer -from asyncserver import AsyncServerException -from database import Fail2BanDb -from fail2ban import version import logging, logging.handlers, sys, os, signal +from fail2ban.server.jails import Jails +from fail2ban.server.filter import FileFilter, JournalFilter +from fail2ban.server.transmitter import Transmitter +from fail2ban.server.asyncserver import AsyncServer +from fail2ban.server.asyncserver import AsyncServerException +from fail2ban.server.database import Fail2BanDb +from fail2ban import version + # Gets the instance of the logger. logSys = logging.getLogger(__name__) From e4a215ca5005ed0fe0b4477d2ec1b1226dcb06c2 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Tue, 31 Dec 2013 19:00:26 +1100 Subject: [PATCH 03/89] BF: fix infinite recursion case in Action.substituteRecursiveTags --- fail2ban/server/action.py | 14 +++++++++++--- fail2ban/tests/actiontestcase.py | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 17f33a6f..7d107d9f 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -276,19 +276,27 @@ class Action: for tag, value in tags.iteritems(): value = str(value) m = t.search(value) + done = [] + #logSys.log(5, 'TAG: %s, value: %s' % (tag, value)) while m: - if m.group(1) == tag: + found_tag = m.group(1) + #logSys.log(5, 'found: %s' % found_tag) + if found_tag == tag or found_tag in done: # recursive definitions are bad + #logSys.log(5, 'recursion fail') return False else: - if tags.has_key(m.group(1)): - value = value[0:m.start()] + tags[m.group(1)] + value[m.end():] + if tags.has_key(found_tag): + value = value[0:m.start()] + tags[found_tag] + value[m.end():] + #logSys.log(5, 'value now: %s' % value) + done.append(found_tag) m = t.search(value, m.start()) else: # Missing tags are ok so we just continue on searching. # cInfo can contain aInfo elements like and valid shell # constructs like . m = t.search(value, m.start() + 1) + #logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value)) tags[tag] = value return tags substituteRecursiveTags = staticmethod(substituteRecursiveTags) diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 36ecc3c9..c9364c07 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -58,6 +58,9 @@ class ExecuteAction(LogCaptureTestCase): self.assertFalse(Action.substituteRecursiveTags({'A': ''})) self.assertFalse(Action.substituteRecursiveTags({'A': '', 'B': ''})) self.assertFalse(Action.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) + # part recursion + self.assertFalse(Action.substituteRecursiveTags({'A': 'to= fromip=', 'C': '', 'B': '', 'D': ''})) + self.assertFalse(Action.substituteRecursiveTags({'failregex': 'to= fromip=', 'sweet': '', 'honeypot': '', 'ignoreregex': ''})) # missing tags are ok self.assertEqual(Action.substituteRecursiveTags({'A': ''}), {'A': ''}) self.assertEqual(Action.substituteRecursiveTags({'A': ' ','X':'fun'}), {'A': ' fun', 'X':'fun'}) From a4c38439df0b882e6b7333b00f3690966862557f Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Tue, 31 Dec 2013 19:01:21 +1100 Subject: [PATCH 04/89] ENH: add substition tags to filter definitions. Closes gh-539 --- MANIFEST | 1 + fail2ban/client/filterreader.py | 11 ++++++-- fail2ban/tests/clientreadertestcase.py | 28 +++++++++++++++++++ fail2ban/tests/files/filter.d/substition.conf | 8 ++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 fail2ban/tests/files/filter.d/substition.conf diff --git a/MANIFEST b/MANIFEST index 0b339b89..b03d309f 100644 --- a/MANIFEST +++ b/MANIFEST @@ -80,6 +80,7 @@ fail2ban/tests/files/config/apache-auth/README fail2ban/tests/files/config/apache-auth/noentry/.htaccess fail2ban/tests/files/database_v1.db fail2ban/tests/files/ignorecommand.py +fail2ban/tests/files/filter.d/substition.conf fail2ban/tests/files/filter.d/testcase-common.conf fail2ban/tests/files/filter.d/testcase01.conf fail2ban/tests/files/testcase01.log diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index d8a6dbe8..8ea854b6 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -26,6 +26,7 @@ __license__ = "GPL" import logging, os, shlex from configreader import ConfigReader, DefinitionInitConfigReader +from fail2ban.server.action import Action # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -42,14 +43,18 @@ class FilterReader(DefinitionInitConfigReader): def convert(self): stream = list() - for opt in self._opts: + combinedopts = dict(list(self._opts.items()) + list(self._initOpts.items())) + opts = Action.substituteRecursiveTags(combinedopts) + if not opts: + raise ValueError('recursive tag definitions unable to be resolved') + for opt, value in opts.iteritems(): if opt == "failregex": - for regex in self._opts[opt].split('\n'): + for regex in value.split('\n'): # Do not send a command if the rule is empty. if regex != '': stream.append(["set", self._jailName, "addfailregex", regex]) elif opt == "ignoreregex": - for regex in self._opts[opt].split('\n'): + for regex in value.split('\n'): # Do not send a command if the rule is empty. if regex != '': stream.append(["set", self._jailName, "addignoreregex", regex]) diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 647b9f86..23b5a4e0 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -308,6 +308,34 @@ class FilterReaderTest(unittest.TestCase): output[-1][-1] = "5" self.assertEqual(sorted(filterReader.convert()), sorted(output)) + + def testFilterReaderSubstitionDefault(self): + output = [['set', 'jailname', 'addfailregex', 'to=sweet@example.com fromip=']] + filterReader = FilterReader('substition', "jailname", {}) + filterReader.setBaseDir(TEST_FILES_DIR) + filterReader.read() + filterReader.getOptions(None) + c = filterReader.convert() + self.assertEqual(sorted(c), sorted(output)) + + def testFilterReaderSubstitionSet(self): + output = [['set', 'jailname', 'addfailregex', 'to=sour@example.com fromip=']] + filterReader = FilterReader('substition', "jailname", {'honeypot': 'sour@example.com'}) + filterReader.setBaseDir(TEST_FILES_DIR) + filterReader.read() + filterReader.getOptions(None) + c = filterReader.convert() + self.assertEqual(sorted(c), sorted(output)) + + def testFilterReaderSubstitionFail(self): + output = [['set', 'jailname', 'addfailregex', 'to=sour@example.com fromip=']] + filterReader = FilterReader('substition', "jailname", {'honeypot': '', 'sweet': ''}) + filterReader.setBaseDir(TEST_FILES_DIR) + filterReader.read() + filterReader.getOptions(None) + self.assertRaises(ValueError, FilterReader.convert, filterReader) + + class JailsReaderTest(LogCaptureTestCase): def testProvidingBadBasedir(self): diff --git a/fail2ban/tests/files/filter.d/substition.conf b/fail2ban/tests/files/filter.d/substition.conf new file mode 100644 index 00000000..aaf62eae --- /dev/null +++ b/fail2ban/tests/files/filter.d/substition.conf @@ -0,0 +1,8 @@ + +[Definition] + +failregex = to= fromip= + +[Init] + +honeypot = sweet@example.com From 1b037a6f291d8b2a0345372d5d1b4bcb454488ec Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Tue, 31 Dec 2013 19:15:11 +1100 Subject: [PATCH 05/89] DOC: document addition of filter options substitution into failregex/ignoreregex --- ChangeLog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 7610c0ff..b2a98046 100644 --- a/ChangeLog +++ b/ChangeLog @@ -62,7 +62,8 @@ configuration before relying on it. * Multiline regex for Disconnecting: Too many authentication failures for root [preauth]\nConnection closed by 6X.XXX.XXX.XXX [preauth] * Replacing use of deprecated API (.warning, .assertEqual, etc) - * [..a648cc2] Filters can have options now too + * [..a648cc2] Filters can have options now too which are substituted into + failregex / ignoreregex * [..e019ab7] Multiple instances of the same action are allowed in the same jail -- use actname option to disambiguate. From f37c90cdba47dfb2028941a0fc662166a87b1690 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Tue, 31 Dec 2013 18:54:34 +0000 Subject: [PATCH 06/89] ENH: Python based actions Python actions are imported from action.d config folder, which have .py file extension. This imports and creates an instance of the Action class (Action can be a variable that points to a class of another name). fail2ban.server.action.ActionBase is a base class which can be inherited from or as a minimum has a subclass hook which is used to ensure any imported actions implements the methods required. All calls to the execAction are also wrapped in a try except such that any errors won't cripple the jail. Action is renamed CommandAction, to clearly distinguish it from other actions. Include is an example smtp.py python action for sending emails via smtp. This is work in progress, as looking to add the and whois elements, and also SSL/TLS support. --- config/action.d/smtp.py | 119 ++++++++++++++++++++++ fail2ban/client/jailreader.py | 32 ++++-- fail2ban/protocol.py | 8 +- fail2ban/server/action.py | 118 ++++++++++++++-------- fail2ban/server/actions.py | 47 +++++++-- fail2ban/server/filter.py | 6 +- fail2ban/server/server.py | 125 ++++++++++++++++++------ fail2ban/server/transmitter.py | 7 +- fail2ban/tests/actionstestcase.py | 29 +++++- fail2ban/tests/actiontestcase.py | 31 +++--- fail2ban/tests/files/action.d/action.py | 22 +++++ fail2ban/tests/misctestcase.py | 2 +- fail2ban/tests/servertestcase.py | 16 +++ man/jail.conf.5 | 8 +- setup.py | 2 +- 15 files changed, 452 insertions(+), 120 deletions(-) create mode 100644 config/action.d/smtp.py create mode 100644 fail2ban/tests/files/action.d/action.py diff --git a/config/action.d/smtp.py b/config/action.d/smtp.py new file mode 100644 index 00000000..4ab76d3f --- /dev/null +++ b/config/action.d/smtp.py @@ -0,0 +1,119 @@ + +import sys +import socket +import smtplib +from email.mime.text import MIMEText +from email.utils import formatdate, formataddr + +from fail2ban.server.actions import ActionBase + +messages = {} +messages['start'] = \ +"""Hi, + +The jail %(jailname)s has been started successfully. + +Regards, +Fail2Ban""" + +messages['stop'] = \ +"""Hi, + +The jail %(jailname)s has been stopped. + +Regards, +Fail2Ban""" + +messages['ban'] = \ +"""Hi, + +The IP %(ip)s has just been banned for %(bantime)s seconds +by Fail2Ban after %(failures)i attempts against %(jailname)s. + +Regards, +Fail2Ban""" + +class SMTPAction(ActionBase): + + def __init__(self, jail, name, initOpts): + super(SMTPAction, self).__init__(jail, name, initOpts) + if initOpts is None: + initOpts = dict() # We have defaults for everything + self.host = initOpts.get('host', "localhost:25") + #TODO: self.ssl = initOpts.get('ssl', "no") == 'yes' + + self.user = initOpts.get('user', '') + self.password = initOpts.get('password', None) + + self.fromname = initOpts.get('sendername', "Fail2Ban") + self.fromaddr = initOpts.get('sender', "fail2ban") + self.toaddr = initOpts.get('dest', "root") + + self.smtp = smtplib.SMTP() + + def _sendMessage(self, subject, text): + msg = MIMEText(text) + msg['Subject'] = subject + msg['From'] = formataddr((self.fromname, self.fromaddr)) + msg['To'] = self.toaddr + msg['Date'] = formatdate() + + try: + self.logSys.debug("Connected to SMTP '%s', response: %i: %s", + *self.smtp.connect(self.host)) + if self.user and self.password: + smtp.login(self.user, self.password) + failed_recipients = self.smtp.sendmail( + self.fromaddr, self.toaddr, msg.as_string()) + except smtplib.SMTPConnectError: + self.logSys.error("Error connecting to host '%s'", self.host) + raise + except smtplib.SMTPAuthenticationError: + self.logSys.error( + "Failed to authenticate with host '%s' user '%s'", + self.host, self.user) + raise + except smtplib.SMTPException: + self.logSys.error( + "Error sending mail to host '%s' from '%s' to '%s'", + self.host, self.fromaddr, self.toaddr) + raise + else: + if failed_recipients: + self.logSys.warning( + "Email to '%s' failed to following recipients: %r", + self.toaddr, failed_recipients) + self.logSys.debug("Email '%s' successfully sent", subject) + finally: + try: + self.smtp.quit() + except smtplib.SMTPServerDisconnected: + pass # Not connected + + @property + def message_values(self): + return { + 'jailname': self.jail.getName(), + 'hostname': socket.gethostname(), + 'bantime': self.jail.getAction().getBanTime(), + } + + def execActionStart(self): + self._sendMessage( + "[Fail2Ban] %(jailname)s: started on %(hostname)s" % + self.message_values, + messages['start'] % self.message_values) + + def execActionStop(self): + self._sendMessage( + "[Fail2Ban] %(jailname)s: stopped on %(hostname)s" % + self.message_values, + messages['stop'] % self.message_values) + + def execActionBan(self, aInfo): + self._sendMessage( + "[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" % + dict(self.message_values, **aInfo), + messages['ban'] % dict(self.message_values, **aInfo)) + +Action = SMTPAction diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 6e83f05c..ab3e0aee 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -25,6 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, re, glob, os.path +import json from fail2ban.client.configreader import ConfigReader from fail2ban.client.filterreader import FilterReader @@ -120,14 +121,26 @@ class JailReader(ConfigReader): if not act: # skip empty actions continue actName, actOpt = JailReader.extractOptions(act) - action = ActionReader( - actName, self.__name, actOpt, basedir=self.getBaseDir()) - ret = action.read() - if ret: - action.getOptions(self.__opts) - self.__actions.append(action) + if actName.endswith(".py"): + self.__actions.append([ + "set", + self.__name, + "addaction", + actOpt.get("actname", os.path.splitext(actName)[0]), + os.path.join( + self.getBaseDir(), "action.d", actName), + json.dumps(actOpt), + ]) else: - raise AttributeError("Unable to read action") + action = ActionReader( + actName, self.__name, actOpt, + basedir=self.getBaseDir()) + ret = action.read() + if ret: + action.getOptions(self.__opts) + self.__actions.append(action) + else: + raise AttributeError("Unable to read action") except Exception, e: logSys.error("Error in action definition " + act) logSys.debug("Caught exception: %s" % (e,)) @@ -193,7 +206,10 @@ class JailReader(ConfigReader): if self.__filter: stream.extend(self.__filter.convert()) for action in self.__actions: - stream.extend(action.convert()) + if isinstance(action, ConfigReader): + stream.extend(action.convert()) + else: + stream.append(action) stream.insert(0, ["add", self.__name, backend]) return stream diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 99a45d25..0ec03b9e 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -76,7 +76,7 @@ protocol = [ ["set unbanip ", "manually Unban in "], ["set maxretry ", "sets the number of failures before banning the host for "], ["set maxlines ", "sets the number of to buffer for regex search for "], -["set addaction ", "adds a new action named for "], +["set addaction [ ]", "adds a new action named for . Optionally for a python based action, a and can be specified"], ["set delaction ", "removes the action from "], ["set setcinfo ", "sets for of the action for "], ["set delcinfo ", "removes for the action for "], @@ -125,13 +125,15 @@ def printFormatted(): print firstHeading = True first = True - for n in textwrap.wrap(m[1], WIDTH): + if len(m[0]) > MARGIN+INDENT: + m[1] = ' ' * WIDTH + m[1] + for n in textwrap.wrap(m[1], WIDTH, drop_whitespace=False): if first: line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n first = False else: line = ' ' * (INDENT + MARGIN) + n - print line + print line.rstrip() ## # Prints the protocol in a "mediawiki" format. diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 17f33a6f..209a452c 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -23,6 +23,7 @@ __license__ = "GPL" import logging, os, subprocess, time, signal, tempfile import threading, re +from abc import ABCMeta #from subprocess import call # Gets the instance of the logger. @@ -53,10 +54,63 @@ signame = dict((num, name) # action has to be taken. A BanManager take care of the banned IP # addresses. -class Action: +class ActionBase(object): + __metaclass__ = ABCMeta + + @classmethod + def __subclasshook__(cls, C): + required = ( + "getName", + "execActionStart", + "execActionStop", + "execActionBan", + "execActionUnban", + ) + for method in required: + if not callable(getattr(C, method, None)): + return False + return True + + def __init__(self, jail, name, initOpts=None): + self._jail = jail + self._name = name + self._logSys = logging.getLogger( + '%s.%s' % (__name__, self.__class__.__name__)) + + @property + def jail(self): + return self._jail + + @property + def logSys(self): + return self._logSys + + ## + # Returns the action name. + # + # @return the name of the action + + def getName(self): + return self._name + + name = property(getName) + + def execActionStart(self): + pass + + def execActionBan(self, aInfo): + pass + + def execActionUnban(self, aInfo): + pass + + def execActionStop(self): + pass + +class CommandAction(ActionBase): def __init__(self, name): - self.__name = name + super(CommandAction, self).__init__(None, name) self.__timeout = 60 self.__cInfo = dict() ## Command executed in order to initialize the system. @@ -71,22 +125,10 @@ class Action: self.__actionStop = '' logSys.debug("Created Action") - ## - # Sets the action name. - # - # @param name the name of the action - - def setName(self, name): - self.__name = name - - ## - # Returns the action name. - # - # @return the name of the action - - def getName(self): - return self.__name - + @classmethod + def __subclasshook__(cls, C): + return NotImplemented # Standard checks + ## # Sets the timeout period for commands. # @@ -94,7 +136,7 @@ class Action: def setTimeout(self, timeout): self.__timeout = int(timeout) - logSys.debug("Set action %s timeout = %i" % (self.__name, timeout)) + logSys.debug("Set action %s timeout = %i" % (self.getName(), timeout)) ## # Returns the action timeout period for commands. @@ -160,11 +202,11 @@ class Action: def execActionStart(self): if self.__cInfo: - if not Action.substituteRecursiveTags(self.__cInfo): + if not self.substituteRecursiveTags(self.__cInfo): logSys.error("Cinfo/definitions contain self referencing definitions and cannot be resolved") return False - startCmd = Action.replaceTag(self.__actionStart, self.__cInfo) - return Action.executeCmd(startCmd, self.__timeout) + startCmd = self.replaceTag(self.__actionStart, self.__cInfo) + return self.executeCmd(startCmd, self.__timeout) ## # Set the "ban" command. @@ -259,8 +301,8 @@ class Action: # @return True if the command succeeded def execActionStop(self): - stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo) - return Action.executeCmd(stopCmd, self.__timeout) + stopCmd = self.replaceTag(self.__actionStop, self.__cInfo) + return self.executeCmd(stopCmd, self.__timeout) ## # Sort out tag definitions within other tags @@ -270,7 +312,7 @@ class Action: # b = _3 b = 3_3 # @param tags, a dictionary # @returns tags altered or False if there is a recursive definition - #@staticmethod + @staticmethod def substituteRecursiveTags(tags): t = re.compile(r'<([^ >]+)>') for tag, value in tags.iteritems(): @@ -291,15 +333,13 @@ class Action: m = t.search(value, m.start() + 1) tags[tag] = value return tags - substituteRecursiveTags = staticmethod(substituteRecursiveTags) - #@staticmethod + @staticmethod def escapeTag(tag): for c in '\\#&;`|*?~<>^()[]{}$\'"': if c in tag: tag = tag.replace(c, '\\' + c) return tag - escapeTag = staticmethod(escapeTag) ## # Replaces tags in query with property values in aInfo. @@ -308,8 +348,8 @@ class Action: # @param aInfo the properties # @return a string - #@staticmethod - def replaceTag(query, aInfo): + @classmethod + def replaceTag(cls, query, aInfo): """ Replace tags in query """ string = query @@ -321,12 +361,11 @@ class Action: if tag.endswith('matches'): # That one needs to be escaped since its content is # out of our control - value = Action.escapeTag(value) + value = cls.escapeTag(value) string = string.replace('<' + tag + '>', value) # New line string = string.replace("
", '\n') return string - replaceTag = staticmethod(replaceTag) ## # Executes a command with preliminary checks and substitutions. @@ -348,26 +387,26 @@ class Action: logSys.debug("Nothing to do") return True - checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo) - if not Action.executeCmd(checkCmd, self.__timeout): + checkCmd = self.replaceTag(self.__actionCheck, self.__cInfo) + if not self.executeCmd(checkCmd, self.__timeout): logSys.error("Invariant check failed. Trying to restore a sane" + " environment") self.execActionStop() self.execActionStart() - if not Action.executeCmd(checkCmd, self.__timeout): + if not self.executeCmd(checkCmd, self.__timeout): logSys.fatal("Unable to restore environment") return False # Replace tags if not aInfo is None: - realCmd = Action.replaceTag(cmd, aInfo) + realCmd = self.replaceTag(cmd, aInfo) else: realCmd = cmd # Replace static fields - realCmd = Action.replaceTag(realCmd, self.__cInfo) + realCmd = self.replaceTag(realCmd, self.__cInfo) - return Action.executeCmd(realCmd, self.__timeout) + return self.executeCmd(realCmd, self.__timeout) ## # Executes a command. @@ -381,7 +420,7 @@ class Action: # @param realCmd the command to execute # @return True if the command succeeded - #@staticmethod + @staticmethod def executeCmd(realCmd, timeout=60): logSys.debug(realCmd) if not realCmd: @@ -440,5 +479,4 @@ class Action: logSys.info("HINT on %i: %s" % (retcode, msg % locals())) return False - executeCmd = staticmethod(executeCmd) diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 2c141895..73c005ef 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -25,10 +25,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import time, logging +import os +import imp from fail2ban.server.banmanager import BanManager from fail2ban.server.jailthread import JailThread -from fail2ban.server.action import Action +from fail2ban.server.action import ActionBase, CommandAction from fail2ban.server.mytime import MyTime # Gets the instance of the logger. @@ -62,11 +64,24 @@ class Actions(JailThread): # # @param name The action name - def addAction(self, name): + def addAction(self, name, pythonModule=None, initOpts=None): # Check is action name already exists if name in [action.getName() for action in self.__actions]: raise ValueError("Action %s already exists" % name) - action = Action(name) + if pythonModule is None: + action = CommandAction(name) + else: + pythonModuleName = os.path.basename(pythonModule.strip(".py")) + customActionModule = imp.load_source( + pythonModuleName, pythonModule) + if not hasattr(customActionModule, "Action"): + raise RuntimeError( + "%s module does not have 'Action' class" % pythonModule) + elif not issubclass(customActionModule.Action, ActionBase): + raise RuntimeError( + "%s module %s does not implment required methods" % ( + pythonModule, customActionModule.Action.__name__)) + action = customActionModule.Action(self.jail, name, initOpts) self.__actions.append(action) ## @@ -153,7 +168,11 @@ class Actions(JailThread): def run(self): self.setActive(True) for action in self.__actions: - action.execActionStart() + try: + action.execActionStart() + except Exception as e: + logSys.error("Failed to start jail '%s' action '%s': %s", + self.jail.getName(), action.getName(), e) while self._isActive(): if not self.getIdle(): #logSys.debug(self.jail.getName() + ": action") @@ -165,7 +184,11 @@ class Actions(JailThread): time.sleep(self.getSleepTime()) self.__flushBan() for action in self.__actions: - action.execActionStop() + try: + action.execActionStop() + except Exception as e: + logSys.error("Failed to stop jail '%s' action '%s': %s", + self.jail.getName(), action.getName(), e) logSys.debug(self.jail.getName() + ": action terminated") return True @@ -201,7 +224,12 @@ class Actions(JailThread): if self.__banManager.addBanTicket(bTicket): logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"])) for action in self.__actions: - action.execActionBan(aInfo) + try: + action.execActionBan(aInfo) + except Exception as e: + logSys.error( + "Failed to execute ban jail '%s' action '%s': %s", + self.jail.getName(), action.getName(), e) return True else: logSys.info("[%s] %s already banned" % (self.jail.getName(), @@ -241,7 +269,12 @@ class Actions(JailThread): aInfo["matches"] = "".join(ticket.getMatches()) logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"])) for action in self.__actions: - action.execActionUnban(aInfo) + try: + action.execActionUnban(aInfo) + except Exception as e: + logSys.error( + "Failed to execute unban jail '%s' action '%s': %s", + self.jail.getName(), action.getName(), e) ## diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index d2ec1356..645ed30e 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -31,7 +31,7 @@ from fail2ban.server.datedetector import DateDetector from fail2ban.server.datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n from fail2ban.server.mytime import MyTime from fail2ban.server.failregex import FailRegex, Regex, RegexException -from fail2ban.server.action import Action +from fail2ban.server.action import CommandAction # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -378,9 +378,9 @@ class Filter(JailThread): return True if self.__ignoreCommand: - command = Action.replaceTag(self.__ignoreCommand, { 'ip': ip } ) + command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } ) logSys.debug('ignore command: ' + command) - return Action.executeCmd(command) + return CommandAction.executeCmd(command) return False diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 805a52e0..bc7bc4ba 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -33,6 +33,7 @@ from fail2ban.server.transmitter import Transmitter from fail2ban.server.asyncserver import AsyncServer from fail2ban.server.asyncserver import AsyncServerException from fail2ban.server.database import Fail2BanDb +from fail2ban.server.action import CommandAction from fail2ban import version # Gets the instance of the logger. @@ -278,8 +279,8 @@ class Server: return self.__jails.getFilter(name).getMaxLines() # Action - def addAction(self, name, value): - self.__jails.getAction(name).addAction(value) + def addAction(self, name, value, *args): + self.__jails.getAction(name).addAction(value, *args) def getLastAction(self, name): return self.__jails.getAction(name).getLastAction() @@ -290,14 +291,26 @@ class Server: def delAction(self, name, value): self.__jails.getAction(name).delAction(value) - def setCInfo(self, name, action, key, value): - self.__jails.getAction(name).getAction(action).setCInfo(key, value) + def setCInfo(self, name, actionName, key, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setCInfo(key, value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getCInfo(self, name, action, key): - return self.__jails.getAction(name).getAction(action).getCInfo(key) + def getCInfo(self, name, actionName, key): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getCInfo(key) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def delCInfo(self, name, action, key): - self.__jails.getAction(name).getAction(action).delCInfo(key) + def delCInfo(self, name, actionName, key): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.delCInfo(key) + else: + raise TypeError("%s is not a CommandAction" % actionName) def setBanTime(self, name, value): self.__jails.getAction(name).setBanTime(value) @@ -311,41 +324,89 @@ class Server: def getBanTime(self, name): return self.__jails.getAction(name).getBanTime() - def setActionStart(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionStart(value) + def setActionStart(self, name, actionName, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setActionStart(value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getActionStart(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionStart() + def getActionStart(self, name, actionName): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getActionStart() + else: + raise TypeError("%s is not a CommandAction" % actionName) - def setActionStop(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionStop(value) + def setActionStop(self, name, actionName, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setActionStop(value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getActionStop(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionStop() + def getActionStop(self, name, actionName): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getActionStop() + else: + raise TypeError("%s is not a CommandAction" % actionName) - def setActionCheck(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionCheck(value) + def setActionCheck(self, name, actionName, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setActionCheck(value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getActionCheck(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionCheck() + def getActionCheck(self, name, actionName): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getActionCheck() + else: + raise TypeError("%s is not a CommandAction" % actionName) - def setActionBan(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionBan(value) + def setActionBan(self, name, actionName, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setActionBan(value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getActionBan(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionBan() + def getActionBan(self, name, actionName): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getActionBan() + else: + raise TypeError("%s is not a CommandAction" % actionName) - def setActionUnban(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionUnban(value) + def setActionUnban(self, name, actionName, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setActionUnban(value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getActionUnban(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionUnban() + def getActionUnban(self, name, actionName): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getActionUnban() + else: + raise TypeError("%s is not a CommandAction" % actionName) - def setActionTimeout(self, name, action, value): - self.__jails.getAction(name).getAction(action).setTimeout(value) + def setActionTimeout(self, name, actionName, value): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + action.setTimeout(value) + else: + raise TypeError("%s is not a CommandAction" % actionName) - def getActionTimeout(self, name, action): - return self.__jails.getAction(name).getAction(action).getTimeout() + def getActionTimeout(self, name, actionName): + action = self.__jails.getAction(name).getAction(actionName) + if isinstance(action, CommandAction): + return action.getTimeout() + else: + raise TypeError("%s is not a CommandAction" % actionName) # Status def status(self): diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 5af19c5a..10ada98a 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -25,6 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, time +import json # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -228,8 +229,10 @@ class Transmitter: value = command[2] return self.__server.setUnbanIP(name,value) elif command[1] == "addaction": - value = command[2] - self.__server.addAction(name, value) + args = [command[2]] + if len(command) > 3: + args.extend([command[3], json.loads(command[4])]) + self.__server.addAction(name, *args) return self.__server.getLastAction(name).getName() elif command[1] == "delaction": value = command[2] diff --git a/fail2ban/tests/actionstestcase.py b/fail2ban/tests/actionstestcase.py index 5697db37..87631dc2 100644 --- a/fail2ban/tests/actionstestcase.py +++ b/fail2ban/tests/actionstestcase.py @@ -26,18 +26,24 @@ __license__ = "GPL" import unittest, time import sys, os, tempfile -from fail2ban.server.actions import Actions -from dummyjail import DummyJail -class ExecuteActions(unittest.TestCase): +from fail2ban.server.actions import Actions +from fail2ban.tests.dummyjail import DummyJail +from fail2ban.tests.utils import LogCaptureTestCase + +TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") + +class ExecuteActions(LogCaptureTestCase): def setUp(self): """Call before every test case.""" + super(ExecuteActions, self).setUp() self.__jail = DummyJail() self.__actions = Actions(self.__jail) self.__tmpfile, self.__tmpfilename = tempfile.mkstemp() def tearDown(self): + super(ExecuteActions, self).tearDown() os.remove(self.__tmpfilename) def defaultActions(self): @@ -77,3 +83,20 @@ class ExecuteActions(unittest.TestCase): self.assertEqual(self.__actions.status(),[("Currently banned", 0 ), ("Total banned", 0 ), ("IP list", [] )]) + + def testAddActionPython(self): + self.__actions.addAction( + "Action", os.path.join(TEST_FILES_DIR, "action.d/action.py"), {}) + + self.assertTrue(self._is_logged("TestAction initialised")) + + self.__actions.start() + time.sleep(3) + self.assertTrue(self._is_logged("TestAction action start")) + + self.__actions.stop() + self.__actions.join() + self.assertTrue(self._is_logged("TestAction action stop")) + + self.assertRaises(IOError, + self.__actions.addAction, "Action3", "/does/not/exist.py", {}) diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 36ecc3c9..0c7f98fb 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -27,7 +27,7 @@ __license__ = "GPL" import time import logging, sys -from fail2ban.server.action import Action +from fail2ban.server.action import CommandAction from fail2ban.tests.utils import LogCaptureTestCase @@ -35,7 +35,7 @@ class ExecuteAction(LogCaptureTestCase): def setUp(self): """Call before every test case.""" - self.__action = Action("Test") + self.__action = CommandAction("Test") LogCaptureTestCase.setUp(self) def tearDown(self): @@ -43,11 +43,6 @@ class ExecuteAction(LogCaptureTestCase): LogCaptureTestCase.tearDown(self) self.__action.execActionStop() - def testNameChange(self): - self.assertEqual(self.__action.getName(), "Test") - self.__action.setName("Tricky Test") - self.assertEqual(self.__action.getName(), "Tricky Test") - def testSubstituteRecursiveTags(self): aInfo = { 'HOST': "192.0.2.0", @@ -55,15 +50,15 @@ class ExecuteAction(LogCaptureTestCase): 'xyz': "890 ", } # Recursion is bad - self.assertFalse(Action.substituteRecursiveTags({'A': '
'})) - self.assertFalse(Action.substituteRecursiveTags({'A': '', 'B': ''})) - self.assertFalse(Action.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) + self.assertFalse(CommandAction.substituteRecursiveTags({'A': ''})) + self.assertFalse(CommandAction.substituteRecursiveTags({'A': '', 'B': ''})) + self.assertFalse(CommandAction.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) # missing tags are ok - self.assertEqual(Action.substituteRecursiveTags({'A': ''}), {'A': ''}) - self.assertEqual(Action.substituteRecursiveTags({'A': ' ','X':'fun'}), {'A': ' fun', 'X':'fun'}) - self.assertEqual(Action.substituteRecursiveTags({'A': ' ', 'B': 'cool'}), {'A': ' cool', 'B': 'cool'}) + self.assertEqual(CommandAction.substituteRecursiveTags({'A': ''}), {'A': ''}) + self.assertEqual(CommandAction.substituteRecursiveTags({'A': ' ','X':'fun'}), {'A': ' fun', 'X':'fun'}) + self.assertEqual(CommandAction.substituteRecursiveTags({'A': ' ', 'B': 'cool'}), {'A': ' cool', 'B': 'cool'}) # rest is just cool - self.assertEqual(Action.substituteRecursiveTags(aInfo), + self.assertEqual(CommandAction.substituteRecursiveTags(aInfo), { 'HOST': "192.0.2.0", 'ABC': '123 192.0.2.0', 'xyz': '890 123 192.0.2.0', @@ -169,20 +164,20 @@ class ExecuteAction(LogCaptureTestCase): self.assertTrue(self._is_logged('Nothing to do')) def testExecuteIncorrectCmd(self): - Action.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null') + CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null') self.assertTrue(self._is_logged('HINT on 127: "Command not found"')) def testExecuteTimeout(self): stime = time.time() - Action.executeCmd('sleep 60', timeout=2) # Should take a minute + CommandAction.executeCmd('sleep 60', timeout=2) # Should take a minute self.assertAlmostEqual(time.time() - stime, 2, places=0) self.assertTrue(self._is_logged('sleep 60 -- timed out after 2 seconds')) self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM')) def testCaptureStdOutErr(self): - Action.executeCmd('echo "How now brown cow"') + CommandAction.executeCmd('echo "How now brown cow"') self.assertTrue(self._is_logged("'How now brown cow\\n'")) - Action.executeCmd( + CommandAction.executeCmd( 'echo "The rain in Spain stays mainly in the plain" 1>&2') self.assertTrue(self._is_logged( "'The rain in Spain stays mainly in the plain\\n'")) diff --git a/fail2ban/tests/files/action.d/action.py b/fail2ban/tests/files/action.d/action.py new file mode 100644 index 00000000..5dcafe31 --- /dev/null +++ b/fail2ban/tests/files/action.d/action.py @@ -0,0 +1,22 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(ActionBase): + + def __init__(self, *args, **kwargs): + super(TestAction, self).__init__(*args, **kwargs) + self.logSys.debug("%s initialised" % self.__class__.__name__) + + def execActionStart(self, *args, **kwargs): + self.logSys.debug("%s action start" % self.__class__.__name__) + + def execActionStop(self, *args, **kwargs): + self.logSys.debug("%s action stop" % self.__class__.__name__) + + def execActionBan(self, *args, **kwargs): + self.logSys.debug("%s action ban" % self.__class__.__name__) + + def execActionUnban(self, *args, **kwargs): + self.logSys.debug("%s action unban" % self.__class__.__name__) + +Action = TestAction diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index 7c6e3283..54ef502d 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -28,7 +28,7 @@ import shutil from glob import glob -from utils import mbasename, TraceBack, FormatterWithTraceBack +from fail2ban.tests.utils import mbasename, TraceBack, FormatterWithTraceBack from fail2ban.helpers import formatExceptionInfo class HelpersTest(unittest.TestCase): diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 66fc5b7c..a2d90db1 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -564,6 +564,22 @@ class Transmitter(TransmitterBase): self.assertEqual( self.transm.proceed( ["set", self.jailName, "delaction", "Doesn't exist"])[0],1) + self.assertEqual( + self.transm.proceed(["set", self.jailName, "addaction", action, + os.path.join(TEST_FILES_DIR, "action.d", "action.py"), "{}"]), + (0, action)) + for cmd, value in zip(cmdList, cmdValueList): + self.assertTrue( + isinstance(self.transm.proceed( + ["set", self.jailName, cmd, action, value])[1], + TypeError), + "set %s for python action did not raise TypeError" % cmd) + for cmd, value in zip(cmdList, cmdValueList): + self.assertTrue( + isinstance(self.transm.proceed( + ["get", self.jailName, cmd, action])[1], + TypeError), + "get %s for python action did not raise TypeError" % cmd) def testNOK(self): self.assertEqual(self.transm.proceed(["INVALID", "COMMAND"])[0],1) diff --git a/man/jail.conf.5 b/man/jail.conf.5 index f384fc8e..2fef6aa7 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -7,7 +7,7 @@ jail.conf \- configuration for the fail2ban server .I jail.conf / jail.local -.I action.d/*.conf action.d/*.local +.I action.d/*.conf action.d/*.local action.d/*.py .I filter.d/*.conf filter.d/*.local .SH DESCRIPTION @@ -113,13 +113,15 @@ uses systemd python library to access the systemd journal. Specifying \fBlogpath will try to use the following backends, in order: pyinotify, gamin, polling .PP .SS Actions -Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename. In the case where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplicatione.g.: +Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename, and in the case of python actions, the ".py" file extension is stripped. Where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplication e.g.: .PP .nf [ssh-iptables-ipset] enabled = true action = sendmail[name=ssh, dest=john@example.com, actname=mail-john] sendmail[name=ssh, dest=paul@example.com, actname=mail-paul] + smtp.py[dest=chris@example.com, actname=smtp-chris] + smtp.py[dest=sally@example.com, actname=smtp-sally] .fi .SH "ACTION FILES" @@ -160,6 +162,8 @@ two commands to be executed. actionban = iptables -I fail2ban- --source -j DROP echo ip=, match=, time=_3 b = 3_3 - # @param tags, a dictionary - # @returns tags altered or False if there is a recursive definition @staticmethod def substituteRecursiveTags(tags): + """Sort out tag definitions within other tags. + + so: becomes: + a = 3 a = 3 + b = _3 b = 3_3 + + Parameters + ---------- + tags : dict + Dictionary of tags(keys) and their values. + + Returns + ------- + dict + Dictionary of tags(keys) and their values, with tags + within the values recursively replaced. + """ t = re.compile(r'<([^ >]+)>') for tag, value in tags.iteritems(): value = str(value) @@ -296,22 +389,46 @@ class CommandAction(ActionBase): return tags @staticmethod - def escapeTag(tag): - for c in '\\#&;`|*?~<>^()[]{}$\'"': - if c in tag: - tag = tag.replace(c, '\\' + c) - return tag + def escapeTag(value): + """Escape characters which may be used for command injection. + + Parameters + ---------- + value : str + A string of which characters will be escaped. + + Returns + ------- + str + `value` with certain characters escaped. + + Notes + ----- + The following characters are escaped:: + + \\#&;`|*?~<>^()[]{}$'" + + """ + for c in '\\#&;`|*?~<>^()[]{}$\'"': + if c in value: + value = value.replace(c, '\\' + c) + return value - ## - # Replaces tags in query with property values in aInfo. - # - # @param query the query string with tags - # @param aInfo the properties - # @return a string - @classmethod def replaceTag(cls, query, aInfo): - """ Replace tags in query + """Replaces tags in `query` with property values. + + Parameters + ---------- + query : str + String with tags. + aInfo : dict + Tags(keys) and associated values for substitution in query. + + Returns + ------- + str + `query` string with tags replaced. """ string = query for tag in aInfo: @@ -325,27 +442,31 @@ class CommandAction(ActionBase): # New line string = string.replace("
", '\n') return string - - ## - # Executes a command with preliminary checks and substitutions. - # - # Before executing any commands, executes the "check" command first - # in order to check if pre-requirements are met. If this check fails, - # it tries to restore a sane environment before executing the real - # command. - # Replaces "aInfo" and "cInfo" in the query too. - # - # @param cmd The command to execute - # @param aInfo Dynamic properties - # @return True if the command succeeded - + def _processCmd(self, cmd, aInfo = None): - """ Executes an OS command. + """Executes a command with preliminary checks and substitutions. + + Before executing any commands, executes the "check" command first + in order to check if pre-requirements are met. If this check fails, + it tries to restore a sane environment before executing the real + command. + + Parameters + ---------- + cmd : str + The command to execute. + aInfo : dictionary + Dynamic properties. + + Returns + ------- + bool + True if the command succeeded. """ if cmd == "": self._logSys.debug("Nothing to do") return True - + checkCmd = self.replaceTag(self.actioncheck, self._properties) if not self.executeCmd(checkCmd, self.timeout): self._logSys.error( @@ -367,20 +488,29 @@ class CommandAction(ActionBase): return self.executeCmd(realCmd, self.timeout) - ## - # Executes a command. - # - # We need a shell here because commands are mainly shell script. They - # contain pipe, redirection, etc. - # - # @todo Force the use of bash!? - # @todo Kill the command after a given timeout - # - # @param realCmd the command to execute - # @return True if the command succeeded - @staticmethod def executeCmd(realCmd, timeout=60): + """Executes a command. + + Parameters + ---------- + realCmd : str + The command to execute. + timeout : int + The time out in seconds for the command. + + Returns + ------- + bool + True if the command succeeded. + + Raises + ------ + OSError + If command fails to be executed. + RuntimeError + If command execution times out. + """ logSys.debug(realCmd) if not realCmd: logSys.debug("Nothing to do") @@ -410,7 +540,6 @@ class CommandAction(ActionBase): retcode = popen.poll() except OSError, e: logSys.error("%s -- failed with %s" % (realCmd, e)) - return False finally: _cmd_lock.release() From 69a850d226161bfe394bc37ec1ca5e173fe8eb6b Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sat, 4 Jan 2014 22:46:57 +0000 Subject: [PATCH 35/89] DOC: Update docstrings for smtp.py action --- config/action.d/smtp.py | 66 +++++++++++++++++++++++++++++++------- fail2ban/server/actions.py | 7 ++-- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/config/action.d/smtp.py b/config/action.d/smtp.py index 8d1b0c95..065a0bba 100644 --- a/config/action.d/smtp.py +++ b/config/action.d/smtp.py @@ -75,18 +75,38 @@ class SMTPAction(ActionBase): """ def __init__( - self, jail, actionname, host="localhost", user=None, password=None, + self, jail, name, host="localhost", user=None, password=None, sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None): - """SMTPAction is initiliased with a Fail2Ban `jail` instance, and - an `actionname`. `host` is the SMTP host, which can include port - number in "host:port" format. `user` and `password` can be specified - for SMTP authentication. `sendername` and `sender` is the email - address and readable name. `dest` is the email address of intended - recipient(s) in comma delimited format. `matches` can be one of - `matches`, `ipmatches` and `ipjailmatches` (see man jail.conf.5). + """Initialise action. + + Parameters + ---------- + jail : Jail + The jail which the action belongs to. + name : str + Named assigned to the action. + host : str, optional + SMTP host, of host:port format. Default host "localhost" and + port "25" + user : str, optional + Username used for authentication with SMTP server. + password : str, optional + Password used for authentication with SMTP server. + sendername : str, optional + Name to use for from address in email. Default "Fail2Ban". + sender : str, optional + Email address to use for from address in email. + Default "fail2ban". + dest : str, optional + Email addresses of intended recipient(s) in comma delimited + format. Default "root". + matches : str, optional + Type of matches to be included from ban in email. Can be one + of "matches", "ipmatches" or "ipjailmatches". Default None + (see man jail.conf.5). """ - super(SMTPAction, self).__init__(jail, actionname) + super(SMTPAction, self).__init__(jail, name) self.host = host #TODO: self.ssl = ssl @@ -107,6 +127,25 @@ class SMTPAction(ActionBase): ) def _sendMessage(self, subject, text): + """Sends message based on arguments and instance's properties. + + Parameters + ---------- + subject : str + Subject of the email. + text : str + Body of the email. + + Raises + ------ + SMTPConnectionError + Error on connecting to host. + SMTPAuthenticationError + Error authenticating with SMTP server. + SMTPException + See Python `smtplib` for full list of other possible + exceptions. + """ msg = MIMEText(text) msg['Subject'] = subject msg['From'] = formataddr((self.fromname, self.fromaddr)) @@ -164,8 +203,13 @@ class SMTPAction(ActionBase): messages['stop'] % self.message_values) def ban(self, aInfo): - """Sends email to recipients informing that ban has occurred and - has associated information about the ban. + """Sends email to recipients informing that ban has occurred. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. """ aInfo.update(self.message_values) message = "".join([ diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index a07afeeb..2426747d 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -72,11 +72,12 @@ class Actions(JailThread, Mapping): ---------- name : str The name of the action. - pythonModule : str + pythonModule : str, optional Path to Python file which must contain `Action` class. - initOpts : dict + Default None, which means `CommandAction` is used. + initOpts : dict, optional Options for Python Action, used as keyword arguments for - initialisation. + initialisation. Default None. Raises ------ From cfcf841ae4324a8a87b410ae60424f7bb7adb923 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sat, 4 Jan 2014 23:07:59 +0000 Subject: [PATCH 36/89] TST: Added some more tests for Python actions --- fail2ban/tests/actionstestcase.py | 24 +++++++++++++++++++ .../tests/files/action.d/action_errors.py | 21 ++++++++++++++++ .../tests/files/action.d/action_noAction.py | 5 ++++ .../tests/files/action.d/action_nomethod.py | 12 ++++++++++ 4 files changed, 62 insertions(+) create mode 100644 fail2ban/tests/files/action.d/action_errors.py create mode 100644 fail2ban/tests/files/action.d/action_noAction.py create mode 100644 fail2ban/tests/files/action.d/action_nomethod.py diff --git a/fail2ban/tests/actionstestcase.py b/fail2ban/tests/actionstestcase.py index 83319cb8..ccc0263d 100644 --- a/fail2ban/tests/actionstestcase.py +++ b/fail2ban/tests/actionstestcase.py @@ -55,6 +55,10 @@ class ExecuteActions(LogCaptureTestCase): self.__ip.actioncheck = 'echo ip check >> "%s"' % self.__tmpfilename self.__ip.actionstop = 'echo ip stop >> "%s"' % self.__tmpfilename + def testActionsAddDuplicateName(self): + self.__actions.add('test') + self.assertRaises(ValueError, self.__actions.add, 'test') + def testActionsManipulation(self): self.__actions.add('test') self.assertTrue(self.__actions['test']) @@ -115,3 +119,23 @@ class ExecuteActions(LogCaptureTestCase): self.assertRaises( TypeError, self.__actions.add, "Action5", os.path.join(TEST_FILES_DIR, "action.d/action.py"), {}) + + def testAddPythonActionNOK(self): + self.assertRaises(RuntimeError, self.__actions.add, + "Action", os.path.join(TEST_FILES_DIR, + "action.d/action_noAction.py"), + {}) + self.assertRaises(RuntimeError, self.__actions.add, + "Action", os.path.join(TEST_FILES_DIR, + "action.d/action_nomethod.py"), + {}) + self.__actions.add( + "Action", os.path.join(TEST_FILES_DIR, + "action.d/action_errors.py"), + {}) + self.__actions.start() + time.sleep(3) + self.assertTrue(self._is_logged("Failed to start")) + self.__actions.stop() + self.__actions.join() + self.assertTrue(self._is_logged("Failed to stop")) diff --git a/fail2ban/tests/files/action.d/action_errors.py b/fail2ban/tests/files/action.d/action_errors.py new file mode 100644 index 00000000..767848c1 --- /dev/null +++ b/fail2ban/tests/files/action.d/action_errors.py @@ -0,0 +1,21 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(ActionBase): + + def __init__(self, jail, name): + super(TestAction, self).__init__(jail, name) + + def start(self): + raise Exception() + + def stop(self): + raise Exception() + + def ban(self): + raise Exception() + + def unban(self): + raise Exception() + +Action = TestAction diff --git a/fail2ban/tests/files/action.d/action_noAction.py b/fail2ban/tests/files/action.d/action_noAction.py new file mode 100644 index 00000000..1aa25e67 --- /dev/null +++ b/fail2ban/tests/files/action.d/action_noAction.py @@ -0,0 +1,5 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(ActionBase): + pass diff --git a/fail2ban/tests/files/action.d/action_nomethod.py b/fail2ban/tests/files/action.d/action_nomethod.py new file mode 100644 index 00000000..8b9fd981 --- /dev/null +++ b/fail2ban/tests/files/action.d/action_nomethod.py @@ -0,0 +1,12 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(): + + def __init__(self, jail, name): + pass + + def start(self): + pass + +Action = TestAction From 6602937ee161bec57f4bd1453c2644f2982c6797 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 11:24:20 +1100 Subject: [PATCH 37/89] DOC: filter.d./pure-ftpd doco from wiki --- config/filter.d/pure-ftpd.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/filter.d/pure-ftpd.conf b/config/filter.d/pure-ftpd.conf index 1698c9ec..e96009b2 100644 --- a/config/filter.d/pure-ftpd.conf +++ b/config/filter.d/pure-ftpd.conf @@ -1,7 +1,11 @@ # Fail2Ban filter for pureftp # +# Disable hostname based logging by: +# +# Start pure-ftpd with the -H switch or on Ubuntu 'echo yes > /etc/pure-ftpd/conf/DontResolve' # # + [INCLUDES] before = common.conf @@ -17,3 +21,4 @@ ignoreregex = # Author: Cyril Jaquier # Modified: Yaroslav Halchenko for pure-ftpd +# Documentation thanks to Blake on http://www.fail2ban.org/wiki/index.php?title=Fail2ban:Community_Portal From c37ee4cc52701a4e2158c29fe7a39e0bd0e14fa9 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 11:30:56 +1100 Subject: [PATCH 38/89] DOC: filter.d/vsftpd doco from wiki --- config/filter.d/vsftpd.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/filter.d/vsftpd.conf b/config/filter.d/vsftpd.conf index 59ce49a3..4de2befb 100644 --- a/config/filter.d/vsftpd.conf +++ b/config/filter.d/vsftpd.conf @@ -1,5 +1,8 @@ # Fail2Ban filter for vsftp # +# Configure VSFTP for "dual_log_enable=YES", and have fail2ban watch +# /var/log/vsftpd.log instead of /var/log/secure. vsftpd.log file shows the +# incoming ip address rather than domain names. [INCLUDES] @@ -16,3 +19,4 @@ failregex = ^%(__prefix_line)s%(__pam_re)s\s+authentication failure; logname=\S* ignoreregex = # Author: Cyril Jaquier +# Documentation from fail2ban wiki From 6ce2ba289548e1da5decda538452f00e29ab7309 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 11:48:35 +1100 Subject: [PATCH 39/89] ENH: additional phpmyadmin tips from Tom on http://www.fail2ban.org/wiki/index.php?title=Fail2ban:Community_Portal. Block is now a prefix of a path --- config/filter.d/apache-botsearch.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/filter.d/apache-botsearch.conf b/config/filter.d/apache-botsearch.conf index f3bb6e70..95df97ca 100644 --- a/config/filter.d/apache-botsearch.conf +++ b/config/filter.d/apache-botsearch.conf @@ -21,13 +21,13 @@ ignoreregex = # Webroot represents the webroot on which all other files are based webroot = /var/www/ # Block is the actual non-found directories to block -block = (||) +block = (||)[^,]* # These are just convient definitions that assist the blocking of stuff that # isn't installed webmail = roundcube|mail|horde|webmail -phpmyadmin = (typo3/|xampp/|)(pma|(php)?myadmin) +phpmyadmin = (typo3/|xampp/|admin/|)(pma|(php)?[Mm]y[Aa]dmin) wordpress = wp-(login|signup)\.php From 51f014fedec58fd497b352bc7c9cf38fd3f816f2 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 18:24:13 +1100 Subject: [PATCH 40/89] DOC: add more content to jail.conf man page --- man/jail.conf.5 | 63 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/man/jail.conf.5 b/man/jail.conf.5 index 4d964341..94329fc8 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -3,22 +3,24 @@ jail.conf \- configuration for the fail2ban server .SH SYNOPSIS -.I fail2ban.conf fail2ban.d/*.conf fail2ban.d/*.local +.I fail2ban.conf fail2ban.d/*.conf / fail2ban.local / fail2ban.d/*.local -.I jail.conf / jail.local +.I jail.conf / jail.d/*.conf / jail.local / jail.d/*.local .I action.d/*.conf action.d/*.local .I filter.d/*.conf filter.d/*.local + .SH DESCRIPTION -Fail2ban has three configuration file types. Action files are the commands for banning and unbanning of IP address, -Filter files tell fail2ban how to detect authentication failures, and Jail configurations combine filters with actions into jails. +Fail2ban has four configuration file types. Failban configuration files that contain global configuration items, Action configuration files are the commands for banning and unbanning of IP address, Filter configuration files tell fail2ban how to detect authentication failures, and Jail configuration files combine filters with actions into jails. + +.SH "CONFIGUATION FILES" There are *.conf files that are distributed by fail2ban and *.local file that contain user customizations. It is recommended that *.conf files should remain unchanged. If needed, customizations should be provided in *.local files. For instance, if you would like to customize the [ssh-iptables-ipset] jail, create a jail.local to extend jail.conf (the configuration for the fail2ban server). The jail.local file will be the following if you only need to enable -it: +it as follows: .TP \fIjail.local\fR @@ -32,7 +34,7 @@ Override only the settings you need to change and the rest of the configuration \fI*.d/\fR .RS -In addition to .local, for any .conf file there can be a corresponding +In addition to .local, for any jail.conf or fail2ban.conf file there can be a corresponding \fI.d/\fR directory to contain additional .conf files that will be read after the appropriate .local file. Last parsed file will take precidence over identical entries, parsed alphabetically, e.g. @@ -55,43 +57,74 @@ jail.d/*.conf (in alphabetical order), jail.local, followed by jail.d/*.local (in alphabetical order). -Likewise for fail2ban configuration. +Likewise for fail2ban configuration except the filenames/directories begin with "fail2ban" and not "jail". -Comments: use '#' for comment lines and ';' (following a space) for inline comments +Configuration files have sections, those specified with [section name], and name = value pairs. For those name items that can accept multiple values, specify the values separated by spaces, or new lines between the values which also requires space at the beginning of the line before the second value.. +Comments: use '#' for comment lines and ';' (following a space) for inline comments. When using Python2.X ';' can only be used on the first line due to an Python library bug. -.SH DEFAULT -The following options are applicable to all jails. Their meaning is described in the default \fIjail.conf\fR file. +.SH "FAIL2BAN CONFIGURATION FILES" + +These files have one section, [Definition]. + +The items that can be set are: +.TP +\fBloglevel\fR +Set the log level output. , 1 = ERROR, 2 = WARN, 3 = INFO, 4 = DEBUG. Default: 1 +.TP +\fBlogtarget\fR +Set the log target. This could be a file, SYSLOG, STDERR or STDOUT. Only one log target can be specified. +If you change logtarget from the default value and you are using logrotate -- also adjust or disable rotation in the +corresponding configuration file (e.g. /etc/logrotate.d/fail2ban on Debian systems). Values can be [ STDOUT | STDERR | SYSLOG | FILE ] Default: STDERR. +.TP +\fBsocket\fR +Set the socket file. This is used to communicate with the fail2ban server daemon. Do not remove this file when Fail2ban runs. It will not be possible to communicate with the server afterwards. Default: /var/run/fail2ban/fail2ban.sock +.TP +\fBpidfile\fR +Set the PID file. This is used to store the process ID of the fail2ban server. +# Values: [ FILE ] Default: /var/run/fail2ban/fail2ban.pid + +.SH "JAIL CONFIGURATION FILES" +The following options are applicable to all jails. They appear in a section specifing the jail name or in the \fI[DEFAULT]\fR section which is used if individual sections don't have a value specified. .TP \fBfilter\fR +This maps to the filename of the filter in /etc/fail2ban/filter.d/ without the .conf/.local extension. Only one filter can be specified. .TP \fBlogpath\fR +This is the log filename(s). Globs, like paths containing * and ? or [0-9], can be used however only the files that exist at startup matching this glob pattern will be read. .TP \fBaction\fR +This maps to the action(s) from \fI/etc/fail2ban/action.d/\fR without the \fI.conf\fR/\fI.local\fR extension. Arguements can be passed to actions to override the default values from the [Init] section. Arguements are specified by [name=value,name2=value]. Values can also be quoted. More that one action can be specified. .TP \fBignoreip\fR -A space separated list of IPs not to ban. +A list of IPs not to ban. These can include a CIDR mask too. .TP \fBignorecommand\fR A command that is executed to determine if the current ban's actionban is to be executed. This command will return true if the current ban should be ignored. A false return value will result in the ban's actionban executed. Like ACTION FILES, tags like are can be included in the ignore command value and will be substitued before execution. Currently only is supported however more will be added later. .TP \fBbantime\fR +When a ban occurs the ban stays in effect for this long (measured in seconds) after which the actionunban command in the action(s) are called. .TP \fBfindtime\fR +Specifies the time interval in seconds before the current time where failures will count towards a ban. .TP \fBmaxretry\fR +This is the number of failures that can occur in the last findtime seconds before a ban of that IP will result. .TP \fBbackend\fR +This is the backend used to detect changes in the logpath. It defaults to "auto" which will try "pyinotify", "gamin" before "polling". Any of these can be specified. "pyinotify" is only valid on Linux systems with the "pyinotify" Python libraries. "gamin" requires the "gamin" libraries. .TP \fBusedns\fR +This tells fail2ban to use DNS to resolve HOST names that appear in the logs. By default it is "warn" which will preform the resolving hostnames to IPs however it will also log a warning. If you are using DNS here you could be blocking the wrong IPs due to the asymetric nature of reverse DNS (that the application used to write the domain name to log) compared to forward DNS that fail2ban uses to resolve this back to an IP (but not necessarly the same one). Idealy configure your applications to log a real IP. This can be set to "yes" to prevent warnings in the log or "no" to disable DNS resolution. .TP \fBfailregex\fR +Here a failregex can be added which is effectively added to the filter's failregexes. If this is useful for others using your application please tell the fail2ban developers by reporting an issue (REPORTING BUGS below). .TP \fBignoreregex\fR +Here you can specify a Python regex that when applied to a log file line will be ignored. This will be ignored even if it matches a failregex of the jail or any of its filters. - -.SH "ACTION FILES" +.SH "ACTION CONFIGURATION FILES" Action files specify which commands are executed to ban and unban an IP address. They are located under \fI/etc/fail2ban/action.d\fR. Like with jail.conf files, if you desire local changes create an \fI[actionname].local\fR file in the \fI/etc/fail2ban/action.d\fR directory @@ -140,7 +173,7 @@ An IPv4 ip address to be banned. e.g. 192.168.0.2 The number of times the failure occurred in the log file. e.g. 3 .TP \fBtime\fR -The unix time of the ban. e.g. 1357508484 +The unix (epoch) time of the ban. e.g. 1357508484 .TP \fBmatches\fR The concatenated string of the log file lines of the matches that generated the ban. Many characters interpreted by shell get escaped. @@ -183,7 +216,7 @@ indicates that this file is read after the [Definition] section. .SH AUTHOR Fail2ban was originally written by Cyril Jaquier . -At the moment it is maintained and further developed by Yaroslav O. Halchenko and a number of contributors. See \fBTHANKS\fR file shipped with Fail2Ban for a full list. +At the moment it is maintained and further developed by Yaroslav O. Halchenko , Daniel Black and Steven Hiscocks along with a number of contributors. See \fBTHANKS\fR file shipped with Fail2Ban for a full list. . Manual page written by Daniel Black and Yaroslav Halchenko. .SH "REPORTING BUGS" From 3d21c4edf9b04ac4355afd61bec3df87a90bc261 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 18:53:31 +1100 Subject: [PATCH 41/89] DOC: consistent filename separator in SYNOPIS --- man/jail.conf.5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/jail.conf.5 b/man/jail.conf.5 index 94329fc8..79324258 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -3,9 +3,9 @@ jail.conf \- configuration for the fail2ban server .SH SYNOPSIS -.I fail2ban.conf fail2ban.d/*.conf / fail2ban.local / fail2ban.d/*.local +.I fail2ban.conf fail2ban.d/*.conf fail2ban.local fail2ban.d/*.local -.I jail.conf / jail.d/*.conf / jail.local / jail.d/*.local +.I jail.conf jail.d/*.conf jail.local jail.d/*.local .I action.d/*.conf action.d/*.local From a9f804e443a723170c86891a8adc826950aff390 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 21:03:16 +1100 Subject: [PATCH 42/89] ENH: complete stock jail.conf to contain all filters --- config/jail.conf | 167 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 4 deletions(-) diff --git a/config/jail.conf b/config/jail.conf index 1d58edbe..1db5b5da 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -75,6 +75,22 @@ usedns = warn # The mail-whois action send a notification e-mail with a whois request # in the body. +[pam-generic] + +enabled = false +filter = pam-generic +action = iptables-allports[name=pam,protocol=all] +logpath = /var/log/secure + + +[xinetd-fail] + +enabled = false +filter = xinetd-fail +action = iptables-allports[name=xinetd,protocol=all] +logpath = /var/log/daemon*log + + [ssh-iptables] enabled = false @@ -84,6 +100,25 @@ action = iptables[name=SSH, port=ssh, protocol=tcp] logpath = /var/log/sshd.log maxretry = 5 + +[ssh-ddos] + +enabled = false +filter = sshd-ddos +action = iptables[name=SSHDDOS, port=ssh, protocol=tcp] +logpath = /var/log/sshd.log +maxretry = 2 + + +[dropbear] + +enabled = false +filter = dropbear +action = iptables[name=dropbear, port=ssh, protocol=tcp] +logpath = /var/log/messages +maxretry = 5 + + [proftpd-iptables] enabled = false @@ -94,6 +129,35 @@ logpath = /var/log/proftpd/proftpd.log maxretry = 6 +[gssftpd-iptables] + +enabled = false +filter = gssftpd +action = iptables[name=GSSFTPd, port=ftp, protocol=tcp] + sendmail-whois[name=GSSFTPd, dest=you@example.com] +logpath = /var/log/daemon.log +maxretry = 6 + + +[pure-ftpd] + +enabled = false +filter = pure-ftpd +action = iptables[name=pureftpd, port=ftp, protocol=tcp] +logpath = /var/log/pureftpd.log +maxretry = 6 + + +[wuftpd] + +enabled = false +filter = wuftpd +action = iptables[name=wuftpd, port=ftp, protocol=tcp] +logpath = /var/log/daemon.log +maxretry = 6 + + + # This jail forces the backend to "polling". [sasl-iptables] @@ -197,7 +261,26 @@ logpath = /var/log/apache*/*error.log maxretry = 2 -[nginx-http-auth] +[apache-overflows] + +enabled = false +filter = apache-overflows +action = iptables-multiport[name=apache-overflows,port="80,443"] +logpath = /var/log/apache*/*error.log + /home/www/myhomepage/error.log +maxretry = 2 + + +[apache-nohome] + +enabled = false +filter = apache-nohome +action = iptables-multiport[name=apache-nohome,port="80,443"] +logpath = /var/log/apache*/*error.log + /home/www/myhomepage/error.log +maxretry = 2 + + [nginx-http-auth] enabled = false @@ -206,6 +289,14 @@ action = iptables-multiport[name=nginx-http-auth,port="80,443"] logpath = /var/log/nginx/error.log +[squid] + +enabled = false +filter = squid +action = iptables-multiport[name=squid,port="80,443,8080"] +logpath = /var/log/squid/access.log + + # The hosts.deny path can be defined with the "file" argument if it is # not in /etc. [postfix-tcpwrapper] @@ -218,6 +309,46 @@ logpath = /var/log/postfix.log bantime = 300 +[cyrus-imap] + +enabled = false +filter = cyrus-imap +action = iptables-multiport[name=cyrus-imap,port="143,993"] +logpath = /var/log/mail*log + + +[courierlogin] + +enabled = false +filter = courierlogin +action = iptables-multiport[name=courierlogin,port="25,110,143,465,587,993,995"] +logpath = /var/log/mail*log + + +[couriersmtp] + +enabled = false +filter = couriersmtp +action = iptables-multiport[name=couriersmtp,port="25,465,587"] +logpath = /var/log/mail*log + + +[qmail-rbl] + +enabled = false +filter = qmail +action = iptables-multiport[name=qmail-rbl,port="25,465,587"] +logpath = /service/qmail/log/main/current + + +[sieve] + +enabled = false +filter = sieve +action = iptables-multiport[name=sieve,port="25,465,587"] +logpath = /var/log/mail*log + + # Do not ban anybody. Just report information about the remote host. # A notification is sent at most every 600 seconds (bantime). [vsftpd-notification] @@ -295,6 +426,25 @@ action = ipfw maxretry = 5 +[horde] + +enabled = false +filter = horde +logpath = /var/log/horde/horde.log +action = iptables-multiport[name=horde, port="http,https"] +maxretry = 5 + + +# Ban attackers that try to use PHP's URL-fopen() functionality +# through GET/POST variables. - Experimental, with more than a year +# of usage in production environments. +[php-url-fopen] + +enabled = false +action = iptables-multiport[name=php-url-open, port="http,https"] +filter = php-url-fopen +logpath = /var/www/*/logs/access_log +maxretry = 1 # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year # of usage in production environments. @@ -372,6 +522,15 @@ logpath = /var/log/named/security.log ignoreip = 168.192.0.1 +[nsd] + +enabled = false +filter = nsd +action = iptables-multiport[name=nsd-tcp, port="domain", protocol=tcp] + iptables-multiport[name=nsd-udp, port="domain", protocol=udp] +logpath = /var/log/nsd.log + + [asterisk] enabled = false @@ -565,9 +724,9 @@ logpath = /var/log/mail.log [selinux-ssh] -enabled = false -filter = selinux-ssh -action = iptables[name=SELINUX-SSH, port=ssh, protocol=tcp] +enabled = false +filter = selinux-ssh +action = iptables[name=SELINUX-SSH, port=ssh, protocol=tcp] logpath = /var/log/audit/audit.log maxretry = 5 From c70091015559917e1c5c3a2879a891433711b70e Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 21:06:30 +1100 Subject: [PATCH 43/89] TST: ensure stock jail has all filters --- client/jailreader.py | 4 ++++ client/jailsreader.py | 3 +++ testcases/clientreadertestcase.py | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/client/jailreader.py b/client/jailreader.py index fb92d806..0750d25e 100644 --- a/client/jailreader.py +++ b/client/jailreader.py @@ -43,7 +43,11 @@ class JailReader(ConfigReader): self.__filter = None self.__force_enable = force_enable self.__actions = list() + self.__opts = None + def getRawOptions(self): + return self.__opts + def setName(self, value): self.__name = value diff --git a/client/jailsreader.py b/client/jailsreader.py index d32e6561..9c7c3dee 100644 --- a/client/jailsreader.py +++ b/client/jailsreader.py @@ -45,6 +45,9 @@ class JailsReader(ConfigReader): self.__jails = list() self.__force_enable = force_enable + def getJails(self): + return self.__jails + def read(self): return ConfigReader.read(self, "jail") diff --git a/testcases/clientreadertestcase.py b/testcases/clientreadertestcase.py index 28387703..97f22dd4 100644 --- a/testcases/clientreadertestcase.py +++ b/testcases/clientreadertestcase.py @@ -21,7 +21,7 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __license__ = "GPL" -import os, tempfile, shutil, unittest +import os, glob, tempfile, shutil, unittest from client.configreader import ConfigReader from client.jailreader import JailReader @@ -251,6 +251,7 @@ class JailsReaderTest(LogCaptureTestCase): comm_commands = jails.convert() # by default None of the jails is enabled and we get no # commands to communicate to the server + self.maxDiff = None self.assertEqual(comm_commands, []) # We should not "read" some bogus jail @@ -260,6 +261,20 @@ class JailsReaderTest(LogCaptureTestCase): # and there should be no side-effects self.assertEqual(jails.convert(), old_comm_commands) + def testReadSockJailConfComplete(self): + jails = JailsReader(basedir='config', force_enable=True) + self.assertTrue(jails.read()) # opens fine + self.assertTrue(jails.getOptions()) # reads fine + # grab all filter names + filters = set(os.path.splitext(os.path.split(a)[1])[0] + for a in glob.glob(os.path.join('config', 'filter.d', '*.conf')) + if not a.endswith('common.conf')) + filters_jail = set(jail.getRawOptions()['filter'] for jail in jails.getJails()) + self.maxDiff = None + self.assertTrue(filters.issubset(filters_jail), + "More filters exists than are referenced in stock jail.conf %r" % filters.difference(filters_jail)) + self.assertTrue(filters_jail.issubset(filters), + "Stock jail.conf references non-existent filters %r" % filters_jail.difference(filters)) def testReadStockJailConfForceEnabled(self): # more of a smoke test to make sure that no obvious surprises From a8e04983896d20a11edbb812a7e0c7b3f12c28b9 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 21:26:26 +1100 Subject: [PATCH 44/89] BF: add expression for ssh filter for code 3: SSH2_DISCONNECT_KEY_EXCHANGE_FAILED. closes gh-289 --- ChangeLog | 2 ++ config/filter.d/sshd.conf | 1 + testcases/files/logs/sshd | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/ChangeLog b/ChangeLog index b412c0f3..9ca3382b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -46,6 +46,8 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better - exim-spam filter to match spamassassin log entry for option SAdevnull. Thanks Ivo Truxa. Closes gh-533 - Added filter.d/openwebmail filter thanks Ivo Truxa. Closes gh-543 + - Added to sshd filter expression for "Received disconnect from : 3: + ...: Auth fail". Thanks Marcel Dopita. Closes gh-289 - New Features: diff --git a/config/filter.d/sshd.conf b/config/filter.d/sshd.conf index a36b050c..9d289e87 100644 --- a/config/filter.d/sshd.conf +++ b/config/filter.d/sshd.conf @@ -21,6 +21,7 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|erro ^%(__prefix_line)sUser .+ from not allowed because listed in DenyUsers\s*$ ^%(__prefix_line)sUser .+ from not allowed because not in any group\s*$ ^%(__prefix_line)srefused connect from \S+ \(\)\s*$ + ^%(__prefix_line)sReceived disconnect from : 3: \S+: Auth fail$ ^%(__prefix_line)sUser .+ from not allowed because a group is listed in DenyGroups\s*$ ^%(__prefix_line)sUser .+ from not allowed because none of user's groups are listed in AllowGroups\s*$ diff --git a/testcases/files/logs/sshd b/testcases/files/logs/sshd index 3c50dcfd..639c0cba 100644 --- a/testcases/files/logs/sshd +++ b/testcases/files/logs/sshd @@ -103,3 +103,7 @@ Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 po # failJSON: { "time": "2004-11-11T08:04:51", "match": true , "host": "127.0.0.1", "desc": "Injecting on username ssh 'from 10.10.1.1'@localhost" } Nov 11 08:04:51 redbamboo sshd[2737]: Failed password for invalid user from 10.10.1.1 from 127.0.0.1 port 58946 ssh2 + +# failJSON: { "time": "2005-07-13T18:44:28", "match": true , "host": "89.24.13.192", "desc": "from gh-289" } +Jul 13 18:44:28 mdop sshd[4931]: Received disconnect from 89.24.13.192: 3: com.jcraft.jsch.JSchException: Auth fail + From 1c5787174f462efbcc907d0679957774146b32a8 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 5 Jan 2014 23:25:49 +1100 Subject: [PATCH 45/89] BF: escape . in stunnel filter --- config/filter.d/stunnel.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/filter.d/stunnel.conf b/config/filter.d/stunnel.conf index e9bd349e..f6097e92 100644 --- a/config/filter.d/stunnel.conf +++ b/config/filter.d/stunnel.conf @@ -4,7 +4,7 @@ failregex = ^ LOG\d\[\d+:\d+\]:\ SSL_accept from :\d+ : (?P[\dA-F]+): error:(?P=CODE):SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate$ -datepattern = ^%Y.%m.%d %H:%M:%S +datepattern = ^%Y\.%m\.%d %H:%M:%S # DEV NOTES: # From f137c7b10774151b8d36b2c9aabf92819f24287e Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 6 Jan 2014 09:53:54 +1100 Subject: [PATCH 46/89] BF: stunnel doesnt need datepattern as its inbuilt --- config/filter.d/stunnel.conf | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/filter.d/stunnel.conf b/config/filter.d/stunnel.conf index f6097e92..c49bab4b 100644 --- a/config/filter.d/stunnel.conf +++ b/config/filter.d/stunnel.conf @@ -4,8 +4,6 @@ failregex = ^ LOG\d\[\d+:\d+\]:\ SSL_accept from :\d+ : (?P[\dA-F]+): error:(?P=CODE):SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate$ -datepattern = ^%Y\.%m\.%d %H:%M:%S - # DEV NOTES: # # Author: Daniel Black From 95add8a1c5cf2e6a91518ed528512038b6fe66e6 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 6 Jan 2014 09:55:53 +1100 Subject: [PATCH 47/89] BF: datepattern handling in fail2ban-regex --- bin/fail2ban-regex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/fail2ban-regex b/bin/fail2ban-regex index ea1ac226..cfaa4a89 100755 --- a/bin/fail2ban-regex +++ b/bin/fail2ban-regex @@ -280,6 +280,9 @@ class Fail2banRegex(object): elif command[2] == 'addjournalmatch': journalmatch = command[3] self.setJournalMatch(shlex.split(journalmatch)) + elif command[2] == 'datepattern': + datepattern = command[3] + self.setDatePattern(datepattern) else: print "ERROR: failed to read %s" % value return False From 5428f5bbc3bf9bf347244376cd289ba5dd5551d1 Mon Sep 17 00:00:00 2001 From: alasdairdc Date: Mon, 6 Jan 2014 10:43:32 +0000 Subject: [PATCH 48/89] Update check_fail2ban Removed unnecessary reference to as yet undeclared $jail_name when checking a specific jail. --- files/nagios/check_fail2ban | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/nagios/check_fail2ban b/files/nagios/check_fail2ban index 9bf14305..90c58713 100755 --- a/files/nagios/check_fail2ban +++ b/files/nagios/check_fail2ban @@ -190,7 +190,7 @@ if ($jail_specific) { else { $how_many_banned = int($current_ban_number); $return_print = $how_many_banned.' current banned IP(s) for the specific jail '.$jail_specific; - $perf_print .= "$jail_name.currentBannedIP=$current_ban_number " if ($perfdata_value); + $perf_print .= "$current_ban_number " if ($perfdata_value); } } ### To analyze all the jail From 67c44a500199b10d7dd5cddc792ea486adb7730d Mon Sep 17 00:00:00 2001 From: alasdairdc Date: Mon, 6 Jan 2014 10:44:21 +0000 Subject: [PATCH 49/89] Update ChangeLog --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index 77fd6a03..ffe27d73 100644 --- a/ChangeLog +++ b/ChangeLog @@ -27,6 +27,7 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better - complain action - ensure where not matching other IPs in log sample. Closes gh-467 - Fix firewall-cmd actioncheck - patch from Adam Tkac. Redhat Bug #979622 + - Removed unnecessary reference to as yet undeclared $jail_name when checking a specific jail. - Enhancements: - long names on jails documented based on iptables limit of 30 less From 50eab4df815e3ccb8b7d98040dfc52980be6b59b Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 6 Jan 2014 21:56:22 +1100 Subject: [PATCH 50/89] ENH: add filter groupoffice. Closes gh-566 --- MANIFEST | 2 ++ config/filter.d/groupoffice.conf | 14 ++++++++++++++ testcases/files/logs/groupoffice | 4 ++++ 3 files changed, 20 insertions(+) create mode 100644 config/filter.d/groupoffice.conf create mode 100644 testcases/files/logs/groupoffice diff --git a/MANIFEST b/MANIFEST index a8195b54..98a98532 100644 --- a/MANIFEST +++ b/MANIFEST @@ -59,6 +59,7 @@ testcases/files/logs/assp testcases/files/logs/asterisk testcases/files/logs/dovecot testcases/files/logs/exim +testcases/files/logs/groupoffice testcases/files/logs/suhosin testcases/files/logs/mysqld-auth testcases/files/logs/named-refused @@ -181,6 +182,7 @@ config/filter.d/3proxy.conf config/filter.d/apache-common.conf config/filter.d/exim-common.conf config/filter.d/exim-spam.conf +config/filter.d/groupoffice.conf config/filter.d/perdition.conf config/filter.d/uwimap-auth.conf config/action.d/apf.conf diff --git a/config/filter.d/groupoffice.conf b/config/filter.d/groupoffice.conf new file mode 100644 index 00000000..d5a4e4d8 --- /dev/null +++ b/config/filter.d/groupoffice.conf @@ -0,0 +1,14 @@ +# Fail2Ban filter for Group-Office +# +# Enable logging with: +# $config['info_log']='/home/groupoffice/log/info.log'; +# + +[Definition] + +failregex = ^\[\]LOGIN FAILED for user: "\S+" from IP: $ + + + +# Author: Daniel Black + diff --git a/testcases/files/logs/groupoffice b/testcases/files/logs/groupoffice new file mode 100644 index 00000000..7809f018 --- /dev/null +++ b/testcases/files/logs/groupoffice @@ -0,0 +1,4 @@ +# failJSON: { "time": "2014-01-06T10:59:38", "match": true, "host": "127.0.0.1" } +[2014-01-06 10:59:38]LOGIN FAILED for user: "asdsad" from IP: 127.0.0.1 +# failJSON: { "time": "2014-01-06T10:59:49", "match": false, "host": "127.0.0.1" } +[2014-01-06 10:59:49]LOGIN SUCCESS for user: "admin" from IP: 127.0.0.1 From db7b7bfefacdbc03fdeb3cc4e0de16f8438eda14 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 6 Jan 2014 22:00:12 +1100 Subject: [PATCH 51/89] Credits for groupoffice --- ChangeLog | 2 ++ THANKS | 1 + 2 files changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 9ca3382b..1e5ab543 100644 --- a/ChangeLog +++ b/ChangeLog @@ -46,6 +46,8 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better - exim-spam filter to match spamassassin log entry for option SAdevnull. Thanks Ivo Truxa. Closes gh-533 - Added filter.d/openwebmail filter thanks Ivo Truxa. Closes gh-543 + - Added filter.d/groupoffice filter thanks to logs from Merijn Schering. + Closes gh-566 - Added to sshd filter expression for "Received disconnect from : 3: ...: Auth fail". Thanks Marcel Dopita. Closes gh-289 diff --git a/THANKS b/THANKS index 04aa1f20..efbcbe75 100644 --- a/THANKS +++ b/THANKS @@ -61,6 +61,7 @@ Markus Hoffmann Marvin Rouge mEDI Мернов Георгий +Merijn Schering Michael C. Haller Michael Hanselmann Nick Munger From 1386d82102c6dd7f495b94f32b7ce84ae24805b6 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Mon, 6 Jan 2014 18:53:23 +0000 Subject: [PATCH 52/89] DOC: Update action files section of jail.conf, and add "timeout" option Closes gh-565 --- man/jail.conf.5 | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/man/jail.conf.5 b/man/jail.conf.5 index 9131c4b5..07e2c30e 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -132,8 +132,6 @@ and override the required settings. Action files are ini files that have two sections, \fBDefinition\fR and \fBInit\fR . -The [Init] section allows for action-specific settings. In \fIjail.conf/jail.local\fR these can be overwritten for a particular jail as options to the jail. - The following commands can be present in the [Definition] section. .TP \fBactionstart\fR @@ -150,20 +148,22 @@ command(s) that bans the IP address after \fBmaxretry\fR log lines matches withi .TP \fBactionunban\fR command(s) that unbans the IP address after \fBbantime\fR. - +.PP +The [Init] section allows for action-specific settings. In \fIjail.conf/jail.local\fR these can be overwritten for a particular jail as options to the jail. The following are special tags which can be set in the [Init] section: +.TP +\fBtimeout\fR +The maximum period of time in seconds that a command can executed, before being killed. +.PP Commands specified in the [Definition] section are executed through a system shell so shell redirection and process control is allowed. The commands should -return 0, otherwise error would be logged. Moreover if \fBactioncheck\fR exits with non-0 status, it is taken as indication that firewall status has changed and fail2ban needs to reinitialize itself (i.e. issue \fBactionstop\fR and \fBactionstart\fR commands). - +return 0, otherwise error would be logged. Moreover if \fBactioncheck\fR exits with non-0 status, it is taken as indication that firewall status has changed and fail2ban needs to reinitialize itself (i.e. issue \fBactionstop\fR and \fBactionstart\fR commands). Tags are enclosed in <>. All the elements of [Init] are tags that are replaced in all action commands. Tags can be added by the -\fBfail2ban-client\fR using the setctag command. \fB
\fR is a tag that is always a new line (\\n). +\fBfail2ban-client\fR using the "set action " command. \fB
\fR is a tag that is always a new line (\\n). More than a single command is allowed to be specified. Each command needs to be on a separate line and indented with whitespaces without blank lines. The following example defines two commands to be executed. actionban = iptables -I fail2ban- --source -j DROP echo ip=, match=, time=