merge master

pull/748/head
pacop 2014-10-25 18:15:34 +02:00
commit e3a037ee3f
59 changed files with 1073 additions and 201 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ htmlcov
*.rej
*.bak
__pycache__
.vagrant/

View File

@ -16,6 +16,8 @@ install:
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cd ..; pip install -q coveralls; cd -; fi
script:
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coverage run --rcfile=.travis_coveragerc setup.py test; else python setup.py test; fi
# test installation
- sudo python setup.py install
after_success:
# Coverage config file must be .coveragerc for coveralls
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cp -v .travis_coveragerc .coveragerc; fi

View File

@ -16,6 +16,9 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
provides defaults for the chain, port, protocol and name tags
- Fixes:
* start of file2ban aborted (on slow hosts, systemd considers the server has
been timed out and kills him), see gh-824
* UTF-8 fixes in pure-ftp thanks to Johannes Weberhofer. Closes gh-806.
* systemd backend error on bad utf-8 in python3
* badips.py action error when logging HTTP error raised with badips request
* fail2ban-regex failed to work in python3 due to space/tab mix
@ -36,18 +39,48 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
Thanks Serg G. Brester
* Correct times for non-timezone date times formats during DST
* Pass a copy of, not original, aInfo into actions to avoid side-effects
* Per-distribution paths to the exim's main log
* Ignored IPs are no longer banned when being restored from persistent
database
* Manually unbanned IPs are now removed from persistent database, such they
wont be banned again when Fail2Ban is restarted
* Pass "bantime" parameter to the actions in default jail's action
definition(s)
* filters.d/sieve.conf - fixed typo in _daemon. Thanks Jisoo Park
* cyrus-imap -- also catch also failed logins via secured (imaps/pop3s).
Regression was introduced while strengthening failregex in 0.8.11 (bd175f)
Debian bug #755173
* postfix-sasl - added journalmatch. Thanks Luc Maisonobe
* postfix* - match with a new daemon string (postfix/submission/smtpd).
Closes gh-804 . Thanks Paul Traina
* apache - added filter for AH01630 client denied by server configuration.
- New features:
- Added monit filter thanks Jason H Martin.
- New filters:
- monit Thanks Jason H Martin
- directadmin Thanks niorg
- apache-shellshock Thanks Eugene Hopkinson (SlowRiot)
- New actions:
- symbiosis-blacklist-allports for Bytemark symbiosis firewall
- fail2ban-client can fetch the running server version
- Added Cloudflare API action
- Enhancements
* Start performance of fail2ban-client (and tests) increased, start time
and cpu usage rapidly reduced. Introduced a shared storage logic, to
bypass reading lots of config files (see gh-824).
Thanks to Joost Molenaar for good catch (reported gh-820).
* Fail2ban-regex - add print-all-matched option. Closes gh-652
* Suppress fail2ban-client warnings for non-critical config options
* Match non "Bye Bye" disconnect messages for sshd locked account regex
* courier-smtp filter:
- match lines with user names
- match lines containing "535 Authentication failed" attempts
* Add <chain> tag to iptables-ipsets
* Realign fail2ban log output with white space to improve readability. Does
not affect SYSLOG output
* Log unhandled exceptions
* cyrus-imap: catch "user not found" attempts
ver. 0.9.0 (2014/03/14) - beta
----------

12
DEVELOP
View File

@ -81,6 +81,18 @@ some quick commands::
status test
Testing with vagrant
--------------------
Testing can now be done inside a vagrant VM. Vagrantfile provided in
source code repository established two VMs:
- VM "secure" which can be used for testing fail2ban code.
- VM "attacker" which hcan be used to perform attack against our "secure" VM.
Both VMs are sharing the 192.168.200/24 network. If you are using this network
take a look into the Vagrantfile and change the IP.
Coding Standards
================

10
THANKS
View File

@ -26,6 +26,7 @@ Christian Rauch
Christophe Carles
Christoph Haas
Christos Psonis
craneworks
Cyril Jaquier
Daniel B. Cid
Daniel B.
@ -34,6 +35,7 @@ David Nutter
Derek Atkins
Eric Gerbier
Enrico Labedzki
Eugene Hopkinson (SlowRiot)
ftoppi
François Boulogne
Frédéric
@ -44,11 +46,13 @@ Hank Leininger
Hanno 'Rince' Wagner
Helmut Grohne
Iain Lea
Ioan Indreias
Ivo Truxa
John Thoe
Jacques Lav!gnotte
Ioan Indreias
Johannes Weberhofer
Jason H Martin
Jisoo Park
Joel M Snyder
Jonathan Kamens
Jonathan Lanning
@ -62,6 +66,7 @@ kjohnsonecl
kojiro
Lars Kneschke
Lee Clemens
leftyfb (Mike Rushton)
Manuel Arostegui Ramirez
Marcel Dopita
Mark Edgington
@ -78,8 +83,10 @@ Mika (mkl)
Nick Munger
onorua
Paul Marrapese
Paul Traina
Noel Butler
Patrick Börjesson
Pressy
Raphaël Marichez
RealRancor
René Berber
@ -88,6 +95,7 @@ Rolf Fokkens
Roman Gelfand
Russell Odom
SATO Kentaro
Sean DuBois
Sebastian Arcus
Serg G. Brester
Sireyessire

30
Vagrantfile vendored Normal file
View File

@ -0,0 +1,30 @@
Vagrant.configure("2") do |config|
config.vm.define "secure" do |secure|
secure.vm.box = "ubuntu/trusty64"
secure.vm.hostname = "secure.dev.fail2ban.org"
secure.vm.network "private_network", ip: "192.168.200.100"
# secure.vm.synced_folder 'salt/roots', '/srv/salt'
# secure.vm.provision :salt do |salt|
# salt.minion_config = 'salt/minion'
# salt.run_highstate = true
# salt.verbose = true
# end
end
config.vm.define "attacker" do |attacker|
attacker.vm.box = "ubuntu/trusty64"
attacker.vm.hostname = "attacker.dev.fail2ban.org"
attacker.vm.network "private_network", ip: "192.168.200.150"
# attacker.vm.synced_folder 'salt/roots', '/srv/salt'
# attacker.vm.provision :salt do |salt|
# salt.minion_config = 'salt/minion'
# salt.run_highstate = true
# salt.verbose = true
# end
end
end

View File

@ -409,6 +409,7 @@ class Fail2banClient:
# TODO: get away from stew of return codes and exception
# handling -- handle via exceptions
try:
self.__configurator.Reload()
self.__configurator.readAll()
ret = self.__configurator.getOptions(jail)
self.__configurator.convertToProtocol()
@ -418,12 +419,11 @@ class Fail2banClient:
ret = False
return ret
#@staticmethod
@staticmethod
def dumpConfig(cmd):
for c in cmd:
print c
return True
dumpConfig = staticmethod(dumpConfig)
class ServerExecutionException(Exception):

View File

@ -0,0 +1,55 @@
#
# Author: Mike Rushton
#
# Referenced from from http://www.normyee.net/blog/2012/02/02/adding-cloudflare-support-to-fail2ban by NORM YEE
#
# To get your Cloudflare API key: https://www.cloudflare.com/my-account
#
[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: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionban = curl https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionunban = curl https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
[Init]
# Default Cloudflare API token
cftoken =
# Default Cloudflare username
cfuser =

View File

@ -0,0 +1,52 @@
# Fail2Ban configuration file for Bytemark Symbiosis firewall
#
# Author: Yaroslav Halchenko
#
[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 = iptables -n -L <chain>
# Option: actionban
# Notes.: command executed when banning an IP.
# Values: CMD
#
actionban = echo 'all' >| /etc/symbiosis/firewall/blacklist.d/<ip>.auto
iptables -I <chain> 1 -s <ip> -j <blocktype>
# Option: actionunban
# Notes.: command executed when unbanning an IP.
# Values: CMD
#
actionunban = rm -f /etc/symbiosis/firewall/blacklist.d/<ip>.auto
iptables -D <chain> -s <ip> -j <blocktype> || :
[Init]
# Option: chain
# Notes specifies the iptables chain to which the fail2ban rules should be
# added to. blacklist is a chain initiated by symbiosis firewall.
# Values: STRING Default: blacklist
chain = blacklist
# Option: blocktype
# Note: This is to match default symbiosis firewall type for blacklisted IPs
# Values: STRING
blocktype = DROP

View File

@ -1,9 +1,9 @@
# Fail2Ban action configuration file for ufw
#
# You are required to run "ufw enable" before this will have an effect.
# You are required to run "ufw enable" before this will have any effect.
#
# The insert position should be approprate to block the required traffic.
# A number after an allow rule to the application won't be much use.
# The insert position should be appropriate to block the required traffic.
# A number after an allow rule to the application won't be of much use.
[Definition]
@ -19,7 +19,7 @@ actionunban = [ -n "<application>" ] && app="app <application>" ; ufw delete <bl
[Init]
# Option: insertpos
# Notes.: The postition number in the firewall list to insert the block rule
# Notes.: The position number in the firewall list to insert the block rule
insertpos = 1
# Option: blocktype

View File

@ -10,7 +10,7 @@ before = apache-common.conf
[Definition]
failregex = ^%(_apache_error_client)s (AH01797: )?client denied by server configuration: (uri )?\S*(, referer: \S+)?\s*$
failregex = ^%(_apache_error_client)s (AH(01797|01630): )?client denied by server configuration: (uri )?\S*(, referer: \S+)?\s*$
^%(_apache_error_client)s (AH01617: )?user .*? authentication failure for "\S*": Password Mismatch(, referer: \S+)?$
^%(_apache_error_client)s (AH01618: )?user .*? not found(: )?\S*(, referer: \S+)?\s*$
^%(_apache_error_client)s (AH01614: )?client used wrong authentication scheme: \S*(, referer: \S+)?\s*$

View File

@ -0,0 +1,26 @@
# Fail2Ban filter to block web requests containing custom headers attempting to exploit the shellshock bug
#
#
[INCLUDES]
# overwrite with apache-common.local if _apache_error_client is incorrect.
before = apache-common.conf
[Definition]
failregex = ^%(_apache_error_client)s (AH01215: )?/bin/(ba)?sh: warning: HTTP_.*?: ignoring function definition attempt(, referer: \S+)?\s*$
^%(_apache_error_client)s (AH01215: )?/bin/(ba)?sh: error importing function definition for `HTTP_.*?'(, referer: \S+)?\s*$
ignoreregex =
# DEV Notes:
#
# https://wiki.apache.org/httpd/ListOfErrors for apache error IDs
#
# example log lines:
# [Thu Sep 25 09:27:18.813902 2014] [cgi:error] [pid 16860] [client 89.207.132.76:59635] AH01215: /bin/bash: warning: HTTP_TEST: ignoring function definition attempt
# [Thu Sep 25 09:29:56.141832 2014] [cgi:error] [pid 16864] [client 162.247.73.206:41273] AH01215: /bin/bash: error importing function definition for `HTTP_TEST'
#
# Author: Eugene Hopkinson (riot@riot.so)

View File

@ -12,7 +12,8 @@ before = common.conf
_daemon = courieresmtpd
failregex = ^%(__prefix_line)serror,relay=<HOST>,.*: 550 User unknown\.$
failregex = ^%(__prefix_line)serror,relay=<HOST>,.*: 550 User (<.*> )?unknown\.?$
^%(__prefix_line)serror,relay=<HOST>,msg="535 Authentication failed\.",cmd:( AUTH \S+)?( [0-9a-zA-Z\+/=]+)?$
ignoreregex =

View File

@ -11,9 +11,9 @@ before = common.conf
[Definition]
_daemon = (?:cyrus/)?(?:imapd?|pop3d?)
_daemon = (?:cyrus/)?(?:imap(d|s)?|pop3(d|s)?)
failregex = ^%(__prefix_line)sbadlogin: \S+ ?\[<HOST>\] \S+ .*?\[?SASL\(-13\): authentication failure: .*\]?$
failregex = ^%(__prefix_line)sbadlogin: \S+ ?\[<HOST>\] \S+ .*?\[?SASL\(-13\): (authentication failure|user not found): .*\]?$
ignoreregex =

View File

@ -0,0 +1,23 @@
# Fail2Ban configuration file for Directadmin
#
#
#
[INCLUDES]
before = common.conf
[Definition]
failregex = ^: \'<HOST>\' \d{1,3} failed login attempt(s)?. \s*
ignoreregex =
[Init]
datepattern = ^%%Y:%%m:%%d-%%H:%%M:%%S
#
# Requires Directadmin v1.45.3 or higher. http://www.directadmin.com/features.php?id=1590
#
# Author: Cyril Roos

View File

@ -7,8 +7,15 @@ before = common.conf
[Definition]
_daemon = postfix/smtpd
_daemon = postfix/(submission/)?smtp(d|s)
failregex = ^%(__prefix_line)swarning: [-._\w]+\[<HOST>\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$
ignoreregex =
[Init]
journalmatch = _SYSTEMD_UNIT=postfix.service
# Author: Yaroslav Halchenko

View File

@ -10,7 +10,7 @@ before = common.conf
[Definition]
_daemon = postfix/smtpd
_daemon = postfix/(submission/)?smtp(d|s)
failregex = ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 554 5\.7\.1 .*$
^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 450 4\.7\.1 : Helo command rejected: Host not found; from=<> to=<> proto=ESMTP helo= *$

View File

@ -15,7 +15,7 @@ before = common.conf
_daemon = pure-ftpd
# Error message specified in multiple languages
__errmsg = (?:<EFBFBD>ϥΪ<EFBFBD>\[.*\]<5D><><EFBFBD>ҥ<EFBFBD><D2A5><EFBFBD>|ʹ<><CAB9><EFBFBD><EFBFBD>\[.*\]<5D><>֤ʧ<D6A4><CAA7>|\[.*\] kullan<61>c<EFBFBD>s<EFBFBD> i<>in giri<72> hatal<61>|<7C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> \[.*\]|Godkjennelse mislyktes for \[.*\]|Beh<65>righetskontroll misslyckas f<>r anv<6E>ndare \[.*\]|Autentifikacia uzivatela zlyhala \[.*\]|Autentificare esuata pentru utilizatorul \[.*\]|Autentica<63><61>o falhou para usu<73>rio \[.*\]|Autentyfikacja nie powiod<6F>a si<73> dla u<>ytkownika \[.*\]|Autorisatie faalde voor gebruiker \[.*\]|\[.*\] <20><><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>|Autenticazione falita per l'utente \[.*\]|Azonos<6F>t<EFBFBD>s sikertelen \[.*\] felhaszn<7A>l<EFBFBD>nak|\[.*\] c'est un batard, il connait pas son code|Erreur d'authentification pour l'utilisateur \[.*\]|Autentificaci<63>n fallida para el usuario \[.*\]|Authentication failed for user \[.*\]|Authentifizierung fehlgeschlagen f<>r Benutzer \[.*\].|Godkendelse mislykkedes for \[.*\]|Autentifikace u<>ivatele selhala \[.*\])
__errmsg = (?:Godkendelse mislykkedes for \[.*\]|Authentifizierung fehlgeschlagen für Benutzer \[.*\].|Authentication failed for user \[.*\]|Autentificación fallida para el usuario \[.*\]|\[.*\] c'est un batard, il connait pas son code|Erreur d'authentification pour l'utilisateur \[.*\]|Azonosítás sikertelen \[.*\] felhasználónak|Autenticazione falita per l'utente \[.*\]|Autorisatie faalde voor gebruiker \[.*\]|Godkjennelse mislyktes for \[.*\]|\[.*\] kullanýcýsý için giriþ hatalý|Autenticação falhou para usuário \[.*\]|Autentificare esuata pentru utilizatorul \[.*\]|Autentifikace uživatele selhala \[.*\]|Autentyfikacja nie powiodła się dla użytkownika \[.*\]|Autentifikacia uzivatela zlyhala \[.*\]|Behörighetskontroll misslyckas för användare \[.*\]|Авторизация не удалась пользователю \[.*\]|\[.*\] 嶸盪 檣隸 褒ぬ|妏蚚氪\[.*\]桄痐囮啖|使用者\[.*\]驗證失敗)
failregex = ^%(__prefix_line)s\(.+?@<HOST>\) \[WARNING\] %(__errmsg)s\s*$
@ -24,7 +24,13 @@ ignoreregex =
# Author: Cyril Jaquier
# Modified: Yaroslav Halchenko for pure-ftpd
# Documentation thanks to Blake on http://www.fail2ban.org/wiki/index.php?title=Fail2ban:Community_Portal
# UTF-8 editing and mechanism thanks to Johannes Weberhofer
#
# Only logs to syslog though facility can be changed configuration file/command line
#
# fgrep -r MSG_AUTH_FAILED_LOG pure-ftpd-1.0.36/src
# To get messages in the right encoding:
# grep MSG_AUTH_FAILED_LOG pure-ftpd-1.0.36/src/messages_[defhint]* | grep -Po '".?"' | recode latin1..utf-8 | tr -d '"' > messages
# grep MSG_AUTH_FAILED_LOG pure-ftpd-1.0.36/src/messages_[pr][to] | grep -Po '".?"' | recode latin1..utf-8 | tr -d '"' >> messages
# grep MSG_AUTH_FAILED_LOG pure-ftpd-1.0.36/src/messages_[cps][slkv] | grep -Po '".?"' | recode latin2..utf-8 | tr -d '"' >> messages
# grep MSG_AUTH_FAILED_LOG pure-ftpd-1.0.36/src/messages_ru | grep -Po '".?"' | recode KOI8-R..utf-8 | tr -d '"' >> messages
# grep MSG_AUTH_FAILED_LOG pure-ftpd-1.0.36/src/messages_[kz] | grep -Po '".*?"' | tr -d '"' | recode big5..utf-8 >> messages

View File

@ -9,7 +9,7 @@ before = common.conf
[Definition]
_deamon = (?:cyrus/)?(?:tim)?sieved?
_daemon = (?:cyrus/)?(?:tim)?sieved?
failregex = ^%(__prefix_line)sbadlogin: \S+ ?\[<HOST>\] \S+ authentication failure$

View File

@ -10,7 +10,7 @@
#
# YOU SHOULD NOT MODIFY THIS FILE.
#
# It will probably be overwitten or improved in a distribution update.
# It will probably be overwritten or improved in a distribution update.
#
# Provide customizations in a jail.local file or a jail.d/customisation.local.
# For example to change the default bantime for all jails and to enable the
@ -151,22 +151,22 @@ port = 0:65535
banaction = iptables-multiport
# The simplest action to take: ban only
action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report to the destemail.
action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report and relevant log lines
# to the destemail.
action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
# See the IMPORTANT note in action.d/xarf-login-attack for when to use this action
#
# ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines
# to the destemail.
action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)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"]
@ -283,6 +283,11 @@ port = http,https
logpath = %(apache_error_log)s
maxretry = 2
[apache-shellshock]
port = http,https
logpath = $(apache_error_log)s
maxretry = 1
[nginx-http-auth]
@ -376,7 +381,7 @@ logpath = /var/log/monit
[webmin-auth]
port = 10000
logpath = /var/log/auth.log
logpath = %(syslog_authpriv)s
#
@ -429,7 +434,7 @@ maxretry = 6
[vsftpd]
# or overwrite it in jails.local to be
# logpath = /var/log/auth.log
# logpath = %(syslog_authpriv)s
# if you want to rely on PAM failed login attempts
# vsftpd's failregex should match both of those formats
port = ftp,ftp-data,ftps,ftps-data
@ -501,13 +506,13 @@ logpath = %(solidpop3d_log)s
[exim]
port = smtp,465,submission
logpath = /var/log/exim/mainlog
logpath = %(exim_main_log)s
[exim-spam]
port = smtp,465,submission
logpath = /var/log/exim/mainlog
logpath = %(exim_main_log)s
[kerio]
@ -539,7 +544,7 @@ logpath = %(postfix_log)s
[perdition]
port = imap3,imaps,pop3,pop3s
logpath = /var/log/maillog
logpath = %(syslog_mail)s
[squirrelmail]
@ -663,13 +668,13 @@ maxretry = 5
[pam-generic]
# pam-generic filter can be customized to monitor specific subset of 'tty's
banaction = iptables-allports
logpath = /var/log/auth.log
logpath = %(syslog_authpriv)s
[xinetd-fail]
banaction = iptables-multiport-log
logpath = /var/log/daemon.log
logpath = %(syslog_daemon)s
maxretry = 2
@ -699,7 +704,7 @@ action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp
[nagios]
enabled = false
logpath = /var/log/messages ; nrpe.cfg may define a different log_facility
logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility
maxretry = 1
@ -710,7 +715,12 @@ logpath = /opt/sun/comms/messaging64/log/mail.log_current
maxretry = 6
banaction = iptables-allports
[directadmin]
enabled = false
logpath = /var/log/directadmin/login.log
port = 2222
[portsentry]
enabled = false
logpath = /var/lib/portsentry/portsentry.history
maxretry = 1
maxretry = 1

View File

@ -12,7 +12,7 @@ sshd_log = %(syslog_authpriv)s
dropbear_log = %(syslog_authpriv)s
# There is no sensible generic defaults for syslog log targets, thus
# leaving them empty here so that no errors while parsing/interpollatin configs
# leaving them empty here so that no errors while parsing/interpolating configs
syslog_daemon =
syslog_ftp =
syslog_local0 =
@ -22,6 +22,7 @@ syslog_user =
# from /etc/audit/auditd.conf
auditd_log = /var/log/audit/audit.log
exim_main_log = /var/log/exim/mainlog
nginx_error_log = /var/log/nginx/error.log

View File

@ -30,6 +30,7 @@ apache_error_log = /var/log/apache2/*error.log
apache_access_log = /var/log/apache2/*access.log
exim_main_log = /var/log/exim4/mainlog
# was in debian squeezy but not in wheezy
# /etc/proftpd/proftpd.conf (SystemLog)

View File

@ -32,4 +32,6 @@ apache_access_log = /var/log/httpd/*access_log
# proftpd_log = /var/log/proftpd/auth.log
# Tested and it worked out in /var/log/messages so assuming syslog_ftp for now.
exim_main_log = /var/log/exim/main.log
mysql_log = /var/lib/mysql/mysqld.log

View File

@ -26,7 +26,7 @@ __license__ = "GPL"
import os
from .configreader import ConfigReader, DefinitionInitConfigReader
from .configreader import DefinitionInitConfigReader
from ..helpers import getLogger
# Gets the instance of the logger.
@ -47,15 +47,19 @@ class ActionReader(DefinitionInitConfigReader):
DefinitionInitConfigReader.__init__(
self, file_, jailName, initOpts, **kwargs)
def setFile(self, fileName):
self.__file = fileName
DefinitionInitConfigReader.setFile(self, os.path.join("action.d", fileName))
def getFile(self):
return self.__file
def setName(self, name):
self._name = name
def getName(self):
return self._name
def read(self):
return ConfigReader.read(self, os.path.join("action.d", self._file))
def convert(self):
head = ["set", self._jailName]
stream = list()

View File

@ -51,6 +51,8 @@ class Beautifier:
try:
if inC[0] == "ping":
msg = "Server replied: " + response
elif inC[0] == "version":
msg = response
elif inC[0] == "start":
msg = "Jail started"
elif inC[0] == "stop":

View File

@ -62,6 +62,7 @@ else: # pragma: no cover
# Gets the instance of the logger.
logSys = getLogger(__name__)
logLevel = 7
__all__ = ['SafeConfigParserWithIncludes']
@ -98,30 +99,73 @@ after = 1.conf
if sys.version_info >= (3,2):
# overload constructor only for fancy new Python3's
def __init__(self, *args, **kwargs):
def __init__(self, share_config=None, *args, **kwargs):
kwargs = kwargs.copy()
kwargs['interpolation'] = BasicInterpolationWithName()
kwargs['inline_comment_prefixes'] = ";"
super(SafeConfigParserWithIncludes, self).__init__(
*args, **kwargs)
self._cfg_share = share_config
#@staticmethod
def getIncludes(resource, seen = []):
else:
def __init__(self, share_config=None, *args, **kwargs):
SafeConfigParser.__init__(self, *args, **kwargs)
self._cfg_share = share_config
@property
def share_config(self):
return self._cfg_share
def _getSharedSCPWI(self, filename):
SCPWI = SafeConfigParserWithIncludes
# read single one, add to return list, use sharing if possible:
if self._cfg_share:
# cache/share each file as include (ex: filter.d/common could be included in each filter config):
hashv = 'inc:'+(filename if not isinstance(filename, list) else '\x01'.join(filename))
cfg, i = self._cfg_share.get(hashv, (None, None))
if cfg is None:
cfg = SCPWI(share_config=self._cfg_share)
i = cfg.read(filename, get_includes=False)
self._cfg_share[hashv] = (cfg, i)
elif logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, " Shared file: %s", filename)
else:
# don't have sharing:
cfg = SCPWI()
i = cfg.read(filename, get_includes=False)
return (cfg, i)
def _getIncludes(self, filenames, seen=[]):
if not isinstance(filenames, list):
filenames = [ filenames ]
# retrieve or cache include paths:
if self._cfg_share:
# cache/share include list:
hashv = 'inc-path:'+('\x01'.join(filenames))
fileNamesFull = self._cfg_share.get(hashv)
if fileNamesFull is None:
fileNamesFull = []
for filename in filenames:
fileNamesFull += self.__getIncludesUncached(filename, seen)
self._cfg_share[hashv] = fileNamesFull
return fileNamesFull
# don't have sharing:
fileNamesFull = []
for filename in filenames:
fileNamesFull += self.__getIncludesUncached(filename, seen)
return fileNamesFull
def __getIncludesUncached(self, resource, seen=[]):
"""
Given 1 config resource returns list of included files
(recursively) with the original one as well
Simple loops are taken care about
"""
# Use a short class name ;)
SCPWI = SafeConfigParserWithIncludes
parser = SafeConfigParser()
try:
if sys.version_info >= (3,2): # pragma: no cover
parser.read(resource, encoding='utf-8')
else:
parser.read(resource)
parser, i = self._getSharedSCPWI(resource)
if not i:
return []
except UnicodeDecodeError, e:
logSys.error("Error decoding config file '%s': %s" % (resource, e))
return []
@ -141,22 +185,60 @@ after = 1.conf
if r in seen:
continue
s = seen + [resource]
option_list += SCPWI.getIncludes(r, s)
option_list += self._getIncludes(r, s)
# combine lists
return newFiles[0][1] + [resource] + newFiles[1][1]
#print "Includes list for " + resource + " is " + `resources`
getIncludes = staticmethod(getIncludes)
def get_defaults(self):
return self._defaults
def read(self, filenames):
fileNamesFull = []
def get_sections(self):
return self._sections
def read(self, filenames, get_includes=True):
if not isinstance(filenames, list):
filenames = [ filenames ]
for filename in filenames:
fileNamesFull += SafeConfigParserWithIncludes.getIncludes(filename)
logSys.debug("Reading files: %s" % fileNamesFull)
# retrieve (and cache) includes:
fileNamesFull = []
if get_includes:
fileNamesFull += self._getIncludes(filenames)
else:
fileNamesFull = filenames
if not fileNamesFull:
return []
logSys.info(" Loading files: %s", fileNamesFull)
if get_includes or len(fileNamesFull) > 1:
# read multiple configs:
ret = []
alld = self.get_defaults()
alls = self.get_sections()
for filename in fileNamesFull:
# read single one, add to return list, use sharing if possible:
cfg, i = self._getSharedSCPWI(filename)
if i:
ret += i
# merge defaults and all sections to self:
alld.update(cfg.get_defaults())
for n, s in cfg.get_sections().iteritems():
if isinstance(s, dict):
s2 = alls.get(n)
if isinstance(s2, dict):
s2.update(s)
else:
alls[n] = s.copy()
else:
alls[n] = s
return ret
# read one config :
if logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, " Reading file: %s", fileNamesFull[0])
# read file(s) :
if sys.version_info >= (3,2): # pragma: no cover
return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8')
else:
return SafeConfigParser.read(self, fileNamesFull)

View File

@ -27,24 +27,127 @@ __license__ = "GPL"
import glob, os
from ConfigParser import NoOptionError, NoSectionError
from .configparserinc import SafeConfigParserWithIncludes
from .configparserinc import SafeConfigParserWithIncludes, logLevel
from ..helpers import getLogger
# Gets the instance of the logger.
logSys = getLogger(__name__)
class ConfigReader(SafeConfigParserWithIncludes):
class ConfigReader():
"""Generic config reader class.
A caching adapter which automatically reuses already shared configuration.
"""
def __init__(self, use_config=None, share_config=None, **kwargs):
# use given shared config if possible (see read):
self._cfg_share = None
self._cfg = None
if use_config is not None:
self._cfg = use_config
# share config if possible:
if share_config is not None:
self._cfg_share = share_config
self._cfg_share_kwargs = kwargs
self._cfg_share_basedir = None
elif self._cfg is None:
self._cfg = ConfigReaderUnshared(**kwargs)
def setBaseDir(self, basedir):
if self._cfg:
self._cfg.setBaseDir(basedir)
else:
self._cfg_share_basedir = basedir
def getBaseDir(self):
if self._cfg:
return self._cfg.getBaseDir()
else:
return self._cfg_share_basedir
@property
def share_config(self):
return self._cfg_share
def read(self, name, once=True):
""" Overloads a default (not shared) read of config reader.
To prevent mutiple reads of config files with it includes, reads into
the config reader, if it was not yet cached/shared by 'name'.
"""
# already shared ?
if not self._cfg:
self.touch(name)
# performance feature - read once if using shared config reader:
if once and self._cfg.read_cfg_files is not None:
return self._cfg.read_cfg_files
# load:
logSys.info("Loading configs for %s under %s ", name, self._cfg.getBaseDir())
ret = self._cfg.read(name)
# save already read and return:
self._cfg.read_cfg_files = ret
return ret
def touch(self, name=''):
""" Allocates and share a config file by it name.
Automatically allocates unshared or reuses shared handle by given 'name' and
init arguments inside a given shared storage.
"""
if not self._cfg and self._cfg_share is not None:
self._cfg = self._cfg_share.get(name)
if not self._cfg:
self._cfg = ConfigReaderUnshared(share_config=self._cfg_share, **self._cfg_share_kwargs)
if self._cfg_share_basedir is not None:
self._cfg.setBaseDir(self._cfg_share_basedir)
self._cfg_share[name] = self._cfg
else:
self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)
def sections(self):
if self._cfg is not None:
return self._cfg.sections()
return []
def has_section(self, sec):
if self._cfg is not None:
return self._cfg.has_section(sec)
return False
def options(self, *args):
if self._cfg is not None:
return self._cfg.options(*args)
return {}
def get(self, sec, opt):
if self._cfg is not None:
return self._cfg.get(sec, opt)
return None
def getOptions(self, *args, **kwargs):
if self._cfg is not None:
return self._cfg.getOptions(*args, **kwargs)
return {}
class ConfigReaderUnshared(SafeConfigParserWithIncludes):
"""Unshared config reader (previously ConfigReader).
Do not use this class (internal not shared/cached represenation).
Use ConfigReader instead.
"""
DEFAULT_BASEDIR = '/etc/fail2ban'
def __init__(self, basedir=None):
SafeConfigParserWithIncludes.__init__(self)
def __init__(self, basedir=None, *args, **kwargs):
SafeConfigParserWithIncludes.__init__(self, *args, **kwargs)
self.read_cfg_files = None
self.setBaseDir(basedir)
self.__opts = None
def setBaseDir(self, basedir):
if basedir is None:
basedir = ConfigReader.DEFAULT_BASEDIR # stock system location
basedir = ConfigReaderUnshared.DEFAULT_BASEDIR # stock system location
self._basedir = basedir.rstrip('/')
def getBaseDir(self):
@ -55,7 +158,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
raise ValueError("Base configuration directory %s does not exist "
% self._basedir)
basename = os.path.join(self._basedir, filename)
logSys.info("Reading configs for %s under %s " % (basename, self._basedir))
logSys.debug("Reading configs for %s under %s " , filename, self._basedir)
config_files = [ basename + ".conf" ]
# possible further customizations under a .conf.d directory
@ -71,14 +174,14 @@ class ConfigReader(SafeConfigParserWithIncludes):
if len(config_files):
# at least one config exists and accessible
logSys.debug("Reading config files: " + ', '.join(config_files))
logSys.debug("Reading config files: %s", ', '.join(config_files))
config_files_read = SafeConfigParserWithIncludes.read(self, config_files)
missed = [ cf for cf in config_files if cf not in config_files_read ]
if missed:
logSys.error("Could not read config files: " + ', '.join(missed))
logSys.error("Could not read config files: %s", ', '.join(missed))
if config_files_read:
return True
logSys.error("Found no accessible config files for %r under %s" %
logSys.error("Found no accessible config files for %r under %s",
( filename, self.getBaseDir() ))
return False
else:
@ -98,7 +201,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
# 1 -> the name of the option
# 2 -> the default value for the option
def getOptions(self, sec, options, pOptions = None):
def getOptions(self, sec, options, pOptions=None):
values = dict()
for option in options:
try:
@ -121,10 +224,8 @@ class ConfigReader(SafeConfigParserWithIncludes):
logSys.warning("'%s' not defined in '%s'. Using default one: %r"
% (option[1], sec, option[2]))
values[option[1]] = option[2]
else:
logSys.debug(
"Non essential option '%s' not defined in '%s'.",
option[1], sec)
elif logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, "Non essential option '%s' not defined in '%s'.", option[1], sec)
except ValueError:
logSys.warning("Wrong value for '" + option[1] + "' in '" + sec +
"'. Using default one: '" + `option[2]` + "'")
@ -133,12 +234,12 @@ class ConfigReader(SafeConfigParserWithIncludes):
class DefinitionInitConfigReader(ConfigReader):
"""Config reader for files with options grouped in [Definition] and
[Init] sections.
[Init] sections.
Is a base class for readers of filters and actions, where definitions
in jails might provide custom values for options defined in [Init]
section.
"""
Is a base class for readers of filters and actions, where definitions
in jails might provide custom values for options defined in [Init]
section.
"""
_configOpts = []

View File

@ -33,12 +33,20 @@ logSys = getLogger(__name__)
class Configurator:
def __init__(self):
def __init__(self, force_enable=False, share_config=None):
self.__settings = dict()
self.__streams = dict()
self.__fail2ban = Fail2banReader()
self.__jails = JailsReader()
# always share all config readers:
if share_config is None:
share_config = dict()
self.__share_config = share_config
self.__fail2ban = Fail2banReader(share_config=share_config)
self.__jails = JailsReader(force_enable=force_enable, share_config=share_config)
def Reload(self):
# clear all shared handlers:
self.__share_config.clear()
def setBaseDir(self, folderName):
self.__fail2ban.setBaseDir(folderName)
self.__jails.setBaseDir(folderName)

View File

@ -57,7 +57,7 @@ class CSocket:
self.__csock.close()
return ret
#@staticmethod
@staticmethod
def receive(sock):
msg = EMPTY_BYTES
while msg.rfind(CSocket.END_STRING) == -1:
@ -66,4 +66,3 @@ class CSocket:
raise RuntimeError, "socket connection broken"
msg = msg + chunk
return loads(msg)
receive = staticmethod(receive)

View File

@ -26,7 +26,7 @@ __license__ = "GPL"
import os, shlex
from .configreader import ConfigReader, DefinitionInitConfigReader
from .configreader import DefinitionInitConfigReader
from ..server.action import CommandAction
from ..helpers import getLogger
@ -40,8 +40,12 @@ class FilterReader(DefinitionInitConfigReader):
["string", "failregex", ""],
]
def read(self):
return ConfigReader.read(self, os.path.join("filter.d", self._file))
def setFile(self, fileName):
self.__file = fileName
DefinitionInitConfigReader.setFile(self, os.path.join("filter.d", fileName))
def getFile(self):
return self.__file
def convert(self):
stream = list()

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import re, glob, os.path
import json
from .configreader import ConfigReader
from .configreader import ConfigReaderUnshared, ConfigReader
from .filterreader import FilterReader
from .actionreader import ActionReader
from ..helpers import getLogger
@ -111,7 +111,7 @@ class JailReader(ConfigReader):
filterName, filterOpt = JailReader.extractOptions(
self.__opts["filter"])
self.__filter = FilterReader(
filterName, self.__name, filterOpt, basedir=self.getBaseDir())
filterName, self.__name, filterOpt, share_config=self.share_config, basedir=self.getBaseDir())
ret = self.__filter.read()
if ret:
self.__filter.getOptions(self.__opts)
@ -141,7 +141,7 @@ class JailReader(ConfigReader):
else:
action = ActionReader(
actName, self.__name, actOpt,
basedir=self.getBaseDir())
share_config=self.share_config, basedir=self.getBaseDir())
ret = action.read()
if ret:
action.getOptions(self.__opts)
@ -213,14 +213,14 @@ class JailReader(ConfigReader):
if self.__filter:
stream.extend(self.__filter.convert())
for action in self.__actions:
if isinstance(action, ConfigReader):
if isinstance(action, (ConfigReaderUnshared, ConfigReader)):
stream.extend(action.convert())
else:
stream.append(action)
stream.insert(0, ["add", self.__name, backend])
return stream
#@staticmethod
@staticmethod
def extractOptions(option):
match = JailReader.optionCRE.match(option)
if not match:
@ -235,4 +235,3 @@ class JailReader(ConfigReader):
val for val in optmatch.group(2,3,4) if val is not None][0]
option_opts[opt.strip()] = value.strip()
return option_name, option_opts
extractOptions = staticmethod(extractOptions)

View File

@ -68,9 +68,10 @@ class JailsReader(ConfigReader):
for sec in sections:
if sec == 'INCLUDES':
continue
jail = JailReader(sec, basedir=self.getBaseDir(),
force_enable=self.__force_enable)
jail.read()
# use the cfg_share for filter/action caching and the same config for all
# jails (use_config=...), therefore don't read it here:
jail = JailReader(sec, force_enable=self.__force_enable,
share_config=self.share_config, use_config=self._cfg)
ret = jail.getOptions()
if ret:
if jail.isEnabled():

View File

@ -38,6 +38,7 @@ protocol = [
["status", "gets the current status of the server"],
["ping", "tests if the server is alive"],
["help", "return this output"],
["version", "return the server version"],
['', "LOGGING", ""],
["set loglevel <LEVEL>", "sets logging level to <LEVEL>. Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG"],
["get loglevel", "gets the logging level"],

View File

@ -197,6 +197,8 @@ class Actions(JailThread, Mapping):
if ticket is not None:
# Unban the IP.
self.__unBan(ticket)
if self._jail.database is not None:
self._jail.database.delBan(self._jail, ticket)
else:
raise ValueError("IP %s is not banned" % ip)

View File

@ -149,12 +149,8 @@ class AsyncServer(asyncore.dispatcher):
self.__init = True
# TODO Add try..catch
# There's a bug report for Python 2.6/3.0 that use_poll=True yields some 2.5 incompatibilities:
if sys.version_info >= (2, 6): # if python 2.6 or greater...
logSys.debug("Detected Python 2.6 or greater. asyncore.loop() not using poll")
asyncore.loop(use_poll = False) # fixes the "Unexpected communication problem" issue on Python 2.6 and 3.0
else: # pragma: no cover
logSys.debug("NOT Python 2.6/3.* - asyncore.loop() using poll")
asyncore.loop(use_poll = True)
logSys.debug("Detected Python 2.6 or greater. asyncore.loop() not using poll")
asyncore.loop(use_poll=False) # fixes the "Unexpected communication problem" issue on Python 2.6 and 3.0
##
# Stops the communication server.
@ -175,12 +171,11 @@ class AsyncServer(asyncore.dispatcher):
# @param sock: socket file.
#@staticmethod
@staticmethod
def __markCloseOnExec(sock):
fd = sock.fileno()
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags|fcntl.FD_CLOEXEC)
__markCloseOnExec = staticmethod(__markCloseOnExec)
##
# AsyncServerException is used to wrap communication exceptions.

View File

@ -126,7 +126,7 @@ class BanManager:
# @param ticket the FailTicket
# @return a BanTicket
#@staticmethod
@staticmethod
def createBanTicket(ticket):
ip = ticket.getIP()
#lastTime = ticket.getTime()
@ -134,7 +134,6 @@ class BanManager:
banTicket = BanTicket(ip, lastTime, ticket.getMatches())
banTicket.setAttempt(ticket.getAttempt())
return banTicket
createBanTicket = staticmethod(createBanTicket)
##
# Add a ban ticket.

View File

@ -368,10 +368,25 @@ class Fail2BanDb(object):
#TODO: Implement data parts once arbitrary match keys completed
cur.execute(
"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
(jail.name, ticket.getIP(), ticket.getTime(),
(jail.name, ticket.getIP(), int(round(ticket.getTime())),
{"matches": ticket.getMatches(),
"failures": ticket.getAttempt()}))
@commitandrollback
def delBan(self, cur, jail, ticket):
"""Delete a ban from the database.
Parameters
----------
jail : Jail
Jail in which the ban has occurred.
ticket : BanTicket
Ticket of the ban to be removed.
"""
cur.execute(
"DELETE FROM bans WHERE jail = ? AND ip = ? AND timeofban = ?",
(jail.name, ticket.getIP(), int(round(ticket.getTime()))))
@commitandrollback
def _getBans(self, cur, jail=None, bantime=None, ip=None):
query = "SELECT ip, timeofban, data FROM bans WHERE 1"

View File

@ -838,7 +838,7 @@ class DNSUtils:
IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$")
#@staticmethod
@staticmethod
def dnsToIp(dns):
""" Convert a DNS into an IP address using the Python socket module.
Thanks to Kevin Drapel.
@ -853,9 +853,8 @@ class DNSUtils:
logSys.warning("Socket error raised trying to resolve hostname %s: %s"
% (dns, e))
return list()
dnsToIp = staticmethod(dnsToIp)
#@staticmethod
@staticmethod
def searchIP(text):
""" Search if an IP address if directly available and return
it.
@ -865,9 +864,8 @@ class DNSUtils:
return match
else:
return None
searchIP = staticmethod(searchIP)
#@staticmethod
@staticmethod
def isValidIP(string):
""" Return true if str is a valid IP
"""
@ -877,9 +875,8 @@ class DNSUtils:
return True
except socket.error:
return False
isValidIP = staticmethod(isValidIP)
#@staticmethod
@staticmethod
def textToIp(text, useDns):
""" Return the IP of DNS found in a given text.
"""
@ -901,9 +898,8 @@ class DNSUtils:
text, ipList)
return ipList
textToIp = staticmethod(textToIp)
#@staticmethod
@staticmethod
def cidr(i, n):
""" Convert an IP address string with a CIDR mask into a 32-bit
integer.
@ -911,18 +907,15 @@ class DNSUtils:
# 32-bit IPv4 address mask
MASK = 0xFFFFFFFFL
return ~(MASK >> n) & MASK & DNSUtils.addr2bin(i)
cidr = staticmethod(cidr)
#@staticmethod
@staticmethod
def addr2bin(string):
""" Convert a string IPv4 address into an unsigned integer.
"""
return struct.unpack("!L", socket.inet_aton(string))[0]
addr2bin = staticmethod(addr2bin)
#@staticmethod
@staticmethod
def bin2addr(addr):
""" Convert a numeric IPv4 address into string n.n.n.n form.
"""
return socket.inet_ntoa(struct.pack("!L", addr))
bin2addr = staticmethod(bin2addr)

View File

@ -26,65 +26,71 @@ import time, datetime
##
# MyTime class.
#
# This class is a wrapper around time.time() and time.gmtime(). When
# performing unit test, it is very useful to get a fixed value from these
# functions.
# Thus, time.time() and time.gmtime() should never be called directly.
# This wrapper should be called instead. The API are equivalent.
class MyTime:
"""A wrapper around time module primarily for testing purposes
This class is a wrapper around time.time() and time.gmtime(). When
performing unit test, it is very useful to get a fixed value from
these functions. Thus, time.time() and time.gmtime() should never
be called directly. This wrapper should be called instead. The API
are equivalent.
"""
myTime = None
##
# Sets the current time.
#
# Use None in order to always get the real current time.
#
# @param t the time to set or None
#@staticmethod
@staticmethod
def setTime(t):
"""Set current time.
Use None in order to always get the real current time.
@param t the time to set or None
"""
MyTime.myTime = t
setTime = staticmethod(setTime)
##
# Equivalent to time.time()
#
# @return time.time() if setTime was called with None
#@staticmethod
@staticmethod
def time():
"""Decorate time.time() for the purpose of testing mocking
@return time.time() if setTime was called with None
"""
if MyTime.myTime is None:
return time.time()
else:
return MyTime.myTime
time = staticmethod(time)
##
# Equivalent to time.gmtime()
#
# @return time.gmtime() if setTime was called with None
#@staticmethod
@staticmethod
def gmtime():
"""Decorate time.gmtime() for the purpose of testing mocking
@return time.gmtime() if setTime was called with None
"""
if MyTime.myTime is None:
return time.gmtime()
else:
return time.gmtime(MyTime.myTime)
gmtime = staticmethod(gmtime)
#@staticmethod
@staticmethod
def now():
"""Decorate datetime.now() for the purpose of testing mocking
@return datetime.now() if setTime was called with None
"""
if MyTime.myTime is None:
return datetime.datetime.now()
else:
return datetime.datetime.fromtimestamp(MyTime.myTime)
now = staticmethod(now)
@staticmethod
def localtime(x=None):
"""Decorate time.localtime() for the purpose of testing mocking
@return time.localtime() if setTime was called with None
"""
if MyTime.myTime is None or x is not None:
return time.localtime(x)
else:
return time.localtime(MyTime.myTime)
localtime = staticmethod(localtime)

View File

@ -28,6 +28,7 @@ import time
import json
from ..helpers import getLogger
from .. import version
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -102,7 +103,9 @@ class Transmitter:
elif command[0] == "get":
return self.__commandGet(command[1:])
elif command[0] == "status":
return self.status(command[1:])
return self.status(command[1:])
elif command[0] == "version":
return version.version
raise Exception("Invalid command")
def __commandSet(self, command):

View File

@ -22,11 +22,7 @@ import unittest
import sys
from ..dummyjail import DummyJail
if os.path.exists('config/fail2ban.conf'):
CONFIG_DIR = "config"
else:
CONFIG_DIR='/etc/fail2ban'
from ..utils import CONFIG_DIR
if sys.version_info >= (2,7):
class BadIPsActionTest(unittest.TestCase):

View File

@ -30,10 +30,7 @@ else:
from ..dummyjail import DummyJail
if os.path.exists('config/fail2ban.conf'):
CONFIG_DIR = "config"
else:
CONFIG_DIR='/etc/fail2ban'
from ..utils import CONFIG_DIR
class TestSMTPServer(smtpd.SMTPServer):

View File

@ -21,9 +21,9 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
__license__ = "GPL"
import os, glob, shutil, tempfile, unittest
from ..client.configreader import ConfigReader
import os, glob, shutil, tempfile, unittest, re, logging
from ..client.configreader import ConfigReaderUnshared
from ..client import configparserinc
from ..client.jailreader import JailReader
from ..client.filterreader import FilterReader
from ..client.jailsreader import JailsReader
@ -32,8 +32,10 @@ from ..client.configurator import Configurator
from .utils import LogCaptureTestCase
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
from .utils import CONFIG_DIR
STOCK = os.path.exists(os.path.join('config','fail2ban.conf'))
CONFIG_DIR='config' if STOCK else '/etc/fail2ban'
IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config')
@ -42,7 +44,7 @@ class ConfigReaderTest(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
self.d = tempfile.mkdtemp(prefix="f2b-temp")
self.c = ConfigReader(basedir=self.d)
self.c = ConfigReaderUnshared(basedir=self.d)
def tearDown(self):
"""Call after every test case."""
@ -333,6 +335,64 @@ class FilterReaderTest(unittest.TestCase):
self.assertRaises(ValueError, FilterReader.convert, filterReader)
class JailsReaderTestCache(LogCaptureTestCase):
def _readWholeConf(self, basedir, force_enable=False, share_config=None):
# read whole configuration like a file2ban-client ...
configurator = Configurator(force_enable=force_enable, share_config=share_config)
configurator.setBaseDir(basedir)
configurator.readEarly()
configurator.getEarlyOptions()
configurator.readAll()
# from here we test a cache with all includes / before / after :
self.assertTrue(configurator.getOptions(None))
def _getLoggedReadCount(self, filematch):
cnt = 0
for s in self.getLog().rsplit('\n'):
if re.match(r"^\s*Reading files?: .*/"+filematch, s):
cnt += 1
return cnt
def testTestJailConfCache(self):
saved_ll = configparserinc.logLevel
configparserinc.logLevel = logging.DEBUG
basedir = tempfile.mkdtemp("fail2ban_conf")
try:
shutil.rmtree(basedir)
shutil.copytree(CONFIG_DIR, basedir)
shutil.copy(CONFIG_DIR + '/jail.conf', basedir + '/jail.local')
shutil.copy(CONFIG_DIR + '/fail2ban.conf', basedir + '/fail2ban.local')
# common sharing handle for this test:
share_cfg = dict()
# read whole configuration like a file2ban-client ...
self._readWholeConf(basedir, share_config=share_cfg)
# how many times jail.local was read:
cnt = self._getLoggedReadCount('jail.local')
# if cnt > 1:
# self.printLog()
self.assertTrue(cnt == 1, "Unexpected count by reading of jail files, cnt = %s" % cnt)
# read whole configuration like a file2ban-client, again ...
# but this time force enable all jails, to check filter and action cached also:
self._readWholeConf(basedir, force_enable=True, share_config=share_cfg)
cnt = self._getLoggedReadCount(r'jail\.local')
# still one (no more reads):
self.assertTrue(cnt == 1, "Unexpected count by second reading of jail files, cnt = %s" % cnt)
# same with filter:
cnt = self._getLoggedReadCount(r'filter\.d/common\.conf')
self.assertTrue(cnt == 1, "Unexpected count by reading of filter files, cnt = %s" % cnt)
# same with action:
cnt = self._getLoggedReadCount(r'action\.d/iptables-common\.conf')
self.assertTrue(cnt == 1, "Unexpected count by reading of action files, cnt = %s" % cnt)
finally:
shutil.rmtree(basedir)
configparserinc.logLevel = saved_ll
class JailsReaderTest(LogCaptureTestCase):
def testProvidingBadBasedir(self):

View File

@ -173,6 +173,12 @@ class DatabaseTest(unittest.TestCase):
self.assertTrue(
isinstance(self.db.getBans(jail=self.jail)[0], FailTicket))
def testDelBan(self):
self.testAddBan()
ticket = self.db.getBans(jail=self.jail)[0]
self.db.delBan(self.jail, ticket)
self.assertEqual(len(self.db.getBans(jail=self.jail)), 0)
def testGetBansWithTime(self):
if Fail2BanDb is None: # pragma: no cover
return

View File

@ -1,14 +1,21 @@
#!/bin/env python
import requests
import md5
try:
import hashlib
md5sum = hashlib.md5
except ImportError: # pragma: no cover
# hashlib was introduced in Python 2.5. For compatibility with those
# elderly Pythons, import from md5
import md5
md5sum = md5.new
def auth(v):
ha1 = md5.new(username + ':' + realm + ':' + password).hexdigest()
ha2 = md5.new("GET:" + url).hexdigest()
ha1 = md5sum(username + ':' + realm + ':' + password).hexdigest()
ha2 = md5sum("GET:" + url).hexdigest()
#response = md5.new(ha1 + ':' + v['nonce'][1:-1] + ':' + v['nc'] + ':' + v['cnonce'][1:-1]
#response = md5sum(ha1 + ':' + v['nonce'][1:-1] + ':' + v['nc'] + ':' + v['cnonce'][1:-1]
# + ':' + v['qop'][1:-1] + ':' + ha2).hexdigest()
nonce = v['nonce'][1:-1]
@ -17,7 +24,7 @@ def auth(v):
#opaque = v.get('opaque') or ''
qop = v['qop'][1:-1]
algorithm = v['algorithm']
response = md5.new(ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2).hexdigest()
response = md5sum(ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2).hexdigest()
p = requests.Request('GET', host + url).prepare()
#p.headers['Authentication-Info'] = response
@ -33,13 +40,13 @@ def auth(v):
response="%s"
""" % ( username, algorithm, realm, url, nonce, qop, response )
# opaque="%s",
print p.method, p.url, p.headers
print(p.method, p.url, p.headers)
s = requests.Session()
return s.send(p)
def preauth():
r = requests.get(host + url)
print r
print(r)
r.headers['www-authenticate'].split(', ')
return dict([ a.split('=',1) for a in r.headers['www-authenticate'].split(', ') ])
@ -51,7 +58,7 @@ v = preauth()
username="username"
password = "password"
print v
print(v)
realm = 'so far away'
r = auth(v)
@ -67,18 +74,18 @@ r = auth(v)
# [Sun Jul 28 21:41:20 2013] [error] [client 127.0.0.1] Digest: unknown algorithm `super funky chicken' received: /digest/
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
v['algorithm'] = algorithm
r = auth(v)
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
nonce = v['nonce']
v['nonce']=v['nonce'][5:-5]
r = auth(v)
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
# [Sun Jul 28 21:05:31.178340 2013] [auth_digest:error] [pid 24224:tid 139895539455744] [client 127.0.0.1:56906] AH01793: invalid qop `auth' received: /digest/qop_none/
@ -86,7 +93,7 @@ print r.status_code,r.headers, r.text
v['nonce']=nonce[0:11] + 'ZZZ' + nonce[14:]
r = auth(v)
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
#[Sun Jul 28 21:18:11.769228 2013] [auth_digest:error] [pid 24752:tid 139895505884928] [client 127.0.0.1:56964] AH01776: invalid nonce b9YAiJDiBAZZZ1b1abe02d20063ea3b16b544ea1b0d981c1bafe received - hash is not d42d824dee7aaf50c3ba0a7c6290bd453e3dd35b
@ -98,7 +105,7 @@ import time
time.sleep(1)
r = auth(v)
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
# Obtained by putting the following code in modules/aaa/mod_auth_digest.c
# in the function initialize_secret
@ -128,7 +135,7 @@ s = sha.sha(apachesecret)
v=preauth()
print v['nonce']
print(v['nonce'])
realm = v['Digest realm'][1:-1]
(t,) = struct.unpack('l',base64.b64decode(v['nonce'][1:13]))
@ -143,17 +150,17 @@ s.update(timepac)
v['nonce'] = v['nonce'][0] + timepac + s.hexdigest() + v['nonce'][-1]
print v
print(v)
r = auth(v)
#[Mon Jul 29 02:12:55.539813 2013] [auth_digest:error] [pid 9647:tid 139895522670336] [client 127.0.0.1:58474] AH01777: invalid nonce 59QJppTiBAA=b08983fd166ade9840407df1b0f75b9e6e07d88d received - user attempted time travel
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
url='/digest_onetime/'
v=preauth()
# Need opaque header handling in auth
r = auth(v)
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)
r = auth(v)
print r.status_code,r.headers, r.text
print(r.status_code,r.headers, r.text)

View File

@ -19,6 +19,9 @@
# failJSON: { "time": "2013-07-20T21:34:49", "match": true , "host": "127.0.0.1" }
[Sat Jul 20 21:34:49.453232 2013] [access_compat:error] [pid 17512:tid 140123104306944] [client 127.0.0.1:51380] AH01797: client denied by server configuration: /var/www/html/noentry/cant_get_me.html
# failJSON: { "time": "2014-09-14T21:44:43", "match": true , "host": "192.3.9.178" }
[Sun Sep 14 21:44:43.008606 2014] [authz_core:error] [pid 10691] [client 192.3.9.178:44271] AH01630: client denied by server configuration: /var/www/html/noentry/cant_get_me.html
# wget --http-user='' --http-password='' http://localhost/basic/file/cant_get_me.html -O /dev/null
# failJSON: { "time": "2013-07-17T23:14:37", "match": true , "host": "127.0.0.1" }
[Wed Jul 17 23:14:37 2013] [error] [client 127.0.0.1] user not found: /basic/anon/cant_get_me.html

View File

@ -0,0 +1,4 @@
# failJSON: { "time": "2014-09-25T09:27:18", "match": true , "host": "89.207.132.76" }
[Thu Sep 25 09:27:18.813902 2014] [cgi:error] [pid 16860] [client 89.207.132.76:59635] AH01215: /bin/bash: warning: HTTP_TEST: ignoring function definition attempt
# failJSON: { "time": "2014-09-25T09:29:56", "match": true , "host": "162.247.73.206" }
[Thu Sep 25 09:29:56.141832 2014] [cgi:error] [pid 16864] [client 162.247.73.206:41273] AH01215: /bin/bash: error importing function definition for `HTTP_TEST'

View File

@ -1,5 +1,9 @@
# failJSON: { "time": "2005-04-10T03:47:57", "match": true , "host": "1.2.3.4" }
Apr 10 03:47:57 web courieresmtpd: error,relay=::ffff:1.2.3.4,ident=tmf,from=<tmf@example.com>,to=<mailman-subscribe@example.com>: 550 User unknown.
# failJSON: { "time": "2005-07-03T23:07:20", "match": true , "host": "1.2.3.4" }
Jul 3 23:07:20 szerver courieresmtpd: error,relay=::ffff:1.2.3.4,msg="535 Authentication failed.",cmd: YWRvYmVhZG9iZQ==
# failJSON: { "time": "2005-07-04T18:39:39", "match": true , "host": "1.2.3.4" }
Jul 4 18:39:39 mail courieresmtpd: error,relay=::ffff:1.2.3.4,from=<picaro@astroboymail.com>,to=<user@update.net>: 550 User <benny> unknown
# failJSON: { "time": "2005-07-06T03:42:28", "match": true , "host": "1.2.3.4" }
Jul 6 03:42:28 whistler courieresmtpd: error,relay=::ffff:1.2.3.4,from=<>,to=<admin at memcpy>: 550 User unknown.
# failJSON: { "time": "2004-11-21T23:16:17", "match": true , "host": "1.2.3.4" }

View File

@ -1,5 +1,7 @@
# failJSON: { "time": "2005-01-04T21:51:05", "match": true , "host": "127.0.0.1" }
Jan 4 21:51:05 hostname cyrus/imap[5355]: badlogin: localhost.localdomain [127.0.0.1] plaintext cyrus@localdomain SASL(-13): authentication failure: checkpass failed
# failJSON: { "time": "2005-01-04T21:51:05", "match": true , "host": "127.0.0.1", "desc": "For secure imaps" }
Jan 4 21:51:05 hostname cyrus/imaps[5355]: badlogin: localhost.localdomain [127.0.0.1] plaintext cyrus@localdomain SASL(-13): authentication failure: checkpass failed
# failJSON: { "time": "2005-02-20T17:23:32", "match": true , "host": "198.51.100.23" }
Feb 20 17:23:32 domain cyrus/pop3[18635]: badlogin: localhost [198.51.100.23] plaintext administrator SASL(-13): authentication failure: checkpass failed
# failJSON: { "time": "2005-02-20T17:23:32", "match": true , "host": "1.2.3.4" }
@ -10,4 +12,7 @@ Jun 8 18:11:13 lampserver imap[4480]: badlogin: example.com [198.51.100.45] DIGE
Dec 21 10:01:57 hostname imapd[18454]: badlogin: example.com [198.51.100.57] CRAM-MD5 [SASL(-13): authentication failure: incorrect digest response]
# failJSON: { "time": "2004-12-30T16:03:27", "match": true , "host": "1.2.3.4" }
Dec 30 16:03:27 somehost imapd[2517]: badlogin: local-somehost[1.2.3.4] OTP [SASL(-13): authentication failure: External SSF not good enough]
# failJSON: { "time": "2005-07-17T22:55:56", "match": true , "host": "1.2.3.4" }
Jul 17 22:55:56 derry cyrus/imaps[7568]: badlogin: serafinat.xxxxxx [1.2.3.4] plain [SASL(-13): user not found: user: pressy@derry property: cmusaslsecretPLAIN not found in sasldb]
# failJSON: { "time": "2005-07-18T16:46:42", "match": true , "host": "1.2.3.4" }
Jul 18 16:46:42 derry cyrus/imaps[27449]: badlogin: serafinat.xxxxxx [1.2.3.4] PLAIN [SASL(-13): user not found: Password verification failed]

View File

@ -0,0 +1,14 @@
# failJSON: { "time": "2014-07-02T00:17:45", "match": true , "host": "3.2.1.4" }
2014:07:02-00:17:45: '3.2.1.4' 2 failed login attempts. Account 'test'
# failJSON: { "time": "2014-07-02T13:07:40", "match": true , "host": "40.40.123.231" }
2014:07:02-13:07:40: '40.40.123.231' 13 failed login attempts. Account 'admin'
# failJSON: { "time": "2014-07-02T13:07:50", "match": true , "host": "40.40.123.231" }
2014:07:02-13:07:50: '40.40.123.231' 5 failed login attempt. Invalid account 'user%2Ename'
# failJSON: { "time": "2014-07-02T13:28:39", "match": false , "host": "12.12.123.231" }
2014:07:02-13:28:39: '12.12.123.231' successful login to 'nobody' after 1 attempts
# failJSON: { "time": "2014-07-02T13:29:38", "match": true , "host": "1.2.3.4" }
2014:07:02-13:29:38: '1.2.3.4' 2 failed login attempts. Account 'user' via 'admin'

View File

@ -5,3 +5,6 @@ Dec 2 22:24:22 hel postfix/smtpd[7676]: warning: 114-44-142-233.dynamic.hinet.n
# failJSON: { "time": "2005-03-10T13:33:30", "match": true , "host": "1.1.1.1" }
Mar 10 13:33:30 gandalf postfix/smtpd[3937]: warning: HOSTNAME[1.1.1.1]: SASL LOGIN authentication failed: authentication failure
#3 Example from postfix post-debian changes to rename to add "submission" to syslog name
# failJSON: { "time": "2004-09-06T00:44:56", "match": true , "host": "82.221.106.233" }
Sep 6 00:44:56 trianon postfix/submission/smtpd[11538]: warning: unknown[82.221.106.233]: SASL LOGIN authentication failed: UGFzc3dvcmQ6

View File

@ -24,6 +24,7 @@ __license__ = "GPL"
from __builtin__ import open as fopen
import unittest
import getpass
import os
import sys
import time
@ -349,10 +350,20 @@ class LogFileMonitor(LogCaptureTestCase):
# shorter wait time for not modified status
return not self.isModified(0.4)
def testNoLogFile(self):
def testUnaccessibleLogFile(self):
os.chmod(self.name, 0)
self.filter.getFailures(self.name)
self.assertTrue(self._is_logged('Unable to open %s' % self.name))
failure_was_logged = self._is_logged('Unable to open %s' % self.name)
is_root = getpass.getuser() == 'root'
# If ran as root, those restrictive permissions would not
# forbid log to be read.
self.assertTrue(failure_was_logged != is_root)
def testNoLogFile(self):
_killfile(self.file, self.name)
self.filter.getFailures(self.name)
failure_was_logged = self._is_logged('Unable to open %s' % self.name)
self.assertTrue(failure_was_logged)
def testRemovingFailRegex(self):
self.filter.delFailRegex(0)

View File

@ -24,21 +24,13 @@ __license__ = "GPL"
import unittest, sys, os, fileinput, re, time, datetime, inspect
if sys.version_info >= (2, 6):
import json
else:
import simplejson as json
next = lambda x: x.next()
import json
from ..server.filter import Filter
from ..client.filterreader import FilterReader
from .utils import setUpMyTime, tearDownMyTime
from .utils import setUpMyTime, tearDownMyTime, CONFIG_DIR
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
if os.path.exists('config/fail2ban.conf'):
CONFIG_DIR = "config"
else:
CONFIG_DIR='/etc/fail2ban'
class FilterSamplesRegex(unittest.TestCase):

View File

@ -37,6 +37,7 @@ from ..server.jail import Jail
from ..server.jailthread import JailThread
from .utils import LogCaptureTestCase
from ..helpers import getLogger
from .. import version
try:
from ..server import filtersystemd
@ -148,6 +149,9 @@ class Transmitter(TransmitterBase):
def testPing(self):
self.assertEqual(self.transm.proceed(["ping"]), (0, "pong"))
def testVersion(self):
self.assertEqual(self.transm.proceed(["version"]), (0, version.version))
def testSleep(self):
t0 = time.time()
self.assertEqual(self.transm.proceed(["sleep", "1"]), (0, None))
@ -800,7 +804,7 @@ class RegexTests(unittest.TestCase):
class _BadThread(JailThread):
def run(self):
int("cat")
int("ignore this exception -- raised for testing")
class LoggingTests(LogCaptureTestCase):

View File

@ -25,6 +25,7 @@ __license__ = "GPL"
import logging
import os
import re
import sys
import time
import unittest
from StringIO import StringIO
@ -34,6 +35,15 @@ from ..helpers import getLogger
logSys = getLogger(__name__)
CONFIG_DIR = os.environ.get('FAIL2BAN_CONFIG_DIR', None)
if not CONFIG_DIR:
# Use heuristic to figure out where configuration files are
if os.path.exists(os.path.join('config','fail2ban.conf')):
CONFIG_DIR = 'config'
else:
CONFIG_DIR = '/etc/fail2ban'
def mtimesleep():
# no sleep now should be necessary since polling tracks now not only
# mtime but also ino and size
@ -102,6 +112,7 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.FilterReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTestCache))
# CSocket and AsyncServer
tests.addTest(unittest.makeSuite(sockettestcase.Socket))
# Misc helpers
@ -195,6 +206,8 @@ class LogCaptureTestCase(unittest.TestCase):
# Let's log everything into a string
self._log = StringIO()
logSys.handlers = [logging.StreamHandler(self._log)]
if self._old_level < logging.DEBUG: # so if HEAVYDEBUG etc -- show them!
logSys.handlers += self._old_handlers
logSys.setLevel(getattr(logging, 'DEBUG'))
def tearDown(self):
@ -207,5 +220,8 @@ class LogCaptureTestCase(unittest.TestCase):
def _is_logged(self, s):
return s in self._log.getvalue()
def getLog(self):
return self._log.getvalue()
def printLog(self):
print(self._log.getvalue())

209
files/logwatch/fail2ban Executable file
View File

@ -0,0 +1,209 @@
#!/usr/bin/perl
##########################################################################
# $Id: fail2ban 150 2013-06-18 22:19:38Z mtremaine $
##########################################################################
# $Log: fail2ban,v $
#
# Revision 1.6 2014/08/11 16:07:46 yoh
# Patches from Yaroslav Halchenko to match adjusted in 0.9.x lines.
# Also reports now total number of hits (matches) along with Ban:Unban
# and relaxed regular expressions for matching any log level
#
# Revision 1.5 2008/08/18 16:07:46 mike
# Patches from Paul Gear <paul at libertysys.com> -mgt
#
# Revision 1.4 2008/06/30 23:07:51 kirk
# fixed copyright holders for files where I know who they should be
#
# Revision 1.3 2008/03/24 23:31:26 kirk
# added copyright/license notice to each script
#
# Revision 1.2 2006/12/15 04:53:59 bjorn
# Additional filtering, by Willi Mann.
#
# Revision 1.1 2006/05/30 19:04:26 bjorn
# Added fail2ban service, written by Yaroslav Halchenko.
#
# Written by Yaroslav Halchenko <debian@onerussian.com> for fail2ban
#
##########################################################################
########################################################
## Copyright (c) 2008 Yaroslav Halchenko
## Covered under the included MIT/X-Consortium License:
## http://www.opensource.org/licenses/mit-license.php
## All modifications and contributions by other persons to
## this script are assumed to have been donated to the
## Logwatch project and thus assume the above copyright
## and licensing terms. If you want to make contributions
## under your own copyright or a different license this
## must be explicitly stated in the contribution an the
## Logwatch project reserves the right to not accept such
## contributions. If you have made significant
## contributions to this script and want to claim
## copyright please contact logwatch-devel@lists.sourceforge.net.
#########################################################
use strict;
use Logwatch ':all';
my $Debug = $ENV{'LOGWATCH_DEBUG'} || 0;
my $Detail = $ENV{'LOGWATCH_DETAIL_LEVEL'} || 0;
my $IgnoreHost = $ENV{'sshd_ignore_host'} || "";
my $DebugCounter = 0;
my $ReInitializations = 0;
my @ActionsErrors = ();
my @CommandsErrors = ();
my $NotValidIP = 0; # reported invalid IPs number
my @OtherList = ();
my %ServicesBans = ();
if ( $Debug >= 5 ) {
print STDERR "\n\nDEBUG: Inside Fail2Ban Filter \n\n";
$DebugCounter = 1;
}
while (defined(my $ThisLine = <STDIN>)) {
if ( $Debug >= 5 ) {
print STDERR "DEBUG($DebugCounter): $ThisLine";
$DebugCounter++;
}
chomp($ThisLine);
if ( ($ThisLine =~ /..,... DEBUG: /) or
($ThisLine =~ /..,... \S*\s*: DEBUG /) or # syntax of 0.7.? fail2ban
($ThisLine =~ /..,... \S+: (Fail2Ban v.* is running|Exiting|Enabled sections:)/) or
($ThisLine =~ /\S+\s+rollover performed on/) or
($ThisLine =~ /\S+\s+Connected to .* persistent database/) or
($ThisLine =~ /\S+\s+Jail '.*' uses .*/) or
($ThisLine =~ /\S+\s+Initiated '.*' backend/) or
($ThisLine =~ /\S+\s+Jail .* is not a JournalFilter instance/) or
($ThisLine =~ /\S+\s+Log rotation detected for/) or
($ThisLine =~ /\S+\s+Jail.+(?:stopped|started|uses poller)/) or
($ThisLine =~ /\S+\s+Changed logging target to/) or
($ThisLine =~ /\S+\s+Creating new jail/) or
($ThisLine =~ /..,... \S+\s*: INFO\s+(Set |Socket|Exiting|Gamin|Created|Added|Using)/) or # syntax of 0.7.? fail2ban
($ThisLine =~ /..,... \S+: Verbose level is /) or
($ThisLine =~ /..,... \S+: Restoring firewall rules/)
)
{
if ( $Debug >= 6 ) {
print STDERR "DEBUG($DebugCounter): line ignored\n";
}
} elsif ( my ($LogLevel,$Service,$Action,$Host) = ($ThisLine =~ m/(WARNING|NOTICE):?\s+\[?(.*?)[]:]?\s(Ban|Unban)[^\.]* (\S+)/)) {
if ( $Debug >= 6 ) {
print STDERR "DEBUG($DebugCounter): Found $Action for $Service from $Host\n";
}
$ServicesBans{$Service}{$Host}{$Action}++;
$ServicesBans{$Service}{"(all)"}{$Action}++;
} elsif ( my ($LogLevel,$Service,$Host) = ($ThisLine =~ m/(INFO|WARNING|NOTICE):?\s+\[?(.*?)[]:]?\sFound[^\.]* (\S+)/)) {
if ( $Debug >= 6 ) {
print STDERR "DEBUG($DebugCounter): Found hit for $Service from $Host\n";
}
$ServicesBans{$Service}{$Host}{"Hit"}++;
$ServicesBans{$Service}{"(all)"}{"Hit"}++;
} elsif ( my ($Service,$Host,$NumFailures) = ($ThisLine =~ m/\S+:\s+(\S+): (.+) has (\d+) login failure\(s\). Banned./)) {
if ($Debug >= 4) {
print STDERR "DEBUG: Found host $Host trying to access $Service - failed $NumFailures times\n";
}
push @{$ServicesBans{$Service}{$Host}{'Failures'}}, $NumFailures;
} elsif ( my ($Service,$Host) = ($ThisLine =~ m/ \S+:\s(.*):\s(\S+)\salready in ban list/)) {
$ServicesBans{$Service}{$Host}{'AlreadyInTheList'}++;
} elsif ( my ($Service,$Host) = ($ThisLine =~ m/\S+:?\s+\[?([^[]*?)[]:]?\s+(\S+)\salready banned/)) {
if ( $Debug >= 6 ) {
print STDERR "DEBUG($DebugCounter): Found hit for already banned $Host against $Service\n";
}
$ServicesBans{$Service}{$Host}{'AlreadyInTheList'}++;
} elsif ( my ($Service,$Host) = ($ThisLine =~ m/ \S+:\s(.*):\sReBan (\S+)/)) {
$ServicesBans{$Service}{$Host}{'ReBan'}++;
} elsif ($ThisLine =~ / ERROR:?\s*(Execution of command )?\'?iptables/) {
push @ActionsErrors, "$ThisLine\n";
} elsif ($ThisLine =~ / ERROR\s*Failed to execute.*action/) {
push @ActionsErrors, "$ThisLine\n";
} elsif ($ThisLine =~ / WARNING Command \[.*\] has failed. Received/) {
push @CommandsErrors, "$ThisLine\n";
} elsif ($ThisLine =~ /ERROR.*returned \d+$/) {
push @ActionsErrors, "$ThisLine\n";
} elsif (($ThisLine =~ /..,... WARNING: \#\S+ reinitialization of firewalls/) or
($ThisLine =~ / ERROR\s*Invariant check failed. Trying to restore a sane environment/)) {
$ReInitializations++;
} elsif ($ThisLine =~ /..,... WARNING: is not a valid IP address/) {
# just ignore - this will be fixed within fail2ban and is harmless warning
}
else
{
# Report any unmatched entries...
push @OtherList, "$ThisLine\n";
}
}
###########################################################
if (keys %ServicesBans) {
printf("\nBanned services with Fail2Ban: Bans:Unbans:Hits\n");
foreach my $service (sort {$a cmp $b} keys %ServicesBans) {
printf(" %-55s [%3d:%d:%-3d]\n", "$service:",
$ServicesBans{$service}{'(all)'}{'Ban'},
$ServicesBans{$service}{'(all)'}{'Unban'},
$ServicesBans{$service}{'(all)'}{'Hit'});
delete $ServicesBans{$service}{'(all)'};
my $totalSort = TotalCountOrder(%{$ServicesBans{$service}}, \&SortIP);
if ($Detail >= 5) {
foreach my $ip (sort $totalSort keys %{$ServicesBans{$service}}) {
my $name = LookupIP($ip);
printf(" %-53s %3d:%d:%-3d\n",
$name,
$ServicesBans{$service}{$ip}{'Ban'},
$ServicesBans{$service}{$ip}{'Unban'},
$ServicesBans{$service}{$ip}{'Hit'});
if (($Detail >= 10) and ($ServicesBans{$service}{$ip}{'Failures'}>0)) {
print " Failed ";
foreach my $fails (@{$ServicesBans{$service}{$ip}{'Failures'}}) {
print " $fails";
}
print " times\n";
}
if ($ServicesBans{$service}{$ip}{'AlreadyInTheList'}>0) {
printf(" %d Duplicate Ban attempt(s)\n", $ServicesBans{$service}{$ip}{'AlreadyInTheList'}) ;
}
if ($ServicesBans{$service}{$ip}{'ReBan'}>0) {
printf(" %d ReBan(s) due to rules reinitilizations\n", $ServicesBans{$service}{$ip}{'ReBan'}) ;
}
}
}
}
}
if ($Detail>0) {
if ($#ActionsErrors >= 0) {
printf("\n%d faulty action invocation(s)", $#ActionsErrors+1);
if ($Detail > 5) {
print ":\n";
print @ActionsErrors ;
}
}
if ($#CommandsErrors >= 0) {
printf("\n%d faulty command invocation(s) from client(s)", $#CommandsErrors+1);
if ($Detail > 5) {
print ":\n";
print @CommandsErrors ;
}
}
if ($ReInitializations > 0) {
printf("\n%d fail2ban rules reinitialization(s)", $ReInitializations);
}
if ($#OtherList >= 0) {
print "\n**Unmatched Entries**\n";
print @OtherList;
}
}
exit(0);
# vi: shiftwidth=3 tabstop=3 syntax=perl et
# Local Variables:
# mode: perl
# perl-indent-level: 3
# indent-tabs-mode: nil
# End:

View File

@ -0,0 +1,2 @@
2014-08-04 03:06:26,161 fail2ban.actions[4822]: WARNING [apache-badbots] Ban 37.152.91.34
2014-08-05 03:06:26,448 fail2ban.actions[4822]: WARNING [apache-badbots] Unban 37.152.91.34

View File

@ -0,0 +1,52 @@
2014-08-08 14:59:35,013 fail2ban.server.server[31122]: INFO Exiting Fail2ban
2014-08-08 14:59:36,041 fail2ban.server.server[21667]: INFO Changed logging target to /var/log/fail2ban.log for Fail2ban v0.9.0
2014-08-08 14:59:36,043 fail2ban.server.database[21667]: INFO Connected to fail2ban persistent database '/var/lib/fail2ban/fail2ban.sqlite3'
2014-08-08 14:59:36,072 fail2ban.server.jail[21667]: INFO Creating new jail 'exim'
2014-08-08 14:59:36,137 fail2ban.server.jail[21667]: INFO Jail 'exim' uses pyinotify
2014-08-08 14:59:36,172 fail2ban.server.filter[21667]: INFO Set jail log file encoding to UTF-8
2014-08-08 14:59:36,172 fail2ban.server.jail[21667]: INFO Initiated 'pyinotify' backend
2014-08-08 14:59:36,233 fail2ban.server.filter[21667]: INFO Added logfile = /var/log/exim4/mainlog
2014-08-08 14:59:36,249 fail2ban.server.filter[21667]: INFO Set maxRetry = 5
2014-08-08 14:59:36,251 fail2ban.server.filter[21667]: INFO Set jail log file encoding to UTF-8
2014-08-08 14:59:36,252 fail2ban.server.actions[21667]: INFO Set banTime = 600
2014-08-08 14:59:36,254 fail2ban.server.filter[21667]: INFO Set findtime = 600
2014-08-08 14:59:36,284 fail2ban.server.jail[21667]: INFO Creating new jail 'sshd'
2014-08-08 14:59:36,284 fail2ban.server.jail[21667]: INFO Jail 'sshd' uses pyinotify
2014-08-08 14:59:36,286 fail2ban.server.filter[21667]: INFO Set jail log file encoding to UTF-8
2014-08-08 14:59:36,286 fail2ban.server.jail[21667]: INFO Initiated 'pyinotify' backend
2014-08-08 14:59:36,499 fail2ban.server.filter[21667]: INFO Added logfile = /var/log/auth.log
2014-08-08 14:59:36,510 fail2ban.server.filter[21667]: INFO Set maxRetry = 5
2014-08-08 14:59:36,512 fail2ban.server.filter[21667]: INFO Set jail log file encoding to UTF-8
2014-08-08 14:59:36,513 fail2ban.server.actions[21667]: INFO Set banTime = 600
2014-08-08 14:59:36,514 fail2ban.server.filter[21667]: INFO Set findtime = 600
2014-08-08 14:59:36,515 fail2ban.server.filter[21667]: INFO Set maxlines = 10
2014-08-08 14:59:36,788 fail2ban.server.server[21667]: INFO Jail sshd is not a JournalFilter instance
2014-08-08 14:59:36,798 fail2ban.server.jail[21667]: INFO Jail 'exim' started
2014-08-08 14:59:36,802 fail2ban.server.jail[21667]: INFO Jail 'sshd' started
2014-08-08 15:01:30,120 fail2ban.server.transmitter[21667]: WARNING Command ['status', 'ssh'] has failed. Received UnknownJailException('ssh',)
2014-08-08 15:09:36,978 fail2ban.server.actions[21667]: NOTICE [sshd] Unban 116.10.191.199
2014-08-08 15:09:37,187 fail2ban.server.action[21667]: ERROR rm -f /etc/symbiosis/firewall/blacklist.d/116.10.191.199.auto
iptables -D INPUT -s 116.10.191.199 -j DROP -- stdout: ''
2014-08-08 15:09:37,188 fail2ban.server.action[21667]: ERROR rm -f /etc/symbiosis/firewall/blacklist.d/116.10.191.199.auto
iptables -D INPUT -s 116.10.191.199 -j DROP -- stderr: 'iptables: Bad rule (does a matching rule exist in that chain?).\n'
2014-08-08 15:09:37,188 fail2ban.server.action[21667]: ERROR rm -f /etc/symbiosis/firewall/blacklist.d/116.10.191.199.auto
iptables -D INPUT -s 116.10.191.199 -j DROP -- returned 1
2014-08-08 15:09:37,188 fail2ban.server.actions[21667]: ERROR Failed to execute unban jail 'sshd' action 'symbiosis-blacklist': Error unbanning 116.10.191.199
2014-08-10 02:27:27,235 fail2ban.server.server[21667]: INFO rollover performed on /var/log/fail2ban.log
2014-08-10 02:27:28,109 fail2ban.server.filter[21667]: INFO Log rotation detected for /var/log/exim4/mainlog
2014-08-10 02:28:01,747 fail2ban.server.filter[21667]: INFO Log rotation detected for /var/log/auth.log
2014-08-10 02:33:29,500 fail2ban.server.filter[21667]: INFO [sshd] Found 86.101.234.57
2014-08-10 02:46:06,846 fail2ban.server.filter[21667]: INFO [sshd] Found 220.130.163.247
2014-08-10 03:10:43,794 fail2ban.server.filter[21667]: INFO [sshd] Found 220.130.163.247
2014-08-10 06:49:27,446 fail2ban.server.actions[21667]: NOTICE [sshd] Ban 116.10.191.181
2014-08-10 06:59:28,375 fail2ban.server.actions[21667]: NOTICE [sshd] Unban 116.10.191.181
2014-08-10 20:06:41,576 fail2ban.server.actions[21667]: NOTICE [sshd] Unban 50.30.34.7
2014-08-13 17:55:50,401 fail2ban.server.actions[17436]: NOTICE [sshd] 144.0.0.25 already banned
2014-08-10 20:06:41,785 fail2ban.server.action[21667]: ERROR rm -f /etc/symbiosis/firewall/blacklist.d/50.30.34.7.auto
iptables -D INPUT -s 50.30.34.7 -j DROP -- stdout: ''
2014-08-10 20:06:41,785 fail2ban.server.action[21667]: ERROR rm -f /etc/symbiosis/firewall/blacklist.d/50.30.34.7.auto
iptables -D INPUT -s 50.30.34.7 -j DROP -- stderr: 'iptables: Bad rule (does a matching rule exist in that chain?).\n'
2014-08-10 20:06:41,786 fail2ban.server.action[21667]: ERROR rm -f /etc/symbiosis/firewall/blacklist.d/50.30.34.7.auto
iptables -D INPUT -s 50.30.34.7 -j DROP -- returned 1
2014-08-10 20:06:41,786 fail2ban.server.actions[21667]: ERROR Failed to execute unban jail 'sshd' action 'symbiosis-blacklist': Error unbanning 50.30.34.7
2014-08-11 02:27:35,433 fail2ban.server.filter[21667]: INFO Log rotation detected for /var/log/exim4/mainlog

View File

@ -71,6 +71,9 @@ tests if the server is alive
.TP
\fBhelp\fR
return this output
.TP
\fBversion\fR
return the server version
.IP
LOGGING
.TP