From 5688c064ad757006bb268bb1e4d1ce6c120df0cd Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 09:50:17 +1100 Subject: [PATCH 01/14] ENH: separate out log capture framework for other test cases - now utils.LogCaptureTestCase --- testcases/actiontestcase.py | 27 +++++---------------------- testcases/utils.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/testcases/actiontestcase.py b/testcases/actiontestcase.py index 14356ee5..e0587b3d 100644 --- a/testcases/actiontestcase.py +++ b/testcases/actiontestcase.py @@ -24,40 +24,23 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -import unittest, time +import time import logging, sys from server.action import Action -from StringIO import StringIO +from utils import LogCaptureTestCase -class ExecuteAction(unittest.TestCase): +class ExecuteAction(LogCaptureTestCase): def setUp(self): """Call before every test case.""" self.__action = Action("Test") - - # For extended testing of what gets output into logging - # system, we will redirect it to a string - logSys = logging.getLogger("fail2ban") - - # Keep old settings - self._old_level = logSys.level - self._old_handlers = logSys.handlers - # Let's log everything into a string - self._log = StringIO() - logSys.handlers = [logging.StreamHandler(self._log)] - logSys.setLevel(getattr(logging, 'DEBUG')) + LogCaptureTestCase.setUp(self) def tearDown(self): """Call after every test case.""" - # print "O: >>%s<<" % self._log.getvalue() - logSys = logging.getLogger("fail2ban") - logSys.handlers = self._old_handlers - logSys.level = self._old_level + LogCaptureTestCase.tearDown(self) self.__action.execActionStop() - def _is_logged(self, s): - return s in self._log.getvalue() - def testNameChange(self): self.assertEqual(self.__action.getName(), "Test") self.__action.setName("Tricky Test") diff --git a/testcases/utils.py b/testcases/utils.py index 643c9ad1..b048c8c6 100644 --- a/testcases/utils.py +++ b/testcases/utils.py @@ -22,8 +22,9 @@ __author__ = "Yaroslav Halchenko" __copyright__ = "Copyright (c) 2013 Yaroslav Halchenko" __license__ = "GPL" -import logging, os, re, tempfile, sys, time, traceback +import unittest, logging, os, re, tempfile, sys, time, traceback from os.path import basename, dirname +from StringIO import StringIO # # Following "traceback" functions are adopted from PyMVPA distributed @@ -105,3 +106,29 @@ def mtimesleep(): # no sleep now should be necessary since polling tracks now not only # mtime but also ino and size pass + +class LogCaptureTestCase(unittest.TestCase): + + def setUp(self): + + # For extended testing of what gets output into logging + # system, we will redirect it to a string + logSys = logging.getLogger("fail2ban") + + # Keep old settings + self._old_level = logSys.level + self._old_handlers = logSys.handlers + # Let's log everything into a string + self._log = StringIO() + logSys.handlers = [logging.StreamHandler(self._log)] + logSys.setLevel(getattr(logging, 'DEBUG')) + + def tearDown(self): + """Call after every test case.""" + # print "O: >>%s<<" % self._log.getvalue() + logSys = logging.getLogger("fail2ban") + logSys.handlers = self._old_handlers + logSys.level = self._old_level + + def _is_logged(self, s): + return s in self._log.getvalue() From f4661d81779683e0ef92b3278347639e3260bb36 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 09:56:04 +1100 Subject: [PATCH 02/14] ENH: rebase LogFileMonitor on LogCaptureTestCase --- testcases/filtertestcase.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 8bc24085..6ab99d82 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -39,7 +39,7 @@ from server.failmanager import FailManagerEmpty # Useful helpers # -from utils import mtimesleep +from utils import mtimesleep, LogCaptureTestCase # yoh: per Steven Hiscocks's insight while troubleshooting # https://github.com/fail2ban/fail2ban/issues/103#issuecomment-15542836 @@ -194,11 +194,12 @@ class LogFile(unittest.TestCase): self.assertTrue(self.filter.isModified(LogFile.FILENAME)) -class LogFileMonitor(unittest.TestCase): +class LogFileMonitor(LogCaptureTestCase): """Few more tests for FilterPoll API """ def setUp(self): """Call before every test case.""" + LogCaptureTestCase.setUp(self) self.filter = self.name = 'NA' _, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures') self.file = open(self.name, 'a') @@ -208,6 +209,7 @@ class LogFileMonitor(unittest.TestCase): self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) ") def tearDown(self): + LogCaptureTestCase.tearDown(self) _killfile(self.file, self.name) pass From f4531e7b45c97cb03480b5e58749bad83f914231 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:10:31 +1100 Subject: [PATCH 03/14] TST: test cases fro filter.delFailRegex and filter.delIgnoreRegex --- testcases/filtertestcase.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 6ab99d82..b7b161d3 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -227,6 +227,16 @@ class LogFileMonitor(LogCaptureTestCase): # shorter wait time for not modified status return not self.isModified(0.4) + def testRemovingFailRegex(self): + self.filter.delFailRegex(0) + self.assertFalse(self._is_logged('Cannot remove regular expression. Index 0 is not valid')) + self.filter.delFailRegex(0) + self.assertTrue(self._is_logged('Cannot remove regular expression. Index 0 is not valid')) + + def testRemovingIgnoreRegex(self): + self.filter.delIgnoreRegex(0) + self.assertTrue(self._is_logged('Cannot remove regular expression. Index 0 is not valid')) + def testNewChangeViaIsModified(self): # it is a brand new one -- so first we think it is modified self.assertTrue(self.isModified()) From 44bbaebfe511e679e8a0dfa251e1383138f90002 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:15:24 +1100 Subject: [PATCH 04/14] TST: CIDR for ignoreip --- testcases/filtertestcase.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index b7b161d3..892a4a6e 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -173,6 +173,15 @@ class IgnoreIP(unittest.TestCase): self.filter.addIgnoreIP("www.epfl.ch") self.assertFalse(self.filter.inIgnoreIPList("127.177.50.10")) + def testIgnoreIPCIDR(self): + self.filter.addIgnoreIP('192.168.1.0/25') + self.assertTrue(self.filter.inIgnoreIPList('192.168.1.0')) + self.assertTrue(self.filter.inIgnoreIPList('192.168.1.1')) + self.assertTrue(self.filter.inIgnoreIPList('192.168.1.127')) + self.assertFalse(self.filter.inIgnoreIPList('192.168.1.128')) + self.assertFalse(self.filter.inIgnoreIPList('192.168.1.255')) + self.assertFalse(self.filter.inIgnoreIPList('192.168.0.255')) + class LogFile(unittest.TestCase): From 988e14d8c652a077fcb47a0ec1a88eb3ca3fdb5f Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:17:55 +1100 Subject: [PATCH 05/14] TST: negative match for DNS lookup test added --- testcases/filtertestcase.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 892a4a6e..0d2b58fb 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -163,6 +163,8 @@ class IgnoreIP(unittest.TestCase): self.filter.addIgnoreIP("www.epfl.ch") self.assertTrue(self.filter.inIgnoreIPList("128.178.50.12")) + self.assertFalse(self.filter.inIgnoreIPList("128.178.50.11")) + self.assertFalse(self.filter.inIgnoreIPList("128.178.50.13")) def testIgnoreIPNOK(self): ipList = "", "999.999.999.999", "abcdef", "192.168.0." From 60c4957a5278b033c41e71c18d2824441507ea14 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:21:19 +1100 Subject: [PATCH 06/14] DOC/TST: remove TODO as all regexs have samples --- testcases/samplestestcase.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testcases/samplestestcase.py b/testcases/samplestestcase.py index d88be5e9..6b4d4530 100644 --- a/testcases/samplestestcase.py +++ b/testcases/samplestestcase.py @@ -123,7 +123,6 @@ def testSampleRegexsFactory(name): regexsUsed.add(failregex) - # TODO: Remove exception handling once all regexs have samples for failRegexIndex, failRegex in enumerate(self.filter.getFailRegex()): self.assertTrue( failRegexIndex in regexsUsed, From a8b5c5b5f3f5de2c5a103c22f40672481ab33906 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:31:58 +1100 Subject: [PATCH 07/14] TST: check IgnoreIP happens in filter.processLine --- testcases/filtertestcase.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 0d2b58fb..63f23400 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -144,15 +144,13 @@ def _copy_lines_between_files(fin, fout, n=None, skip=0, mode='a', terminal_line # Actual tests # -class IgnoreIP(unittest.TestCase): +class IgnoreIP(LogCaptureTestCase): def setUp(self): """Call before every test case.""" + LogCaptureTestCase.setUp(self) self.filter = FileFilter(None) - def tearDown(self): - """Call after every test case.""" - def testIgnoreIPOK(self): ipList = "127.0.0.1", "192.168.0.1", "255.255.255.255", "99.99.99.99" for ip in ipList: @@ -184,6 +182,12 @@ class IgnoreIP(unittest.TestCase): self.assertFalse(self.filter.inIgnoreIPList('192.168.1.255')) self.assertFalse(self.filter.inIgnoreIPList('192.168.0.255')) + def testIgnoreInProcessLine(self): + self.filter.addIgnoreIP('192.168.1.0/25') + self.filter.addFailRegex('') + self.filter.processLineAndAdd('Thu Jul 11 01:21:43 2013 192.168.1.32') + self.assertTrue(self._is_logged('Ignore 192.168.1.32')) + class LogFile(unittest.TestCase): From ebf4a02004ed2d312a3329f0dd4ab36cb0028f8f Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:43:47 +1100 Subject: [PATCH 08/14] TST: get/set use DNS on Filters --- fail2ban-testcases | 1 + testcases/filtertestcase.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/fail2ban-testcases b/fail2ban-testcases index f44e84d4..16c92c5f 100755 --- a/fail2ban-testcases +++ b/fail2ban-testcases @@ -174,6 +174,7 @@ tests.addTest(unittest.makeSuite(misctestcase.CustomDateFormatsTest)) # Filter if not opts.no_network: tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIP)) +tests.addTest(unittest.makeSuite(filtertestcase.BasicFilter)) tests.addTest(unittest.makeSuite(filtertestcase.LogFile)) tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor)) if not opts.no_network: diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 63f23400..7b09ffed 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -31,7 +31,7 @@ import tempfile from server.jail import Jail from server.filterpoll import FilterPoll -from server.filter import FileFilter, DNSUtils +from server.filter import Filter, FileFilter, DNSUtils from server.failmanager import FailManager from server.failmanager import FailManagerEmpty @@ -144,6 +144,20 @@ def _copy_lines_between_files(fin, fout, n=None, skip=0, mode='a', terminal_line # Actual tests # +class BasicFilter(unittest.TestCase): + + def setUp(self): + self.filter = Filter('name') + + def testGetSetUseDNS(self): + # default is warn + self.assertEqual(self.filter.getUseDns(), 'warn') + self.filter.setUseDns(True) + self.assertEqual(self.filter.getUseDns(), 'yes') + self.filter.setUseDns(False) + self.assertEqual(self.filter.getUseDns(), 'no') + + class IgnoreIP(LogCaptureTestCase): def setUp(self): From f3c42851180f08316a5b898ad5566000ab7b4af9 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:46:52 +1100 Subject: [PATCH 09/14] TST: no test coverage on subclass overwritten function _delLogPath --- server/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/filter.py b/server/filter.py index 0ef756d2..52bb8c4c 100644 --- a/server/filter.py +++ b/server/filter.py @@ -446,7 +446,7 @@ class FileFilter(Filter): self._delLogPath(path) return - def _delLogPath(self, path): + def _delLogPath(self, path): # pragma: no cover - overwritten function # nothing to do by default # to be overridden by backends pass From 2b89457dc94249d44295cc5e79d17e0ec0fb3522 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 10:55:06 +1100 Subject: [PATCH 10/14] TST: addBanned IP when ignore exists --- testcases/filtertestcase.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 7b09ffed..29491ae5 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -34,6 +34,7 @@ from server.filterpoll import FilterPoll from server.filter import Filter, FileFilter, DNSUtils from server.failmanager import FailManager from server.failmanager import FailManagerEmpty +from dummyjail import DummyJail # # Useful helpers @@ -163,7 +164,8 @@ class IgnoreIP(LogCaptureTestCase): def setUp(self): """Call before every test case.""" LogCaptureTestCase.setUp(self) - self.filter = FileFilter(None) + self.jail = DummyJail() + self.filter = FileFilter(self.jail) def testIgnoreIPOK(self): ipList = "127.0.0.1", "192.168.0.1", "255.255.255.255", "99.99.99.99" @@ -202,6 +204,11 @@ class IgnoreIP(LogCaptureTestCase): self.filter.processLineAndAdd('Thu Jul 11 01:21:43 2013 192.168.1.32') self.assertTrue(self._is_logged('Ignore 192.168.1.32')) + def testIgnoreAddBannedIP(self): + self.filter.addIgnoreIP('192.168.1.0/25') + self.filter.addBannedIP('192.168.1.32') + self.assertFalse(self._is_logged('Ignore 192.168.1.32')) + self.assertTrue(self._is_logged('Requested to manually ban an ignored IP 192.168.1.32. User knows best. Proceeding to ban it.')) class LogFile(unittest.TestCase): @@ -347,7 +354,6 @@ class LogFileMonitor(LogCaptureTestCase): from threading import Lock -from dummyjail import DummyJail def get_monitor_failures_testcase(Filter_): """Generator of TestCase's for different filters/backends From c13b91fa709a39e8c402d55453178af35dd20c29 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 12:08:23 +1100 Subject: [PATCH 11/14] TST: separate out DNS based IgnoreIP tests --- fail2ban-testcases | 3 ++- testcases/filtertestcase.py | 25 ++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/fail2ban-testcases b/fail2ban-testcases index 16c92c5f..21b8fda4 100755 --- a/fail2ban-testcases +++ b/fail2ban-testcases @@ -173,7 +173,8 @@ tests.addTest(unittest.makeSuite(misctestcase.CustomDateFormatsTest)) # Filter if not opts.no_network: - tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIP)) + tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIPDNS)) +tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIP)) tests.addTest(unittest.makeSuite(filtertestcase.BasicFilter)) tests.addTest(unittest.makeSuite(filtertestcase.LogFile)) tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor)) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 29491ae5..46870d41 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -173,21 +173,12 @@ class IgnoreIP(LogCaptureTestCase): self.filter.addIgnoreIP(ip) self.assertTrue(self.filter.inIgnoreIPList(ip)) - # Test DNS - self.filter.addIgnoreIP("www.epfl.ch") - - self.assertTrue(self.filter.inIgnoreIPList("128.178.50.12")) - self.assertFalse(self.filter.inIgnoreIPList("128.178.50.11")) - self.assertFalse(self.filter.inIgnoreIPList("128.178.50.13")) def testIgnoreIPNOK(self): ipList = "", "999.999.999.999", "abcdef", "192.168.0." for ip in ipList: self.filter.addIgnoreIP(ip) self.assertFalse(self.filter.inIgnoreIPList(ip)) - # Test DNS - self.filter.addIgnoreIP("www.epfl.ch") - self.assertFalse(self.filter.inIgnoreIPList("127.177.50.10")) def testIgnoreIPCIDR(self): self.filter.addIgnoreIP('192.168.1.0/25') @@ -210,6 +201,22 @@ class IgnoreIP(LogCaptureTestCase): self.assertFalse(self._is_logged('Ignore 192.168.1.32')) self.assertTrue(self._is_logged('Requested to manually ban an ignored IP 192.168.1.32. User knows best. Proceeding to ban it.')) + +class IgnoreIPDNS(IgnoreIP): + + def testIgnoreIPDNSOK(self): + self.filter.addIgnoreIP("www.epfl.ch") + self.assertTrue(self.filter.inIgnoreIPList("128.178.50.12")) + + def testIgnoreIPDNSNOK(self): + # Test DNS + self.filter.addIgnoreIP("www.epfl.ch") + print self._log.getvalue() + self.assertFalse(self.filter.inIgnoreIPList("127.177.50.10")) + self.assertFalse(self.filter.inIgnoreIPList("128.178.50.11")) + self.assertFalse(self.filter.inIgnoreIPList("128.178.50.13")) + + class LogFile(unittest.TestCase): FILENAME = "testcases/files/testcase01.log" From 33d96ae4626d0b9d40afef39dfc5a02bafb7aeb1 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 12:10:44 +1100 Subject: [PATCH 12/14] TST: separate out DNS based IgnoreIP tests --- testcases/filtertestcase.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 46870d41..a4d0230f 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -211,7 +211,6 @@ class IgnoreIPDNS(IgnoreIP): def testIgnoreIPDNSNOK(self): # Test DNS self.filter.addIgnoreIP("www.epfl.ch") - print self._log.getvalue() self.assertFalse(self.filter.inIgnoreIPList("127.177.50.10")) self.assertFalse(self.filter.inIgnoreIPList("128.178.50.11")) self.assertFalse(self.filter.inIgnoreIPList("128.178.50.13")) From 5005719180bb7a344b00740851fc0b133233f0bb Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 12:34:26 +1100 Subject: [PATCH 13/14] TST: permission denied on log file --- testcases/filtertestcase.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index a4d0230f..000ff353 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -269,6 +269,11 @@ class LogFileMonitor(LogCaptureTestCase): # shorter wait time for not modified status return not self.isModified(0.4) + def testNoLogFile(self): + os.chmod(self.name, 0) + self.filter.getFailures(self.name) + self.assertTrue(self._is_logged('Unable to open %s' % self.name)) + def testRemovingFailRegex(self): self.filter.delFailRegex(0) self.assertFalse(self._is_logged('Cannot remove regular expression. Index 0 is not valid')) From a03815facf198472273918b0763d97dd61286ac7 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Wed, 11 Dec 2013 13:07:08 +1100 Subject: [PATCH 14/14] TST: FileFilter tail tests --- server/filter.py | 3 +++ testcases/filtertestcase.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/server/filter.py b/server/filter.py index 52bb8c4c..b92289ef 100644 --- a/server/filter.py +++ b/server/filter.py @@ -568,6 +568,9 @@ class FileContainer: def getFileName(self): return self.__filename + def getPos(self): + return self.__pos + def open(self): self.__handler = open(self.__filename) # Set the file descriptor to be FD_CLOEXEC diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 000ff353..7b18c4bf 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -603,7 +603,13 @@ class GetFailures(unittest.TestCase): def tearDown(self): """Call after every test case.""" - + def testTail(self): + self.filter.addLogPath(LogFile.FILENAME, tail=True) + self.assertEqual(self.filter.getLogPath()[-1].getPos(), 1653) + self.filter.getLogPath()[-1].close() + self.assertEqual(self.filter.getLogPath()[-1].readline(), "") + self.filter.delLogPath(LogFile.FILENAME) + self.assertEqual(self.filter.getLogPath(),[]) def testGetFailures01(self, filename=None, failures=None): filename = filename or GetFailures.FILENAME_01