Add extended info to status output using Cyrmu

pull/924/head
Lee Clemens 2015-01-23 15:12:04 -05:00
parent cfaa76a355
commit 60ac0a1a17
12 changed files with 227 additions and 10 deletions

View File

@ -12,6 +12,8 @@ before_install:
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then travis_retry sudo apt-get update -qq; fi - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then travis_retry sudo apt-get update -qq; fi
install: install:
- travis_retry pip install pyinotify - travis_retry pip install pyinotify
- if [[ $TRAVIS_PYTHON_VERSION == 2* || $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then travis_retry pip install dnspython; fi
- if [[ $TRAVIS_PYTHON_VERSION == 3* || $TRAVIS_PYTHON_VERSION == 'pypy3' ]]; then travis_retry pip install dnspython3; fi
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then travis_retry sudo apt-get install -qq python-gamin; cp /usr/share/pyshared/gamin.py /usr/lib/pyshared/python2.7/_gamin.so $VIRTUAL_ENV/lib/python2.7/site-packages/; fi - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then travis_retry sudo apt-get install -qq python-gamin; cp /usr/share/pyshared/gamin.py /usr/lib/pyshared/python2.7/_gamin.so $VIRTUAL_ENV/lib/python2.7/site-packages/; fi
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cd ..; travis_retry pip install -q coveralls; cd -; fi - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cd ..; travis_retry pip install -q coveralls; cd -; fi
script: script:

View File

@ -41,7 +41,11 @@ ver. 0.9.2 (2014/XX/XXX) - wanna-be-released
- Monit config for fail2ban in /files/monit - Monit config for fail2ban in /files/monit
- New actions: - New actions:
- action.d/firewallcmd-multiport and action.d/firewallcmd-allports Thanks Donald Yandt - action.d/firewallcmd-multiport and action.d/firewallcmd-allports Thanks Donald Yandt
- New status commands:
- fail2ban-client status <jail> extended
- prints Cymru data (ASN, Country RIR) per banned IP
- Requires dnspython or dnspython3
- Enhancements: - Enhancements:
* Enable multiport for firewallcmd-new action. Closes gh-834 * Enable multiport for firewallcmd-new action. Closes gh-834
* files/debian-initd migrated from the debian branch and should be * files/debian-initd migrated from the debian branch and should be

View File

@ -55,6 +55,7 @@ protocol = [
["start <JAIL>", "starts the jail <JAIL>"], ["start <JAIL>", "starts the jail <JAIL>"],
["stop <JAIL>", "stops the jail <JAIL>. The jail is removed"], ["stop <JAIL>", "stops the jail <JAIL>. The jail is removed"],
["status <JAIL>", "gets the current status of <JAIL>"], ["status <JAIL>", "gets the current status of <JAIL>"],
["status <JAIL> extended", "gets the current status of <JAIL> with extended info"],
['', "JAIL CONFIGURATION", ""], ['', "JAIL CONFIGURATION", ""],
["set <JAIL> idle on|off", "sets the idle state of <JAIL>"], ["set <JAIL> idle on|off", "sets the idle state of <JAIL>"],
["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"], ["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"],

View File

@ -372,9 +372,20 @@ class Actions(JailThread, Mapping):
@property @property
def status(self): def status(self):
"""Status of active bans, and total ban counts. """Status of current and total ban counts and current banned IP list.
""" """
ret = [("Currently banned", self.__banManager.size()), ret = [("Currently banned", self.__banManager.size()),
("Total banned", self.__banManager.getBanTotal()), ("Total banned", self.__banManager.getBanTotal()),
("Banned IP list", self.__banManager.getBanList())] ("Banned IP list", self.__banManager.getBanList())]
return ret return ret
@property
def statusExtended(self):
"""Jail status plus banned IPs' ASN, Country and RIR
"""
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
ret = self.status +\
[("Banned ASN list", self.__banManager.geBanListExtendedASN(cymru_info)),
("Banned Country list", self.__banManager.geBanListExtendedCountry(cymru_info)),
("Banned RIR list", self.__banManager.geBanListExtendedRIR(cymru_info))]
return ret

View File

@ -26,6 +26,9 @@ __license__ = "GPL"
from threading import Lock from threading import Lock
import dns.exception
import dns.resolver
from .ticket import BanTicket from .ticket import BanTicket
from .mytime import MyTime from .mytime import MyTime
from ..helpers import getLogger from ..helpers import getLogger
@ -118,6 +121,115 @@ class BanManager:
finally: finally:
self.__lock.release() self.__lock.release()
##
# Returns normalized value
#
# @return value or "unknown" if value is None or empty string
@staticmethod
def handleBlankResult(value):
if value is None or len(value) == 0:
return "unknown"
else:
return value
##
# Returns Cymru DNS query information
#
# @return {"asn": [], "country": [], "rir": []} dict for self.__banList IPs
def getBanListExtendedCymruInfo(self):
self.__lock.acquire()
return_dict = {"asn": [], "country": [], "rir": []}
try:
for banData in self.__banList:
ip = banData.getIP()
# Reference: http://www.team-cymru.org/Services/ip-to-asn.html#dns
# TODO: IPv6 compatibility
reversed_ip = ".".join(reversed(ip.split(".")))
question = "%s.origin.asn.cymru.com" % reversed_ip
try:
answers = dns.resolver.query(question, "TXT")
for rdata in answers:
asn, net, country, rir, changed =\
[answer.strip("'\" ") for answer in rdata.to_text().split("|")]
asn = self.handleBlankResult(asn)
country = self.handleBlankResult(country)
rir = self.handleBlankResult(rir)
return_dict["asn"].append(self.handleBlankResult(asn))
return_dict["country"].append(self.handleBlankResult(country))
return_dict["rir"].append(self.handleBlankResult(rir))
except dns.resolver.NXDOMAIN:
return_dict["asn"].append("nxdomain")
return_dict["country"].append("nxdomain")
return_dict["rir"].append("nxdomain")
except dns.exception.DNSException as dnse:
logSys.error("Unhandled DNSException querying Cymru for %s TXT" % question)
logSys.exception(dnse)
except Exception as e:
logSys.error("Unhandled Exception querying Cymru for %s TXT" % question)
logSys.exception(e)
except Exception as e:
logSys.error("Failure looking up extended Cymru info")
logSys.exception(e)
finally:
self.__lock.release()
return return_dict
##
# Returns list of Banned ASNs from Cymru info
#
# Use getBanListExtendedCymruInfo() to provide cymru_info
#
# @return list of Banned ASNs
def geBanListExtendedASN(self, cymru_info):
self.__lock.acquire()
try:
return [asn for asn in cymru_info["asn"]]
except Exception as e:
logSys.error("Failed to lookup ASN")
logSys.exception(e)
return []
finally:
self.__lock.release()
##
# Returns list of Banned Countries from Cymru info
#
# Use getBanListExtendedCymruInfo() to provide cymru_info
#
# @return list of Banned Countries
def geBanListExtendedCountry(self, cymru_info):
self.__lock.acquire()
try:
return [country for country in cymru_info["country"]]
except Exception as e:
logSys.error("Failed to lookup Country")
logSys.exception(e)
return []
finally:
self.__lock.release()
##
# Returns list of Banned RIRs from Cymru info
#
# Use getBanListExtendedCymruInfo() to provide cymru_info
#
# @return list of Banned RIRs
def geBanListExtendedRIR(self, cymru_info):
self.__lock.acquire()
try:
return [rir for rir in cymru_info["rir"]]
except Exception as e:
logSys.error("Failed to lookup RIR")
logSys.exception(e)
return []
finally:
self.__lock.release()
## ##
# Create a ban ticket. # Create a ban ticket.
# #

View File

@ -183,6 +183,15 @@ class Jail:
("Actions", self.actions.status), ("Actions", self.actions.status),
] ]
@property
def statusExtended(self):
"""The extended status of the jail.
"""
return [
("Filter", self.filter.status),
("Actions", self.actions.statusExtended),
]
def putFailTicket(self, ticket): def putFailTicket(self, ticket):
"""Add a fail ticket to the jail. """Add a fail ticket to the jail.

View File

@ -72,6 +72,12 @@ class JailThread(Thread):
""" """
pass pass
@abstractproperty
def statusExtended(self): # pragma: no cover - abstract
"""Abstract - Should provide extended status information.
"""
pass
def start(self): def start(self):
"""Sets active flag and starts thread. """Sets active flag and starts thread.
""" """

View File

@ -322,7 +322,10 @@ class Server:
def statusJail(self, name): def statusJail(self, name):
return self.__jails[name].status return self.__jails[name].status
def statusJailExtended(self, name):
return self.__jails[name].statusExtended
# Logging # Logging
## ##

View File

@ -333,5 +333,10 @@ class Transmitter:
elif len(command) == 1: elif len(command) == 1:
name = command[0] name = command[0]
return self.__server.statusJail(name) return self.__server.statusJail(name)
elif len(command) == 2:
name = command[0]
if command[1] == "extended":
return self.__server.statusJailExtended(name)
else:
raise Exception("Invalid command (invalid status extension)")
raise Exception("Invalid command (no status)") raise Exception("Invalid command (no status)")

View File

@ -30,7 +30,6 @@ from ..server.banmanager import BanManager
from ..server.ticket import BanTicket from ..server.ticket import BanTicket
class AddFailure(unittest.TestCase): class AddFailure(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.__ticket = BanTicket('193.168.0.128', 1167605999.0) self.__ticket = BanTicket('193.168.0.128', 1167605999.0)
@ -39,19 +38,58 @@ class AddFailure(unittest.TestCase):
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
def testAdd(self): def testAdd(self):
self.assertEqual(self.__banManager.size(), 1) self.assertEqual(self.__banManager.size(), 1)
def testAddDuplicate(self): def testAddDuplicate(self):
self.assertFalse(self.__banManager.addBanTicket(self.__ticket)) self.assertFalse(self.__banManager.addBanTicket(self.__ticket))
self.assertEqual(self.__banManager.size(), 1) self.assertEqual(self.__banManager.size(), 1)
def testInListOK(self): def testInListOK(self):
ticket = BanTicket('193.168.0.128', 1167605999.0) ticket = BanTicket('193.168.0.128', 1167605999.0)
self.assertTrue(self.__banManager._inBanList(ticket)) self.assertTrue(self.__banManager._inBanList(ticket))
def testInListNOK(self): def testInListNOK(self):
ticket = BanTicket('111.111.1.111', 1167605999.0) ticket = BanTicket('111.111.1.111', 1167605999.0)
self.assertFalse(self.__banManager._inBanList(ticket)) self.assertFalse(self.__banManager._inBanList(ticket))
class StatusExtendedCymruInfo(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
self.__ban_ip = "93.184.216.34"
self.__asn = "15133"
self.__country = "EU"
self.__rir = "ripencc"
self.__ticket = BanTicket(self.__ban_ip, 1167605999.0)
self.__banManager = BanManager()
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
def tearDown(self):
"""Call after every test case."""
def testCymruInfo(self):
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
if "assertDictEqual" in dir(self):
self.assertDictEqual(cymru_info, {"asn": [self.__asn], "country": [self.__country], "rir": [self.__rir]})
else:
# Python 2.6 does not support assertDictEqual()
self.assertEqual(cymru_info["asn"], [self.__asn])
self.assertEqual(cymru_info["country"], [self.__country])
self.assertEqual(cymru_info["rir"], [self.__rir])
def testCymruInfoASN(self):
self.assertEqual(
self.__banManager.geBanListExtendedASN(self.__banManager.getBanListExtendedCymruInfo()),
[self.__asn])
def testCymruInfoCountry(self):
self.assertEqual(
self.__banManager.geBanListExtendedCountry(self.__banManager.getBanListExtendedCymruInfo()),
[self.__country])
def testCymruInfoRIR(self):
self.assertEqual(
self.__banManager.geBanListExtendedRIR(self.__banManager.getBanListExtendedCymruInfo()),
[self.__rir])

View File

@ -474,6 +474,27 @@ class Transmitter(TransmitterBase):
) )
) )
def testJailStatusExtended(self):
self.assertEqual(self.transm.proceed(["status", self.jailName, "extended"]),
(0,
[
('Filter', [
('Currently failed', 0),
('Total failed', 0),
('File list', [])]
),
('Actions', [
('Currently banned', 0),
('Total banned', 0),
('Banned IP list', []),
('Banned ASN list', []),
('Banned Country list', []),
('Banned RIR list', [])]
)
]
)
)
def testAction(self): def testAction(self):
action = "TestCaseAction" action = "TestCaseAction"
cmdList = [ cmdList = [
@ -601,6 +622,10 @@ class Transmitter(TransmitterBase):
self.assertEqual( self.assertEqual(
self.transm.proceed(["status", "INVALID", "COMMAND"])[0],1) self.transm.proceed(["status", "INVALID", "COMMAND"])[0],1)
def testStatusJailExtendedNOK(self):
self.assertEqual(
self.transm.proceed(["status", self.jailName, "INVALID_COMMAND"])[0],1)
def testJournalMatch(self): def testJournalMatch(self):
if not filtersystemd: # pragma: no cover if not filtersystemd: # pragma: no cover
if sys.version_info >= (2, 7): if sys.version_info >= (2, 7):

View File

@ -107,6 +107,7 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure)) tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure))
# BanManager # BanManager
tests.addTest(unittest.makeSuite(banmanagertestcase.AddFailure)) tests.addTest(unittest.makeSuite(banmanagertestcase.AddFailure))
tests.addTest(unittest.makeSuite(banmanagertestcase.StatusExtendedCymruInfo))
# ClientReaders # ClientReaders
tests.addTest(unittest.makeSuite(clientreadertestcase.ConfigReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.ConfigReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest))