From dff8909473e19814b0fcf1d23563692c17c5bd11 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sun, 9 Feb 2014 12:23:14 +0000 Subject: [PATCH 1/5] ENH: Add badips.com reporting and blacklisting action (python based) --- config/action.d/badips.py | 309 +++++++++++++++++++++++++ fail2ban/server/actions.py | 9 +- fail2ban/tests/action_d/test_badips.py | 89 +++++++ 3 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 config/action.d/badips.py create mode 100644 fail2ban/tests/action_d/test_badips.py diff --git a/config/action.d/badips.py b/config/action.d/badips.py new file mode 100644 index 00000000..d09b905d --- /dev/null +++ b/config/action.d/badips.py @@ -0,0 +1,309 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import json +from functools import partial +import threading +import sys +if sys.version_info >= (3, ): + from urllib.request import Request, urlopen + from urllib.parse import urlencode + from urllib.error import HTTPError +else: + from urllib2 import Request, urlopen, HTTPError + from urllib import urlencode + +from fail2ban.server.actions import ActionBase +from fail2ban.version import version as f2bVersion + +class BadIPsAction(ActionBase): + """Fail2Ban action which resports bans to badips.com, and also + blacklist bad IPs listed on badips.com by using another action's + ban method. + """ + badips = "http://www.badips.com" + Request = partial( + Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion}) + + def __init__(self, jail, name, category, score=5, age="24h", + banaction=None, updateperiod=900): + """Initialise action. + + Parameters + ---------- + jail : Jail + The jail which the action belongs to. + name : str + Name assigned to the action. + category : str + Valid badips.com category. + score : int, optional + Minimum score for bad IPs. Default 5. + age : str, optional + Age of last report for bad IPs, per badips.com syntax. + Default "24h" (24 hours) + banaction : str, optional + Name of banaction to use for blacklisting bad IPs. If `None`, + no blacklist of IPs will take place. + Default `None`. + updateperiod : int, optional + Time in seconds between updating bad IPs blacklist. + Default 900 (15 minutes) + + Raises + ------ + ValueError + If invalid `category`, `score`, `banaction` or `updateperiod`. + """ + super(BadIPsAction, self).__init__(jail, name) + + self.category = category + self.score = score + self.age = age + self.banaction = banaction + self.updateperiod = updateperiod + + self._bannedips = set() + # Used later for threading.Timer for updating badips + self._timer = None + + @classmethod + def getCategories(cls): + """Get badips.com categories. + + Returns + ------- + set + Set of categories. + + Raises + ------ + HTTPError + Any issues with badips.com request. + """ + try: + response = urlopen( + cls.Request("/".join([cls.badips, "get", "categories"]))) + except HTTPError as response: + messages = json.loads(response.read().decode('utf-8')) + self._logSys.error( + "Failed to fetch categories. badips.com response: '%s'", + messages['err']) + raise + else: + categories = json.loads(response.read().decode('utf-8'))['categories'] + categories_names = set( + value['Name'] for value in categories) + return categories_names + + @classmethod + def getList(cls, category, score, age): + """Get badips.com list of bad IPs. + + Parameters + ---------- + category : str + Valid badips.com category. + score : int + Minimum score for bad IPs. + age : str + Age of last report for bad IPs, per badips.com syntax. + + Returns + ------- + set + Set of bad IPs. + + Raises + ------ + HTTPError + Any issues with badips.com request. + """ + try: + response = urlopen(cls.Request("?".join([ + "/".join([cls.badips, "get", "list", category, str(score)]), + urlencode({'age': age})]))) + except HTTPError as response: + messages = json.loads(response.read().decode('utf-8')) + self._logSys.error( + "Failed to fetch bad IP list. badips.com response: '%s'", + messages['err']) + raise + else: + return set(response.read().decode('utf-8').split()) + + @property + def category(self): + """badips.com category for fetching/reporting IPs. + """ + return self._category + + @category.setter + def category(self, category): + if category not in self.getCategories(): + self._logSys.error("Category name '%s' not valid. " + "see badips.com for list of valid categories", + category) + raise ValueError("Invalid category: %s" % category) + self._category = category + + @property + def score(self): + """badips.com minimum score for fetching IPs. + """ + return self._score + + @score.setter + def score(self, score): + score = int(score) + if 0 <= score <= 5: + self._score = score + else: + raise ValueError("Score must be 0-5") + + @property + def banaction(self): + """Jail action to use for banning/unbanning. + """ + return self._banaction + + @banaction.setter + def banaction(self, banaction): + if banaction is not None and banaction not in self._jail.actions: + self._logSys.error("Action name '%s' not in jail '%s'", + banaction, self._jail.getName()) + raise ValueError("Invalid banaction") + self._banaction = banaction + + @property + def updateperiod(self): + """Period in seconds between banned bad IPs will be updated. + """ + return self._updateperiod + + @updateperiod.setter + def updateperiod(self, updateperiod): + updateperiod = int(updateperiod) + if updateperiod > 0: + self._updateperiod = updateperiod + else: + raise ValueError("Update period must be integer greater than 0") + + def _banIPs(self, ips): + for ip in ips: + self._jail.actions[self.banaction].ban({ + 'ip': ip, + 'failures': 0, + 'matches': "", + 'ipmatches': "", + 'ipjailmatches': "", + }) + self._bannedips.add(ip) + self._logSys.info( + "Banned IP %s for jail '%s' with action '%s'", + ip, self._jail.getName(), self.banaction) + + def _unbanIPs(self, ips): + for ip in ips: + self._jail.actions[self.banaction].unban({ + 'ip': ip, + 'failures': 0, + 'matches': "", + 'ipmatches': "", + 'ipjailmatches': "", + }) + self._bannedips.remove(ip) + self._logSys.info( + "Unbanned IP %s for jail '%s' with action '%s'", + ip, self._jail.getName(), self.banaction) + + def start(self): + """If `banaction` set, blacklists bad IPs. + """ + if self.banaction is not None: + self._banIPs(self.getList(self.category, self.score, self.age)) + self._timer = threading.Timer(self.updateperiod, self.update) + self._timer.start() + self._logSys.info( + "Banned IPs for jail '%s'. Update in %i seconds", + self._jail.getName(), self.updateperiod) + + def update(self): + """If `banaction` set, updates blacklisted IPs. + + Queries badips.com for list of bad IPs, removing IPs from the + blacklist if no longer present, and adds new bad IPs to the + blacklist. + """ + if self.banaction is not None: + if self._timer: + self._timer.cancel() + self._timer = None + + ips = self.getList(self.category, self.score, self.age) + # Remove old IPs no longer listed + self._unbanIPs(self._bannedips - ips) + # Add new IPs which are now listed + self._banIPs(ips - self._bannedips) + + self._timer = threading.Timer(self.updateperiod, self.update) + self._timer.start() + self._logSys.info( + "Updated IPs for jail '%s'. Update again in %i seconds", + self._jail.getName(), self.updateperiod) + + def stop(self): + """If `banaction` set, clears blacklisted IPs. + """ + if self.banaction is not None: + if self._timer: + self._timer.cancel() + self._timer = None + self._unbanIPs(self._bannedips.copy()) + + def ban(self, aInfo): + """Reports banned IP to badips.com. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + + Raises + ------ + HTTPError + Any issues with badips.com request. + """ + try: + response = urlopen(self.Request( + "/".join([self.badips, "add", self.category, aInfo['ip']]))) + except HTTPError as response: + messages = json.loads(response.read().decode('utf-8')) + self._logSys.error( + "Response from badips.com report: '%s'", + messages['err']) + raise + else: + messages = json.loads(response.read().decode('utf-8')) + self._logSys.info( + "Response from badips.com report: '%s'", + messages['suc']) + +Action = BadIPsAction diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 52624f29..003e3ca5 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -31,7 +31,7 @@ if sys.version_info >= (3, 3): import importlib.machinery else: import imp -from collections import Mapping +from collections import Mapping, OrderedDict from .banmanager import BanManager from .jailthread import JailThread @@ -62,7 +62,7 @@ class Actions(JailThread, Mapping): JailThread.__init__(self) ## The jail which contains this action. self._jail = jail - self._actions = dict() + self._actions = OrderedDict() ## The ban manager. self.__banManager = BanManager() @@ -209,7 +209,10 @@ class Actions(JailThread, Mapping): else: time.sleep(self.getSleepTime()) self.__flushBan() - for name, action in self._actions.iteritems(): + + actions = self._actions.items() + actions.reverse() + for name, action in actions: try: action.stop() except Exception as e: diff --git a/fail2ban/tests/action_d/test_badips.py b/fail2ban/tests/action_d/test_badips.py new file mode 100644 index 00000000..a203c40b --- /dev/null +++ b/fail2ban/tests/action_d/test_badips.py @@ -0,0 +1,89 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import os +import unittest +import sys +if sys.version_info >= (3, 3): + import importlib +else: + import imp + +from ..dummyjail import DummyJail + +if os.path.exists('config/fail2ban.conf'): + CONFIG_DIR = "config" +else: + CONFIG_DIR='/etc/fail2ban' + +class BadIPsActionTest(unittest.TestCase): + + def setUp(self): + """Call before every test case.""" + self.jail = DummyJail() + + self.jail.actions.add("test") + + pythonModule = os.path.join(CONFIG_DIR, "action.d", "badips.py") + self.jail.actions.add("badips", pythonModule, initOpts={ + 'category': "ssh", + 'banaction': "test", + }) + self.action = self.jail.actions["badips"] + + def tearDown(self): + """Call after every test case.""" + # Must cancel timer! + if self.action._timer: + self.action._timer.cancel() + + def testCategory(self): + categories = self.action.getCategories() + self.assertTrue("ssh" in categories) + self.assertTrue(len(categories) >= 10) + + self.assertRaises( + ValueError, setattr, self.action, "category", "invalid-category") + + def testScore(self): + self.assertRaises(ValueError, setattr, self.action, "score", -5) + self.action.score = 5 + self.action.score = "5" + + def testBanaction(self): + self.assertRaises( + ValueError, setattr, self.action, "banaction", "invalid-action") + self.action.banaction = "test" + + def testUpdateperiod(self): + self.assertRaises( + ValueError, setattr, self.action, "updateperiod", -50) + self.assertRaises( + ValueError, setattr, self.action, "updateperiod", 0) + self.action.updateperiod = 900 + self.action.updateperiod = "900" + + def testStart(self): + self.action.start() + self.assertTrue(len(self.action._bannedips) > 10) + + def testStop(self): + self.testStart() + self.action.stop() + self.assertTrue(len(self.action._bannedips) == 0) From cf81ddd8e2c5e36c74244e274c5e2afa8753a93a Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 14 Feb 2014 17:10:34 +0000 Subject: [PATCH 2/5] BF: Add error handling in badips.py action --- config/action.d/badips.py | 89 ++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/config/action.d/badips.py b/config/action.d/badips.py index d09b905d..5c26277b 100644 --- a/config/action.d/badips.py +++ b/config/action.d/badips.py @@ -20,6 +20,7 @@ import json from functools import partial import threading +import logging import sys if sys.version_info >= (3, ): from urllib.request import Request, urlopen @@ -207,42 +208,52 @@ class BadIPsAction(ActionBase): def _banIPs(self, ips): for ip in ips: - self._jail.actions[self.banaction].ban({ - 'ip': ip, - 'failures': 0, - 'matches': "", - 'ipmatches': "", - 'ipjailmatches': "", - }) - self._bannedips.add(ip) - self._logSys.info( - "Banned IP %s for jail '%s' with action '%s'", - ip, self._jail.getName(), self.banaction) + try: + self._jail.actions[self.banaction].ban({ + 'ip': ip, + 'failures': 0, + 'matches': "", + 'ipmatches': "", + 'ipjailmatches': "", + }) + except Exception as e: + self._logSys.error( + "Error banning IP %s for jail '%s' with action '%s': %s", + ip, self._jail.getName(), self.banaction, e, + exc_info=self._logSys.getEffectiveLevel<=logging.DEBUG) + else: + self._bannedips.add(ip) + self._logSys.info( + "Banned IP %s for jail '%s' with action '%s'", + ip, self._jail.getName(), self.banaction) def _unbanIPs(self, ips): for ip in ips: - self._jail.actions[self.banaction].unban({ - 'ip': ip, - 'failures': 0, - 'matches': "", - 'ipmatches': "", - 'ipjailmatches': "", - }) - self._bannedips.remove(ip) - self._logSys.info( - "Unbanned IP %s for jail '%s' with action '%s'", - ip, self._jail.getName(), self.banaction) + try: + self._jail.actions[self.banaction].unban({ + 'ip': ip, + 'failures': 0, + 'matches': "", + 'ipmatches': "", + 'ipjailmatches': "", + }) + except Exception as e: + self._logSys.info( + "Error unbanning IP %s for jail '%s' with action '%s': %s", + ip, self._jail.getName(), self.banaction, e, + exc_info=self._logSys.getEffectiveLevel<=logging.DEBUG) + else: + self._logSys.info( + "Unbanned IP %s for jail '%s' with action '%s'", + ip, self._jail.getName(), self.banaction) + finally: + self._bannedips.remove(ip) def start(self): """If `banaction` set, blacklists bad IPs. """ if self.banaction is not None: - self._banIPs(self.getList(self.category, self.score, self.age)) - self._timer = threading.Timer(self.updateperiod, self.update) - self._timer.start() - self._logSys.info( - "Banned IPs for jail '%s'. Update in %i seconds", - self._jail.getName(), self.updateperiod) + self.update() def update(self): """If `banaction` set, updates blacklisted IPs. @@ -256,17 +267,19 @@ class BadIPsAction(ActionBase): self._timer.cancel() self._timer = None - ips = self.getList(self.category, self.score, self.age) - # Remove old IPs no longer listed - self._unbanIPs(self._bannedips - ips) - # Add new IPs which are now listed - self._banIPs(ips - self._bannedips) + try: + ips = self.getList(self.category, self.score, self.age) + # Remove old IPs no longer listed + self._unbanIPs(self._bannedips - ips) + # Add new IPs which are now listed + self._banIPs(ips - self._bannedips) - self._timer = threading.Timer(self.updateperiod, self.update) - self._timer.start() - self._logSys.info( - "Updated IPs for jail '%s'. Update again in %i seconds", - self._jail.getName(), self.updateperiod) + self._logSys.info( + "Updated IPs for jail '%s'. Update again in %i seconds", + self._jail.getName(), self.updateperiod) + finally: + self._timer = threading.Timer(self.updateperiod, self.update) + self._timer.start() def stop(self): """If `banaction` set, clears blacklisted IPs. From 5c7630c4be34c80fe9439f9e065d966b68e8e974 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Fri, 14 Feb 2014 17:45:08 +0000 Subject: [PATCH 3/5] ENH: Allow separate blacklist category for badips.py action --- config/action.d/badips.py | 35 ++++++++++++++++++++++---- fail2ban/tests/action_d/test_badips.py | 6 +++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/config/action.d/badips.py b/config/action.d/badips.py index 5c26277b..34d899fb 100644 --- a/config/action.d/badips.py +++ b/config/action.d/badips.py @@ -43,7 +43,7 @@ class BadIPsAction(ActionBase): Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion}) def __init__(self, jail, name, category, score=5, age="24h", - banaction=None, updateperiod=900): + banaction=None, bancategory=None, updateperiod=900): """Initialise action. Parameters @@ -53,7 +53,7 @@ class BadIPsAction(ActionBase): name : str Name assigned to the action. category : str - Valid badips.com category. + Valid badips.com category for reporting failures. score : int, optional Minimum score for bad IPs. Default 5. age : str, optional @@ -63,6 +63,11 @@ class BadIPsAction(ActionBase): Name of banaction to use for blacklisting bad IPs. If `None`, no blacklist of IPs will take place. Default `None`. + bancategory : str, optional + Name of category to use for blacklisting, which can differ + from category used for reporting. e.g. may want to report + "postfix", but want to use whole "mail" category for blacklist. + Default `category`. updateperiod : int, optional Time in seconds between updating bad IPs blacklist. Default 900 (15 minutes) @@ -78,6 +83,7 @@ class BadIPsAction(ActionBase): self.score = score self.age = age self.banaction = banaction + self.bancategory = bancategory or category self.updateperiod = updateperiod self._bannedips = set() @@ -85,7 +91,7 @@ class BadIPsAction(ActionBase): self._timer = None @classmethod - def getCategories(cls): + def getCategories(cls, incParents=False): """Get badips.com categories. Returns @@ -111,6 +117,10 @@ class BadIPsAction(ActionBase): categories = json.loads(response.read().decode('utf-8'))['categories'] categories_names = set( value['Name'] for value in categories) + if incParents: + categories_names.update(set( + value['Parent'] for value in categories + if "Parent" in value)) return categories_names @classmethod @@ -151,7 +161,7 @@ class BadIPsAction(ActionBase): @property def category(self): - """badips.com category for fetching/reporting IPs. + """badips.com category for reporting IPs. """ return self._category @@ -164,6 +174,21 @@ class BadIPsAction(ActionBase): raise ValueError("Invalid category: %s" % category) self._category = category + @property + def bancategory(self): + """badips.com bancategory for fetching IPs. + """ + return self._bancategory + + @bancategory.setter + def bancategory(self, bancategory): + if bancategory not in self.getCategories(incParents=True): + self._logSys.error("Category name '%s' not valid. " + "see badips.com for list of valid categories", + bancategory) + raise ValueError("Invalid bancategory: %s" % bancategory) + self._bancategory = bancategory + @property def score(self): """badips.com minimum score for fetching IPs. @@ -268,7 +293,7 @@ class BadIPsAction(ActionBase): self._timer = None try: - ips = self.getList(self.category, self.score, self.age) + ips = self.getList(self.bancategory, self.score, self.age) # Remove old IPs no longer listed self._unbanIPs(self._bannedips - ips) # Add new IPs which are now listed diff --git a/fail2ban/tests/action_d/test_badips.py b/fail2ban/tests/action_d/test_badips.py index a203c40b..c9ec6866 100644 --- a/fail2ban/tests/action_d/test_badips.py +++ b/fail2ban/tests/action_d/test_badips.py @@ -61,6 +61,12 @@ class BadIPsActionTest(unittest.TestCase): self.assertRaises( ValueError, setattr, self.action, "category", "invalid-category") + # Not valid for reporting category... + self.assertRaises( + ValueError, setattr, self.action, "category", "mail") + # but valid for blacklisting. + self.action.bancategory = "mail" + def testScore(self): self.assertRaises(ValueError, setattr, self.action, "score", -5) self.action.score = 5 From dfb46cfda696d982f8b8d6001a6000e5a00796dc Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Wed, 12 Mar 2014 21:54:15 +0000 Subject: [PATCH 4/5] BF: Require Python 2.7+ for badips.py action --- config/action.d/badips.py | 4 +- fail2ban/server/actions.py | 11 ++- fail2ban/tests/action_d/test_badips.py | 101 +++++++++++++------------ 3 files changed, 64 insertions(+), 52 deletions(-) diff --git a/config/action.d/badips.py b/config/action.d/badips.py index 34d899fb..ff25544d 100644 --- a/config/action.d/badips.py +++ b/config/action.d/badips.py @@ -17,11 +17,13 @@ # along with Fail2Ban; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +import sys +if sys.version_info < (2, 7): + raise ImportError("badips.py action requires Python >= 2.7") import json from functools import partial import threading import logging -import sys if sys.version_info >= (3, ): from urllib.request import Request, urlopen from urllib.parse import urlencode diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 003e3ca5..793afaf1 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -31,7 +31,11 @@ if sys.version_info >= (3, 3): import importlib.machinery else: import imp -from collections import Mapping, OrderedDict +from collections import Mapping +try: + from collections import OrderedDict +except ImportError: + OrderedDict = None from .banmanager import BanManager from .jailthread import JailThread @@ -62,7 +66,10 @@ class Actions(JailThread, Mapping): JailThread.__init__(self) ## The jail which contains this action. self._jail = jail - self._actions = OrderedDict() + if OrderedDict is not None: + self._actions = OrderedDict() + else: + self._actions = dict() ## The ban manager. self.__banManager = BanManager() diff --git a/fail2ban/tests/action_d/test_badips.py b/fail2ban/tests/action_d/test_badips.py index c9ec6866..583d8ab7 100644 --- a/fail2ban/tests/action_d/test_badips.py +++ b/fail2ban/tests/action_d/test_badips.py @@ -32,64 +32,67 @@ if os.path.exists('config/fail2ban.conf'): else: CONFIG_DIR='/etc/fail2ban' -class BadIPsActionTest(unittest.TestCase): +if sys.version_info >= (2,7): + class BadIPsActionTest(unittest.TestCase): - def setUp(self): - """Call before every test case.""" - self.jail = DummyJail() + def setUp(self): + """Call before every test case.""" + self.jail = DummyJail() - self.jail.actions.add("test") + self.jail.actions.add("test") - pythonModule = os.path.join(CONFIG_DIR, "action.d", "badips.py") - self.jail.actions.add("badips", pythonModule, initOpts={ - 'category': "ssh", - 'banaction': "test", - }) - self.action = self.jail.actions["badips"] + pythonModule = os.path.join(CONFIG_DIR, "action.d", "badips.py") + self.jail.actions.add("badips", pythonModule, initOpts={ + 'category': "ssh", + 'banaction': "test", + }) + self.action = self.jail.actions["badips"] - def tearDown(self): - """Call after every test case.""" - # Must cancel timer! - if self.action._timer: - self.action._timer.cancel() + def tearDown(self): + """Call after every test case.""" + # Must cancel timer! + if self.action._timer: + self.action._timer.cancel() - def testCategory(self): - categories = self.action.getCategories() - self.assertTrue("ssh" in categories) - self.assertTrue(len(categories) >= 10) + def testCategory(self): + categories = self.action.getCategories() + self.assertTrue("ssh" in categories) + self.assertTrue(len(categories) >= 10) - self.assertRaises( - ValueError, setattr, self.action, "category", "invalid-category") + self.assertRaises( + ValueError, setattr, self.action, "category", + "invalid-category") - # Not valid for reporting category... - self.assertRaises( - ValueError, setattr, self.action, "category", "mail") - # but valid for blacklisting. - self.action.bancategory = "mail" + # Not valid for reporting category... + self.assertRaises( + ValueError, setattr, self.action, "category", "mail") + # but valid for blacklisting. + self.action.bancategory = "mail" - def testScore(self): - self.assertRaises(ValueError, setattr, self.action, "score", -5) - self.action.score = 5 - self.action.score = "5" + def testScore(self): + self.assertRaises(ValueError, setattr, self.action, "score", -5) + self.action.score = 5 + self.action.score = "5" - def testBanaction(self): - self.assertRaises( - ValueError, setattr, self.action, "banaction", "invalid-action") - self.action.banaction = "test" + def testBanaction(self): + self.assertRaises( + ValueError, setattr, self.action, "banaction", + "invalid-action") + self.action.banaction = "test" - def testUpdateperiod(self): - self.assertRaises( - ValueError, setattr, self.action, "updateperiod", -50) - self.assertRaises( - ValueError, setattr, self.action, "updateperiod", 0) - self.action.updateperiod = 900 - self.action.updateperiod = "900" + def testUpdateperiod(self): + self.assertRaises( + ValueError, setattr, self.action, "updateperiod", -50) + self.assertRaises( + ValueError, setattr, self.action, "updateperiod", 0) + self.action.updateperiod = 900 + self.action.updateperiod = "900" - def testStart(self): - self.action.start() - self.assertTrue(len(self.action._bannedips) > 10) + def testStart(self): + self.action.start() + self.assertTrue(len(self.action._bannedips) > 10) - def testStop(self): - self.testStart() - self.action.stop() - self.assertTrue(len(self.action._bannedips) == 0) + def testStop(self): + self.testStart() + self.action.stop() + self.assertTrue(len(self.action._bannedips) == 0) From 0c63d0061a11069e89fbfc22a92988b3974edfb7 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Thu, 13 Mar 2014 19:58:32 +0000 Subject: [PATCH 5/5] DOC: Add documentation for badips.py action --- ChangeLog | 2 + config/action.d/badips.py | 79 +++++++++++++++++++-------------------- config/jail.conf | 6 +++ 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/ChangeLog b/ChangeLog index ac775a44..c3cc9d1b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -62,6 +62,8 @@ configuration before relying on it. Close gh-347 * Filter for squirrelmail. Close gh-261 * Filter for tine20. Close gh-583 + * Added action to use badips.com reporting and blacklist + - Requires Python 2.7+ - Enhancements * Jail names increased to 26 characters and iptables prefix reduced diff --git a/config/action.d/badips.py b/config/action.d/badips.py index ff25544d..b4928ff3 100644 --- a/config/action.d/badips.py +++ b/config/action.d/badips.py @@ -39,46 +39,45 @@ class BadIPsAction(ActionBase): """Fail2Ban action which resports bans to badips.com, and also blacklist bad IPs listed on badips.com by using another action's ban method. + + Parameters + ---------- + jail : Jail + The jail which the action belongs to. + name : str + Name assigned to the action. + category : str + Valid badips.com category for reporting failures. + score : int, optional + Minimum score for bad IPs. Default 5. + age : str, optional + Age of last report for bad IPs, per badips.com syntax. + Default "24h" (24 hours) + banaction : str, optional + Name of banaction to use for blacklisting bad IPs. If `None`, + no blacklist of IPs will take place. + Default `None`. + bancategory : str, optional + Name of category to use for blacklisting, which can differ + from category used for reporting. e.g. may want to report + "postfix", but want to use whole "mail" category for blacklist. + Default `category`. + updateperiod : int, optional + Time in seconds between updating bad IPs blacklist. + Default 900 (15 minutes) + + Raises + ------ + ValueError + If invalid `category`, `score`, `banaction` or `updateperiod`. """ - badips = "http://www.badips.com" - Request = partial( + + _badips = "http://www.badips.com" + _Request = partial( Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion}) def __init__(self, jail, name, category, score=5, age="24h", banaction=None, bancategory=None, updateperiod=900): - """Initialise action. - - Parameters - ---------- - jail : Jail - The jail which the action belongs to. - name : str - Name assigned to the action. - category : str - Valid badips.com category for reporting failures. - score : int, optional - Minimum score for bad IPs. Default 5. - age : str, optional - Age of last report for bad IPs, per badips.com syntax. - Default "24h" (24 hours) - banaction : str, optional - Name of banaction to use for blacklisting bad IPs. If `None`, - no blacklist of IPs will take place. - Default `None`. - bancategory : str, optional - Name of category to use for blacklisting, which can differ - from category used for reporting. e.g. may want to report - "postfix", but want to use whole "mail" category for blacklist. - Default `category`. - updateperiod : int, optional - Time in seconds between updating bad IPs blacklist. - Default 900 (15 minutes) - - Raises - ------ - ValueError - If invalid `category`, `score`, `banaction` or `updateperiod`. - """ super(BadIPsAction, self).__init__(jail, name) self.category = category @@ -108,7 +107,7 @@ class BadIPsAction(ActionBase): """ try: response = urlopen( - cls.Request("/".join([cls.badips, "get", "categories"]))) + cls._Request("/".join([cls._badips, "get", "categories"]))) except HTTPError as response: messages = json.loads(response.read().decode('utf-8')) self._logSys.error( @@ -149,8 +148,8 @@ class BadIPsAction(ActionBase): Any issues with badips.com request. """ try: - response = urlopen(cls.Request("?".join([ - "/".join([cls.badips, "get", "list", category, str(score)]), + response = urlopen(cls._Request("?".join([ + "/".join([cls._badips, "get", "list", category, str(score)]), urlencode({'age': age})]))) except HTTPError as response: messages = json.loads(response.read().decode('utf-8')) @@ -332,8 +331,8 @@ class BadIPsAction(ActionBase): Any issues with badips.com request. """ try: - response = urlopen(self.Request( - "/".join([self.badips, "add", self.category, aInfo['ip']]))) + response = urlopen(self._Request( + "/".join([self._badips, "add", self.category, aInfo['ip']]))) except HTTPError as response: messages = json.loads(response.read().decode('utf-8')) self._logSys.error( diff --git a/config/jail.conf b/config/jail.conf index bb3cb38d..a51aea4c 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -174,6 +174,12 @@ action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(prot # action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s"] +# Report ban via badips.com, and use as blacklist +# +# See BadIPsAction docstring in config/action.d/badips.py for +# documentation for this action. +# +action_badips = badips.py[category="%(name)s", banaction="%(banaction)s"] # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local