diff --git a/ChangeLog b/ChangeLog index 6234636b..e55188eb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -70,8 +70,27 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better - IMPORTANT incompatible changes: - Fixes: + - Rename firewall-cmd-direct-new to firewall-cmd-new to fit within jail name + name length. As per gh-395 - allow for ",milliseconds" in the custom date format of proftpd.log - allow for ", referer ..." in apache-* filter for apache error logs. + - allow for spaces at the beginning of kernel messages. Closes gh-448 + - recidive jail to block all protocols. Closes gh-440. Thanks Ioan Indreias + - smtps not a IANA standard and has been removed from Arch. Replaced with + 465. Thanks Stefan. Closes gh-447 + - mysqld-syslog-iptables rule was too long. Part of gh-447. + - add 'flushlogs' command to allow logrotation without clobbering logtarget + settings. Closes gh-458, Debian bug #697333, Redhat bug #891798. + - 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 + +- Enhancements: + - long names on jails documented based on iptables limit of 30 less + len("fail2ban-"). + - remove indentation of name and loglevel while logging to SYSLOG to + resolve syslog(-ng) parsing problems. Closes Debian bug #730202. + - added squid filter. Thanks Roman Gelfand. - New Features: @@ -79,6 +98,7 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better * filter.d/solid-pop3d -- added thanks to Jacques Lav!gnotte on mailinglist. - Enhancements: + - loglines now also report "[PID]" after the name portion ver. 0.8.11 (2013/11/13) - loves-unittests-and-tight-DoS-free-filter-regexes diff --git a/DEVELOP b/DEVELOP index b45c0524..f10ff448 100644 --- a/DEVELOP +++ b/DEVELOP @@ -743,10 +743,14 @@ Releasing * https://github.com/fail2ban/fail2ban/issues?sort=updated&state=open * http://bugs.debian.org/cgi-bin/pkgreport.cgi?dist=unstable;package=fail2ban + * https://bugs.launchpad.net/ubuntu/+source/fail2ban * http://bugs.sabayon.org/buglist.cgi?quicksearch=net-analyzer%2Ffail2ban + * https://bugs.archlinux.org/?project=5&cat%5B%5D=33&string=fail2ban * https://bugs.gentoo.org/buglist.cgi?query_format=advanced&short_desc=fail2ban&bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=IN_PROGRESS&short_desc_type=allwords * https://bugzilla.redhat.com/buglist.cgi?query_format=advanced&bug_status=NEW&bug_status=ASSIGNED&component=fail2ban&classification=Red%20Hat&classification=Fedora * http://www.freebsd.org/cgi/query-pr-summary.cgi?text=fail2ban + * https://bugs.mageia.org/buglist.cgi?quicksearch=fail2ban + * https://build.opensuse.org/package/requests/openSUSE:Factory/fail2ban # Make sure the tests pass diff --git a/MANIFEST b/MANIFEST index e60b2685..1f8ac991 100644 --- a/MANIFEST +++ b/MANIFEST @@ -261,3 +261,7 @@ files/fail2ban-tmpfiles.conf files/fail2ban.service files/ipmasq-ZZZzzz_fail2ban.rul files/gen_badbots +testcases/config/jail.conf +testcases/config/fail2ban.conf +testcases/config/filter.d/simple.conf +testcases/config/action.d/brokenaction.conf diff --git a/THANKS b/THANKS index b57ab709..d62b3150 100644 --- a/THANKS +++ b/THANKS @@ -6,8 +6,10 @@ the project. If you have been left off, please let us know (preferably send a pull request on github with the "fix") and you will be added +Adam Tkac Adrien Clerc ache +ag4ve (Shawn) Amir Caspi Andrey G. Grozin Andy Fragen @@ -35,6 +37,7 @@ Hanno 'Rince' Wagner Iain Lea John Thoe Jacques Lav!gnotte +Ioan Indreias Jonathan Kamens Jonathan Lanning Jonathan Underwood @@ -61,10 +64,12 @@ RealRancor René Berber Robert Edeker Rolf Fokkens +Roman Gelfand Russell Odom Sebastian Arcus Sireyessire silviogarbes +Stefan Tatschner Stephen Gildea Steven Hiscocks TESTOVIK diff --git a/bin/fail2ban-testcases b/bin/fail2ban-testcases index bc1cb79a..578763d9 100755 --- a/bin/fail2ban-testcases +++ b/bin/fail2ban-testcases @@ -48,7 +48,7 @@ def get_opt_parser(): p.add_options([ Option('-l', "--log-level", type="choice", dest="log_level", - choices=('heavydebug', 'debug', 'info', 'warn', 'error', 'fatal'), + choices=('heavydebug', 'debug', 'info', 'warning', 'error', 'fatal'), default=None, help="Log level for the logger to use during running tests"), Option('-n', "--no-network", action="store_true", @@ -72,7 +72,7 @@ parser = get_opt_parser() logSys = logging.getLogger("fail2ban") # Numerical level of verbosity corresponding to a log "level" -verbosity = {'heavydebug': 3, +verbosity = {'heavydebug': 4, 'debug': 3, 'info': 2, 'warning': 1, diff --git a/config/action.d/apf.conf b/config/action.d/apf.conf index 9af3066d..f1d54dd2 100644 --- a/config/action.d/apf.conf +++ b/config/action.d/apf.conf @@ -41,3 +41,10 @@ actionban = apf --deny "banned by Fail2Ban " # Values: CMD # actionunban = apf --remove + +[Init] + +# Name used in APF configuration +# +name = default + diff --git a/config/action.d/blocklist_de.conf b/config/action.d/blocklist_de.conf new file mode 100644 index 00000000..d4170cab --- /dev/null +++ b/config/action.d/blocklist_de.conf @@ -0,0 +1,86 @@ +# Fail2Ban configuration file +# +# Author: Steven Hiscocks +# +# + +# Action to report IP address to blocklist.de +# Blocklist.de must be signed up to at www.blocklist.de +# Once registered, one or more servers can be added. +# This action requires the server 'email address' and the assoicate apikey. +# +# From blocklist.de: +# www.blocklist.de is a free and voluntary service provided by a +# Fraud/Abuse-specialist, whose servers are often attacked on SSH-, +# Mail-Login-, FTP-, Webserver- and other services. +# The mission is to report all attacks to the abuse deparments of the +# infected PCs/servers to ensure that the responsible provider can inform +# the customer about the infection and disable them +# +# IMPORTANT: +# +# Reporting an IP of abuse is a serious complaint. Make sure that it is +# serious. Fail2ban developers and network owners recommend you only use this +# action for: +# * The recidive where the IP has been banned multiple times +# * Where maxretry has been set quite high, beyond the normal user typing +# password incorrectly. +# * For filters that have a low likelyhood of receiving human errors +# + +[Definition] + +# Option: actionstart +# Notes.: command executed once at the start of Fail2Ban. +# Values: CMD +# +actionstart = + +# Option: actionstop +# Notes.: command executed once at the end of Fail2Ban +# Values: CMD +# +actionstop = + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionban = curl --fail --data-urlencode 'server=' --data 'apikey=' --data 'service=' --data 'ip=' --data-urlencode 'logs=' --data 'format=text' --user-agent "fail2ban v0.8.12" "https://www.blocklist.de/en/httpreports.html" + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = + +[Init] + +# Option: email +# Notes server email address, as per blocklise.de account +# Values: STRING Default: None +# +#email = + +# Option: apikey +# Notes your user blocklist.de user account apikey +# Values: STRING Default: None +# +#apikey = + +# Option: service +# Notes service name you are reporting on, typically aligns with filter name +# see http://www.blocklist.de/en/httpreports.html for full list +# Values: STRING Default: None +# +#service = diff --git a/config/action.d/complain.conf b/config/action.d/complain.conf index ad14a87e..62331f19 100644 --- a/config/action.d/complain.conf +++ b/config/action.d/complain.conf @@ -58,7 +58,7 @@ actioncheck = actionban = ADDRESSES=`whois | perl -e 'while () { next if /^changed|@(ripe|apnic)\.net/io; $m += (/abuse|trouble:|report|spam|security/io?3:0); if (/([a-z0-9_\-\.+]+@[a-z0-9\-]+(\.[[a-z0-9\-]+)+)/io) { while (s/([a-z0-9_\-\.+]+@[a-z0-9\-]+(\.[[a-z0-9\-]+)+)//io) { if ($m) { $a{lc($1)}=$m } else { $b{lc($1)}=$m } } $m=0 } else { $m && --$m } } if (%%a) {print join(",",keys(%%a))} else {print join(",",keys(%%b))}'` IP= if [ ! -z "$ADDRESSES" ]; then - (printf %%b "\n"; date '+Note: Local timezone is %%z (%%Z)'; grep '' ) | "Abuse from " $ADDRESSES + (printf %%b "\n"; date '+Note: Local timezone is %%z (%%Z)'; grep -E '(^|[^0-9])([^0-9]|$)' ) | "Abuse from " $ADDRESSES fi # Option: actionunban diff --git a/config/action.d/firewall-cmd-direct-new.conf b/config/action.d/firewallcmd-new.conf similarity index 71% rename from config/action.d/firewall-cmd-direct-new.conf rename to config/action.d/firewallcmd-new.conf index 55b6762d..bae72ca2 100644 --- a/config/action.d/firewall-cmd-direct-new.conf +++ b/config/action.d/firewallcmd-new.conf @@ -1,9 +1,5 @@ # Fail2Ban configuration file # -# Author: Edgar Hoch -# Copied from iptables-new.conf and modified for use with firewalld by Edgar Hoch. -# It uses "firewall-cmd" instead of "iptables". -# # Because of the --remove-rules in stop this action requires firewalld-0.3.8+ [INCLUDES] @@ -20,7 +16,7 @@ actionstop = firewall-cmd --direct --remove-rule ipv4 filter 0 -m state firewall-cmd --direct --remove-rules ipv4 filter fail2ban- firewall-cmd --direct --remove-chain ipv4 filter fail2ban- -actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q 'fail2ban-[ \t]' +actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q '^fail2ban-$' actionban = firewall-cmd --direct --add-rule ipv4 filter fail2ban- 0 -s -j @@ -50,3 +46,27 @@ protocol = tcp # Values: [ STRING ] # chain = INPUT_direct + +# DEV NOTES: +# +# Author: Edgar Hoch +# Copied from iptables-new.conf and modified for use with firewalld by Edgar Hoch. +# It uses "firewall-cmd" instead of "iptables". +# +# Output: +# +# $ firewall-cmd --direct --add-chain ipv4 filter fail2ban-name +# success +# $ firewall-cmd --direct --add-rule ipv4 filter fail2ban-name 1000 -j RETURN +# success +# $ sudo firewall-cmd --direct --add-rule ipv4 filter INPUT_direct 0 -m state --state NEW -p tcp --dport 22 -j fail2ban-name +# success +# $ firewall-cmd --direct --get-chains ipv4 filter +# fail2ban-name +# $ firewall-cmd --direct --get-chains ipv4 filter | od -h +# 0000000 6166 6c69 6232 6e61 6e2d 6d61 0a65 +# $ firewall-cmd --direct --get-chains ipv4 filter | grep -Eq 'fail2ban-name( |$)' ; echo $? +# 0 +# $ firewall-cmd -V +# 0.3.8 + diff --git a/config/action.d/ipfw.conf b/config/action.d/ipfw.conf index 09045815..37625209 100644 --- a/config/action.d/ipfw.conf +++ b/config/action.d/ipfw.conf @@ -43,7 +43,7 @@ actionban = ipfw add tcp from to # Tags: See jail.conf(5) man page # Values: CMD # -actionunban = ipfw delete `ipfw list | grep -i | awk '{print $1;}'` +actionunban = ipfw delete `ipfw list | grep -i "[^0-9][^0-9]" | awk '{print $1;}'` [Init] diff --git a/config/action.d/mail-whois-lines.conf b/config/action.d/mail-whois-lines.conf index 758c4eff..aa7d0950 100644 --- a/config/action.d/mail-whois-lines.conf +++ b/config/action.d/mail-whois-lines.conf @@ -39,10 +39,10 @@ actioncheck = actionban = printf %%b "Hi,\n The IP has just been banned by Fail2Ban after attempts against .\n\n - Here are more information about :\n - `whois `\n\n + Here is more information about :\n + `whois || echo missing whois program`\n\n Lines containing IP: in \n - `grep '\<\>' `\n\n + `grep '[^0-9][^0-9]' `\n\n Regards,\n Fail2Ban"|mail -s "[Fail2Ban] : banned from `uname -n`" diff --git a/config/action.d/mail-whois.conf b/config/action.d/mail-whois.conf index fa133ab3..e4c8450e 100644 --- a/config/action.d/mail-whois.conf +++ b/config/action.d/mail-whois.conf @@ -39,8 +39,8 @@ actioncheck = actionban = printf %%b "Hi,\n The IP has just been banned by Fail2Ban after attempts against .\n\n - Here are more information about :\n - `whois `\n + Here is more information about :\n + `whois || echo missing whois program`\n Regards,\n Fail2Ban"|mail -s "[Fail2Ban] : banned from `uname -n`" diff --git a/config/action.d/sendmail-whois-lines.conf b/config/action.d/sendmail-whois-lines.conf index 2ec71aa5..270373e7 100644 --- a/config/action.d/sendmail-whois-lines.conf +++ b/config/action.d/sendmail-whois-lines.conf @@ -23,10 +23,10 @@ actionban = printf %%b "Subject: [Fail2Ban] : banned from `uname -n` Hi,\n The IP has just been banned by Fail2Ban after attempts against .\n\n - Here are more information about :\n - `/usr/bin/whois `\n\n + Here is more information about :\n + `/usr/bin/whois || echo missing whois program`\n\n Lines containing IP: in \n - `grep '\<\>' `\n\n + `grep '[^0-9][^0-9]' `\n\n Regards,\n Fail2Ban" | /usr/sbin/sendmail -f diff --git a/config/action.d/sendmail-whois.conf b/config/action.d/sendmail-whois.conf index d6a7c3c1..fc601277 100644 --- a/config/action.d/sendmail-whois.conf +++ b/config/action.d/sendmail-whois.conf @@ -23,8 +23,8 @@ actionban = printf %%b "Subject: [Fail2Ban] : banned from `uname -n` Hi,\n The IP has just been banned by Fail2Ban after attempts against .\n\n - Here are more information about :\n - `/usr/bin/whois `\n + Here is more information about :\n + `/usr/bin/whois || echo missing whois program`\n Regards,\n Fail2Ban" | /usr/sbin/sendmail -f diff --git a/config/filter.d/common.conf b/config/filter.d/common.conf index b992e4b8..ae8e8b7b 100644 --- a/config/filter.d/common.conf +++ b/config/filter.d/common.conf @@ -34,7 +34,7 @@ __daemon_combs_re = (?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_r # Some messages have a kernel prefix with a timestamp # EXAMPLES: kernel: [769570.846956] -__kernel_prefix = kernel: \[\d+\.\d+\] +__kernel_prefix = kernel: \[ *\d+\.\d+\] __hostname = \S+ diff --git a/config/filter.d/proftpd.conf b/config/filter.d/proftpd.conf index bf8f9b5f..ac714cc1 100644 --- a/config/filter.d/proftpd.conf +++ b/config/filter.d/proftpd.conf @@ -1,5 +1,7 @@ # Fail2Ban fitler for the Proftpd FTP daemon # +# Set "UseReverseDNS off" in proftpd.conf to avoid the need for DNS. +# See: http://www.proftpd.org/docs/howto/DNS.html [INCLUDES] diff --git a/config/filter.d/recidive.conf b/config/filter.d/recidive.conf index 6d513116..14e82cc9 100644 --- a/config/filter.d/recidive.conf +++ b/config/filter.d/recidive.conf @@ -27,7 +27,7 @@ _daemon = fail2ban\.server\.actions # jail using this filter 'recidive', or change this line! _jailname = recidive -failregex = ^(%(__prefix_line)s|,\d{3} %(_daemon)s:\s+)WARNING\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+\s*$ +failregex = ^(%(__prefix_line)s| %(_daemon)s%(__pid_re)s?:\s+)WARNING\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+\s*$ [Init] diff --git a/config/filter.d/squid.conf b/config/filter.d/squid.conf new file mode 100644 index 00000000..da282692 --- /dev/null +++ b/config/filter.d/squid.conf @@ -0,0 +1,13 @@ +# Fail2Ban filter for Squid attempted proxy bypasses +# +# + +[Definition] + +failregex = ^\s+\d\s\s+[A-Z_]+_DENIED/403 .*$ + ^\s+\d\s\s+NONE/405 .*$ + + + +# Author: Daniel Black + diff --git a/config/jail.conf b/config/jail.conf index 5d98f73d..701b92ff 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -158,6 +158,17 @@ action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(proto action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"] + +# Report block via blocklist.de fail2ban reporting service API +# +# See the IMPORTANT note in action.d/blocklist_de.conf for when to +# use this action. Create a file jail.d/blocklist_de.local containing +# [Init] +# blocklist_de_apikey = {api key from registration] +# +action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)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 # globally (section [DEFAULT]) or per specific section diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 815a35d4..ab795a08 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -63,6 +63,8 @@ class Beautifier: msg = "Jail stopped" elif inC[0] == "add": msg = "Added jail " + response + elif inC[0] == "flushlogs": + msg = "logs: " + response elif inC[0:1] == ['status']: if len(inC) > 1: # Create IP list diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index 67e746d4..d3e22474 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -113,6 +113,7 @@ class ConfigReader(SafeConfigParserWithIncludes): # No "Definition" section or wrong basedir logSys.error(e) values[option[1]] = option[2] + # TODO: validate error handling here. except NoOptionError: if not option[2] is None: logSys.warning("'%s' not defined in '%s'. Using default one: %r" diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 0edfb2d3..1fcce4bb 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -62,7 +62,7 @@ class JailReader(ConfigReader): return out def isEnabled(self): - return self.__force_enable or self.__opts["enabled"] + return self.__force_enable or ( self.__opts and self.__opts["enabled"] ) @staticmethod def _glob(path): @@ -72,12 +72,10 @@ class JailReader(ConfigReader): """ pathList = [] for p in glob.glob(path): - if not os.path.exists(p): - logSys.warning("File %s doesn't even exist, thus cannot be monitored" % p) - elif not os.path.lexists(p): - logSys.warning("File %s is a dangling link, thus cannot be monitored" % p) - else: + if os.path.exists(p): pathList.append(p) + else: + logSys.warning("File %s is a dangling link, thus cannot be monitored" % p) return pathList def getOptions(self): @@ -95,20 +93,26 @@ class JailReader(ConfigReader): ["string", "filter", ""], ["string", "action", ""]] self.__opts = ConfigReader.getOptions(self, self.__name, opts) + if not self.__opts: + return False if self.isEnabled(): # Read filter - filterName, filterOpt = JailReader.extractOptions( - self.__opts["filter"]) - self.__filter = FilterReader( - filterName, self.__name, filterOpt, basedir=self.getBaseDir()) - ret = self.__filter.read() - if ret: - self.__filter.getOptions(self.__opts) + if self.__opts["filter"]: + filterName, filterOpt = JailReader.extractOptions( + self.__opts["filter"]) + self.__filter = FilterReader( + filterName, self.__name, filterOpt, basedir=self.getBaseDir()) + ret = self.__filter.read() + if ret: + self.__filter.getOptions(self.__opts) + else: + logSys.error("Unable to read the filter") + return False else: - logSys.error("Unable to read the filter") - return False - + self.__filter = None + logSys.warn("No filter set for jail %s" % self.__name) + # Read action for act in self.__opts["action"].split('\n'): try: @@ -180,7 +184,8 @@ class JailReader(ConfigReader): # Do not send a command if the rule is empty. if regex != '': stream.append(["set", self.__name, "addignoreregex", regex]) - stream.extend(self.__filter.convert()) + if self.__filter: + stream.extend(self.__filter.convert()) for action in self.__actions: stream.extend(action.convert()) stream.insert(0, ["add", self.__name, backend]) @@ -188,7 +193,11 @@ class JailReader(ConfigReader): #@staticmethod def extractOptions(option): - option_name, optstr = JailReader.optionCRE.match(option).groups() + match = JailReader.optionCRE.match(option) + if not match: + # TODO propper error handling + return None, None + option_name, optstr = match.groups() option_opts = dict() if optstr: for optmatch in JailReader.optionExtractRE.finditer(optstr): diff --git a/fail2ban/client/jailsreader.py b/fail2ban/client/jailsreader.py index 15d56c1c..c52f1b2b 100644 --- a/fail2ban/client/jailsreader.py +++ b/fail2ban/client/jailsreader.py @@ -60,6 +60,7 @@ class JailsReader(ConfigReader): sections = [ section ] # Get the options of all jails. + parse_status = True for sec in sections: jail = JailReader(sec, basedir=self.getBaseDir(), force_enable=self.__force_enable) @@ -71,8 +72,8 @@ class JailsReader(ConfigReader): self.__jails.append(jail) else: logSys.error("Errors in jail %r. Skipping..." % sec) - return False - return True + parse_status = False + return parse_status def convert(self, allow_no_files=False): """Convert read before __opts and jails to the commands stream diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 0435a415..0361fcc3 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -43,6 +43,7 @@ protocol = [ ["get loglevel", "gets the logging level"], ["set logtarget ", "sets logging target to . Can be STDOUT, STDERR, SYSLOG or a file"], ["get logtarget", "gets logging target"], +["flushlogs", "flushes the logtarget if a file and reopens it. For log rotation."], ['', "DATABASE", ""], ["set dbfile ", "set the location of fail2ban persistent datastore. Set to \"None\" to disable"], ["get dbfile", "get the location of fail2ban persistent datastore"], diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index dabbe1c3..6fc50b10 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -140,7 +140,7 @@ class DateDetector: date = template.getDate(line) if date is None: continue - logSys.debug("Got time %i for \"%r\" using template %s" % (date[0], date[1].group(), template.getName())) + logSys.debug("Got time %f for \"%r\" using template %s" % (date[0], date[1].group(), template.getName())) return date except ValueError: pass diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index c5adaea6..f713ee67 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -293,6 +293,9 @@ class Filter(JailThread): # to enable banip fail2ban-client BAN command def addBannedIP(self, ip): + if self.inIgnoreIPList(ip): + logSys.warning('Requested to manually ban an ignored IP %s. User knows best. Proceeding to ban it.' % ip) + unixTime = MyTime.time() for i in xrange(self.failManager.getMaxRetry()): self.failManager.addFailure(FailTicket(ip, unixTime)) @@ -561,7 +564,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 diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index ff780fe9..c4e091fe 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -110,9 +110,11 @@ class Jail: self.__filter = FilterSystemd(self) def setName(self, name): + # 20 based on iptable chain name limit of 30 less len('fail2ban-') if len(name) >= 20: - logSys.warning("Jail name %r might be too long and some commands " - "might not function correctly. Please shorten" + logSys.warning("Jail name %r might be too long and some commands" + " (e.g. iptables) might not function correctly." + " Please shorten" % name) self.__name = name diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 572d40ea..4cb379bf 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -409,13 +409,12 @@ class Server: try: self.__loggingLock.acquire() # set a format which is simpler for console use - formatter = logging.Formatter("%(asctime)s %(name)-16s: %(levelname)-6s %(message)s") + formatter = logging.Formatter("%(asctime)s %(name)-16s[%(process)d]: %(levelname)-7s %(message)s") if target == "SYSLOG": # Syslog daemons already add date to the message. - formatter = logging.Formatter("%(name)-16s: %(levelname)-6s %(message)s") + formatter = logging.Formatter("%(name)s[%(process)d]: %(levelname)s %(message)s") facility = logging.handlers.SysLogHandler.LOG_DAEMON - hdlr = logging.handlers.SysLogHandler("/dev/log", - facility = facility) + hdlr = logging.handlers.SysLogHandler("/dev/log", facility=facility) elif target == "STDOUT": hdlr = logging.StreamHandler(sys.stdout) elif target == "STDERR": @@ -424,7 +423,7 @@ class Server: # Target should be a file try: open(target, "a").close() - hdlr = logging.FileHandler(target) + hdlr = logging.handlers.RotatingFileHandler(target) except IOError: logSys.error("Unable to log to " + target) logSys.info("Logging to previous target " + self.__logTarget) @@ -466,6 +465,22 @@ class Server: finally: self.__loggingLock.release() + def flushLogs(self): + if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']: + for handler in logging.getLogger(__name__).parent.parent.handlers: + try: + handler.doRollover() + logSys.info("rollover performed on %s" % self.__logTarget) + except AttributeError: + handler.flush() + logSys.info("flush performed on %s" % self.__logTarget) + return "rolled over" + else: + for handler in logging.getLogger(__name__).parent.parent.handlers: + handler.flush() + logSys.info("flush performed on %s" % self.__logTarget) + return "flushed" + def setDatabase(self, filename): if self.__jails.size() == 0: if filename.lower() == "none": @@ -480,6 +495,7 @@ class Server: def getDatabase(self): return self.__db + def __createDaemon(self): # pragma: no cover """ Detach a process from the controlling terminal and run it in the background as a daemon. @@ -487,6 +503,14 @@ class Server: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731 """ + # When the first child terminates, all processes in the second child + # are sent a SIGHUP, so it's ignored. + + # We need to set this in the parent process, so it gets inherited by the + # child process, and this makes sure that it is effect even if the parent + # terminates quickly. + signal.signal(signal.SIGHUP, signal.SIG_IGN) + try: # Fork a child process so the parent can exit. This will return control # to the command line or shell. This is required so that the new process @@ -509,10 +533,6 @@ class Server: # leader. os.setsid() - # When the first child terminates, all processes in the second child - # are sent a SIGHUP, so it's ignored. - signal.signal(signal.SIGHUP, signal.SIG_IGN) - try: # Fork a second child to prevent zombies. Since the first child is # a session leader without a controlling terminal, it's possible for diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 3e4be6bf..c8c43f0e 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -92,6 +92,8 @@ class Transmitter: value = command[1] time.sleep(int(value)) return None + elif command[0] == "flushlogs": + return self.__server.flushLogs() elif command[0] == "set": return self.__commandSet(command[1:]) elif command[0] == "get": diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index ad22d2a2..36ecc3c9 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -24,41 +24,25 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -import unittest, time +import time import logging, sys -from StringIO import StringIO from fail2ban.server.action import Action -class ExecuteAction(unittest.TestCase): +from fail2ban.tests.utils import LogCaptureTestCase + +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/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 7b88d029..647b9f86 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -29,6 +29,7 @@ from fail2ban.client.filterreader import FilterReader from fail2ban.client.jailsreader import JailsReader from fail2ban.client.actionreader import ActionReader from fail2ban.client.configurator import Configurator +from fail2ban.tests.utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") if os.path.exists('config/fail2ban.conf'): @@ -36,6 +37,9 @@ if os.path.exists('config/fail2ban.conf'): else: CONFIG_DIR='/etc/fail2ban' +IMPERFECT_CONFIG = os.path.join('fail2ban', 'tests','config') + + class ConfigReaderTest(unittest.TestCase): def setUp(self): @@ -79,7 +83,14 @@ option = %s self._write('d.conf', 0) self.assertEqual(self._getoption('d'), 0) os.chmod(f, 0) - self.assertFalse(self.c.read('d')) # should not be readable BUT present + # fragile test and known to fail e.g. under Cygwin where permissions + # seems to be not enforced, thus condition + if not os.access(f, os.R_OK): + self.assertFalse(self.c.read('d')) # should not be readable BUT present + else: + # SkipTest introduced only in 2.7 thus can't yet use generally + # raise unittest.SkipTest("Skipping on %s -- access rights are not enforced" % platform) + pass def testOptionalDotDDir(self): @@ -143,11 +154,37 @@ c = d ;in line comment self.assertEqual(self.c.get('DEFAULT', 'b'), 'a') self.assertEqual(self.c.get('DEFAULT', 'c'), 'd') -class JailReaderTest(unittest.TestCase): +class JailReaderTest(LogCaptureTestCase): def testIncorrectJail(self): jail = JailReader('XXXABSENTXXX', basedir=CONFIG_DIR) self.assertRaises(ValueError, jail.read) + + def testJailActionEmpty(self): + jail = JailReader('emptyaction', basedir=IMPERFECT_CONFIG) + self.assertTrue(jail.read()) + self.assertTrue(jail.getOptions()) + self.assertTrue(jail.isEnabled()) + self.assertTrue(self._is_logged('No filter set for jail emptyaction')) + self.assertTrue(self._is_logged('No actions were defined for emptyaction')) + + def testJailActionFilterMissing(self): + jail = JailReader('missingbitsjail', basedir=IMPERFECT_CONFIG) + self.assertTrue(jail.read()) + self.assertFalse(jail.getOptions()) + self.assertTrue(jail.isEnabled()) + self.assertTrue(self._is_logged("Found no accessible config files for 'filter.d/catchallthebadies' under %s" % IMPERFECT_CONFIG)) + self.assertTrue(self._is_logged('Unable to read the filter')) + + def TODOtestJailActionBrokenDef(self): + jail = JailReader('brokenactiondef', basedir=IMPERFECT_CONFIG) + self.assertTrue(jail.read()) + self.assertFalse(jail.getOptions()) + self.assertTrue(jail.isEnabled()) + self.printLog() + self.assertTrue(self._is_logged('Error in action definition joho[foo')) + self.assertTrue(self._is_logged('Caught exception: While reading action joho[foo we should have got 1 or 2 groups. Got: 0')) + def testStockSSHJail(self): jail = JailReader('sshd', basedir=CONFIG_DIR) # we are running tests from root project dir atm @@ -155,7 +192,9 @@ class JailReaderTest(unittest.TestCase): self.assertTrue(jail.getOptions()) self.assertFalse(jail.isEnabled()) self.assertEqual(jail.getName(), 'sshd') - + jail.setName('ssh-funky-blocker') + self.assertEqual(jail.getName(), 'ssh-funky-blocker') + def testSplitOption(self): # Simple example option = "mail-whois[name=SSH]" @@ -163,6 +202,19 @@ class JailReaderTest(unittest.TestCase): result = JailReader.extractOptions(option) self.assertEqual(expected, result) + self.assertEqual(('mail.who_is', {}), JailReader.extractOptions("mail.who_is")) + self.assertEqual(('mail.who_is', {'a':'cat', 'b':'dog'}), JailReader.extractOptions("mail.who_is[a=cat,b=dog]")) + self.assertEqual(('mail--ho_is', {}), JailReader.extractOptions("mail--ho_is")) + + self.assertEqual(('mail--ho_is', {}), JailReader.extractOptions("mail--ho_is['s']")) + #self.printLog() + #self.assertTrue(self._is_logged("Invalid argument ['s'] in ''s''")) + + self.assertEqual(('mail', {'a': ','}), JailReader.extractOptions("mail[a=',']")) + + #self.assertRaises(ValueError, JailReader.extractOptions ,'mail-how[') + + # Empty option option = "abc[]" expected = ('abc', {}) @@ -187,6 +239,27 @@ class JailReaderTest(unittest.TestCase): result = JailReader.extractOptions(option) self.assertEqual(expected, result) + def testGlob(self): + d = tempfile.mkdtemp(prefix="f2b-temp") + # Generate few files + # regular file + f1 = os.path.join(d, 'f1') + open(f1, 'w').close() + # dangling link + f2 = os.path.join(d, 'f2') + os.symlink('nonexisting',f2) + + # must be only f1 + self.assertEqual(JailReader._glob(os.path.join(d, '*')), [f1]) + # since f2 is dangling -- empty list + self.assertEqual(JailReader._glob(f2), []) + self.assertTrue(self._is_logged('File %s is a dangling link, thus cannot be monitored' % f2)) + self.assertEqual(JailReader._glob(os.path.join(d, 'nonexisting')), []) + os.remove(f1) + os.remove(f2) + os.rmdir(d) + + class FilterReaderTest(unittest.TestCase): def testConvert(self): @@ -235,13 +308,74 @@ class FilterReaderTest(unittest.TestCase): output[-1][-1] = "5" self.assertEqual(sorted(filterReader.convert()), sorted(output)) -class JailsReaderTest(unittest.TestCase): +class JailsReaderTest(LogCaptureTestCase): def testProvidingBadBasedir(self): if not os.path.exists('/XXX'): reader = JailsReader(basedir='/XXX') self.assertRaises(ValueError, reader.read) + def testReadTestJailConf(self): + jails = JailsReader(basedir=IMPERFECT_CONFIG) + self.assertTrue(jails.read()) + self.assertFalse(jails.getOptions()) + self.assertRaises(ValueError, jails.convert) + comm_commands = jails.convert(allow_no_files=True) + self.maxDiff = None + self.assertEqual(sorted(comm_commands), + sorted([['add', 'emptyaction', 'auto'], + ['set', 'emptyaction', 'usedns', 'warn'], + ['set', 'emptyaction', 'maxretry', 3], + ['set', 'emptyaction', 'findtime', 600], + ['set', 'emptyaction', 'logencoding', 'auto'], + ['set', 'emptyaction', 'bantime', 600], + ['add', 'special', 'auto'], + ['set', 'special', 'usedns', 'warn'], + ['set', 'special', 'maxretry', 3], + ['set', 'special', 'addfailregex', ''], + ['set', 'special', 'findtime', 600], + ['set', 'special', 'logencoding', 'auto'], + ['set', 'special', 'bantime', 600], + ['add', 'missinglogfiles', 'auto'], + ['set', 'missinglogfiles', 'usedns', 'warn'], + ['set', 'missinglogfiles', 'maxretry', 3], + ['set', 'missinglogfiles', 'findtime', 600], + ['set', 'missinglogfiles', 'logencoding', 'auto'], + ['set', 'missinglogfiles', 'bantime', 600], + ['set', 'missinglogfiles', 'addfailregex', ''], + ['add', 'brokenaction', 'auto'], + ['set', 'brokenaction', 'usedns', 'warn'], + ['set', 'brokenaction', 'maxretry', 3], + ['set', 'brokenaction', 'findtime', 600], + ['set', 'brokenaction', 'logencoding', 'auto'], + ['set', 'brokenaction', 'bantime', 600], + ['set', 'brokenaction', 'addfailregex', ''], + ['set', 'brokenaction', 'addaction', 'brokenaction'], + ['set', + 'brokenaction', + 'actionban', + 'brokenaction', + 'hit with big stick '], + ['set', 'brokenaction', 'actionstop', 'brokenaction', ''], + ['set', 'brokenaction', 'actionstart', 'brokenaction', ''], + ['set', 'brokenaction', 'actionunban', 'brokenaction', ''], + ['set', 'brokenaction', 'actioncheck', 'brokenaction', ''], + ['add', 'parse_to_end_of_jail.conf', 'auto'], + ['set', 'parse_to_end_of_jail.conf', 'usedns', 'warn'], + ['set', 'parse_to_end_of_jail.conf', 'maxretry', 3], + ['set', 'parse_to_end_of_jail.conf', 'findtime', 600], + ['set', 'parse_to_end_of_jail.conf', 'logencoding', 'auto'], + ['set', 'parse_to_end_of_jail.conf', 'bantime', 600], + ['set', 'parse_to_end_of_jail.conf', 'addfailregex', ''], + ['start', 'emptyaction'], + ['start', 'special'], + ['start', 'missinglogfiles'], + ['start', 'brokenaction'], + ['start', 'parse_to_end_of_jail.conf'],])) + self.assertTrue(self._is_logged("Errors in jail 'missingbitsjail'. Skipping...")) + self.assertTrue(self._is_logged("No file(s) found for glob /weapons/of/mass/destruction")) + + def testReadStockJailConf(self): jails = JailsReader(basedir=CONFIG_DIR) # we are running tests from root project dir atm self.assertTrue(jails.read()) # opens fine @@ -251,6 +385,15 @@ class JailsReaderTest(unittest.TestCase): # commands to communicate to the server self.assertEqual(comm_commands, []) + # TODO: make sure this is handled well + ## We should not "read" some bogus jail + #old_comm_commands = comm_commands[:] # make a copy + #self.assertRaises(ValueError, jails.getOptions, "BOGUS") + #self.printLog() + #self.assertTrue(self._is_logged("No section: 'BOGUS'")) + ## and there should be no side-effects + #self.assertEqual(jails.convert(), old_comm_commands) + allFilters = set() # All jails must have filter and action set diff --git a/fail2ban/tests/config/action.d/brokenaction.conf b/fail2ban/tests/config/action.d/brokenaction.conf new file mode 100644 index 00000000..59e97b7f --- /dev/null +++ b/fail2ban/tests/config/action.d/brokenaction.conf @@ -0,0 +1,4 @@ + +[Definition] + +actionban = hit with big stick diff --git a/fail2ban/tests/config/fail2ban.conf b/fail2ban/tests/config/fail2ban.conf new file mode 100644 index 00000000..36984c78 --- /dev/null +++ b/fail2ban/tests/config/fail2ban.conf @@ -0,0 +1,5 @@ +[Definition] + +# 3 = INFO +loglevel = 3 + diff --git a/fail2ban/tests/config/filter.d/simple.conf b/fail2ban/tests/config/filter.d/simple.conf new file mode 100644 index 00000000..4a2c0bb7 --- /dev/null +++ b/fail2ban/tests/config/filter.d/simple.conf @@ -0,0 +1,4 @@ + +[Definition] + +failregex = diff --git a/fail2ban/tests/config/jail.conf b/fail2ban/tests/config/jail.conf new file mode 100644 index 00000000..525308e3 --- /dev/null +++ b/fail2ban/tests/config/jail.conf @@ -0,0 +1,33 @@ + +[DEFAULT] +filter = simple +logpath = /non/exist + +[emptyaction] +enabled = true +filter = +action = + +[special] +failregex = +ignoreregex = +ignoreip = + +[missinglogfiles] +logpath = /weapons/of/mass/destruction + +[brokenactiondef] +enabled = true +action = joho[foo + +[brokenaction] +enabled = true +action = brokenaction + +[missingbitsjail] +filter = catchallthebadies +action = thefunkychickendance + +[parse_to_end_of_jail.conf] +enabled = true +action = diff --git a/fail2ban/tests/config/apache-auth/README b/fail2ban/tests/files/config/apache-auth/README similarity index 100% rename from fail2ban/tests/config/apache-auth/README rename to fail2ban/tests/files/config/apache-auth/README diff --git a/fail2ban/tests/config/apache-auth/basic/authz_owner/.htaccess b/fail2ban/tests/files/config/apache-auth/basic/authz_owner/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/basic/authz_owner/.htaccess rename to fail2ban/tests/files/config/apache-auth/basic/authz_owner/.htaccess diff --git a/fail2ban/tests/config/apache-auth/basic/authz_owner/.htpasswd b/fail2ban/tests/files/config/apache-auth/basic/authz_owner/.htpasswd similarity index 100% rename from fail2ban/tests/config/apache-auth/basic/authz_owner/.htpasswd rename to fail2ban/tests/files/config/apache-auth/basic/authz_owner/.htpasswd diff --git a/fail2ban/tests/config/apache-auth/basic/authz_owner/cant_get_me.html b/fail2ban/tests/files/config/apache-auth/basic/authz_owner/cant_get_me.html similarity index 100% rename from fail2ban/tests/config/apache-auth/basic/authz_owner/cant_get_me.html rename to fail2ban/tests/files/config/apache-auth/basic/authz_owner/cant_get_me.html diff --git a/fail2ban/tests/config/apache-auth/basic/file/.htaccess b/fail2ban/tests/files/config/apache-auth/basic/file/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/basic/file/.htaccess rename to fail2ban/tests/files/config/apache-auth/basic/file/.htaccess diff --git a/fail2ban/tests/config/apache-auth/basic/file/.htpasswd b/fail2ban/tests/files/config/apache-auth/basic/file/.htpasswd similarity index 100% rename from fail2ban/tests/config/apache-auth/basic/file/.htpasswd rename to fail2ban/tests/files/config/apache-auth/basic/file/.htpasswd diff --git a/fail2ban/tests/config/apache-auth/digest.py b/fail2ban/tests/files/config/apache-auth/digest.py similarity index 100% rename from fail2ban/tests/config/apache-auth/digest.py rename to fail2ban/tests/files/config/apache-auth/digest.py diff --git a/fail2ban/tests/config/apache-auth/digest/.htaccess b/fail2ban/tests/files/config/apache-auth/digest/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/digest/.htaccess rename to fail2ban/tests/files/config/apache-auth/digest/.htaccess diff --git a/fail2ban/tests/config/apache-auth/digest/.htpasswd b/fail2ban/tests/files/config/apache-auth/digest/.htpasswd similarity index 100% rename from fail2ban/tests/config/apache-auth/digest/.htpasswd rename to fail2ban/tests/files/config/apache-auth/digest/.htpasswd diff --git a/fail2ban/tests/config/apache-auth/digest_anon/.htaccess b/fail2ban/tests/files/config/apache-auth/digest_anon/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/digest_anon/.htaccess rename to fail2ban/tests/files/config/apache-auth/digest_anon/.htaccess diff --git a/fail2ban/tests/config/apache-auth/digest_anon/.htpasswd b/fail2ban/tests/files/config/apache-auth/digest_anon/.htpasswd similarity index 100% rename from fail2ban/tests/config/apache-auth/digest_anon/.htpasswd rename to fail2ban/tests/files/config/apache-auth/digest_anon/.htpasswd diff --git a/fail2ban/tests/config/apache-auth/digest_time/.htaccess b/fail2ban/tests/files/config/apache-auth/digest_time/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/digest_time/.htaccess rename to fail2ban/tests/files/config/apache-auth/digest_time/.htaccess diff --git a/fail2ban/tests/config/apache-auth/digest_time/.htpasswd b/fail2ban/tests/files/config/apache-auth/digest_time/.htpasswd similarity index 100% rename from fail2ban/tests/config/apache-auth/digest_time/.htpasswd rename to fail2ban/tests/files/config/apache-auth/digest_time/.htpasswd diff --git a/fail2ban/tests/config/apache-auth/digest_wrongrelm/.htaccess b/fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/digest_wrongrelm/.htaccess rename to fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htaccess diff --git a/fail2ban/tests/config/apache-auth/digest_wrongrelm/.htpasswd b/fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htpasswd similarity index 100% rename from fail2ban/tests/config/apache-auth/digest_wrongrelm/.htpasswd rename to fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htpasswd diff --git a/fail2ban/tests/config/apache-auth/noentry/.htaccess b/fail2ban/tests/files/config/apache-auth/noentry/.htaccess similarity index 100% rename from fail2ban/tests/config/apache-auth/noentry/.htaccess rename to fail2ban/tests/files/config/apache-auth/noentry/.htaccess diff --git a/fail2ban/tests/files/logs/recidive b/fail2ban/tests/files/logs/recidive index 634f103c..b9c39105 100644 --- a/fail2ban/tests/files/logs/recidive +++ b/fail2ban/tests/files/logs/recidive @@ -1,5 +1,7 @@ # failJSON: { "time": "2006-02-13T15:52:30", "match": true , "host": "1.2.3.4" } 2006-02-13 15:52:30,388 fail2ban.server.actions: WARNING [sendmail] Ban 1.2.3.4 +# failJSON: { "time": "2006-02-13T15:52:30", "match": true , "host": "1.2.3.4", "desc": "Extended with [PID]" } +2006-02-13 15:52:30,388 fail2ban.server.actions[123]: WARNING [sendmail] Ban 1.2.3.4 # failJSON: { "match": false } 2006-02-13 16:07:31,183 fail2ban.server.actions: WARNING [sendmail] Unban 1.2.3.4 # failJSON: { "match": false } diff --git a/fail2ban/tests/files/logs/squid b/fail2ban/tests/files/logs/squid new file mode 100644 index 00000000..300a8ac5 --- /dev/null +++ b/fail2ban/tests/files/logs/squid @@ -0,0 +1,13 @@ +# Logs thanks to Roman Gelfand +# +# failJSON: { "time": "2013-12-08T23:55:23.000", "match": true , "host": "91.188.124.227" } +1386543323.000 4 91.188.124.227 TCP_DENIED/403 4099 GET http://www.proxy-listen.de/azenv.php - HIER_NONE/- text/html + +# failJSON: { "time": "2013-12-08T23:58:20", "match": true , "host": "175.44.0.184" } +1386543500.000 5 175.44.0.184 NONE/405 3364 CONNECT error:method-not-allowed - HIER_NONE/- text/html + +# failJSON: { "time": "2013-12-09T00:08:04.000", "match": true , "host": "198.74.125.200" } +1386544084.000 3 198.74.125.200 TCP_DENIED/403 3722 GET http://www2t.biglobe.ne.jp/~take52/test/env.cgi - HIER_NONE/- text/html + +# failJSON: { "time": "2013-12-09T00:09:06.000", "match": true , "host": "175.42.91.151" } +1386544146.000 1 175.42.91.151 TCP_DENIED/403 3745 GET http://pkfsp.ru/wp-content/uploads/proxyc/engine.php - HIER_NONE/- text/html diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index cdd20c9e..5f7fba23 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -41,14 +41,11 @@ from fail2ban.server.failmanager import FailManager from fail2ban.server.failmanager import FailManagerEmpty from fail2ban.server.mytime import MyTime from fail2ban.tests.utils import setUpMyTime, tearDownMyTime +from fail2ban.tests.utils import mtimesleep, LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") -# -# Useful helpers -# - -from utils import mtimesleep +from fail2ban.tests.dummyjail import DummyJail # yoh: per Steven Hiscocks's insight while troubleshooting # https://github.com/fail2ban/fail2ban/issues/103#issuecomment-15542836 @@ -192,14 +189,27 @@ def _copy_lines_to_journal(in_, fields={},n=None, skip=0, terminal_line=""): # p # Actual tests # -class IgnoreIP(unittest.TestCase): +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): """Call before every test case.""" - self.filter = FileFilter(None) - - def tearDown(self): - """Call after every test case.""" + LogCaptureTestCase.setUp(self) + 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" @@ -207,19 +217,49 @@ class IgnoreIP(unittest.TestCase): 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")) 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)) + + 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')) + + def testIgnoreInProcessLine(self): + setUpMyTime() + self.filter.addIgnoreIP('192.168.1.0/25') + self.filter.addFailRegex('') + self.filter.processLineAndAdd('1387203300.222 192.168.1.32') + self.assertTrue(self._is_logged('Ignore 192.168.1.32')) + tearDownMyTime() + + 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 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") 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): @@ -242,12 +282,13 @@ 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.""" setUpMyTime() + LogCaptureTestCase.setUp(self) self.filter = self.name = 'NA' _, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures') self.file = open(self.name, 'a') @@ -258,6 +299,7 @@ class LogFileMonitor(unittest.TestCase): def tearDown(self): tearDownMyTime() + LogCaptureTestCase.tearDown(self) _killfile(self.file, self.name) pass @@ -275,6 +317,21 @@ class LogFileMonitor(unittest.TestCase): # 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')) + 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()) @@ -357,7 +414,6 @@ class LogFileMonitor(unittest.TestCase): from threading import Lock -from dummyjail import DummyJail def get_monitor_failures_testcase(Filter_): """Generator of TestCase's for different filters/backends @@ -726,7 +782,13 @@ class GetFailures(unittest.TestCase): """Call after every test case.""" tearDownMyTime() - + 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 diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index f616ba1e..28513937 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -141,7 +141,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, diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 9d9a9f57..26ff8e0a 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -24,7 +24,7 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -import unittest, socket, time, tempfile, os, locale, sys +import unittest, socket, time, tempfile, os, locale, sys, logging from fail2ban.server.server import Server from fail2ban.server.jail import Jail @@ -644,7 +644,7 @@ class TransmitterLogging(TransmitterBase): value = "/this/path/should/not/exist" self.setGetTestNOK("logtarget", value) - self.transm.proceed(["set", "/dev/null"]) + self.transm.proceed(["set", "logtarget", "/dev/null"]) for logTarget in logTargets: os.remove(logTarget) @@ -662,6 +662,53 @@ class TransmitterLogging(TransmitterBase): self.setGetTest("loglevel", "0", 0) self.setGetTestNOK("loglevel", "Bird") + def testFlushLogs(self): + self.assertEqual(self.transm.proceed(["flushlogs"]), (0, "rolled over")) + try: + f, fn = tempfile.mkstemp("fail2ban.log") + os.close(f) + self.server.setLogLevel(2) + self.assertEqual(self.transm.proceed(["set", "logtarget", fn]), (0, fn)) + l = logging.getLogger('fail2ban.server.server').parent.parent + l.warn("Before file moved") + try: + f2, fn2 = tempfile.mkstemp("fail2ban.log") + os.close(f2) + os.rename(fn, fn2) + l.warn("After file moved") + self.assertEqual(self.transm.proceed(["flushlogs"]), (0, "rolled over")) + l.warn("After flushlogs") + with open(fn2,'r') as f: + line1 = f.next() + if line1.find('Changed logging target to') >= 0: + line1 = f.next() + self.assertTrue(line1.endswith("Before file moved\n")) + line2 = f.next() + self.assertTrue(line2.endswith("After file moved\n")) + try: + n = f.next() + if n.find("Command: ['flushlogs']") >=0: + self.assertRaises(StopIteration, f.next) + else: + self.fail("Exception StopIteration or Command: ['flushlogs'] expected. Got: %s" % n) + except StopIteration: + pass # on higher debugging levels this is expected + with open(fn,'r') as f: + line1 = f.next() + if line1.find('rollover performed on') >= 0: + line1 = f.next() + self.assertTrue(line1.endswith("After flushlogs\n")) + self.assertRaises(StopIteration, f.next) + finally: + os.remove(fn2) + finally: + try: + os.remove(fn) + except OSError: + pass + self.assertEqual(self.transm.proceed(["set", "logtarget", "STDERR"]), (0, "STDERR")) + self.assertEqual(self.transm.proceed(["flushlogs"]), (0, "flushed")) + class JailTests(unittest.TestCase): diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 4321c3a6..5e181d02 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -24,6 +24,7 @@ __license__ = "GPL" import logging, os, re, traceback, time, unittest, sys from os.path import basename, dirname +from StringIO import StringIO if sys.version_info >= (2, 6): import json @@ -244,3 +245,32 @@ def gatherTests(regexps=None, no_network=False): tests.addTest(unittest.makeSuite(servertestcase.TransmitterLogging)) return tests + +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() + + def printLog(self): + print(self._log.getvalue()) diff --git a/files/fail2ban-logrotate b/files/fail2ban-logrotate index 67c6364a..a09870af 100644 --- a/files/fail2ban-logrotate +++ b/files/fail2ban-logrotate @@ -13,6 +13,6 @@ missingok compress postrotate - /usr/bin/fail2ban-client set logtarget /var/log/fail2ban.log 1>/dev/null || true + /usr/bin/fail2ban-client flushlogs 1>/dev/null || true endscript } diff --git a/files/redhat-initd b/files/redhat-initd index 43147c95..08eb0f3c 100755 --- a/files/redhat-initd +++ b/files/redhat-initd @@ -1,6 +1,6 @@ #!/bin/bash # -# chkconfig: 345 92 08 +# chkconfig: - 92 08 # processname: fail2ban-server # config: /etc/fail2ban/fail2ban.conf # pidfile: /var/run/fail2ban/fail2ban.pid diff --git a/man/jail.conf.5 b/man/jail.conf.5 index a0ca6ca0..a59ce6f6 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -64,6 +64,12 @@ Comments: use '#' for comment lines and ';' (following a space) for inline comme .SH DEFAULT The following options are applicable to all jails. Their meaning is described in the default \fIjail.conf\fR file. .TP +\fBfilter\fR +.TP +\fBlogpath\fR +.TP +\fBaction\fR +.TP \fBignoreip\fR .TP \fBbantime\fR @@ -75,6 +81,11 @@ The following options are applicable to all jails. Their meaning is described in \fBbackend\fR .TP \fBusedns\fR +.TP +\fBfailregex\fR +.TP +\fBignoreregex\fR + .PP .SS Backends \fBbackend\fR specifies the backend used to get files modification. This option can be overridden in each jail as well.