mirror of https://github.com/fail2ban/fail2ban
				
				
				
			ENH+BF+TST: Filter now returns reference to failregex and ignoreregex
This avoids duplication of code across fail2ban-regex and samples test cases. This also now more neatly resolves the issue of double counting date templates matches in fail2ban-regex. In addition, the samples test cases now also print a warning message that not all regexs have samples for them, with future plan to change this to an assertion.pull/298/merge^2
							parent
							
								
									5bd186b854
								
							
						
					
					
						commit
						1a2b6442a0
					
				|  | @ -46,7 +46,6 @@ from client.configparserinc import SafeConfigParserWithIncludes | ||||||
| from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError | from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError | ||||||
| from server.filter import Filter | from server.filter import Filter | ||||||
| from server.failregex import RegexException | from server.failregex import RegexException | ||||||
| from server.datedetector import DateDetector |  | ||||||
| 
 | 
 | ||||||
| from testcases.utils import FormatterWithTraceBack | from testcases.utils import FormatterWithTraceBack | ||||||
| # Gets the instance of the logger. | # Gets the instance of the logger. | ||||||
|  | @ -130,7 +129,7 @@ class RegexStat(object): | ||||||
| 		return self._failregex | 		return self._failregex | ||||||
| 
 | 
 | ||||||
| 	def appendIP(self, value): | 	def appendIP(self, value): | ||||||
| 		self._ipList.extend(value) | 		self._ipList.append(value) | ||||||
| 
 | 
 | ||||||
| 	def getIPList(self): | 	def getIPList(self): | ||||||
| 		return self._ipList | 		return self._ipList | ||||||
|  | @ -173,8 +172,6 @@ class Fail2banRegex(object): | ||||||
| 		self._ignoreregex = list() | 		self._ignoreregex = list() | ||||||
| 		self._failregex = list() | 		self._failregex = list() | ||||||
| 		self._line_stats = LineStats() | 		self._line_stats = LineStats() | ||||||
| 		self._dateDetector = DateDetector() |  | ||||||
| 		self._dateDetector.addDefaultTemplate() |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	def readRegex(self, value, regextype): | 	def readRegex(self, value, regextype): | ||||||
|  | @ -204,53 +201,39 @@ class Fail2banRegex(object): | ||||||
| 			regex_values = [RegexStat(value)] | 			regex_values = [RegexStat(value)] | ||||||
| 
 | 
 | ||||||
| 		setattr(self, "_" + regex, regex_values) | 		setattr(self, "_" + regex, regex_values) | ||||||
|  | 		for regex in regex_values: | ||||||
|  | 			getattr( | ||||||
|  | 				self._filter, | ||||||
|  | 				'add%sRegex' % regextype.title())(regex.getFailRegex()) | ||||||
| 		return True | 		return True | ||||||
| 
 | 
 | ||||||
| 	def testIgnoreRegex(self, line): | 	def testIgnoreRegex(self, line): | ||||||
| 		found = False | 		found = False | ||||||
| 		for regex in self._ignoreregex: | 		try: | ||||||
| 			try: | 			ret = self._filter.ignoreLine(line) | ||||||
| 				self._filter.addIgnoreRegex(regex.getFailRegex()) | 			if ret is not None: | ||||||
| 				try: | 				found = True | ||||||
| 					ret = self._filter.ignoreLine(line) | 				regex = self._ignoreregex[ret].inc() | ||||||
| 					if ret: | 		except RegexException, e: | ||||||
| 						found = True | 			print e | ||||||
| 						regex.inc() | 			return False | ||||||
| 				except RegexException, e: |  | ||||||
| 					print e |  | ||||||
| 					return False |  | ||||||
| 			finally: |  | ||||||
| 				self._filter.delIgnoreRegex(0) |  | ||||||
| 		return found | 		return found | ||||||
| 
 | 
 | ||||||
| 	def testRegex(self, line): | 	def testRegex(self, line): | ||||||
| 		found = False | 		try: | ||||||
| 		for regex in self._ignoreregex: | 			ret = self._filter.processLine(line, checkAllRegex=True) | ||||||
| 			self._filter.addIgnoreRegex(regex.getFailRegex()) | 			for match in ret: | ||||||
| 		for regex in self._failregex: | 				match.append(len(ret)>1) | ||||||
| 			try: | 				regex = self._failregex[match[0]] | ||||||
| 				self._filter.addFailRegex(regex.getFailRegex()) | 				regex.inc() | ||||||
| 				try: | 				regex.appendIP(match) | ||||||
| 					ret = self._filter.processLine(line) | 		except RegexException, e: | ||||||
| 					if len(ret): | 			print e | ||||||
| 						if found == True: | 			return False | ||||||
| 							ret[0].append(True) | 		except IndexError: | ||||||
| 						else: | 			print "Sorry, but no <host> found in regex" | ||||||
| 							found = True | 			return False | ||||||
| 							ret[0].append(False) | 		return len(ret) > 0 | ||||||
| 						regex.inc() |  | ||||||
| 						regex.appendIP(ret) |  | ||||||
| 				except RegexException, e: |  | ||||||
| 					print e |  | ||||||
| 					return False |  | ||||||
| 				except IndexError: |  | ||||||
| 					print "Sorry, but no <host> found in regex" |  | ||||||
| 					return False |  | ||||||
| 			finally: |  | ||||||
| 				self._filter.delFailRegex(0) |  | ||||||
| 		for regex in self._ignoreregex: |  | ||||||
| 			self._filter.delIgnoreRegex(0) |  | ||||||
| 		return found |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	def process(self, test_lines): | 	def process(self, test_lines): | ||||||
|  | @ -259,9 +242,6 @@ class Fail2banRegex(object): | ||||||
| 			if line.startswith('#') or not line.strip(): | 			if line.startswith('#') or not line.strip(): | ||||||
| 				# skip comment and empty lines | 				# skip comment and empty lines | ||||||
| 				continue | 				continue | ||||||
| 
 |  | ||||||
| 			self._dateDetector.matchTime(line) |  | ||||||
| 
 |  | ||||||
| 			is_ignored = fail2banRegex.testIgnoreRegex(line) | 			is_ignored = fail2banRegex.testIgnoreRegex(line) | ||||||
| 			if is_ignored: | 			if is_ignored: | ||||||
| 				self._line_stats.ignored_lines.append(line) | 				self._line_stats.ignored_lines.append(line) | ||||||
|  | @ -302,10 +282,13 @@ class Fail2banRegex(object): | ||||||
| 
 | 
 | ||||||
| 				if self._verbose and len(failregex.getIPList()): | 				if self._verbose and len(failregex.getIPList()): | ||||||
| 					for ip in failregex.getIPList(): | 					for ip in failregex.getIPList(): | ||||||
| 						timeTuple = time.localtime(ip[1]) | 						timeTuple = time.localtime(ip[2]) | ||||||
| 						timeString = time.strftime("%a %b %d %H:%M:%S %Y", timeTuple) | 						timeString = time.strftime("%a %b %d %H:%M:%S %Y", timeTuple) | ||||||
| 						out.append("    %s  %s%s" % ( | 						out.append( | ||||||
| 							ip[0], timeString, ip[2] and " (already matched)" or "")) | 							"    %s  %s%s" % ( | ||||||
|  | 								ip[1], | ||||||
|  | 								timeString, | ||||||
|  | 								ip[3] and " (multiple regex matched)" or "")) | ||||||
| 
 | 
 | ||||||
| 			print "\n%s: %d total" % (title, total) | 			print "\n%s: %d total" % (title, total) | ||||||
| 			pprint_list(out, " #) [# of hits] regular expression") | 			pprint_list(out, " #) [# of hits] regular expression") | ||||||
|  | @ -318,7 +301,7 @@ class Fail2banRegex(object): | ||||||
| 
 | 
 | ||||||
| 		print "\nDate template hits:" | 		print "\nDate template hits:" | ||||||
| 		out = [] | 		out = [] | ||||||
| 		for template in self._dateDetector.getTemplates(): | 		for template in self._filter.dateDetector.getTemplates(): | ||||||
| 			if self._verbose or template.getHits(): | 			if self._verbose or template.getHits(): | ||||||
| 				out.append("[%d] %s" % (template.getHits(), template.getName())) | 				out.append("[%d] %s" % (template.getHits(), template.getName())) | ||||||
| 		pprint_list(out, "[# of hits] date format") | 		pprint_list(out, "[# of hits] date format") | ||||||
|  |  | ||||||
|  | @ -174,6 +174,7 @@ class DateDetector: | ||||||
| 				match = template.matchDate(line) | 				match = template.matchDate(line) | ||||||
| 				if not match is None: | 				if not match is None: | ||||||
| 					logSys.debug("Matched time template %s" % template.getName()) | 					logSys.debug("Matched time template %s" % template.getName()) | ||||||
|  | 					template.incHits() | ||||||
| 					return match | 					return match | ||||||
| 			return None | 			return None | ||||||
| 		finally: | 		finally: | ||||||
|  |  | ||||||
|  | @ -60,10 +60,11 @@ class DateTemplate: | ||||||
| 	def getHits(self): | 	def getHits(self): | ||||||
| 		return self.__hits | 		return self.__hits | ||||||
| 
 | 
 | ||||||
|  | 	def incHits(self): | ||||||
|  | 		self.__hits += 1 | ||||||
|  | 	 | ||||||
| 	def matchDate(self, line): | 	def matchDate(self, line): | ||||||
| 		dateMatch = self.__cRegex.search(line) | 		dateMatch = self.__cRegex.search(line) | ||||||
| 		if not dateMatch is None: |  | ||||||
| 			self.__hits += 1 |  | ||||||
| 		return dateMatch | 		return dateMatch | ||||||
| 	 | 	 | ||||||
| 	def getDate(self, line): | 	def getDate(self, line): | ||||||
|  |  | ||||||
|  | @ -284,7 +284,7 @@ class Filter(JailThread): | ||||||
| 		return False | 		return False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	def processLine(self, line, returnRawHost=False): | 	def processLine(self, line, returnRawHost=False, checkAllRegex=False): | ||||||
| 		"""Split the time portion from log msg and return findFailures on them | 		"""Split the time portion from log msg and return findFailures on them | ||||||
| 		""" | 		""" | ||||||
| 		try: | 		try: | ||||||
|  | @ -306,14 +306,15 @@ class Filter(JailThread): | ||||||
| 		else: | 		else: | ||||||
| 			timeLine = l | 			timeLine = l | ||||||
| 			logLine = l | 			logLine = l | ||||||
| 		return self.findFailure(timeLine, logLine, returnRawHost) | 		return self.findFailure(timeLine, logLine, returnRawHost, checkAllRegex) | ||||||
| 
 | 
 | ||||||
| 	def processLineAndAdd(self, line): | 	def processLineAndAdd(self, line): | ||||||
| 		"""Processes the line for failures and populates failManager | 		"""Processes the line for failures and populates failManager | ||||||
| 		""" | 		""" | ||||||
| 		for element in self.processLine(line): | 		for element in self.processLine(line): | ||||||
| 			ip = element[0] | 			failregex = element[0] | ||||||
| 			unixTime = element[1] | 			ip = element[1] | ||||||
|  | 			unixTime = element[2] | ||||||
| 			logSys.debug("Processing line with time:%s and ip:%s" | 			logSys.debug("Processing line with time:%s and ip:%s" | ||||||
| 						 % (unixTime, ip)) | 						 % (unixTime, ip)) | ||||||
| 			if unixTime < MyTime.time() - self.getFindTime(): | 			if unixTime < MyTime.time() - self.getFindTime(): | ||||||
|  | @ -335,11 +336,11 @@ class Filter(JailThread): | ||||||
| 	# @return: a boolean | 	# @return: a boolean | ||||||
| 
 | 
 | ||||||
| 	def ignoreLine(self, line): | 	def ignoreLine(self, line): | ||||||
| 		for ignoreRegex in self.__ignoreRegex: | 		for ignoreRegexIndex, ignoreRegex in enumerate(self.__ignoreRegex): | ||||||
| 			ignoreRegex.search(line) | 			ignoreRegex.search(line) | ||||||
| 			if ignoreRegex.hasMatched(): | 			if ignoreRegex.hasMatched(): | ||||||
| 				return True | 				return ignoreRegexIndex | ||||||
| 		return False | 		return None | ||||||
| 
 | 
 | ||||||
| 	## | 	## | ||||||
| 	# Finds the failure in a line given split into time and log parts. | 	# Finds the failure in a line given split into time and log parts. | ||||||
|  | @ -348,18 +349,19 @@ class Filter(JailThread): | ||||||
| 	# to find the logging time. | 	# to find the logging time. | ||||||
| 	# @return a dict with IP and timestamp. | 	# @return a dict with IP and timestamp. | ||||||
| 
 | 
 | ||||||
| 	def findFailure(self, timeLine, logLine, returnRawHost=False): | 	def findFailure(self, timeLine, logLine, | ||||||
|  | 			returnRawHost=False, checkAllRegex=False): | ||||||
| 		failList = list() | 		failList = list() | ||||||
| 		# Checks if we must ignore this line. | 		# Checks if we must ignore this line. | ||||||
| 		if self.ignoreLine(logLine): | 		if self.ignoreLine(logLine) is not None: | ||||||
| 			# The ignoreregex matched. Return. | 			# The ignoreregex matched. Return. | ||||||
| 			return failList | 			return failList | ||||||
|  | 		date = self.dateDetector.getUnixTime(timeLine) | ||||||
| 		# Iterates over all the regular expressions. | 		# Iterates over all the regular expressions. | ||||||
| 		for failRegex in self.__failRegex: | 		for failRegexIndex, failRegex in enumerate(self.__failRegex): | ||||||
| 			failRegex.search(logLine) | 			failRegex.search(logLine) | ||||||
| 			if failRegex.hasMatched(): | 			if failRegex.hasMatched(): | ||||||
| 				# The failregex matched. | 				# The failregex matched. | ||||||
| 				date = self.dateDetector.getUnixTime(timeLine) |  | ||||||
| 				logSys.log(7, "Date: %r, message: %r", | 				logSys.log(7, "Date: %r, message: %r", | ||||||
| 							  timeLine, logLine) | 							  timeLine, logLine) | ||||||
| 				if date is None: | 				if date is None: | ||||||
|  | @ -372,14 +374,16 @@ class Filter(JailThread): | ||||||
| 					try: | 					try: | ||||||
| 						host = failRegex.getHost() | 						host = failRegex.getHost() | ||||||
| 						if returnRawHost: | 						if returnRawHost: | ||||||
| 							failList.append([host, date]) | 							failList.append([failRegexIndex, host, date]) | ||||||
| 							break | 							if not checkAllRegex: | ||||||
| 						ipMatch = DNSUtils.textToIp(host, self.__useDns) | 								break | ||||||
| 						if ipMatch: | 						else: | ||||||
| 							for ip in ipMatch: | 							ipMatch = DNSUtils.textToIp(host, self.__useDns) | ||||||
| 								failList.append([ip, date]) | 							if ipMatch: | ||||||
| 							# We matched a regex, it is enough to stop. | 								for ip in ipMatch: | ||||||
| 							break | 									failList.append([failRegexIndex, ip, date]) | ||||||
|  | 								if not checkAllRegex: | ||||||
|  | 									break | ||||||
| 					except RegexException, e: # pragma: no cover - unsure if reachable | 					except RegexException, e: # pragma: no cover - unsure if reachable | ||||||
| 						logSys.error(e) | 						logSys.error(e) | ||||||
| 		return failList | 		return failList | ||||||
|  |  | ||||||
|  | @ -82,6 +82,7 @@ def testSampleRegexsFactory(name): | ||||||
| 		logFile = fileinput.FileInput( | 		logFile = fileinput.FileInput( | ||||||
| 			os.path.join(TEST_FILES_DIR, "logs", name)) | 			os.path.join(TEST_FILES_DIR, "logs", name)) | ||||||
| 
 | 
 | ||||||
|  | 		regexsUsed = set() | ||||||
| 		for line in logFile: | 		for line in logFile: | ||||||
| 			jsonREMatch = re.match("^# ?failJSON:(.+)$", line) | 			jsonREMatch = re.match("^# ?failJSON:(.+)$", line) | ||||||
| 			if jsonREMatch: | 			if jsonREMatch: | ||||||
|  | @ -96,7 +97,8 @@ def testSampleRegexsFactory(name): | ||||||
| 			else: | 			else: | ||||||
| 				faildata = {} | 				faildata = {} | ||||||
| 
 | 
 | ||||||
| 			ret = self.filter.processLine(line, returnRawHost=True) | 			ret = self.filter.processLine( | ||||||
|  | 				line, returnRawHost=True, checkAllRegex=True) | ||||||
| 			if not ret: | 			if not ret: | ||||||
| 				# Check line is flagged as none match | 				# Check line is flagged as none match | ||||||
| 				self.assertFalse(faildata.get('match', True), | 				self.assertFalse(faildata.get('match', True), | ||||||
|  | @ -107,15 +109,28 @@ def testSampleRegexsFactory(name): | ||||||
| 				self.assertTrue(faildata.get('match', False), | 				self.assertTrue(faildata.get('match', False), | ||||||
| 					"Line matched when shouldn't have: %s:%i %r" % | 					"Line matched when shouldn't have: %s:%i %r" % | ||||||
| 					(logFile.filename(), logFile.filelineno(), line)) | 					(logFile.filename(), logFile.filelineno(), line)) | ||||||
| 				self.assertEqual(len(ret), 1) | 				self.assertEqual(len(ret), 1, "Multiple regexs matched") | ||||||
| 				# Verify timestamp and host as expected | 				# Verify timestamp and host as expected | ||||||
| 				host, time = ret[0] | 				failregex, host, time = ret[0] | ||||||
| 				self.assertEqual(host, faildata.get("host", None)) | 				self.assertEqual(host, faildata.get("host", None)) | ||||||
| 				self.assertEqual( | 				self.assertEqual( | ||||||
| 					datetime.datetime.fromtimestamp(time), | 					datetime.datetime.fromtimestamp(time), | ||||||
| 					datetime.datetime.strptime( | 					datetime.datetime.strptime( | ||||||
| 						faildata.get("time", None), "%Y-%m-%dT%H:%M:%S")) | 						faildata.get("time", None), "%Y-%m-%dT%H:%M:%S")) | ||||||
| 
 | 
 | ||||||
|  | 				regexsUsed.add(failregex) | ||||||
|  | 
 | ||||||
|  | 		# TODO: Remove exception handling once all regexs have samples | ||||||
|  | 		for failRegexIndex, failRegex in enumerate(self.filter.getFailRegex()): | ||||||
|  | 			try: | ||||||
|  | 				self.assertTrue( | ||||||
|  | 					failRegexIndex in regexsUsed, | ||||||
|  | 					"Regex for filter '%s' has no samples: %i: %r" % | ||||||
|  | 						(name, failRegexIndex, failRegex)) | ||||||
|  | 			except AssertionError: | ||||||
|  | 				print "I: Regex for filter '%s' has no samples: %i: %r" % ( | ||||||
|  | 					name, failRegexIndex, failRegex) | ||||||
|  | 
 | ||||||
| 	return testFilter | 	return testFilter | ||||||
| 
 | 
 | ||||||
| for filter_ in os.listdir(os.path.join(CONFIG_DIR, "filter.d")): | for filter_ in os.listdir(os.path.join(CONFIG_DIR, "filter.d")): | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Steven Hiscocks
						Steven Hiscocks